OAuth Integrations
The /oauth/* endpoints are a different mechanism from your API token. They don't authenticate
you against the Oho API — they help Oho obtain and store credentials for third-party
providers on your behalf. They run in the operator's authenticated browser session, so they
carry no API token or signature of their own (security: [] in the spec). You will rarely
call them directly; the Oho app drives them during integration setup. They're documented here so
the flow isn't a black box.
This page covers the server-side proxy endpoints that broker third-party integrations. For the
bearer token you send on every API request — scopes, 401 vs 403, and rotation — see
Authentication & Tokens.
There are two flows today, defined in api/v1/oauth.openapi.yaml.
JobAdder token exchange
POST /oauth/jobadder/token is a CORS-safe, server-to-server proxy for the OAuth
authorization_code exchange against https://id.jobadder.com/connect/token. The browser holds
the one-time authorization code but never the client secret — it sends only a
clientSecretRef, the name of an encrypted secret stored server-side (created in
Settings → Secrets), which Oho resolves and decrypts before calling JobAdder. The 200
response is a verbatim passthrough of JobAdder's token JSON.
curl -sS -X POST "$OHO_BASE/oauth/jobadder/token" \
-H "Content-Type: application/json" \
-d '{
"code": "<one-time-authorization-code>",
"clientId": "<jobadder-client-id>",
"clientSecretRef": "jobadder-client-secret",
"redirectUri": "https://<tenant>.weareoho.com/oauth/jobadder/callback"
}'
{
"access_token": "...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 3600
}
redirectUri must match the one used in the authorize step exactly. Replace <tenant> with your
organisation's subdomain — the Oho app is served per-tenant at <tenant>.weareoho.com, and the
callback path the app registers is /oauth/jobadder/callback. Notable failure modes:
| Status | Meaning |
|---|---|
400 | clientSecretRef couldn't be resolved or decrypted |
504 | JobAdder didn't respond within 15 seconds |
500 | Unexpected internal error during the exchange |
default | JobAdder rejected the exchange (e.g. 401/403); its status is passed through, with the body wrapped in Oho's error envelope |
NCC police-check consent flow
The NCC (police-check provider) flow is a browser-redirect consent handshake across two endpoints, scoped to one organisation:
GET /oauth/ncc/authorize?organizationUrn=... mints a CSRF state, builds the NCC consent URL
(response_type=code, client_id, redirect_uri, scope, state), and returns a 302
redirect to it. The operator approves consent at NCC. If the deployment's internal NCC app or
authorize URL isn't configured, it returns 503 instead.
curl -sS -i "$OHO_BASE/oauth/ncc/authorize?organizationUrn=urn:li:organisation:org_V1StGXR8Z5jdHi6B"
# 302 Location: https://<ncc-consent-url>?response_type=code&...
GET /oauth/ncc/callback is the redirect target NCC sends the operator back to. It validates the
state, exchanges the code for tokens, and persists the refresh token as an encrypted
per-tenant secret, updating the organisation's police-check settings to point at it. This is the
provider credential Oho rotates against silently from then on, so the operator never handles the
NCC token themselves.
GET /oauth/ncc/callback?code=<auth-code>&state=<csrf-state>
# or, on denial:
GET /oauth/ncc/callback?error=access_denied&error_description=<msg>
The callback always returns 200 with a small self-closing HTML page that postMessages the
result back to the window that opened it:
// posted to window.opener — the message always carries type, ok, and error
{ type: "policecheck.ncc.oauth", ok: true, error: "" }
// every failure mode — consent denied, missing/expired state, expired session,
// exchange or persistence failure — also returns 200, with ok: false and a message
{ type: "policecheck.ncc.oauth", ok: false, error: "<msg>" }
Because there is no non-200 path, the opener window must branch on the ok field rather than on
an HTTP status.
The JobAdder and NCC controllers are annotated @Hidden, so they do not appear in the live
springdoc Swagger UI or the API Reference. api/v1/oauth.openapi.yaml is the
hand-maintained contract for them — treat it as the source of truth.
Where to go next
- Authentication & Tokens — the bearer token, scopes,
401vs403, and rotation - API Basics — base URLs, identifiers, response envelope, and error shapes
- API Reference — every endpoint, field, and query parameter