Skip to main content

Workers & Credentials Tutorial

A complete, copy-paste flow against the REST API: create a worker, attach a credential, verify it, and read the result back. Each step links to its full per-endpoint reference for every field and query parameter.

Three things that sit off this path have their own pages, linked from the step they belong to: Manager Links, the Registry Results Reference, and Credential Ownership.

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": { "code": "INVALID_ARGUMENT", "message": "identity.displayName is required", "requestId": "..." } } — the same envelope every error uses. See Errors, Retries & Idempotency.

workerType is free text, status is not

employment.status is normalised to upper case on the way in, so active and ACTIVE are the same value. employment.workerType isn't — it's stored and displayed exactly as you send it. Pick one casing and stick to it across your integration and your spreadsheet imports, or the same workforce ends up filterable under both Employee and EMPLOYEE. These examples use Employee, matching the spreadsheet column reference.

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.

employment.manager works the same way but has rules of its own — pending links when the manager hasn't synced yet, and an HRIS feed that overwrites what you set. See Manager Links.

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": { "id": "wkr_V1StGXR8Z5jdHi6B", "externalId": "EMP-00412" }
}
},
"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 with error.code INVALID_ARGUMENT, e.g. "message": "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": "..."
}

Underneath the standardised verdict, attributes.verification.registryDetails carries what the register itself said — holder name, card type, expiry, and a registryFlags map of credential-type-specific detail. See Registry Results Reference.

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. Change who owns a credential​

owners is immutable on PATCH/PUT — attempting it returns 400. Ownership moves through dedicated link / unlink / transfer endpoints, singly or in bulk; the applicant-to-worker hire flow is a transfer. See Credential Ownership.

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. Deleting a worker also deactivates them (isActive: false), which stops scheduled verification of every credential they hold — a deleted worker generates no further registry scans. References: Soft delete credential · Soft delete worker.

Finding and restoring deleted workers​

List only the soft-deleted workers with deletedOnly=true, then restore one with POST /workers/{id}/restore:

curl -sS "$OHO_BASE/workers?deletedOnly=true" -H "Authorization: Bearer $OHO_TOKEN"
curl -sS -X POST "$OHO_BASE/workers/$WKR/restore" -H "Authorization: Bearer $OHO_TOKEN"

Restore clears the deleted flag and returns the worker envelope; the record reappears in lists and search with every credential and its verification history intact. Restore is idempotent — restoring a worker who is not deleted is a no-op. The worker comes back inactive (the delete paused scanning): PATCH with isActive: true to resume scheduled verification, which picks each credential's own scan settings back up unchanged. Other side effects of the original delete are not undone: fetch requests cancelled by the deletion stay cancelled (their emailed links are permanently dead) and review tasks resolved by it stay resolved, so re-request anything still needed after restoring.

12. Re-emit a lifecycle webhook​

Republish credential.added or credential.updated for an existing credential, built from its current stored state:

curl -sS -X POST "$OHO_BASE/credentials/$CRED/emit" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"eventType": "credential.added", "source": "backfill"}'

Nothing is written and no registry is called — this only publishes the event. Use it when a subscription was created after the credential existed, or when a delivery was lost: credential.added otherwise fires exactly once, on a credential's first properties write, so this is the only way to replay it.

eventType defaults to credential.added and accepts only those two events. credential.verified is rejected — its payload carries a registry block only a live verification produces, so use section 6 instead.

Treat a re-emitted credential.added as an upsert: entityCreatedAt still carries the credential's true original creation time, and request.source echoes whatever you passed, so a backfill is distinguishable from a genuinely new record.

13. Qualifications​

Training a worker has completed — a VET certificate, a degree, a first aid ticket — is a qualification, not a credential. It is a separate entity with its own qlf_ ID and its own endpoint group, and it is a peer of Credential rather than a subtype of it: a credential is something a register can be asked about a person, a qualification is something an issuing organisation attests to. See Qualifications for the concept.

Required on create: identification.qualificationType, identification.category, issuing.issuingOrganisation, and at least one of owners.worker / owners.applicant. As with credentials, owners are immutable after create — PATCH and PUT reject them. displayName is synthesised from the qualification type when omitted, and status defaults to ATTAINED.

curl -sS -X POST "$OHO_BASE/qualifications" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"identification": { "displayName": "Certificate III in Individual Support", "qualificationType": "CHC33021", "nationalCode": "CHC33021", "category": "VET", "nrtType": "Qualification", "aqfLevel": "CERTIFICATE_III" },
"issuing": { "issuingOrganisation": "TAFE NSW", "issuingOrganisationCode": "90003", "issuingCountry": "AU" },
"attainment": { "status": "ATTAINED", "verificationStatus": "DOCUMENT_VERIFIED", "completionDate": "2024-11-30" },
"owners": { "worker": "'"$WKR"'" }
}'
{
"data": {
"id": "qlf_V1StGXR8Z5jdHi6B",
"type": "qualification",
"attributes": {
"identification": {
"displayName": "Certificate III in Individual Support",
"qualificationType": "CHC33021",
"nationalCode": "CHC33021",
"category": "VET",
"nrtType": "Qualification",
"aqfLevel": "CERTIFICATE_III"
},
"issuing": {
"issuingOrganisation": "TAFE NSW",
"issuingOrganisationCode": "90003",
"issuingCountry": "AU"
},
"attainment": {
"status": "ATTAINED",
"verificationStatus": "DOCUMENT_VERIFIED",
"completionDate": "2024-11-30"
},
"owners": { "worker": "wkr_V1StGXR8Z5jdHi6B" },
"isActive": true
},
"createdAt": "2026-06-29T02:31:44Z",
"lastUpdated": "2026-06-29T02:31:44Z"
},
"meta": { "requestId": "..." }
}
export QLF="qlf_V1StGXR8Z5jdHi6B"

Two shape differences from the credential envelope are worth noting: owners here carries plain ID strings rather than hydrated objects, and registerCheck is absent entirely until a check has run — absent means never checked, which is not the same as passing.

Not yet in the generated API Reference

The /qualifications endpoints are live, but the published API Reference is generated from a snapshot that does not carry them yet. The shapes below are the current contract.

Verifying a qualification​

POST /qualifications/{id}/verify is not the credential verify. training.gov.au publishes no per-person attainment lookup, so this does not ask "does this worker hold this?". It asks whether what they hold still stands — is the national code still current, and is the RTO that issued it still registered:

curl -sS -X POST "$OHO_BASE/qualifications/$QLF/verify" -H "Authorization: Bearer $OHO_TOKEN"
{
"data": {
"outcome": "REVIEW_REQUIRED",
"detail": "Superseded by CHC33021",
"checkedAt": "2026-06-29T02:34:02Z"
},
"meta": { "requestId": "..." }
}

outcome is VALID, REVIEW_REQUIRED or NOT_ON_REGISTER, and is stored back onto the qualification. NOT_ON_REGISTER is not a failure — degrees, overseas study and older statements of attainment are legitimately absent from the register. You rarely need to call this yourself: Oho re-checks every qualification after each register refresh, writing back only the ones whose verdict changed.

Listing and filtering​

# Everything this worker holds
curl -sS "$OHO_BASE/qualifications?ownerType=worker&ownerId=$WKR" -H "Authorization: Bearer $OHO_TOKEN"

# One national code, newest-edited first
curl -sS "$OHO_BASE/qualifications?nationalCode=HLTAID011&sort=lastUpdated:desc" -H "Authorization: Bearer $OHO_TOKEN"

# Everything changed since a checkpoint, for an incremental sync
curl -sS "$OHO_BASE/qualifications?updatedAfter=2026-06-01T00:00:00Z" -H "Authorization: Bearer $OHO_TOKEN"

Filters: ownerType + ownerId, qualificationType, nationalCode, category, status, issuingOrganisation, and the updatedAfter / updatedBefore window (updatedAfter inclusive, updatedBefore exclusive). Paging, sorting and includeDeleted follow the same contract as credentials — see Pagination, Filtering & Sorting. PATCH, PUT and DELETE behave as they do for credentials, with DELETE a soft delete.

Which qualificationType?

qualificationType is the code your position requirements are written against, and the national code folded to it — CHC33021 stays CHC33021, a title folds to PROVIDE_FIRST_AID. category is Oho's sector split: VET, HIGHER_EDUCATION, SHORT_COURSE, PROFESSIONAL_DEVELOPMENT. nrtType is separate and is the register's own wording, verbatim — Qualification, Skill set, Accredited course, Unit of competency, Accredited unit/module — which is what separates a unit from the qualification it belongs to, since both are VET.

Where to go next​