Evaluating Compliance
Two API surfaces read as opaque from their endpoint schemas alone: evaluating compliance checks in bulk, and credential equivalence sets. This page explains what each is for and how to drive it.
This is the developer view. If you're not sure what a compliance check or an equivalence set is, start at Compliance rules & position requirements.
Evaluating checks
A compliance check is a rule with a scope (which workers) and a condition tree (what must be true). Evaluation is what runs that rule and records who passed and who didn't. Checks are evaluated on their own schedule; these endpoints are for when you need an answer now — after a bulk import, after changing a rule, or to reconcile against your own system.
There are three ways to evaluate, and choosing wrongly is the main trap.
| Endpoint | Shape | Use when |
|---|---|---|
POST /compliance-checks/{id}/evaluate | Synchronous, one check | A single rule, small scope |
POST /compliance-checks/evaluate | Synchronous, many checks | A handful of rules, small tenant |
POST /compliance-checks/evaluate/jobs | Asynchronous, many checks, returns 202 | Anything large — the safe default |
Both synchronous endpoints run the evaluation inside your request and return only when it's finished. Runtime scales linearly with the number of workers in scope, so on a large workforce these will hold the connection open for a long time. Prefer the async job for anything beyond a small tenant or a narrow scope.
One check, synchronously
curl -sS -X POST "$OHO_BASE/compliance-checks/wwcc-nsw-active/evaluate" \
-H "Authorization: Bearer $OHO_TOKEN"
{
"data": {
"checkId": "wwcc-nsw-active",
"totalInScope": 412,
"passing": 397,
"failing": 14,
"errors": 1,
"evaluatedAt": "2026-09-16T04:11:07Z",
"failingWorkerUrns": ["urn:li:worker:wkr_V1St...", "…"]
},
"meta": { "requestId": "req_8c1…" }
}
errors counts workers the rule could not be evaluated against — a missing field, a malformed
value — as distinct from workers who were evaluated and failed. A rule reporting mostly errors is
usually misconfigured rather than revealing a compliance problem.
Many checks, asynchronously
Submit the job, then poll it. With no body, every enabled check is evaluated; ids and
severity narrow it, and AND together when both are supplied.
# Submit
curl -sS -X POST "$OHO_BASE/compliance-checks/evaluate/jobs" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"severity": "CRITICAL"}'
{
"data": {
"jobId": "3f2a9c14-...",
"status": "PENDING",
"targetCount": 6,
"submittedAt": "2026-09-16T04:11:07Z"
}
}
The 202 also carries a Location header pointing at the poll URL. Targets are validated and
authorized synchronously at submit time, so a 400 or 403 comes back immediately rather than
surfacing later as a failed job.
# Poll
curl -sS "$OHO_BASE/compliance-checks/evaluate/jobs/$JOB_ID" \
-H "Authorization: Bearer $OHO_TOKEN"
status moves PENDING → RUNNING → SUCCEEDED (or FAILED). results — one summary per check —
is populated once the job succeeds; error carries the reason when it fails.
# Poll until terminal, then print the failures
while :; do
body=$(curl -sS "$OHO_BASE/compliance-checks/evaluate/jobs/$JOB_ID" \
-H "Authorization: Bearer $OHO_TOKEN")
status=$(echo "$body" | jq -r '.data.status')
case "$status" in
SUCCEEDED) echo "$body" | jq -r '.data.results[] | "\(.checkId): \(.failing) failing"'; break ;;
FAILED) echo "$body" | jq -r '.data.error'; break ;;
*) sleep 5 ;;
esac
done
Two things to know about jobs:
- Jobs are private to their submitter. Polling a job someone else submitted returns
404, not403. If you submit from one service identity and poll from another, you'll never see it. - The backlog can be full. Submission answers
503with aRetry-Afterheader when the evaluation queue is saturated. Honour the header rather than retrying in a tight loop.
Reading results without re-evaluating
You rarely need to evaluate just to read the current picture. These are cheap and don't re-run anything:
| Endpoint | Returns |
|---|---|
GET /compliance-checks/{id}/results | The latest summary (empty if never evaluated) |
GET /compliance-checks/{id}/failing-workers | Paginated failing workers, hydrated from that summary |
GET /compliance-checks/{id}/runs | Per-worker run history — PASS/FAIL/ERROR/SKIPPED |
/runs is a timeseries: newest first, bounded with startTimeMillis / endTimeMillis, limit
between 1 and 1000 (default 100). It carries the per-condition breakdown and any actions applied,
which is what you want when the question is "why did this worker fail?" rather than "how many
failed?".
Credential equivalence sets
An equivalence set is a named group of credential types that are interchangeable for a requirement. "Right to Work" accepting a passport, a visa, or a citizenship certificate is the canonical example: hold any one member and the requirement is satisfied.
Without sets you'd have to write the same requirement three times, once per acceptable credential, and keep all three in step. With a set, a position requirement names the set and Oho accepts any member.
Create a set
curl -sS -X POST "$OHO_BASE/credential-equivalence-sets" \
-H "Authorization: Bearer $OHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Right to Work",
"label": "Right to Work evidence",
"members": ["PASSPORT", "VISA", "CITIZENSHIP"],
"guidingText": "Any one of these satisfies the right-to-work requirement."
}'
{
"data": {
"id": "right-to-work-a1b2c3",
"type": "credentialEquivalenceSet",
"attributes": {
"name": "Right to Work",
"members": ["PASSPORT", "VISA", "CITIZENSHIP"],
"active": true,
"audit": {
"createdAt": "2026-09-16T04:11:07Z",
"createdBy": "urn:li:corpuser:…"
}
}
}
}
The set's code — its entity id — is derived from the name plus a short random suffix, and is
immutable. A code in a POST body is ignored; on PATCH/PUT the path is the source of
truth and a differing body code is rejected. Read the generated code from the response or the
Location header and store it; don't try to predict it from the name.
members are credential type codes — fetch the catalogue from
List supported credential types rather than hard-coding them.
members must be non-empty whenever it's supplied.
Update, retire, delete
| Action | Call | Effect |
|---|---|---|
| Change some fields | PATCH /credential-equivalence-sets/{code} | Only fields in the body change |
| Replace wholesale | PUT /credential-equivalence-sets/{code} | Absent fields are cleared (audit fields are preserved) |
| Stop it being used | POST /credential-equivalence-sets/{code}/deactivate | Sets active: false; reversible with /activate |
| Remove it | DELETE /credential-equivalence-sets/{code} | Soft delete — the record is preserved |
Prefer deactivating over deleting a set that requirements still point at: deactivating is reversible and legible, and the set stays readable in historical records either way.
Deleted sets are hidden from GET /credential-equivalence-sets unless you pass
includeDeleted=true. Listing is additionally filtered per-set by authorization, so a caller only
sees the sets they may read — an empty list can mean "none visible to you", not "none exist".
Where to go next
- Compliance rules & position requirements — the concept behind both APIs
- API Reference: Compliance Checks — every endpoint and field
- API Reference: Credential Equivalence Sets
- API Reference: Job Requirements — attaching requirements to positions
- Errors, Retries & Idempotency — handling
503andRetry-After