Skip to main content

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 */}
}
FieldDescription
deliveryIdStable 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.
eventTypeSame string as in your subscription.
emittedAtWhen the event was generated server-side (ISO 8601, nanosecond precision).
entityUrnThe Oho URN of the entity the event is about.
tenantWhich 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.
dataEvent-specific body. See examples below.
replayPresent 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:

FieldDescription
successtrue 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.
eligibilityThe primary compliance signal: MAY_ENGAGE, MAY_NOT_ENGAGE, REVIEW_REQUIRED. (IN_PROGRESS / ERROR never reach a webhook.)
statusDetailThe reason behind the signal: VALID, EXPIRING_SOON, EXPIRED, NOT_CURRENT, REVOKED, NOT_FOUND, CONDITIONS_TO_REVIEW, PENDING_DECISION, etc.
verifiedAtWhen Oho recorded the outcome (ISO 8601).
entityCreatedAtWhen 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 / credentialIdThe Oho URN of the credential claim and its bare id.
credential.verifiedCredentialUrnThe URN of the verification result, paired with the claim.
credential.credentialTypeThe credential type code — WWCC_VIC, WWCC_NSW, WWCC_QLD, AHPRA_G, etc. The same code the API and the other credential.* events use.
credential.typeThe broader family — wwcc, teacher, health, etc. Absent when the family is unknown; fall back to credentialType.
credential.jurisdictionState / national code — VIC, NSW, AUS.
credential.identifierThe card / registration number you submitted (distinct from registry.identifier, which is what the registry returned).
credential.externalReferenceIdOptional. 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.holderStructured name parts (firstName, middleName, lastName) as Oho has them on file. middleName present only when known.
credential.issueDate / expiryDateOptional, ISO 8601. Prefer the registry's value from this run; fall back to the stored credential value. Omitted if neither is known.
credential.displayNameOptional. Human-readable label, e.g. WWCC - VIC.
credential.categoryOptional. High-level grouping: SCREENING, GOVERNMENT_ID, LICENSE, CERTIFICATION.
credential.statusOptional. Card lifecycle state — ACTIVE, EXPIRED, REVOKED, SUSPENDED, PENDING, REVIEW_REQUIRED — reflecting the just-computed outcome. Distinct from eligibility (the compliance signal).
credential.issuingAuthorityOptional. Who issued the credential (e.g. VicRoads). Distinct from registry.authority (who Oho verified it against), which may differ.
credential.countryOfIssueOptional. Issuing country — mainly for passport / travel-document (VEVO) credentials.
credential.customFieldValuesOptional. Admin-defined custom fields on the credential's type, as {label, value} entries. Present only for custom types that define them.
registryWhat 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.
ownersObject 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.).
requestOptional 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.
changesField-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 array

Earlier 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:

FieldDescription
credential.credentialTypeThe 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 / statusDetailThe 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.verifiedCredentialUrnThe verification this credential is linked to, or null when no registry has ever answered for it.
registryThe 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.sourceHow the record was written — ingestion, edit, api, manual_resend, etc. No correlationId: there is no synchronous verification request to correlate against.
A 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.) with acceptedAt / acceptedBy and, when signed on the capture form, signedAt.
  • bans — a single summary object: overall ban-check status (CLEAR / REVIEW_REQUIRED / MATCH_FOUND / CHECK_FAILED / PENDING), any matchedVerifiedBanUrns, and the registriesChecked the 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.attachmentRemoved for the old file and credential.attachmentAdded for 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. Its contentUrl is 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 eventPer-file event
credential.transferredcredential.attachmentTransferred
credential.linkedcredential.attachmentLinked
credential.unlinkedcredential.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.added fires once, when a qualification is first saved — from the app, the REST API or a Fetch Request form.
  • qualification.updated fires when a field in the payload's qualification block changes. That includes verification outcomes — a register check, a USI transcript, a document review. There is no qualification.verified. A save that changes nothing in the block sends nothing.
  • qualification.linked fires 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.unlinked fires when an owner is removed and none is added — the worker or applicant slot goes from set to empty.
  • qualification.transferred fires 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" }
}
}
FieldDescription
entityCreatedAtWhen the qualification was first saved. Unchanged on later events, including a re-emitted qualification.added.
qualificationThe 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.holdernull when neither a holder name nor a USI is recorded.
qualification.registerChecknull until the national register has been asked. Unlike the REST response, where the block is absent, the webhook always carries the key.
ownersThe 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.sourceHow 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 / fetchRequestUrnPresent only when the qualification was submitted on a Fetch Request form. Absent otherwise.
changesqualification.linked, qualification.unlinked and qualification.transferred only — one {from, to} entry per owner slot that moved. Absent on added and updated.
Expect qualification.updated straight after qualification.added

One 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.

  1. Same shape on every event. Every qualification.* event carries the full current record: entityCreatedAt, qualification, owners and request. Only the ownership events add changes. Upsert on data.qualification.qualificationId, as you upsert credential.* events on data.credential.credentialId. Key on the data field rather than on entityUrn.
  2. Owners. Replace your owner links with data.owners on every event, not only on the ownership events. changes says what moved, but owners is the current state.
  3. Repeats. Drop repeats by deliveryId. Every retry of a delivery reuses it. A redrive sends the stored data again as a new delivery, with a new deliveryId and emittedAt. The upsert in point 1 absorbs it.
  4. 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's emittedAt is the redrive time, so after a redrive, re-read the qualification with GET /qualifications/{id}.
  5. Replays. A POST /qualifications/{id}/emit replay is a new event, with a new deliveryId, but it carries the original entityCreatedAt and request.source is manual_emit (or the source you passed). It has no replay flag. Treat it as an upsert, not a new record.
  6. Deletes. No event fires when a qualification is deleted. DELETE /qualifications/{id} is a soft delete. To reconcile deletions, list with GET /qualifications?includeDeleted=true and remove the records that come back with "deleted": true. Setting isActive to false is not a delete: it changes the qualification block, so it arrives as qualification.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
}
}
FieldMeaning
subscriptionUrnThe subscription that was disabled — same value as entityUrn.
reasonCurrently always AUTO_DISABLED.
consecutiveFailuresFailures counted when the threshold tripped.
thresholdThe configured auto-disable threshold (50).
Delivered to a different subscription

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​