Skip to main content

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.

Learn the concept first

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.

EndpointShapeUse when
POST /compliance-checks/{id}/evaluateSynchronous, one checkA single rule, small scope
POST /compliance-checks/evaluateSynchronous, many checksA handful of rules, small tenant
POST /compliance-checks/evaluate/jobsAsynchronous, many checks, returns 202Anything large — the safe default
Synchronous evaluation blocks

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, not 403. If you submit from one service identity and poll from another, you'll never see it.
  • The backlog can be full. Submission answers 503 with a Retry-After header 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:

EndpointReturns
GET /compliance-checks/{id}/resultsThe latest summary (empty if never evaluated)
GET /compliance-checks/{id}/failing-workersPaginated failing workers, hydrated from that summary
GET /compliance-checks/{id}/runsPer-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 code is server-generated

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

ActionCallEffect
Change some fieldsPATCH /credential-equivalence-sets/{code}Only fields in the body change
Replace wholesalePUT /credential-equivalence-sets/{code}Absent fields are cleared (audit fields are preserved)
Stop it being usedPOST /credential-equivalence-sets/{code}/deactivateSets active: false; reversible with /activate
Remove itDELETE /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