Skip to main content

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.

Before you start

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"
note

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.

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"
Which credentialType?

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.

Common validation errors
  • A missing required field → 400, e.g. { "error": "identification.referenceNumber is required" }
  • owners with neither worker nor applicant → 400
  • owners.worker not matching the wkr_<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": "..."
}

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.

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.

Conflicts

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