API Basics
Everything you need before making your first call to the Oho REST API: where to send requests, how to authenticate, and the shape of what comes back. These conventions apply to every endpoint, so the Workers & Credentials tutorial and the per-endpoint API Reference assume you've read this page.
Base URL
The API is served per tenant:
https://<tenant>.weareoho.com/openapi/v1
Replace <tenant> with your Oho tenant subdomain — tenant demo is reached at
https://demo.weareoho.com/openapi/v1.
The examples throughout these guides use an $OHO_BASE variable so your tenant is set in one
place:
export OHO_BASE="https://<tenant>.weareoho.com/openapi/v1"
export OHO_TOKEN="<your-access-token>"
Authentication
Every request carries a bearer token in the Authorization header:
curl -sS "$OHO_BASE/workers" \
-H "Authorization: Bearer $OHO_TOKEN"
Generate a token from Settings → Access Tokens in the Oho app. A token carries the entity
permissions of the user who created it — it has no separately configured permission set of its
own. So calls to /workers require that user to have worker access, and calls to
/credentials require credential access.
Treat your token like a password. Don't commit it to source control or paste it into shared docs — set it as an environment variable as shown above.
For the full picture — how entity types and operations combine, the 401 vs 403 distinction,
safe rotation, and the server-side oauth integration endpoints — see the
Authentication & Tokens deep-dive.
Identifiers
Resources are addressed by a synthetic, prefixed ID generated by the server at create time:
| Entity | ID shape | Example |
|---|---|---|
| Worker | wkr_<id> | wkr_V1StGXR8Z5jdHi6B |
| Credential | cred_<id> | cred_8x2Kf9aQ4mN7pLrT |
| Applicant | app_<id> | app_Qz7yT2mB |
| Attachment | att_<id> | att_V1StGXR8Z5jdHi6B |
| Fetch request | cap_<id> | cap_V1StGXR8Z5jdHi6B |
| Recruitment check | chk_<id> | chk_V1StGXR8Z5jdHi6B |
| Qualification | qlf_<id> | qlf_V1StGXR8Z5jdHi6B |
| Exemption | exm_<id> | exm_V1StGXR8Z5jdHi6B |
| Webhook | whk_<id> | whk_V1StGXR8Z5jdHi6B |
You never supply these — the server mints them and returns them in the create response and the
Location header. Internal resource URNs are never used to address resources through the API.
One exception: authorization (403) error messages include the caller's own account URN (see
Authentication & Tokens). To find a worker by your own upstream HRIS
identifier instead, use GET /workers?externalId=....
Response envelope
Single-resource reads and writes return a data object with a meta companion:
{
"data": {
"id": "wkr_V1StGXR8Z5jdHi6B",
"type": "worker",
"attributes": { "...": "..." }
},
"meta": { "requestId": "f3c1a0e2-..." }
}
List endpoints return a data array and paging metadata:
{
"data": [{ "id": "wkr_V1StGXR8Z5jdHi6B", "type": "worker", "...": "..." }],
"meta": { "page": 1, "pageSize": 25, "total": 1, "requestId": "..." }
}
pageSize defaults to 25 and is clamped to a maximum of 100.
Soft deletes
Most DELETEs are soft — they set deleted: true and preserve the record and its history.
Soft-deleted resources are excluded from lists unless you pass includeDeleted=true, or
deletedOnly=true to list nothing else (see
Pagination, filtering & sorting).
Workers and applicants can be brought back:
| Endpoint | What it does |
|---|---|
POST /workers/{workerId}/restore | Clears the flag; the worker returns inactive, aspects intact. |
POST /applicants/{applicantId}/restore | Clears the flag; the applicant returns with its history intact. |
Both are idempotent — restoring something that is not deleted is a no-op — and both need the
same permission as DELETE. Restore undoes the tombstone, not the delete's side effects:
cancelled fetch requests and resolved review tasks stay as they are, and a recruitment check
cancelled with an applicant cannot be revived (its emailed link is permanently dead). Re-request
whatever is still needed afterwards, and PATCH a restored worker with isActive: true to
resume scheduled verification.
The exception is credential attachments. DELETE /credentials/{id}/attachments/{attachmentId}
removes the file permanently and cannot be undone — there is nothing to restore and
includeDeleted=true will not bring it back. See
Bulk Import & Attachments before you call it.
Request tracing
Every response carries a server-generated meta.requestId, echoed in the X-Request-Id response
header. Quote it in support tickets — it's how Oho finds your call in its logs. If you send your
own X-Request-Id (a UUID or 26-character ULID), the server keeps its own id as canonical and
echoes yours back in a separate X-Client-Request-Id header so you can line the two systems up.
Errors
Every error — on every resource endpoint, at every status — returns the same envelope: a single
error object holding a code, a message, and the requestId of the failed call. There is
no second shape: error is never a bare string.
{
"error": {
"code": "ALREADY_LINKED",
"message": "credential is already linked to applicant app_Qz7yT2mB; use /transfer to replace",
"requestId": "01JD8Y2Q4K7N3M0P5R8T9V2W6X"
}
}
codeis stable and machine-readable — branch on it, together with the HTTP status.messageis human-readable detail and may be reworded between releases. Show it; don't parse it.requestIdis the same id asmeta.requestIdand theX-Request-Idheader. Log it, and quote it in support tickets.
| Status | Meaning |
|---|---|
200 | OK |
201 | Created (create endpoints; includes a Location header) |
204 | No Content (successful delete) |
400 | Validation error or unparseable filter/path value |
401 | Missing or invalid token |
403 | Authenticated, but not authorized for that entity type |
404 | Resource not found |
409 | Conflict with stored state (code: ALREADY_LINKED, OWNER_MISMATCH, DUPLICATE_ID) |
429 | Rate limited — sets a Retry-After header (see Rate limiting) |
Two things sit outside this envelope: the OAuth2 token endpoint, which returns the
{ "error": ..., "error_description": ... } shape the OAuth2 spec mandates (see
Authentication), and 401, which the authentication layer raises before
the request reaches the handler that builds the envelope.
For the full picture — conflict recovery, safe retries, idempotency, and matching async webhook deliveries back to a request — see Errors, Retries & Idempotency.
Rate limiting
Every response from /openapi/** carries the current state of your rate-limit budget, so you can
throttle before you hit the ceiling rather than reacting to a 429:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests per minute allowed for this caller on this tier. |
X-RateLimit-Remaining | Requests left in the current budget. |
X-RateLimit-Reset | Seconds (not a timestamp) until the budget is back to X-RateLimit-Limit. |
All three are present on every response, success or failure — not just on a 429. On a 429
the response also carries Retry-After, in seconds; at that point X-RateLimit-Remaining is 0
and X-RateLimit-Reset counts down to the next single request rather than to a full budget.
There are two tiers, and X-RateLimit-Limit tells you which one you're on:
- Standard — 3000 requests/minute. A ceiling on runaway clients, not a commercial quota; a normal integration should never see it.
- Fan-out — 10 requests/minute on
POST /webhooks/{id}/pingandPOST /webhooks/{id}/redrive. These endpoints spend someone else's budget — a ping makes a live outbound call to your endpoint, a redrive replays up to 1000 deliveries — so they're metered far more tightly.
Budgets are counted per caller: by actor when you're authenticated, by client IP when you're not.
The counters live in memory on each API pod, so a deployment running several replicas can allow somewhat more than these numbers in aggregate. Treat the limits as a safety net you shouldn't approach, not a precise meter to run against.
Retry guidance for a 429 — backoff, jitter, and which calls are safe to repeat — is in
Errors, Retries & Idempotency.
Try it live
Each endpoint in the API Reference has a built-in "Try it" panel, and a
live Swagger UI is served at /openapi/swagger-ui/index.html (group openapi-v1).
Ready? Walk through a complete flow in the Workers & Credentials tutorial.