Workers & Credentials Tutorial
A complete, copy-paste flow against the REST API: create a worker, attach a credential, verify it, search, and move ownership around. Each step links to its full per-endpoint reference for every field and query parameter.
Read API Basics first — it covers base URLs, the Authorization header, IDs,
the response envelope, and error shapes that every step here relies on. The examples assume
$OHO_BASE and $OHO_TOKEN are set.
1. Create a worker
identity.displayName is the only required field. The request below adds the fields a typical
caller sends; everything else is optional. The server returns 201 with the generated wkr_
ID and a Location header. See Create worker for the full field list.
curl -sS -X POST "$OHO_BASE/workers" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"identity": { "displayName": "Jordan Avery", "firstName": "Jordan", "lastName": "Avery", "birthDate": "1992-03-14" },
"contact": { "workEmail": "jordan.avery@example.com" },
"employment": { "jobTitle": "Support Worker", "workerType": "EMPLOYEE", "status": "ACTIVE" },
"source": { "externalId": "WD-100482", "sourceSystem": "workday" }
}'
{
"data": {
"id": "wkr_V1StGXR8Z5jdHi6B",
"type": "worker",
"attributes": {
"identity": {
"displayName": "Jordan Avery",
"firstName": "Jordan",
"lastName": "Avery",
"birthDate": "1992-03-14"
},
"contact": { "workEmail": "jordan.avery@example.com" },
"employment": {
"jobTitle": "Support Worker",
"workerType": "EMPLOYEE",
"status": "ACTIVE"
},
"source": { "externalId": "WD-100482", "sourceSystem": "workday" }
},
"lastUpdated": "2026-06-29T02:14:07Z"
},
"meta": { "requestId": "f3c1a0e2-..." }
}
Save the ID for the steps that follow:
export WKR="wkr_V1StGXR8Z5jdHi6B"
Omitting identity.displayName returns 400 with { "error": "identity.displayName is required" }.
2. Fetch the worker
curl -sS "$OHO_BASE/workers/$WKR" -H "Authorization: Bearer $OHO_TOKEN"
Returns the same envelope as the create response. Add ?include=credentials to embed a summary
of the worker's credentials (capped at 50) — handy after step 5. See Get worker.
3. List and search workers
# Free-text search, newest first
curl -sS "$OHO_BASE/workers?query=Jordan&sort=lastUpdated:desc" -H "Authorization: Bearer $OHO_TOKEN"
# Look up by your upstream HRIS id
curl -sS "$OHO_BASE/workers?externalId=WD-100482" -H "Authorization: Bearer $OHO_TOKEN"
Filter by recency with updatedAfter (inclusive) and updatedBefore (exclusive); both accept
an ISO-8601 instant or a bare date. Full parameter list: List workers.
4. Update a worker
Use PATCH to change only the fields you send:
curl -sS -X PATCH "$OHO_BASE/workers/$WKR" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "employment": { "jobTitle": "Senior Support Worker" } }'
Use PUT to replace the worker wholesale — fields absent from the body
are cleared, and identity.displayName is required.
Set the worker's manager
employment.manager links a worker to their direct manager (another worker). Reference the
manager with exactly one of id (the synthetic wkr_ identifier) or externalId (your
upstream HRIS identifier) — the same pattern as credential owner references.
# By synthetic id
curl -sS -X PATCH "$OHO_BASE/workers/$WKR" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "employment": { "manager": { "id": "wkr_M9nAgErW0rKeRiD1" } } }'
# By your HRIS id — add sourceSystem if the same externalId exists in multiple systems
curl -sS -X PATCH "$OHO_BASE/workers/$WKR" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "employment": { "manager": { "externalId": "WD-100001", "sourceSystem": "workday" } } }'
Responses return "manager": { "id": "wkr_..." } once linked.
Out-of-order syncs are fine. If the manager's externalId doesn't match any worker yet, the
reference is accepted and parked as pending — you'll see it echoed back as
"manager": { "externalId": "WD-100001", "sourceSystem": "workday" } (no id). The moment a
worker carrying that source.externalId is created or updated via the API, the link completes
automatically and the response switches to the id form. No two-pass ordering is needed when you
bulk-sync from your HRIS.
- A
manager.idmust reference an existing, non-deleted worker → otherwise400 - A worker cannot be their own manager →
400 - An
externalIdmatching multiple workers →400; narrow withsourceSystem - An
externalIdmatching no worker → accepted as pending (see above) - To clear the manager (linked or pending), use
PUTand omitemployment.manager(PATCH leaves an omitted manager unchanged)
If this worker is synced from an HRIS feed (e.g. the SFTP workforce connector with
manager_worker_id / manager_email columns), the next sync run overwrites whatever the API
set — the most recent writer wins. Set the manager in your HRIS when it is the source of truth.
5. Create a credential
Required: identification.displayName, .credentialType, .category, .referenceNumber, and
verification.status. On create only, owners must carry at least one of worker or
applicant. The example adds the issuing details a typical caller sends. Full reference:
Create credential.
curl -sS -X POST "$OHO_BASE/credentials" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"identification": { "displayName": "WWCC — NSW", "credentialType": "WWCC_NSW", "category": "SCREENING", "referenceNumber": "WWC1234567E" },
"issuing": { "issuingAuthority": "NSW Office of the Children'\''s Guardian", "jurisdiction": "NSW", "issueDate": "2024-05-01", "expiryDate": "2029-05-01" },
"verification": { "status": "PENDING", "isVerifiable": true },
"owners": { "worker": "'"$WKR"'" }
}'
{
"data": {
"id": "cred_8x2Kf9aQ4mN7pLrT",
"type": "credential",
"attributes": {
"identification": {
"displayName": "WWCC — NSW",
"credentialType": "WWCC_NSW",
"category": "SCREENING",
"referenceNumber": "WWC1234567E"
},
"issuing": {
"issuingAuthority": "NSW Office of the Children's Guardian",
"jurisdiction": "NSW",
"issueDate": "2024-05-01",
"expiryDate": "2029-05-01"
},
"verification": { "status": "PENDING", "isVerifiable": true },
"owners": { "worker": "wkr_V1StGXR8Z5jdHi6B" }
},
"lastUpdated": "2026-06-29T02:20:11Z"
},
"meta": {
"requestId": "...",
"correlationId": "9b6d6c44-2a1e-4f0b-8b7a-3a2f1e0d9c8b"
}
}
export CRED="cred_8x2Kf9aQ4mN7pLrT"
credentialType codes are jurisdiction-specific (WWCC, Blue Card, Ochre Card, teacher
registration, AHPRA, NDIS, and more). Fetch the live list any time from
List supported credential types, and see the
Credentials concept pages for what each one verifies against.
category is one of SCREENING, GOVERNMENT_ID, LICENSE, CERTIFICATION.
- A missing required field →
400, e.g.{ "error": "identification.referenceNumber is required" } ownerswith neither worker nor applicant →400owners.workernot matching thewkr_<id>pattern →400
6. Verify the credential
Runs a registry-backed check and updates the credential's verification fields. The body is
optional; a correlationId you supply is echoed back and into the credential.verified webhook
so you can match the asynchronous delivery to this request. See Verify a credential.
curl -sS -X POST "$OHO_BASE/credentials/$CRED/verify" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "forceRefresh": true }'
{
"eligibility": "MAY_ENGAGE",
"statusDetail": "VALID",
"expiryDate": "2029-05-01",
"errorMessage": null,
"correlationId": "5b1c8b8a-2c3d-4e5f-9a0b-1c2d3e4f5a6b",
"requestId": "..."
}
Reading the registry result
Once a credential has been verified, every GET of it returns the latest registry result under
attributes.verification.registryDetails — what the issuing authority's register actually said,
not just the standardised eligibility/statusDetail verdict. Worker responses with
?include=credentials carry the same object on each credential summary.
"verification": {
"status": "ACTIVE",
"eligibility": "MAY_ENGAGE",
"statusDetail": "VALID",
"lastVerifiedDate": "2026-08-01",
"registryDetails": {
"registryCardType": "Work Entitlement",
"registryHolderName": "JANE CITIZEN",
"registryIdentifier": "PA1234567",
"registryExpiryDate": "2027-03-15",
"lastVerifiedAt": "2026-08-01T02:11:05Z",
"verificationMethod": "ONGOING",
"sourceSystem": "vevo",
"registryFlags": {
"visa_class": "TSS/482",
"visa_subclass": "482",
"entitlement_status": "Entitled to work"
}
}
}
The block is type-agnostic: the structured fields are filled where the registry provides them
(some registries publish no expiry or holder name), and registryFlags carries the
credential-type-specific detail verbatim — the same keys the credential.verified webhook sends.
Common keys by credential type:
| Credential type | Typical registryFlags keys |
|---|---|
| VEVO (right to work) | visa_class, visa_subclass, visa_stream, condition_code, condition_description, country_of_issue, grant_date, grant_expiry, entitlement_status, entitlement_type, status_message, is_conditional |
| AHPRA | is_conditional, ahpra_non_practising, conditions_text, notations_text, undertakings_text, endorsements_text, registration_requirements_text, reprimands_text |
| WWCC (state checks) | card_type, wwcc_sa_status_raw, is_new_card, is_application, suspension, exclusion, expiring |
| Teacher registration boards | registration_type / category / member_status / registration_status, registration_since, fee_due_date, expiring |
| NDIS Worker Screening (NDWS) | ndws_pending, ndws_interim_bar, ndws_exclusion, ndws_suspension, ndws_no_valid_clearance, ndws_raw_status |
| Police checks (NPC/NCC) | none — structured fields only |
registryDetails is absent on credentials that have never been verified against a registry.
7. Manual review
For a human decision instead of a registry check, use
approve (sets status=ACTIVE, eligibility=MAY_ENGAGE,
statusDetail=VALID) or reject (sets status=REVOKED,
eligibility=MAY_NOT_ENGAGE). Both take an optional reason/notes body.
curl -sS -X POST "$OHO_BASE/credentials/$CRED/approve" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "reason": "Sighted original certificate" }'
8. List and filter credentials
# All credentials for this worker
curl -sS "$OHO_BASE/credentials?ownerType=worker&ownerId=$WKR" -H "Authorization: Bearer $OHO_TOKEN"
# Filter by type, category, jurisdiction, status, eligibility
curl -sS "$OHO_BASE/credentials?category=SCREENING&jurisdiction=NSW&status=ACTIVE&sort=lastUpdated:desc" \
-H "Authorization: Bearer $OHO_TOKEN"
You can also filter by upstream owner id with ownerExternalId (narrow with ownerSourceSystem
if it could be ambiguous). Full parameter list: List credentials.
9. Ownership: link, unlink, transfer
owners is immutable on PATCH/PUT — attempting it returns 400. Change ownership through
the dedicated endpoints instead. Each owner reference is a type plus exactly one of id
or externalId. A credential holds at most one worker and one applicant slot at a time.
# Link an additional owner to one credential
curl -sS -X POST "$OHO_BASE/credentials/$CRED/link" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "to": { "type": "applicant", "id": "app_Qz7yT2mB" } }'
# Transfer an owner — the canonical applicant -> worker hire flow
curl -sS -X POST "$OHO_BASE/credentials/$CRED/transfer" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "from": { "type": "applicant", "id": "app_Qz7yT2mB" }, "to": { "type": "worker", "id": "'"$WKR"'" } }'
# Unlink an owner (refuses if it would leave the credential with no owners)
curl -sS -X POST "$OHO_BASE/credentials/$CRED/unlink" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "ownerType": "applicant" }'
References: Link · Transfer · Unlink.
Linking a slot that already holds a different id returns 409 with code ALREADY_LINKED — use
transfer to replace it. Transferring a credential not currently owned by from returns 409
with code OWNER_MISMATCH.
Bulk ownership
POST /credentials/link and POST /credentials/transfer (no
{id} in the path) operate on many credentials at once. Omit credentialIds to process every
credential currently linked to from; the response reports a per-item status. Best-effort batch,
capped at 500.
curl -sS -X POST "$OHO_BASE/credentials/transfer" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from": { "type": "applicant", "externalId": "ATS-55021" },
"to": { "type": "worker", "externalId": "WD-100482", "sourceSystem": "workday" }
}'
{
"data": [
{ "credentialId": "cred_8x2Kf9aQ4mN7pLrT", "status": "transferred" },
{
"credentialId": "cred_bad1",
"status": "skipped",
"error": {
"code": "OWNER_MISMATCH",
"message": "not owned by applicant ..."
}
}
],
"meta": { "total": 2, "succeeded": 1, "failed": 1, "requestId": "..." }
}
10. Compliance summary
Aggregated roll-ups by status, category, jurisdiction, and eligibility — optionally scoped to one owner. See Compliance roll-ups.
curl -sS "$OHO_BASE/credentials/compliance-summary?ownerType=worker&ownerId=$WKR" \
-H "Authorization: Bearer $OHO_TOKEN"
{
"total": 3,
"byStatus": { "ACTIVE": 2, "PENDING": 1 },
"byCategory": { "SCREENING": 2, "LICENSE": 1 },
"byJurisdiction": { "NSW": 1, "VIC": 1, "QLD": 1 },
"byEligibility": { "MAY_ENGAGE": 2, "IN_PROGRESS": 1 },
"requestId": "..."
}
11. Soft delete
curl -sS -X DELETE "$OHO_BASE/credentials/$CRED" -H "Authorization: Bearer $OHO_TOKEN" -i # 204
curl -sS -X DELETE "$OHO_BASE/workers/$WKR" -H "Authorization: Bearer $OHO_TOKEN" -i # 204
Both set deleted: true and preserve the record. Re-query with includeDeleted=true to confirm
they still exist. References: Soft delete credential ·
Soft delete worker.
Where to go next
- API Reference — every endpoint, field, and query parameter
- Credentials concept pages — what each credential type verifies against
- Webhooks & Event Delivery — receive
credential.verifiedand other events