Webhook Payload Reference
The envelope every Oho webhook delivery uses, and a field-by-field breakdown of each event type's
data body. Reach for this page when you're writing the handler that consumes an event, or working
out which field carries the outcome you need.
New to Oho webhooks? Start with Webhooks & Event Delivery — it
covers subscribing, signature verification, and testing. GET /webhooks/events returns the live
catalogue of event types, which is authoritative if it ever disagrees with this page.
Every payload uses the same envelope:
{
"deliveryId": "<uuid>",
"eventType": "credential.verified",
"emittedAt": "<iso-8601>",
"entityUrn": "urn:li:credential:...",
"tenant": { "id": "xref-poc", "url": "https://xref-poc.weareoho.com" },
"data": {/* event-specific */}
}
| Field | Description |
|---|---|
deliveryId | Stable per-event UUID. Sent in the X-Oho-Delivery header too. Use it as your idempotency key — retries of the same event re-use the same id. |
eventType | Same string as in your subscription. |
emittedAt | When the event was generated server-side (ISO 8601, nanosecond precision). |
entityUrn | The Oho URN of the entity the event is about. |
tenant | Which Oho deployment sent this webhook: tenant.id is the deployment slug (e.g. xref-poc), tenant.url its base URL. Present on every event; also sent (id only) in the X-Oho-Tenant header. Route on it when one endpoint receives events from more than one Oho instance. |
data | Event-specific body. See examples below. |
replay | Present and true only on an event re-emitted via replay — a completion that was never delivered, usually because the subscription was created after the check finished. Absent on live events. Treat the body as current state, not as news. |
credential.verified
credential.verified fires on every terminal verification outcome — success (MAY_ENGAGE),
failure (MAY_NOT_ENGAGE), or REVIEW_REQUIRED. It does not fire for IN_PROGRESS (not
terminal) or ERROR (transient — retries are expected, so emitting would create noise).
A payload from a successful VIC WWC verification (MAY_ENGAGE / VALID):
{
"deliveryId": "0167d799-f51c-41a9-a777-58a65bb7d305",
"eventType": "credential.verified",
"emittedAt": "2026-06-09T02:23:35.496341557Z",
"entityUrn": "urn:li:credential:wwcc-vic-1234567A",
"tenant": { "id": "xref-poc", "url": "https://xref-poc.weareoho.com" },
"data": {
"success": true,
"eligibility": "MAY_ENGAGE",
"statusDetail": "VALID",
"verifiedAt": "2026-06-09T02:23:35.486984338Z",
"entityCreatedAt": "2024-01-15T03:22:10.114Z",
"credential": {
"credentialUrn": "urn:li:credential:wwcc-vic-1234567A",
"credentialId": "wwcc-vic-1234567A",
"verifiedCredentialUrn": "urn:li:verifiedCredential:vicwwc-VIC-1234567A",
"credentialType": "WWCC_VIC",
"type": "wwcc",
"jurisdiction": "VIC",
"identifier": "1234567A",
"externalReferenceId": "HRIS-99213",
"holder": {
"firstName": "Jane",
"middleName": "Mary",
"lastName": "Smith"
},
"issueDate": "2024-01-15",
"expiryDate": "2029-01-15",
"displayName": "WWCC - VIC",
"category": "SCREENING",
"status": "ACTIVE",
"issuingAuthority": "Services Victoria",
"countryOfIssue": "Australia",
"customFieldValues": [{ "label": "Cost centre", "value": "CC-4471" }]
},
"registry": {
"authority": "Services Victoria",
"expiryDate": "2029-01-15",
"cardType": "Employee",
"holderName": "SMITH, Jane Mary",
"identifier": "1234567A",
"flags": { "conditions": "NONE" }
},
"owners": {
"applicants": [
{
"urn": "urn:li:applicant:app_8d3c2e91",
"id": "app_8d3c2e91",
"externalId": "ATS-55021"
}
],
"workers": []
},
"request": {
"source": "api",
"correlationId": "req_7f3a9c22"
},
"changes": {}
}
}
What each data.* field carries:
| Field | Description |
|---|---|
success | true only when eligibility is MAY_ENGAGE; false for every other outcome, including REVIEW_REQUIRED. A convenience flag — drive real business logic from eligibility and statusDetail. |
eligibility | The primary compliance signal: MAY_ENGAGE, MAY_NOT_ENGAGE, REVIEW_REQUIRED. (IN_PROGRESS / ERROR never reach a webhook.) |
statusDetail | The reason behind the signal: VALID, EXPIRING_SOON, EXPIRED, NOT_CURRENT, REVOKED, NOT_FOUND, CONDITIONS_TO_REVIEW, PENDING_DECISION, etc. |
verifiedAt | When Oho recorded the outcome (ISO 8601). |
entityCreatedAt | When the credential record was first created in Oho (ISO 8601). Stamped on the credential at creation and persisted, so it is stable and present on every credential.* event (verified, added, updated, and the ownership events). Records created before this field existed are backfilled on their next create-time write or verification — from the first-write timestamp when available, otherwise the earliest date otherwise known on the record. |
credential.credentialUrn / credentialId | The Oho URN of the credential claim and its bare id. |
credential.verifiedCredentialUrn | The URN of the verification result, paired with the claim. |
credential.credentialType | The credential type code — WWCC_VIC, WWCC_NSW, WWCC_QLD, AHPRA_G, etc. The same code the API and the other credential.* events use. |
credential.type | The broader family — wwcc, teacher, health, etc. Absent when the family is unknown; fall back to credentialType. |
credential.jurisdiction | State / national code — VIC, NSW, AUS. |
credential.identifier | The card / registration number you submitted (distinct from registry.identifier, which is what the registry returned). |
credential.externalReferenceId | Optional. Your own ID for this credential in your upstream system, if you supplied one. Lets you match back without keeping an Oho-ID map. |
credential.holder | Structured name parts (firstName, middleName, lastName) as Oho has them on file. middleName present only when known. |
credential.issueDate / expiryDate | Optional, ISO 8601. Prefer the registry's value from this run; fall back to the stored credential value. Omitted if neither is known. |
credential.displayName | Optional. Human-readable label, e.g. WWCC - VIC. |
credential.category | Optional. High-level grouping: SCREENING, GOVERNMENT_ID, LICENSE, CERTIFICATION. |
credential.status | Optional. Card lifecycle state — ACTIVE, EXPIRED, REVOKED, SUSPENDED, PENDING, REVIEW_REQUIRED — reflecting the just-computed outcome. Distinct from eligibility (the compliance signal). |
credential.issuingAuthority | Optional. Who issued the credential (e.g. VicRoads). Distinct from registry.authority (who Oho verified it against), which may differ. |
credential.countryOfIssue | Optional. Issuing country — mainly for passport / travel-document (VEVO) credentials. |
credential.customFieldValues | Optional. Admin-defined custom fields on the credential's type, as {label, value} entries. Present only for custom types that define them. |
registry | What the issuing authority returned. authority is the display name; expiryDate, cardType, holderName, identifier, and a free-form flags map are each present only when the registry supplied them. |
owners | Object grouped by owner type: owners.applicants and owners.workers. Both keys are always present (empty array when none). Each entry carries the full urn, the bare id (app_… / wkr_…), and — best-effort — externalId, the owner's id in your own system (Workday Employee_ID, ATS reference, etc.). |
request | Optional block. source (e.g. api, manual_ui, auto_verify, ongoing, ingestion) and the caller's correlationId, echoed so you can match the delivery back to the originating request. |
changes | Field-level diff against the prior verification (e.g. MAY_ENGAGE → EXPIRED). The key is omitted entirely on the first-ever verification (no prior to compare); present-but-empty ({}) on a re-verify where nothing changed. |
owners is an object, not an arrayEarlier drafts showed owners as a flat array of ids (["app_8d3c2e91"]). The real payload is the
grouped { "applicants": [...], "workers": [...] } object above. Index owners.applicants /
owners.workers.
The exact set of fields under credential, registry, and changes can vary by credential type —
different registries return different metadata. Treat unknown fields as forward-compatible additions.
credential.added / credential.updated
credential.added fires when a credential record is first created — any type, verifiable or custom.
credential.updated fires when its stored fields are edited, and excludes verification-driven
changes (those publish as credential.verified) and ownership changes (those publish as the
ownership events below).
The data body is the same credential / registry / owners / request shape as
credential.verified, minus the verification-run fields (success, eligibility, statusDetail,
verifiedAt at the top level, and changes). A credential.added for a pre-verified import:
{
"deliveryId": "d401cbb0-3fff-4157-9a25-96f253e63166",
"eventType": "credential.added",
"emittedAt": "2026-09-14T02:58:01.688679267Z",
"entityUrn": "urn:li:credential:cred_640386fa53146ce3",
"tenant": { "id": "xref-poc", "url": "https://xref-poc.weareoho.com" },
"data": {
"entityCreatedAt": "2026-08-28T01:17:49.395900507Z",
"credential": {
"credentialUrn": "urn:li:credential:cred_640386fa53146ce3",
"credentialId": "cred_640386fa53146ce3",
"verifiedCredentialUrn": "urn:li:verifiedCredential:vevo-AUS-EJ4354478",
"displayName": "VEVO Work Entitlement",
"credentialType": "VEVO",
"type": "vevo",
"category": "SCREENING",
"jurisdiction": "AUS",
"identifier": "EJ4354478",
"externalReferenceId": null,
"isVerifiable": true,
"status": "ACTIVE",
"holder": {
"firstName": "Hillary",
"middleName": null,
"lastName": "Song"
},
"issueDate": "2023-02-01",
"expiryDate": null,
"eligibility": "MAY_ENGAGE",
"statusDetail": "VALID",
"issuingAuthority": null,
"countryOfIssue": "China",
"customFieldValues": null
},
"registry": {
"authority": null,
"expiryDate": null,
"cardType": "Temporary Visa",
"holderName": "SONG, Hillary",
"identifier": "EJ4354478",
"flags": {
"work_entitlement": "Unlimited",
"visa_class": "500",
"grant_date": "2023-02-01"
}
},
"owners": {
"applicants": [],
"workers": [
{
"urn": "urn:li:worker:wkr_c2978381f16f1c10",
"id": "wkr_c2978381f16f1c10",
"externalId": "40410"
}
]
},
"request": { "source": "manual_emit" }
}
}
The fields carry the same meanings as in the credential.verified table
above, with these differences:
| Field | Description |
|---|---|
credential.credentialType | The credential's type code as Oho holds it (e.g. VEVO, WWCC_VIC) — the same code credential.verified reports. credential.type is the family in both, so branch on that when you want one code path. |
credential.eligibility / statusDetail | The current verification snapshot carried inside the credential block (not at the top level as on credential.verified). Null on a freshly created credential no registry has answered for. |
credential.verifiedCredentialUrn | The verification this credential is linked to, or null when no registry has ever answered for it. |
registry | The last stored registry answer, in the same shape credential.verified publishes — authority, expiryDate, cardType, holderName, identifier, and the free-form flags map. null (the whole block) when no registry has ever answered: a custom type, or a credential created moments ago. |
request.source | How the record was written — ingestion, edit, api, manual_resend, etc. No correlationId: there is no synchronous verification request to correlate against. |
null registry block is not the same as an empty one"registry": null means no registry has ever been asked. A block that is present with null members
means the registry was asked and returned nothing for those fields. If you branch on registry flags,
check the block for null first.
recruitmentCheck.completed
{
"deliveryId": "8a0d5e3f-c4a2-4b6e-9c7d-1f2e3a4b5c6d",
"eventType": "recruitmentCheck.completed",
"emittedAt": "2026-06-09T03:14:22.118Z",
"entityUrn": "urn:li:credentialCheck:chk_2bX9pK4mN1qR8sT3",
"tenant": { "id": "xref-poc", "url": "https://xref-poc.weareoho.com" },
"data": {
"checkId": "chk_2bX9pK4mN1qR8sT3",
"checkUrn": "urn:li:credentialCheck:chk_2bX9pK4mN1qR8sT3",
"applicantUrn": "urn:li:applicant:app_8d3c2e91",
"screeningPackageCode": "oho0001",
"status": "COMPLETED",
"submittedAt": "2026-06-09T03:14:21.802Z",
"completedAt": "2026-06-09T03:14:22.118Z",
"credentials": [
{
"credentialId": "cred_5fA1bC2d",
"credentialUrn": "urn:li:credential:cred_5fA1bC2d",
"credentialType": "WWCC_NSW",
"displayName": "WWCC - NSW",
"status": "ACTIVE",
"eligibility": "MAY_ENGAGE",
"statusDetail": "VALID",
"verificationAuthority": "Service NSW",
"lastVerifiedDate": "2026-06-09T03:14:21.900Z",
"expiryDate": "2029-01-15",
"referenceNumber": "WWC1234567E",
"jurisdiction": "NSW"
}
],
"policeChecks": [
{
"policeCheckId": "pc_9zY8xW7v",
"policeCheckUrn": "urn:li:policeCheck:pc_9zY8xW7v",
"displayName": "NPC - Jane Doe - 2026-06-09",
"kind": "NPC",
"provider": "NCC",
"externalId": "ncc_abc123",
"expiryDate": "2029-06-09",
"decision": "SUITABLE",
"result": "NDCO",
"checkStatus": "complete",
"resultDate": "2026-06-09",
"reviewRequired": false,
"pending": false
}
],
"exemptions": [
{
"exemptionId": "exm_3kL8dF2a",
"exemptionUrn": "urn:li:exemption:exm_3kL8dF2a",
"exemptionType": "BLUE_CARD_EXEMPTION",
"exemptsFromCredentialType": "BLUE_CARD",
"displayName": "QLD Blue Card Exemption",
"status": "ACTIVE",
"eligibility": "MAY_ENGAGE",
"statusDetail": "VALID",
"verificationAuthority": "Blue Card Services",
"lastVerifiedDate": "2026-06-09T03:14:21.960Z",
"referenceNumber": "1986805/1",
"expiryDate": "2027-01-15",
"jurisdiction": "QLD",
"grantingAuthority": "Blue Card Services"
}
],
"declarations": [
{
"declarationId": "decl_6mP2qR9t",
"declarationUrn": "urn:li:declaration:decl_6mP2qR9t",
"declarationCode": "CUSTOM_decl0001",
"displayName": "Code of Conduct Acknowledgement",
"status": "ACTIVE",
"acceptedAt": "2026-06-09T03:14:15Z",
"acceptedBy": "urn:li:applicant:app_8d3c2e91",
"signedAt": "2026-06-09T03:14:16Z"
}
],
"bans": {
"status": "CLEAR",
"lastCheckedAt": "2026-06-09T03:14:22.000Z",
"matchedVerifiedBanUrns": [],
"weakSignalMatchedUrns": [],
"registriesChecked": ["AGED_CARE_QUALITY"],
"consentRecordedAt": "2026-06-09T03:14:21.802Z"
}
}
}
The payload groups results by type:
credentials[]— each verifiable credential the applicant submitted, with its latest verification outcome (eligibility,statusDetail,verificationAuthority,lastVerifiedDate).policeChecks[]— National/International Police Check results (decision,result,reviewRequired).exemptions[]— the applicant's active exemptions (dispensations from holding a normally-required credential), with the same verification-outcome fields as credentials.declarations[]— the applicant's active declaration acceptances (Code of Conduct, Right to Work, etc.) withacceptedAt/acceptedByand, when signed on the capture form,signedAt.bans— a single summary object: overall ban-checkstatus(CLEAR/REVIEW_REQUIRED/MATCH_FOUND/CHECK_FAILED/PENDING), anymatchedVerifiedBanUrns, and theregistriesCheckedthe applicant consented to on this check.
Exemptions and declarations link to the applicant rather than to a specific check, so they reflect
the applicant's active records at completion time. Each array (or the bans object) is omitted
entirely when there's nothing of that type, and individual fields are omitted when unset — treat them
all as forward-compatible additions.
Ownership events: credential.linked / credential.unlinked / credential.transferred
These fire when a credential's ownership changes via the POST /credentials/{id}/link, /unlink,
and /transfer endpoints (and their bulk variants). They share the exact credential / registry
/ owners / request shape of every other credential.* event, and add a changes block
describing the owner-slot diff in the same {from, to} form as credential.verified. The owners
block always reflects the post-change state, and registry carries the last stored registry
answer for the credential (null when none) — so a credential.linked for an already-verified
credential tells you what the registry said without a second call.
credential.transferred for the canonical applicant→worker hire flow — the applicant slot was
cleared and the worker slot set:
{
"deliveryId": "3f1b2c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"eventType": "credential.transferred",
"emittedAt": "2026-06-09T04:05:12.220Z",
"entityUrn": "urn:li:credential:cred_5fA1bC2d",
"tenant": { "id": "xref-poc", "url": "https://xref-poc.weareoho.com" },
"data": {
"entityCreatedAt": "2024-01-15T03:22:10.114Z",
"credential": {
"credentialUrn": "urn:li:credential:cred_5fA1bC2d",
"credentialId": "cred_5fA1bC2d",
"displayName": "WWCC - NSW",
"credentialType": "WWCC_NSW",
"type": "wwcc",
"jurisdiction": "NSW",
"identifier": "WWC1234567E",
"isVerifiable": true,
"status": "ACTIVE"
},
"registry": {
"authority": "Service NSW",
"expiryDate": "2029-01-15",
"cardType": "Employee",
"holderName": "DOE, Jane",
"identifier": "WWC1234567E",
"flags": { "conditions": "NONE" }
},
"owners": {
"applicants": [],
"workers": [
{
"urn": "urn:li:worker:wrk_9a1b",
"id": "wrk_9a1b",
"externalId": "EMP-4471"
}
]
},
"request": { "source": "api" },
"changes": {
"applicantUrn": { "from": "urn:li:applicant:app_8d3c2e91", "to": null },
"workerUrn": { "from": null, "to": "urn:li:worker:wrk_9a1b" }
}
}
}
For credential.linked the changes block carries only the slot that was set ("from": null); for
credential.unlinked, only the slot that was cleared ("to": null).
Attachment events: credential.attachmentAdded / credential.attachmentRemoved
These fire once per file when a file is attached to, or removed from, a credential — whether it came
in through the capture form, the Oho app or the attachments API. They share the credential /
registry / owners / request shape of every other credential.* event and add an attachment
block describing the file.
- A replaced file arrives as
credential.attachmentRemovedfor the old file andcredential.attachmentAddedfor the new one. There is no "updated" event. - A change to a file's document check (the type match and confidence) sends nothing.
- The payload never contains the file or a download link. Fetch a fresh link with
GET /credentials/{credentialId}/attachments/{attachmentId}when you process the event. ItscontentUrlis valid for 5 minutes and only for the API token that requested it.
{
"eventType": "credential.attachmentAdded",
"entityUrn": "urn:li:credential:cred_5fA1bC2d",
"data": {
"credential": {
"credentialId": "cred_5fA1bC2d",
"credentialType": "WWCC_NSW",
"identifier": "WWC1234567E"
},
"owners": {
"applicants": [],
"workers": [
{
"urn": "urn:li:worker:wrk_9a1b",
"id": "wrk_9a1b",
"externalId": "EMP-4471"
}
]
},
"request": { "source": "edit" },
"attachment": {
"attachmentId": "att_9f2c1a7b3d4e5f60",
"fileName": "wwcc-card.pdf",
"mimeType": "application/pdf",
"size": 48213,
"checksum": "9b74c9897bac770ffc029102a200c5de...",
"uploadedAt": "2026-10-19T01:02:03Z",
"source": "capture"
}
}
}
The example is trimmed to the fields that matter here; the full credential and registry blocks are
the same as on credential.added. checksum and source are
null for files uploaded before Oho recorded them.
When the owner changes: credential.attachmentTransferred / attachmentLinked / attachmentUnlinked
When a credential's owner changes, its files stay on the credential, so no attachmentAdded fires.
Instead, each ownership event is followed by one event per file:
| Ownership event | Per-file event |
|---|---|
credential.transferred | credential.attachmentTransferred |
credential.linked | credential.attachmentLinked |
credential.unlinked | credential.attachmentUnlinked |
A credential with no files sends only the ownership event. Each per-file payload is the ownership
event's payload, with owners after the change and the same changes block, plus the file's
attachment block.
Use these to copy files that were uploaded before the credential belonged to the person you sync. A
common case is an applicant uploading a card through the capture form before being hired: the file
then arrives as credential.attachmentTransferred with the hired worker in owners.workers.
Qualification events: qualification.added / qualification.updated / qualification.linked / qualification.unlinked / qualification.transferred
A qualification is a separate entity from a credential, with its own events:
qualification.addedfires once, when a qualification is first saved — from the app, the REST API or a Fetch Request form.qualification.updatedfires when a field in the payload'squalificationblock changes. That includes verification outcomes — a register check, a USI transcript, a document review. There is noqualification.verified. A save that changes nothing in the block sends nothing.qualification.linkedfires when an owner is added and none is removed — the worker or applicant slot goes from empty to set. This is what you receive when an applicant is converted to a worker and their qualifications are carried across: the worker is set and the applicant is kept.qualification.unlinkedfires when an owner is removed and none is added — the worker or applicant slot goes from set to empty.qualification.transferredfires when the qualification moves from one holder to another — a slot's value is replaced, or one slot is set while the other is cleared.
Owner changes are classified exactly as credential.linked, credential.unlinked and
credential.transferred are.
A qualification.added:
{
"deliveryId": "52686b74-4b77-4b36-b320-b8f99146f42e",
"eventType": "qualification.added",
"emittedAt": "2026-10-21T05:58:54.054898Z",
"entityUrn": "urn:li:qualification:qlf_AoWz-g6XFka9HUKo",
"tenant": { "id": "xref-poc", "url": "https://xref-poc.weareoho.com" },
"data": {
"entityCreatedAt": "2026-10-21T05:57:38.519968341Z",
"qualification": {
"qualificationUrn": "urn:li:qualification:qlf_AoWz-g6XFka9HUKo",
"qualificationId": "qlf_AoWz-g6XFka9HUKo",
"identification": {
"displayName": "Cert Iii Individual Support",
"qualificationType": "CERT_III_INDIVIDUAL_SUPPORT",
"nationalCode": "CHC33021",
"category": "VET",
"nrtType": null,
"aqfLevel": null,
"fieldOfStudy": null,
"certificateNumber": null,
"externalReferenceId": null
},
"holder": null,
"issuing": {
"issuingOrganisation": "TAFE NSW",
"issuingOrganisationCode": null,
"issuingCountry": null
},
"attainment": {
"status": "ATTAINED",
"verificationStatus": "SELF_DECLARED",
"completionDate": null,
"issueDate": null,
"expiryDate": null,
"expectedCompletionDate": null
},
"registerCheck": {
"outcome": "VALID",
"detail": "Current on the national register.",
"checkedAt": "2026-10-21T05:57:38.885151924Z"
},
"notes": "Walkthrough note",
"isActive": true
},
"owners": {
"applicants": [],
"workers": [
{
"urn": "urn:li:worker:wkr_00700e1602492135",
"id": "wkr_00700e1602492135",
"externalId": "EMP-4471"
}
]
},
"request": { "source": "manual_emit" }
}
}
| Field | Description |
|---|---|
entityCreatedAt | When the qualification was first saved. Unchanged on later events, including a re-emitted qualification.added. |
qualification | The same fields as the attributes of GET /qualifications/{id}, plus qualificationUrn and qualificationId. Every key is present; an unset field is null. There is no lastUpdated. |
qualification.holder | null when neither a holder name nor a USI is recorded. |
qualification.registerCheck | null until the national register has been asked. Unlike the REST response, where the block is absent, the webhook always carries the key. |
owners | The same shape as on credential.* events: both keys always present, each entry {urn, id, externalId}. externalId (the worker's employee ID, or the applicant's external ID) is omitted, not null, when unknown. On the ownership events it shows the owners after the change. |
request.source | How the change was made: ingestion for added, edit for updated, api for linked, unlinked and transferred, manual_emit for a re-emit — unless the write carried its own source. |
request.fetchRequestId / fetchRequestUrn | Present only when the qualification was submitted on a Fetch Request form. Absent otherwise. |
changes | qualification.linked, qualification.unlinked and qualification.transferred only — one {from, to} entry per owner slot that moved. Absent on added and updated. |
qualification.updated straight after qualification.addedOne change in Oho often saves a qualification more than once — a create, then the register check
that follows it. The register check arrives as qualification.updated, the way
credential.verified follows credential.added. Fields the payload doesn't carry — the USI
transcript outcome itself, a reviewer's decision, ACECQA approval — don't send an event on their
own.
To replay qualification.added or qualification.updated for an existing qualification, use
POST /qualifications/{id}/emit.
qualification.linked, qualification.unlinked and qualification.transferred cannot be
replayed.
qualification.updated
The same qualification after a USI transcript confirmed it: attainment.verificationStatus moved
from SELF_DECLARED to ISSUER_VERIFIED. The payload is still the whole record, not a diff, and
entityCreatedAt is unchanged:
{
"deliveryId": "8b0e6f2a-1c3d-4e5f-9a7b-2c4d6e8f0a1b",
"eventType": "qualification.updated",
"emittedAt": "2026-10-21T06:12:40.318227Z",
"entityUrn": "urn:li:qualification:qlf_AoWz-g6XFka9HUKo",
"tenant": { "id": "xref-poc", "url": "https://xref-poc.weareoho.com" },
"data": {
"entityCreatedAt": "2026-10-21T05:57:38.519968341Z",
"qualification": {
"qualificationUrn": "urn:li:qualification:qlf_AoWz-g6XFka9HUKo",
"qualificationId": "qlf_AoWz-g6XFka9HUKo",
"identification": {
"displayName": "Cert Iii Individual Support",
"qualificationType": "CERT_III_INDIVIDUAL_SUPPORT",
"nationalCode": "CHC33021",
"category": "VET",
"nrtType": null,
"aqfLevel": null,
"fieldOfStudy": null,
"certificateNumber": null,
"externalReferenceId": null
},
"holder": null,
"issuing": {
"issuingOrganisation": "TAFE NSW",
"issuingOrganisationCode": null,
"issuingCountry": null
},
"attainment": {
"status": "ATTAINED",
"verificationStatus": "ISSUER_VERIFIED",
"completionDate": null,
"issueDate": null,
"expiryDate": null,
"expectedCompletionDate": null
},
"registerCheck": {
"outcome": "VALID",
"detail": "Current on the national register.",
"checkedAt": "2026-10-21T05:57:38.885151924Z"
},
"notes": "Walkthrough note",
"isActive": true
},
"owners": {
"applicants": [],
"workers": [
{
"urn": "urn:li:worker:wkr_00700e1602492135",
"id": "wkr_00700e1602492135",
"externalId": "EMP-4471"
}
]
},
"request": { "source": "edit" }
}
}
qualification.linked
An applicant was converted to a worker and their qualifications were carried across. The worker
slot was set and the applicant slot kept, so owners lists both people and changes lists only
the worker slot:
{
"deliveryId": "e41d7c90-5b2a-4f86-a3c1-7d9e0b2f4a68",
"eventType": "qualification.linked",
"emittedAt": "2026-10-22T02:41:31.904512Z",
"entityUrn": "urn:li:qualification:qlf_hReT4yi82XsiD0Lm",
"tenant": { "id": "xref-poc", "url": "https://xref-poc.weareoho.com" },
"data": {
"entityCreatedAt": "2026-10-22T02:41:25.611093Z",
"qualification": {
"qualificationUrn": "urn:li:qualification:qlf_hReT4yi82XsiD0Lm",
"qualificationId": "qlf_hReT4yi82XsiD0Lm",
"identification": {
"displayName": "Provide First Aid",
"qualificationType": "HLTAID011",
"nationalCode": "HLTAID011",
"category": "VET",
"nrtType": "Unit of competency",
"aqfLevel": null,
"fieldOfStudy": null,
"certificateNumber": "FA-778120",
"externalReferenceId": null
},
"holder": {
"holderFullName": "Jane Citizen",
"uniqueStudentIdentifier": null
},
"issuing": {
"issuingOrganisation": "St John Ambulance Australia",
"issuingOrganisationCode": "88022",
"issuingCountry": "AU"
},
"attainment": {
"status": "ATTAINED",
"verificationStatus": "DOCUMENT_VERIFIED",
"completionDate": "2025-03-14",
"issueDate": "2025-03-14",
"expiryDate": "2028-03-14",
"expectedCompletionDate": null
},
"registerCheck": {
"outcome": "VALID",
"detail": "HLTAID011 is current and RTO 88022 is registered.",
"checkedAt": "2026-10-22T02:41:26.002781Z"
},
"notes": null,
"isActive": true
},
"owners": {
"applicants": [
{
"urn": "urn:li:applicant:app_8d3c2e91",
"id": "app_8d3c2e91",
"externalId": "ATS-20931"
}
],
"workers": [
{
"urn": "urn:li:worker:wkr_c2978381",
"id": "wkr_c2978381",
"externalId": "EMP-500"
}
]
},
"request": { "source": "api" },
"changes": {
"workerUrn": { "from": null, "to": "urn:li:worker:wkr_c2978381" }
}
}
}
qualification.unlinked and qualification.transferred
These carry the same data keys as qualification.linked. Only changes and owners differ. For a
qualification.unlinked, changes lists only the cleared slot, and owners lists who is left:
"changes": {
"applicantUrn": { "from": "urn:li:applicant:app_8d3c2e91", "to": null }
}
For a qualification.transferred, changes lists each slot that moved:
"changes": {
"workerUrn": {
"from": "urn:li:worker:wkr_c2978381",
"to": "urn:li:worker:wkr_00700e1602492135"
}
}
Upserting from qualification events
You can process every qualification.* event with the same upsert, the way you process
credential.* events.
- Same shape on every event. Every
qualification.*event carries the full current record:entityCreatedAt,qualification,ownersandrequest. Only the ownership events addchanges. Upsert ondata.qualification.qualificationId, as you upsertcredential.*events ondata.credential.credentialId. Key on thedatafield rather than onentityUrn. - Owners. Replace your owner links with
data.ownerson every event, not only on the ownership events.changessays what moved, butownersis the current state. - Repeats. Drop repeats by
deliveryId. Every retry of a delivery reuses it. A redrive sends the storeddataagain as a new delivery, with a newdeliveryIdandemittedAt. The upsert in point 1 absorbs it. - Order. Delivery order isn't guaranteed: Oho delivers on several threads and retries with a
backoff, so a later event can arrive first. Keep the newest by
emittedAt, which is set once per event and reused on every retry, and ignore an event older than the one you applied. A redriven event'semittedAtis the redrive time, so after a redrive, re-read the qualification withGET /qualifications/{id}. - Replays. A
POST /qualifications/{id}/emitreplay is a new event, with a newdeliveryId, but it carries the originalentityCreatedAtandrequest.sourceismanual_emit(or thesourceyou passed). It has noreplayflag. Treat it as an upsert, not a new record. - Deletes. No event fires when a qualification is deleted.
DELETE /qualifications/{id}is a soft delete. To reconcile deletions, list withGET /qualifications?includeDeleted=trueand remove the records that come back with"deleted": true. SettingisActivetofalseis not a delete: it changes thequalificationblock, so it arrives asqualification.updated.
webhook.test
Fired by POST /webhooks/{id}/ping — use it to validate your endpoint end-to-end without waiting for
a real event.
{
"deliveryId": "c4a2-...",
"eventType": "webhook.test",
"emittedAt": "2026-06-09T03:20:00Z",
"entityUrn": "urn:li:webhook:whk_2bX9pK4mN1qR8sT3",
"tenant": { "id": "xref-poc", "url": "https://xref-poc.weareoho.com" },
"data": {
"subscriptionId": "whk_2bX9pK4mN1qR8sT3",
"message": "Synthetic test event delivered by POST /webhooks/{id}/ping",
"deliveredAt": "2026-06-09T03:20:00Z"
}
}
webhook.disabled
Emitted when Oho auto-disables a subscription after 50 consecutive delivery failures. This is the signal to fix the endpoint and re-enable it — see Recovering a Broken Webhook Endpoint.
{
"deliveryId": "9f31-...",
"eventType": "webhook.disabled",
"emittedAt": "2026-06-09T03:20:00Z",
"entityUrn": "urn:li:webhook:whk_2bX9pK4mN1qR8sT3",
"tenant": { "id": "xref-poc", "url": "https://xref-poc.weareoho.com" },
"data": {
"subscriptionUrn": "urn:li:webhook:whk_2bX9pK4mN1qR8sT3",
"reason": "AUTO_DISABLED",
"consecutiveFailures": 50,
"threshold": 50
}
}
| Field | Meaning |
|---|---|
subscriptionUrn | The subscription that was disabled — same value as entityUrn. |
reason | Currently always AUTO_DISABLED. |
consecutiveFailures | Failures counted when the threshold tripped. |
threshold | The configured auto-disable threshold (50). |
Only ACTIVE subscriptions receive events, and the subscription this event names is already
AUTO_DISABLED when it fires — so that endpoint never receives it. Catch it on a separate
subscription listening to webhook.disabled. See
the event catalogue.
Where to go next
- Webhooks & Event Delivery — subscribe, verify signatures, send a test event
- Webhook Operations — rotate the secret, read delivery history, redrive and replay
- Recovering a Broken Webhook Endpoint — the runbook for after an outage
- API Reference — every endpoint, field, and query parameter