# 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. |