# Talent-Ray API — Full Documentation OpenAPI 3.1 spec: https://www.talent-ray.com/docs/api/openapi.json This file concatenates every Talent-Ray API documentation page as Markdown. --- # Talent-Ray API Programmatic access to the Talent-Ray hiring platform for backend systems and AI agents. The Talent-Ray API lets backend systems and AI agents authenticate with an **API key** and call platform endpoints over HTTPS. It powers integrations such as syncing hires into an HRIS, auditing key usage, and automating recruiting workflows. ## Base URL The base URL is **your Talent-Ray instance**: ``` https://app.talent-ray.com ``` `app.talent-ray.com` is the **shared platform**. Customers with a **dedicated deployment** call their own subdomain instead — for example `https://acme.talent-ray.com`. The API paths and contracts are identical on every instance; only the host changes. If you sign in at `app.talent-ray.com`, use that host. If your team has a dedicated Talent-Ray instance, use the same subdomain you sign in with. The examples in these docs use `app` — replace it with your subdomain if you have a dedicated deployment. ## How it works 1. An administrator mints an **API key** for a specific user (see [Authentication](/docs/api/authentication/)). 2. You send that key on every request via the `Authorization: Bearer` header. 3. The key acts as its owner — it has exactly that user's role and organization access. An API key can never exceed the permissions of the user it was minted for — that's its ceiling. A key may *additionally* be granted **per-key scopes** (e.g. `candidates:read`) that gate the versioned `/api/v1/*` endpoints. A scope only narrows access; it never grants anything the owner lacks. See [Authentication](/docs/api/authentication/#scopes). ## For AI agents This documentation is built to be consumed by AI agents: - **OpenAPI 3.1 spec** (every documented endpoint — API keys, career portal, and the full `/api/v1` Hiring Pipeline surface): [`/docs/api/openapi.json`](/docs/api/openapi.json). Each operation lists its required scope (in its description and an `x-required-scopes` extension). - **Machine index:** [`/llms.txt`](/llms.txt) — a curated map of every page with behavioral instructions - **Full docs in one file:** [`/llms-full.txt`](/llms-full.txt) - **Any page as Markdown:** append `.md` to its URL (e.g. `/docs/api/authentication.md`) ## Endpoint groups - **[Endpoints](/docs/api/endpoints/create-api-key/)** — API-key management, the public career portal, and the `/api/v1/me` identity check. Stable. - **[Hiring Pipeline](/docs/api/pipeline/candidates-list/)** — the stable, scoped `/api/v1` surface: candidates, roles, tests, sourcing, pipeline steps, and CV-screening batches. Each endpoint requires a specific scope (e.g. `candidates:read`) and returns a curated, versioned shape. ## Next steps - [Authentication](/docs/api/authentication/) — how API keys work and how to send them - [Conventions](/docs/api/conventions/) — base URL, rate limits, pagination, timestamps - [Errors](/docs/api/errors/) — error format and status codes --- # Authentication Authenticate every request with a Talent-Ray API key. Every API request is authenticated with an **API key**. Keys are issued by an administrator on behalf of a user, and a key carries exactly that user's role and organization memberships. ## Key format A Talent-Ray API key: - starts with the prefix `tr_` - is **67 characters** long in total - is shown **once**, at creation — it is stored hashed and can never be retrieved again ``` tr_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ``` The full key is returned only in the response that creates it. If you lose it, revoke the key and mint a new one — there is no way to recover the original value. ## Sending the key Send the key on every request using the `Authorization` header with the `Bearer` scheme (preferred): ```bash curl https://app.talent-ray.com/api/admin/api-keys \ -H "Authorization: Bearer tr_YOUR_KEY_HERE" ``` The `x-api-key` header is accepted as a fallback: ```bash curl https://app.talent-ray.com/api/admin/api-keys \ -H "x-api-key: tr_YOUR_KEY_HERE" ``` Do not pass the key as a query parameter. Keys must only travel in a request header. ## Permissions A key's access equals its owner's role: | Owner role | Can access | | --- | --- | | `admin` | Platform administration, including API key management | | Employer / org owner | Their organization's data | | Hiring manager | Only roles assigned to them | This is the key's **ceiling** — the most it can ever do. ## Scopes For the versioned `/api/v1/*` endpoints, a key also carries **per-key scopes** — a least-privilege gate *on top of* the owner's permissions. A scope only ever narrows access; it never grants anything the owner can't already do. - Scopes are named `resource:action`, e.g. `candidates:read`, `roles:write`. - A key minted with **no scopes** authenticates but cannot call any scope-gated `/api/v1` endpoint. - Calling an endpoint without its required scope returns `403` with `error: "insufficient_scope"` and `requiredScopes` / `grantedScopes` arrays so you can see exactly what's missing. | Scope | Grants | | --- | --- | | `candidates:read` / `candidates:write` | Read / update candidates | | `roles:read` / `roles:write` | Read / update roles (jobs) | | `tests:read` / `tests:write` | Read / update tests | | `sourcing:read` / `sourcing:write` | Read / create / update / delete potential candidates | | `pipeline:read` | Read role step templates + candidate step progress | | `cv-screening:read` | Read CV-screening batch status + results | Scopes are chosen when the key is minted. The legacy endpoints outside `/api/v1` are not scope-gated — they are governed by the owner's role only. ## Expiry Keys expire after a configurable period (default **90 days**, range 1–365). An expired key is rejected and must be replaced. ## Getting a key API keys are minted by an administrator (there is no self-serve flow yet). To request one, [contact us](https://www.talent-ray.com/contact/) with the integration you are building and the access it needs. --- # Conventions Base URL, rate limits, pagination, and timestamp formats shared across all endpoints. These rules apply across the entire API. Resolve them once and reuse them on every endpoint. ## Base URL & transport All requests go to your Talent-Ray instance over HTTPS. Request and response bodies are JSON (`Content-Type: application/json`). - **Shared platform:** `https://app.talent-ray.com` - **Dedicated deployment:** your own subdomain, e.g. `https://acme.talent-ray.com` Paths and contracts are identical across instances — only the host changes. The examples in these docs use `app`. ## Rate limits Each key is limited to **600 requests per minute** by default. Exceeding the limit returns `429 Too Many Requests`. ``` HTTP/1.1 429 Too Many Requests Retry-After: 30 ``` On a `429`, wait the number of seconds in the `Retry-After` header before retrying, and back off on repeated limits. ## Pagination There are two pagination styles, depending on the endpoint family. **Offset pagination (`/api/v1` lists).** Pass `page` (0-indexed) and `pageSize` (max 100, default 20). The response wraps results in a `{ data, pagination }` envelope: ```json { "data": [ "...rows..." ], "pagination": { "page": 0, "pageSize": 20, "totalCount": 134, "totalPages": 7 } } ``` Request `page=1`, `page=2`, … up to `totalPages - 1`. Some small, naturally-ordered `/api/v1` sub-lists (a role's steps, a candidate's steps) are returned in full as `{ data: [...] }` with no `pagination` object. **Cursor pagination (API-key usage log).** Ordered newest-first. Pass `limit` for page size and `before` (an ISO 8601 timestamp) to fetch older rows: ```json { "pagination": { "limit": 50, "hasMore": true, "nextBefore": "2026-06-04T15:20:00Z" } } ``` To get the next page, pass `nextBefore` as the `before` parameter. When `hasMore` is `false`, you have reached the end. ## Timestamps All timestamps are **ISO 8601 in UTC** (e.g. `2026-06-04T15:30:45Z`). ## Scopes The versioned `/api/v1/*` endpoints are gated by **per-key scopes** in addition to the key owner's role. Each endpoint documents the scope it needs (e.g. `candidates:read`); a missing scope returns `403 insufficient_scope`. See [Authentication → Scopes](/docs/api/authentication/#scopes). ## Versioning The stable surface lives under **`/api/v1/*`** and changes **additively only** — new fields and endpoints may be added, but existing ones keep their contract. Breaking changes would ship under a new version namespace (`/api/v2`). The legacy unversioned routes are not a stable contract. The [OpenAPI spec](/docs/api/openapi.json) carries the current API version in its `info.version` field. --- # Create an API key `POST /api/admin/api-keys` — Mint a new API key on behalf of a user — for example, to set up a SAP or HRIS integration. Creates a new API key for a user. The plaintext key is returned **exactly once** in this response — store it securely. The key inherits the target user's role and organization memberships. ## Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | Yes | Human-readable label (max 255 chars). | | `userId` | string | Yes | ID of the user the key acts as. Must exist. | | `expiresInDays` | integer | No | Days until expiry. Default `90`, range 1–365. | ## Example request ```bash curl -X POST https://app.talent-ray.com/api/admin/api-keys \ -H "Authorization: Bearer tr_YOUR_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Karaca SAP nightly sync", "userId": "user_abc123", "expiresInDays": 30 }' ``` ## Response `200 OK` — the `key` field is shown only here and never again. ```json { "success": true, "data": { "id": "apikey_xyz789", "name": "Karaca SAP nightly sync", "key": "tr_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "prefix": "tr_", "start": "tr_aaaa", "expiresAt": "2026-09-03T12:00:00Z", "userId": "user_abc123" } } ``` | Field | Type | Description | | --- | --- | --- | | `id` | string | The key's ID — use it to revoke or audit the key. | | `key` | string | The plaintext key. Shown once. | | `prefix` | string | Always `tr_`. | | `start` | string | First characters, for display. | | `expiresAt` | string | ISO 8601 expiry timestamp. | | `userId` | string | The owner the key acts as. | ## Status codes | Status | Meaning | | --- | --- | | `200` | Key created. | | `400` | Validation error (missing `name`/`userId`, bad `expiresInDays`). | | `401` | No valid API key. | | `403` | Caller is not an admin. | | `404` | `userId` not found. | | `429` | Rate limited. | --- # List API keys `GET /api/admin/api-keys` — Retrieve all API keys with their owner and usage metadata. Returns every API key with owner and usage metadata. The full secret is never returned after creation — only the `start` prefix is shown for display. ## Example request ```bash curl https://app.talent-ray.com/api/admin/api-keys \ -H "Authorization: Bearer tr_YOUR_ADMIN_KEY" ``` ## Response `200 OK` ```json { "success": true, "data": [ { "id": "apikey_xyz789", "name": "Karaca SAP nightly sync", "prefix": "tr_", "start": "tr_aaaa", "enabled": true, "createdAt": "2026-06-04T10:00:00Z", "updatedAt": "2026-06-04T10:00:00Z", "lastRequest": "2026-06-04T15:30:45Z", "expiresAt": "2026-09-03T12:00:00Z", "requestCount": 1250, "owner": { "id": "user_abc123", "email": "admin@company.com", "name": "Admin User", "platformRole": "admin", "orgRole": null, "organizationId": null } } ] } ``` | Field | Type | Description | | --- | --- | --- | | `id` | string | Key ID. | | `name` | string \| null | Label set at creation. | | `start` | string | First characters of the key, for display. | | `enabled` | boolean | Whether the key is active. | | `lastRequest` | string \| null | ISO 8601 timestamp of last use. | | `expiresAt` | string \| null | ISO 8601 expiry. | | `requestCount` | integer | Lifetime request count. | | `owner` | object | The user the key acts as. | ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | Caller is not an admin. | | `429` | Rate limited. | --- # Get API key usage `GET /api/admin/api-keys/{id}/usage` — Read a cursor-paginated forensic log of requests made with a key — useful for audits. Returns a cursor-paginated log of requests made with a key, newest first. Use it to answer "what did this key touch?" during an audit or incident. ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The API key ID. | ## Query parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | No | Rows per page. Default `100`, range 1–500. | | `before` | string | No | ISO 8601 cursor — return rows older than this timestamp. | ## Example request ```bash curl "https://app.talent-ray.com/api/admin/api-keys/apikey_xyz789/usage?limit=50" \ -H "Authorization: Bearer tr_YOUR_ADMIN_KEY" ``` ## Response `200 OK` ```json { "success": true, "data": { "key": { "id": "apikey_xyz789", "name": "Karaca SAP nightly sync", "createdAt": "2026-06-04T10:00:00Z", "lastRequest": "2026-06-04T15:30:45Z", "requestCount": 1250, "owner": { "id": "user_abc123", "email": "admin@company.com", "name": "Admin User" } }, "rows": [ { "id": "usage_001", "timestamp": "2026-06-04T15:30:45Z", "method": "POST", "path": "/api/admin/api-keys", "ip": "192.0.2.100", "userAgent": "curl/7.68.0", "authEndpoint": "/get-session" } ], "pagination": { "limit": 50, "hasMore": true, "nextBefore": "2026-06-04T15:20:00Z" } } } ``` To fetch the next page, pass `pagination.nextBefore` as the `before` query parameter. When `hasMore` is `false`, there are no more rows. ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | Caller is not an admin. | | `404` | Key not found. | | `429` | Rate limited. | --- # Revoke an API key `DELETE /api/admin/api-keys/{id}` — Permanently revoke a key so it can no longer authenticate requests. Permanently revokes a key. Any request using it afterward is rejected. Use this when a key is compromised or no longer needed. ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The API key ID to revoke. | ## Example request ```bash curl -X DELETE https://app.talent-ray.com/api/admin/api-keys/apikey_xyz789 \ -H "Authorization: Bearer tr_YOUR_ADMIN_KEY" ``` ## Response `200 OK` ```json { "success": true } ``` ## Status codes | Status | Meaning | | --- | --- | | `200` | Key revoked. | | `401` | No valid API key. | | `403` | Caller is not an admin. | | `404` | Key not found. | | `429` | Rate limited. | --- # Get a career portal `GET /api/public/portal/{orgSlug}` — List an organization's public career portal — branding and all open roles. No authentication. Returns an organization's public career portal: its branding/theme and every open, public role. This endpoint is **public** — no API key is required. Each role may include `salaryMin`, `salaryMax`, `salaryCurrency`, and `salaryPeriod`. These are only present when the organization enables salary display (`org.portalTheme.showSalary` is `true`); otherwise they are omitted. ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `orgSlug` | string | The organization's portal slug (e.g. `acme`). | ## Example request ```bash curl https://app.talent-ray.com/api/public/portal/acme ``` ## Response `200 OK` ```json { "org": { "name": "Acme Inc.", "logo": "https://.../logo.png", "slug": "acme", "domain": "acme.com", "portalTheme": { "primaryColor": "#4F1AD6", "showSalary": true } }, "roles": [ { "id": "clx123abc", "name": "Senior Backend Engineer", "description": { }, "department": "Engineering", "location": "Remote", "workType": "remote", "collarType": "white", "salaryMin": 90000, "salaryMax": 120000, "salaryCurrency": "EUR", "salaryPeriod": "year", "createdAt": "2026-06-01T09:00:00Z" } ] } ``` | Field | Type | Description | | --- | --- | --- | | `org` | object | Portal branding and metadata. | | `org.portalTheme.showSalary` | boolean | Whether salary fields are included on roles. | | `roles` | array | All open, public roles for the org. | | `roles[].workType` | string \| null | `remote`, `hybrid`, or `onsite`. | | `roles[].collarType` | string \| null | `white`, `gray`, or `blue`. | ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `404` | Portal not found or not enabled. | | `500` | Server error. | --- # Get a public job `GET /api/public/portal/{orgSlug}/roles/{roleId}` — Fetch a single open, public role with its organization metadata. No authentication. Returns a single public, open role together with its organization metadata. This endpoint is **public** — no API key is required. The role must be public and open; otherwise a `404` is returned. ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `orgSlug` | string | The organization's portal slug. | | `roleId` | string | The role ID. | ## Example request ```bash curl https://app.talent-ray.com/api/public/portal/acme/roles/clx123abc ``` ## Response `200 OK` ```json { "org": { "name": "Acme Inc.", "logo": "https://.../logo.png", "slug": "acme", "domain": "acme.com", "portalTheme": { "showSalary": true } }, "role": { "id": "clx123abc", "name": "Senior Backend Engineer", "description": { }, "department": "Engineering", "location": "Remote", "workType": "remote", "collarType": "white", "salaryMin": 90000, "salaryMax": 120000, "salaryCurrency": "EUR", "salaryPeriod": "year", "createdAt": "2026-06-01T09:00:00Z" } } ``` Salary fields are omitted unless `org.portalTheme.showSalary` is `true` (see [Get a career portal](/docs/api/endpoints/get-career-portal/)). ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `404` | Portal not enabled, or job not found / no longer available. | | `500` | Server error. | --- # Apply to a public job `POST /api/public/portal/{orgSlug}/apply` — Generate a candidate signup invite link for a public role. No authentication. Generates (or reuses) a candidate **signup invitation link** for a public, open role and returns its URL. This endpoint is **public** — no API key is required. Redirect the candidate to the returned `inviteUrl` to start their application. ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `orgSlug` | string | The organization's portal slug. | ## Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `roleId` | string | Yes | The public role to apply for. | | `locale` | string | No | Language for the signup link. Default `en`. | ## Example request ```bash curl -X POST https://app.talent-ray.com/api/public/portal/acme/apply \ -H "Content-Type: application/json" \ -d '{ "roleId": "clx123abc", "locale": "en" }' ``` ## Response `200 OK` ```json { "inviteUrl": "https://app.talent-ray.com/en/signup/invite/inv_abc123" } ``` | Field | Type | Description | | --- | --- | --- | | `inviteUrl` | string | Full signup URL to share with or redirect the candidate to. | ## Status codes | Status | Meaning | | --- | --- | | `200` | Invite link returned. | | `400` | `roleId` is missing. | | `404` | Portal not enabled, or job not found / not accepting applications. | | `500` | Server error. | --- # Get the authenticated principal `GET /api/v1/me` — Resolve the current user and, for API-key callers, the key id and its granted scopes. Returns the authenticated principal. For an API-key caller it also reports the key id and the scopes granted to it — the quickest way to verify a key's auth and scope setup. Works for both API-key and signed-in (session) callers. ## Example request ```bash curl https://app.talent-ray.com/api/v1/me \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `200 OK` ```json { "user": { "id": "user_abc123", "email": "integrations@acme.com", "role": "user" }, "auth": { "type": "api_key", "keyId": "apikey_xyz789", "scopes": ["candidates:read", "roles:read"] } } ``` For a session (cookie) caller, `auth.type` is `"session"` and there is no `keyId`/`scopes`. ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid credentials. | | `429` | Rate limit exceeded. | --- # List candidates `GET /api/v1/candidates` — Page through candidates in a stable, curated shape. Scope: candidates:read. Lists candidates your key can see, in the stable v1 shape. **Scope:** `candidates:read`. Visibility is organization-membership based — admins see all; employer-write members see their org's candidates; hiring managers see candidates in roles assigned to them. ## Query parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `page` | integer | No | 0-indexed page. Default `0`. | | `pageSize` | integer | No | Rows per page (max 100). Default `20`. | | `roleId` | string | No | Filter to candidates actively assigned to this role. | ## Example request ```bash curl "https://app.talent-ray.com/api/v1/candidates?page=0&pageSize=20" \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `200 OK` ```json { "data": [ { "id": "cand_1", "fullName": "Jane Doe", "email": "jane@example.com", "phone": "+905551112233", "status": "Active", "createdAt": "2026-06-01T10:00:00Z", "updatedAt": "2026-06-02T08:00:00Z", "roles": [ { "roleId": "role_eng_be", "roleName": "Senior Backend Engineer", "organizationId": "org_acme", "status": "In Pipeline", "overallFitScore": 82, "approved": false } ] } ], "pagination": { "page": 0, "pageSize": 20, "totalCount": 134, "totalPages": 7 } } ``` `overallFitScore` is `-1` until an assessment has been scored, then `0–100`. ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | `insufficient_scope` — the key lacks `candidates:read`. | | `429` | Rate limit exceeded. | --- # Get a candidate `GET /api/v1/candidates/{id}` — Fetch one candidate in the curated v1 shape. Scope: candidates:read. Fetches a single candidate. **Scope:** `candidates:read`. A candidate the key cannot see returns `404` (no existence leak). ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The candidate id. | ## Example request ```bash curl https://app.talent-ray.com/api/v1/candidates/cand_1 \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `200 OK` ```json { "id": "cand_1", "fullName": "Jane Doe", "email": "jane@example.com", "phone": "+905551112233", "status": "Active", "createdAt": "2026-06-01T10:00:00Z", "updatedAt": "2026-06-02T08:00:00Z", "roles": [ { "roleId": "role_eng_be", "roleName": "Senior Backend Engineer", "organizationId": "org_acme", "status": "In Pipeline", "overallFitScore": 82, "approved": false } ] } ``` ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | `insufficient_scope` — the key lacks `candidates:read`. | | `404` | Not found, or not visible to the key. | | `429` | Rate limit exceeded. | --- # Update a candidate `PATCH /api/v1/candidates/{id}` — Update a curated set of candidate fields. Scope: candidates:write. Updates a curated subset of a candidate's fields. **Scope:** `candidates:write`. Requires employer-write authority in one of the candidate's organizations (admins bypass; hiring managers are read-only). Send only the fields you want to change — unknown fields are ignored, wrong-typed fields return `400`. ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The candidate id. | ## Request body | Field | Type | Description | | --- | --- | --- | | `fullName` | string | Candidate's full name. | | `status` | string | Candidate status. | | `email` | string \| null | Email (validated). | | `phone` | string \| null | Phone (validated). | | `summary` | string \| null | Free-text summary. | ## Example request ```bash curl -X PATCH https://app.talent-ray.com/api/v1/candidates/cand_1 \ -H "Authorization: Bearer tr_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "Active", "phone": "+905551112233" }' ``` ## Response `200 OK` — the updated candidate (same shape as [Get a candidate](/docs/api/pipeline/candidates-get/)). ## Status codes | Status | Meaning | | --- | --- | | `200` | Updated. | | `400` | `bad_request` — invalid or empty body. | | `401` | No valid API key. | | `403` | `insufficient_scope` (missing `candidates:write`) or `forbidden` (no write authority in the candidate's org). | | `404` | Not found, or not visible to the key. | | `429` | Rate limit exceeded. | --- # Get a candidate's pipeline progress `GET /api/v1/candidates/{id}/steps` — A candidate's step progress across all their roles. Scope: pipeline:read. Returns where a candidate stands in their hiring pipeline — their per-step progress across every role they're in. **Scope:** `pipeline:read`. The parent candidate must be visible to the key (else `404`). The list is not paginated (it is inherently small, ordered by role then step order). ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The candidate id. | ## Example request ```bash curl https://app.talent-ray.com/api/v1/candidates/cand_1/steps \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `200 OK` ```json { "data": [ { "id": "crs_1", "roleId": "role_eng_be", "roleStepId": "step_cv", "name": "CV Screening", "order": 1, "stepType": "cv_screening", "status": "validated", "startedAt": "2026-06-01T10:00:00Z", "completedAt": "2026-06-01T11:00:00Z", "validatedAt": "2026-06-01T11:05:00Z", "rejectedAt": null, "validationScore": 78, "rejectionReason": null, "offerResponse": null, "createdAt": "2026-06-01T10:00:00Z", "updatedAt": "2026-06-01T11:05:00Z" } ] } ``` `status` is one of `locked`, `active`, `completed`, `validated`, `rejected`, `skipped`. ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | `insufficient_scope` — the key lacks `pipeline:read`. | | `404` | Candidate not found, or not visible to the key. | | `429` | Rate limit exceeded. | --- # List roles `GET /api/v1/roles` — Page through roles (jobs) in a stable, curated shape. Scope: roles:read. Lists roles (jobs) your key can see, in the stable v1 shape. **Scope:** `roles:read`. Visibility: admins all; employer-write members see their org's roles (confidential roles only if they're the HR rep or hiring manager); hiring managers see assigned roles. ## Query parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `page` | integer | No | 0-indexed page. Default `0`. | | `pageSize` | integer | No | Rows per page (max 100). Default `20`. | | `organizationId` | string | No | Filter to a single organization. | | `status` | string | No | Filter by role status. | ## Example request ```bash curl "https://app.talent-ray.com/api/v1/roles?status=open" \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `200 OK` ```json { "data": [ { "id": "role_eng_be", "name": "Senior Backend Engineer", "organizationId": "org_acme", "status": "open", "priority": "high", "isPublic": true, "department": "Engineering", "location": "Remote", "workType": "remote", "salaryMin": 90000, "salaryMax": 120000, "salaryCurrency": "EUR", "salaryPeriod": "year", "targetHireCount": 2, "roleLevel": "senior", "createdAt": "2026-05-01T09:00:00Z", "updatedAt": "2026-06-01T09:00:00Z" } ], "pagination": { "page": 0, "pageSize": 20, "totalCount": 12, "totalPages": 1 } } ``` ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | `insufficient_scope` — the key lacks `roles:read`. | | `429` | Rate limit exceeded. | --- # Get a role `GET /api/v1/roles/{id}` — Fetch one role in the curated v1 shape. Scope: roles:read. Fetches a single role (job). **Scope:** `roles:read`. A role the key cannot see returns `404` (no existence leak). ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The role id. | ## Example request ```bash curl https://app.talent-ray.com/api/v1/roles/role_eng_be \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `200 OK` — a single role object (same shape as the items in [List roles](/docs/api/pipeline/roles-list/)). ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | `insufficient_scope` — the key lacks `roles:read`. | | `404` | Not found, or not visible to the key. | | `429` | Rate limit exceeded. | --- # Update a role `PATCH /api/v1/roles/{id}` — Update a curated set of role fields. Scope: roles:write. Updates a curated subset of a role's fields. **Scope:** `roles:write`. Requires employer-write authority in the role's organization (admins bypass). Pipeline structure, fit criteria, confidentiality, and assignment fields are intentionally excluded — those stay on the internal product. Send only the fields you want to change. ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The role id. | ## Request body | Field | Type | Description | | --- | --- | --- | | `name` | string | Role title. | | `status` | string | Role status. | | `priority` | string \| null | Priority. | | `department` | string \| null | Department. | | `location` | string \| null | Location. | | `workType` | string \| null | remote / hybrid / onsite. | | `salaryMin` / `salaryMax` | integer \| null | Salary band. | | `salaryCurrency` / `salaryPeriod` | string \| null | Salary currency / period. | | `targetHireCount` | integer \| null | Target hires. | | `roleLevel` | string \| null | Seniority. | | `isPublic` | boolean | Whether the role is public. | ## Example request ```bash curl -X PATCH https://app.talent-ray.com/api/v1/roles/role_eng_be \ -H "Authorization: Bearer tr_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "open", "priority": "high", "salaryMax": 130000 }' ``` ## Response `200 OK` — the updated role (same shape as [Get a role](/docs/api/pipeline/roles-get/)). ## Status codes | Status | Meaning | | --- | --- | | `200` | Updated. | | `400` | `bad_request` — invalid or empty body. | | `401` | No valid API key. | | `403` | `insufficient_scope` (missing `roles:write`) or `forbidden` (no write authority in the role's org). | | `404` | Not found, or not visible to the key. | | `429` | Rate limit exceeded. | --- # Get a role's pipeline template `GET /api/v1/roles/{id}/steps` — A role's ordered pipeline steps (the template). Scope: pipeline:read. Returns the role's pipeline **template** — the ordered list of steps a candidate moves through. **Scope:** `pipeline:read`. The parent role must be visible to the key (else `404`). The list is not paginated (small, ordered by step order). ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The role id. | ## Example request ```bash curl https://app.talent-ray.com/api/v1/roles/role_eng_be/steps \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `200 OK` ```json { "data": [ { "id": "step_cv", "roleId": "role_eng_be", "name": "CV Screening", "description": null, "order": 1, "stepType": "cv_screening", "validationType": "auto", "passingScore": 70, "isRequired": true, "allowSkip": false, "createdAt": "2026-05-01T09:00:00Z", "updatedAt": "2026-05-01T09:00:00Z" } ] } ``` `stepType` is one of `cv_screening`, `ai_assessment`, `interview`, `application_form`, `document_upload`, `offer`, `reference_check`, `contract`, `custom`. `validationType` is `auto`, `manual`, or `score_threshold`. ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | `insufficient_scope` — the key lacks `pipeline:read`. | | `404` | Role not found, or not visible to the key. | | `429` | Rate limit exceeded. | --- # List tests `GET /api/v1/tests` — Page through assessment tests in a stable, curated shape. Scope: tests:read. Lists assessment tests in the stable v1 shape. The question **content is never included** — only metadata. **Scope:** `tests:read`. Visibility: admins all; everyone else sees their org's tests plus platform-global (public) tests. ## Query parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `page` | integer | No | 0-indexed page. Default `0`. | | `pageSize` | integer | No | Rows per page (max 100). Default `20`. | | `type` | string | No | Filter by test type. | | `analysisCategory` | string | No | Filter by analysis category. | | `language` | string | No | Filter by language (ISO 639-1). | ## Example request ```bash curl "https://app.talent-ray.com/api/v1/tests?analysisCategory=hard_skill" \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `200 OK` ```json { "data": [ { "id": "test_js", "name": "JavaScript Fundamentals", "type": "multiple_choice", "description": "Core JS assessment.", "duration": 30, "targetPersona": "Backend Engineer", "analysisCategory": "hard_skill", "language": "en", "isPublic": false, "organizationIds": ["org_acme"], "createdAt": "2026-04-01T09:00:00Z", "updatedAt": "2026-04-10T09:00:00Z" } ], "pagination": { "page": 0, "pageSize": 20, "totalCount": 48, "totalPages": 3 } } ``` `isPublic` is `true` when the test has no linked organization (a platform-global template). `duration` is in minutes. ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | `insufficient_scope` — the key lacks `tests:read`. | | `429` | Rate limit exceeded. | --- # Get a test `GET /api/v1/tests/{id}` — Fetch one test's metadata in the curated v1 shape. Scope: tests:read. Fetches a single test's metadata (never the question `content`). **Scope:** `tests:read`. A test the key cannot see returns `404`. ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The test id. | ## Example request ```bash curl https://app.talent-ray.com/api/v1/tests/test_js \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `200 OK` — a single test object (same shape as the items in [List tests](/docs/api/pipeline/tests-list/)). ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | `insufficient_scope` — the key lacks `tests:read`. | | `404` | Not found, or not visible to the key. | | `429` | Rate limit exceeded. | --- # Update a test `PATCH /api/v1/tests/{id}` — Update a curated set of test metadata fields. Scope: tests:write. Updates a curated subset of a test's **metadata** — never the question `content`. **Scope:** `tests:write`. Requires employer-write authority in one of the test's organizations (admins bypass; platform-global tests with no organization are admin-only). Send only the fields you want to change. ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The test id. | ## Request body | Field | Type | Description | | --- | --- | --- | | `name` | string | Test name. | | `description` | string | Test description. | | `analysisCategory` | string | Analysis category. | | `language` | string | Language (ISO 639-1). | | `type` | string | Test type. | | `targetPersona` | string \| null | Target persona. | | `duration` | integer | Duration in minutes. | ## Example request ```bash curl -X PATCH https://app.talent-ray.com/api/v1/tests/test_js \ -H "Authorization: Bearer tr_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "JavaScript Fundamentals (2026)", "duration": 35 }' ``` ## Response `200 OK` — the updated test (same shape as [Get a test](/docs/api/pipeline/tests-get/)). ## Status codes | Status | Meaning | | --- | --- | | `200` | Updated. | | `400` | `bad_request` — invalid or empty body. | | `401` | No valid API key. | | `403` | `insufficient_scope` (missing `tests:write`) or `forbidden` (no write authority in the test's org). | | `404` | Not found, or not visible to the key. | | `429` | Rate limit exceeded. | --- # List potential candidates `GET /api/v1/sourcing` — Page through sourced/CV-screened leads. Scope: sourcing:read. Lists potential candidates — sourced or CV-screened **leads** that exist before a candidate creates an account. **Scope:** `sourcing:read`. Visibility: admins all; everyone else sees leads linked to an organization they belong to. ## Query parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `page` | integer | No | 0-indexed page. Default `0`. | | `pageSize` | integer | No | Rows per page (max 100). Default `20`. | | `roleId` | string | No | Filter to leads linked to this role. | | `status` | string | No | Filter by lead status. | ## Example request ```bash curl "https://app.talent-ray.com/api/v1/sourcing?status=parsed" \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `200 OK` ```json { "data": [ { "id": "pc_1", "fullName": "Sam Lee", "email": "sam.cv@example.com", "contactEmail": "sam@example.com", "phone": "+905559998877", "status": "parsed", "skills": ["python", "sql"], "summary": "5y data engineering.", "candidateId": null, "organizations": [ { "organizationId": "org_acme", "status": "Pool" } ], "roles": [ { "roleId": "role_eng_be", "roleName": "Senior Backend Engineer", "organizationId": "org_acme", "status": "Assigned", "resumeFitScore": 71, "overallFitScore": -1 } ], "createdAt": "2026-06-01T10:00:00Z", "updatedAt": "2026-06-01T10:30:00Z" } ], "pagination": { "page": 0, "pageSize": 20, "totalCount": 60, "totalPages": 3 } } ``` `email` is parsed from the CV; `contactEmail` is the account email used for invitations. `candidateId` is set once the lead is converted into a candidate. Fit scores are `-1` until screened/scored. ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | `insufficient_scope` — the key lacks `sourcing:read`. | | `429` | Rate limit exceeded. | --- # Create a potential candidate `POST /api/v1/sourcing` — Create a sourced lead in an organization. Scope: sourcing:write. Creates a potential candidate (lead) in a target organization. **Scope:** `sourcing:write`. Requires employer-write authority in `organizationId` (admins bypass) and that the organization exists. The lead is linked to that organization on creation. ## Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `fullName` | string | Yes | Lead's full name. | | `organizationId` | string | Yes | Organization to create the lead in. | | `email` | string | No | CV email (validated). | | `contactEmail` | string | No | Account/contact email (validated). | | `phone` | string | No | Phone (validated). | | `summary` | string | No | Free-text summary. | | `skills` | string[] | No | Skill tags. | | `status` | string | No | Lead status. Defaults to `uploaded`. | ## Example request ```bash curl -X POST https://app.talent-ray.com/api/v1/sourcing \ -H "Authorization: Bearer tr_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "fullName": "Sam Lee", "organizationId": "org_acme", "contactEmail": "sam@example.com", "skills": ["python", "sql"] }' ``` ## Response `201 Created` — the created potential candidate (same shape as [List potential candidates](/docs/api/pipeline/sourcing-list/)). ## Status codes | Status | Meaning | | --- | --- | | `201` | Created. | | `400` | `bad_request` — missing/invalid fields, or `organizationId` does not exist. | | `401` | No valid API key. | | `403` | `insufficient_scope` (missing `sourcing:write`) or `forbidden` (no write authority in the target org). | | `429` | Rate limit exceeded. | --- # Get a potential candidate `GET /api/v1/sourcing/{id}` — Fetch one sourced lead in the curated v1 shape. Scope: sourcing:read. Fetches a single potential candidate (lead). **Scope:** `sourcing:read`. A lead the key cannot see returns `404` (no existence leak). ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The potential-candidate id. | ## Example request ```bash curl https://app.talent-ray.com/api/v1/sourcing/pc_1 \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `200 OK` — a single potential-candidate object (same shape as the items in [List potential candidates](/docs/api/pipeline/sourcing-list/)). ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | `insufficient_scope` — the key lacks `sourcing:read`. | | `404` | Not found, or not visible to the key. | | `429` | Rate limit exceeded. | --- # Update a potential candidate `PATCH /api/v1/sourcing/{id}` — Update a curated set of lead fields. Scope: sourcing:write. Updates a curated subset of a potential candidate's fields. **Scope:** `sourcing:write`. Requires employer-write authority in one of the lead's organizations (admins bypass). Send only the fields you want to change. ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The potential-candidate id. | ## Request body | Field | Type | Description | | --- | --- | --- | | `fullName` | string | Lead's full name. | | `status` | string | Lead status. | | `email` | string \| null | CV email (validated). | | `contactEmail` | string \| null | Account/contact email (validated). | | `phone` | string \| null | Phone (validated). | | `summary` | string \| null | Free-text summary. | | `skills` | string[] | Skill tags. | ## Example request ```bash curl -X PATCH https://app.talent-ray.com/api/v1/sourcing/pc_1 \ -H "Authorization: Bearer tr_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "parsed", "skills": ["python", "sql", "airflow"] }' ``` ## Response `200 OK` — the updated potential candidate (same shape as [Get a potential candidate](/docs/api/pipeline/sourcing-get/)). ## Status codes | Status | Meaning | | --- | --- | | `200` | Updated. | | `400` | `bad_request` — invalid or empty body. | | `401` | No valid API key. | | `403` | `insufficient_scope` (missing `sourcing:write`) or `forbidden` (no write authority in the lead's org). | | `404` | Not found, or not visible to the key. | | `429` | Rate limit exceeded. | --- # Delete a potential candidate `DELETE /api/v1/sourcing/{id}` — Permanently delete a sourced lead. Scope: sourcing:write. Permanently deletes a potential candidate. The delete cascades to its role and organization links. **Scope:** `sourcing:write`. Requires employer-write authority in one of the lead's organizations (admins bypass). A role-activity audit entry is recorded for each active role before deletion. ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `id` | string | The potential-candidate id. | ## Example request ```bash curl -X DELETE https://app.talent-ray.com/api/v1/sourcing/pc_1 \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `204 No Content` — the lead and its links were deleted. ## Status codes | Status | Meaning | | --- | --- | | `204` | Deleted. | | `401` | No valid API key. | | `403` | `insufficient_scope` (missing `sourcing:write`) or `forbidden` (no write authority in the lead's org). | | `404` | Not found, or not visible to the key. | | `429` | Rate limit exceeded. | --- # Get a CV-screening batch `GET /api/v1/cv-screening/batches/{batchId}` — Batch processing status plus per-member screening results. Scope: cv-screening:read. Returns a CV-screening batch's **processing status** together with its per-member results (each member uses the [potential-candidate shape](/docs/api/pipeline/sourcing-list/)). **Scope:** `cv-screening:read`. Only batch members linked to an organization the key can see are counted and returned; a batch with no visible members returns `404`. ## Path parameters | Parameter | Type | Description | | --- | --- | --- | | `batchId` | string | The CV-screening batch id. | ## Query parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `page` | integer | No | 0-indexed page of members. Default `0`. | | `pageSize` | integer | No | Members per page (max 100). Default `20`. | ## Example request ```bash curl https://app.talent-ray.com/api/v1/cv-screening/batches/batch_2026_06_01 \ -H "Authorization: Bearer tr_YOUR_KEY" ``` ## Response `200 OK` ```json { "batchId": "batch_2026_06_01", "status": "completed", "totalCandidates": 25, "completedCandidates": 25, "processingCandidates": 0, "data": [ { "id": "pc_1", "fullName": "Sam Lee", "email": "sam.cv@example.com", "contactEmail": null, "phone": null, "status": "parsed", "skills": ["python"], "summary": null, "candidateId": null, "organizations": [ { "organizationId": "org_acme", "status": "Pool" } ], "roles": [], "createdAt": "2026-06-01T10:00:00Z", "updatedAt": "2026-06-01T10:30:00Z" } ], "pagination": { "page": 0, "pageSize": 20, "totalCount": 25, "totalPages": 2 } } ``` `status` is `processing` while any member is still being processed, otherwise `completed`. Poll this endpoint after an upload to track completion. ## Status codes | Status | Meaning | | --- | --- | | `200` | Success. | | `401` | No valid API key. | | `403` | `insufficient_scope` — the key lacks `cv-screening:read`. | | `404` | Unknown batch, or no members visible to the key. | | `429` | Rate limit exceeded. | --- # Errors Error response format and the status codes the API returns. When a request fails, the API returns a non-2xx status code and a JSON body with an `error` field describing the problem. ```json { "error": "Forbidden - Admin access required" } ``` Successful responses instead carry `"success": true` and a `data` payload (legacy endpoints), or the resource / `{ data, pagination }` envelope (`/api/v1`). ## Error shapes The legacy endpoints return a human-readable `{ "error": "..." }` message. The **`/api/v1`** endpoints return a stable **machine code** in `error`, optionally with a `message` and extra fields: ```json { "error": "insufficient_scope", "message": "This API key is missing required scope(s): candidates:read.", "requiredScopes": ["candidates:read"], "grantedScopes": ["roles:read"] } ``` ```json { "error": "bad_request", "message": "Invalid field(s)", "details": ["status must be a non-empty string"] } ``` `/api/v1` machine codes: `bad_request` (400), `insufficient_scope` (403), `forbidden` (403), `not_found` (404), `internal_error` (500). On `insufficient_scope`, read `requiredScopes` / `grantedScopes` to self-correct. ## Status codes | Status | Meaning | What to do | | --- | --- | --- | | `200` | Success | Read the response body. | | `201` | Created | A `POST` created the resource; read it from the body. | | `204` | No content | A `DELETE` succeeded; there is no body. | | `400` | Validation error | Fix the request body or parameters; `error`/`details` say what is wrong. | | `401` | Unauthorized | No valid API key was supplied. Check the `Authorization` header. | | `403` | Forbidden / insufficient scope | The key's owner lacks the required role, **or** the key is missing the required `/api/v1` scope (`insufficient_scope`). | | `404` | Not found | The resource does not exist — or, on `/api/v1`, exists but is not visible to the key (no existence leak). | | `429` | Rate limited | Honor the `Retry-After` header and back off. | | `500` | Server error | Transient — retry with backoff; if it persists, contact support. | ## Common error messages | Endpoint | Message | Cause | | --- | --- | --- | | Create key | `Name is required` | Missing `name` in the body. | | Create key | `userId is required` | Missing `userId` in the body. | | Create key | `expiresInDays must be between 1 and 365` | Out-of-range expiry. | | Create key | `Target user not found` | `userId` does not match a user. | | Revoke / usage | `Key not found` | The `id` does not match a key. | | Any | `Unauthorized` | Missing or invalid key. | | Any (admin) | `Forbidden - Admin access required` | Key owner is not an admin. |