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.
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": { "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 notemployment.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"
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 →
400witherror.codeINVALID_ARGUMENT, e.g."message": "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": "..."
}
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.
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.
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
- Credential Ownership — move a credential from applicant to worker
- Registry Results Reference — what the register itself reported
- Manager Links — reporting lines, pending links, and HRIS precedence
- 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