# Aiviue Partner API > Hiring workflow API for HCM/ATS partners: publish jobs, receive screened candidates and > hiring events by webhook. All guides from the Aiviue developer portal follow. - OpenAPI spec: https://aiviue.com/developers/partner-openapi.json - API reference: https://aiviue.com/developers/api-reference --- # Quickstart Source: https://aiviue.com/developers/guides/quickstart This guide takes you from credentials to your first published job. You need a **test client secret** (it starts with `aiv_test_`) and a **tenant ID**. If you don't have them yet, email [partners@aiviue.com](mailto:partners@aiviue.com). All requests go to the base URL `https://api.aiviue.com`. Paths start with `/v1`. ## 1. Check connectivity The health endpoint needs no credentials: ```bash curl https://api.aiviue.com/v1/integration/health ``` ## 2. Get an access token Exchange your client secret for a short-lived bearer token (OAuth 2.0 client credentials). With a prefixed secret you can leave `client_id` out. ```bash curl -X POST https://api.aiviue.com/v1/auth/token \ -H "Content-Type: application/json" \ -d '{"grant_type": "client_credentials", "client_secret": "aiv_test_..."}' ``` ```json { "access_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 3600, "scope": "jobs:read jobs:write candidates:read webhooks:read webhooks:write" } ``` Tokens last one hour. Cache the token and request a new one shortly before it expires. See [Authentication](/developers/guides/authentication). ## 3. Publish a job Every resource call carries three headers: - `Authorization: Bearer ` - `X-Tenant-ID: ` — which of your customers this job belongs to ([Tenants](/developers/guides/tenants)) - `X-Idempotency-Key: ` — required on every write ([Idempotency](/developers/guides/idempotency)) ```bash curl -X POST https://api.aiviue.com/v1/jobs \ -H "Authorization: Bearer $TOKEN" \ -H "X-Tenant-ID: $TENANT_ID" \ -H "X-Idempotency-Key: create-job-REQ-1042" \ -H "Content-Type: application/json" \ -d '{ "external_job_id": "REQ-1042", "title": "Delivery Executive", "description": "Deliver orders across Koramangala. Two-wheeler and licence required.", "openings": 5, "location": {"city": "Bengaluru", "state": "Karnataka", "country": "IN"} }' ``` `external_job_id` is your own stable ID for the job. `title` and a `description` of at least 10 characters are required unless you send a `jd_url` instead. The response uses the standard envelope: ```json { "success": true, "data": { "job_id": "...", "external_job_id": "REQ-1042", "campaign_id": "...", "status": "...", "aiviue_url": "https://...", "created_at": "2026-11-02T10:00:00Z" }, "meta": { "requestId": "...", "timestamp": "...", "version": "1.0" } } ``` ## 4. Receive candidates Aiviue now runs the campaign and screens applicants. To get results: - **Webhooks (recommended):** subscribe to events such as `screening.completed` — see [Webhooks](/developers/guides/webhooks). - **Polling:** `GET /v1/jobs/{job_id}/candidates`, then `GET /v1/candidates/{candidate_id}` and `/scores`. ## Next steps - Browse every endpoint in the [API reference](/developers/api-reference). - Try the full flow end to end in the [Sandbox](/developers/guides/sandbox). --- # Authentication Source: https://aiviue.com/developers/guides/authentication The Partner API uses **OAuth 2.0 client credentials**. You exchange a client secret for a signed bearer token (RS256 JWT) and send that token on every request. ## Test and live secrets Client secrets carry their environment in the prefix: | Prefix | Environment | Use | | --- | --- | --- | | `aiv_test_` | `partner_test` | Development and testing. Isolated test data. | | `aiv_live_` | `production` | Real jobs and candidates. | The prefix must match the environment of the client it belongs to, otherwise the token request is rejected. Clients issued before prefixes were introduced have unprefixed secrets; they keep working but must send `client_id` too. Keep secrets on your server. Never ship them in a browser or mobile app. ## Requesting a token `POST /v1/auth/token` accepts any of these: - a JSON body: `{"grant_type": "client_credentials", "client_secret": "...", "client_id": "..."}` - a form body (`application/x-www-form-urlencoded`) with the same fields - HTTP Basic auth with `client_id:client_secret` `grant_type` must be `client_credentials`. `client_id` is optional with a prefixed secret. ```bash curl -X POST https://api.aiviue.com/v1/auth/token \ -H "Content-Type: application/json" \ -d '{"grant_type": "client_credentials", "client_secret": "aiv_test_..."}' ``` The response is plain JSON (not wrapped in the response envelope): ```json { "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "scope": "jobs:read jobs:write" } ``` Invalid credentials return `401`. ## Using the token ```http Authorization: Bearer ``` Tokens expire after `expires_in` seconds (one hour). Reuse a token until it is close to expiry; don't request one per call. ## Scopes Your client is granted a fixed set of scopes when it is created. The token's `scope` field lists them. Calls outside your scopes return `403`. | Scope | Allows | | --- | --- | | `jobs:read` | Read jobs | | `jobs:write` | Create and update jobs | | `candidates:read` | Read candidates and scores | | `candidates:write` | Submit selection decisions | | `webhooks:read` | Read webhook subscriptions | | `webhooks:write` | Manage webhook subscriptions | | `webhooks:manage` | Full webhook access (includes `webhooks:read` and `webhooks:write`) | | `analytics:read` | Read partner analytics | | `analytics:write` | Write partner analytics events | | `partner_test:reset` | Reset your own `partner_test` data (test environment only) | ## Verifying tokens yourself Tokens are signed with RS256. The public keys are published at `GET /v1/auth/.well-known/jwks.json`; the issuer is `https://api.aiviue.com`. ## Revoking credentials `POST /v1/auth/token/revoke` with a JSON body `{"client_id": "...", "client_secret": "..."}` deactivates the client. Use it if a secret leaks, then ask us for a new one. --- # Tenants Source: https://aiviue.com/developers/guides/tenants A **tenant** is one of your customers — the employer you are hiring for. Each tenant has its own jobs, candidates and usage inside Aiviue, and one partner account can manage several tenants. ## The X-Tenant-ID header Send the tenant on every resource request: ```http X-Tenant-ID: ``` The value is the **Aiviue tenant ID** we give you when a tenant is set up — not an ID from your own system. Store the mapping between your customer and its Aiviue tenant ID on your side. Requests that don't need a tenant: - `POST /v1/auth/token`, `POST /v1/auth/token/revoke`, `GET /v1/auth/.well-known/jwks.json` - `GET /v1/integration/health` and `GET /v1/integration/health/detailed` ## Errors | Situation | Status | `error.code` | | --- | --- | --- | | Header missing | `400` | `VALIDATION_ERROR` (with `field: "X-Tenant-ID"`) | | Tenant ID not found | `404` | `NOT_FOUND` | | Tenant belongs to a different partner | `403` | `FORBIDDEN` | ## Your own IDs Your IDs live in the request bodies, not in headers. For example `external_job_id` on a job is your stable job ID, and `external_client_id` can carry your own customer reference. --- # Idempotency Source: https://aiviue.com/developers/guides/idempotency Networks fail. To make retries safe, every write request (`POST`, `PUT`, `PATCH`, `DELETE`) must carry an idempotency key: ```http X-Idempotency-Key: create-job-REQ-1042 ``` A write without the header is rejected with `400 VALIDATION_ERROR`. The token endpoints are the exception. ## How keys behave Keys are scoped to your partner account **and** the tenant in `X-Tenant-ID`, and are kept for **24 hours**. | You send | Result | | --- | --- | | A new key | The request runs normally. | | The same key and the same body again | The original status and response body are returned; nothing runs twice. | | The same key with a different body | `409 IDEMPOTENCY_CONFLICT` | | The same key while the first request is still running | `409 IDEMPOTENCY_CONFLICT` with `retryable: true` — wait and retry. | Only responses below `500` are stored. If the original request failed with a server error, retrying with the same key runs it again. ## Choosing keys - Use one key per logical operation, and reuse it for every retry of that operation. - Derive it from your own data where you can, e.g. `create-job-` or `decision--`. A random UUID stored with the pending operation also works. - Don't reuse a key for a different operation. If the idempotency store is briefly unavailable the API returns `503 SERVICE_UNAVAILABLE`; retry with the same key after a short backoff. --- # Webhooks Source: https://aiviue.com/developers/guides/webhooks Webhooks push hiring events to your server as they happen, so you don't have to poll. ## Subscribing Subscribe one HTTPS endpoint per event type (scope `webhooks:write`): ```bash curl -X POST https://api.aiviue.com/v1/webhooks/subscriptions \ -H "Authorization: Bearer $TOKEN" \ -H "X-Tenant-ID: $TENANT_ID" \ -H "X-Idempotency-Key: sub-screening-completed" \ -H "Content-Type: application/json" \ -d '{"event_type": "screening.completed", "url": "https://example.com/aiviue/webhooks"}' ``` - `GET /v1/webhooks/subscriptions` lists your subscriptions (`webhooks:read`). - `DELETE /v1/webhooks/subscriptions/{subscription_id}` removes one. - `GET /v1/reference-data` returns the full event catalog, including when each event fires. ## Event types `creative.ready`, `campaign.live`, `screening.ready`, `voice.ready`, `distribution.started`, `candidate.created`, `candidate.updated`, `screening.started`, `screening.completed`, `interview.scheduled`, `interview.completed`, `grading.completed`, `campaign.analytics.updated`, `campaign.paused`, `campaign.closed`. The [API reference](/developers/api-reference) describes each event's payload. ## The request we send A `POST` with a JSON body: ```json { "eventId": "...", "eventType": "screening.completed", "eventVersion": "...", "occurredAt": "2026-11-02T10:00:00Z", "sourceSystem": "AIVIUE", "targetSystem": "YOUR_PARTNER_SLUG", "idempotencyKey": "...", "payload": { } } ``` Optional fields: `correlationId`, `parentEventId`, `applicationId`, `sequenceNumber`. Headers: | Header | Meaning | | --- | --- | | `X-Aiviue-Signature` | `sha256=` of the raw body — verify it | | `X-Aiviue-Event` | The event type | | `X-Aiviue-Delivery` | Unique ID of this delivery attempt | | `X-Aiviue-Sequence` | Event sequence number, when present | | `X-Correlation-Id` | Correlation ID, when present | ## Verifying the signature Your webhook secret is issued with your partner account. Compute an HMAC-SHA256 of the **raw request body bytes** with that secret and compare it to the header in constant time. Don't parse and re-serialize the JSON first. ```python import hashlib, hmac def is_valid(secret: str, raw_body: bytes, header: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header or "") ``` ```javascript import crypto from "node:crypto" function isValid(secret, rawBody, header) { const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex") const a = Buffer.from(expected) const b = Buffer.from(header ?? "") return a.length === b.length && crypto.timingSafeEqual(a, b) } ``` The signature does not include a timestamp, so de-duplicate on `eventId` (or `idempotencyKey`) to ignore replays and retries you have already processed. ## Responding and retries - Return any `2xx` status within **30 seconds** to acknowledge. Do slow work after responding. - Anything else, or a timeout, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 8 hours — **6 attempts** in total. - After the last failed attempt the delivery is marked dead and our team is alerted. - Deliveries can arrive more than once and out of order. Use `eventId` for de-duplication and `sequenceNumber`/`occurredAt` for ordering. ## Delivery log and replay - `GET /v1/webhooks/deliveries` shows recent deliveries and their status. - `POST /v1/webhooks/deliveries/{delivery_id}/replay` (`webhooks:write`) sends a delivery again with the same `idempotencyKey`. Deliveries older than 30 days can't be replayed (`403 REPLAY_EXPIRED`), a pending delivery returns `409 CONFLICT`, and replays are rate limited (`429 RATE_LIMITED`). --- # Errors and rate limits Source: https://aiviue.com/developers/guides/errors ## Response envelope Resource endpoints wrap every response in the same envelope: ```json { "success": false, "error": { "code": "VALIDATION_ERROR", "message": "Human-readable explanation", "field": "X-Tenant-ID", "retryable": false }, "meta": { "requestId": "...", "correlationId": "...", "timestamp": "2026-11-02T10:00:00Z", "version": "1.0" } } ``` On success `success` is `true` and the result is in `data`. Fields without a value are left out. Include `meta.requestId` when you contact support. The token endpoints (`/v1/auth/...`) return plain OAuth-style JSON instead of the envelope. Always branch on the **HTTP status** first and use `error.code` for detail. Some statuses currently carry a generic code — for example a missing or invalid bearer token returns `401` with `error.code` `VALIDATION_ERROR`. Treat any `401` as "get a new token". ## Error codes | Code | Typical status | Meaning | | --- | --- | --- | | `VALIDATION_ERROR` | 400, 422 | A header or field is missing or invalid, or the body doesn't match the schema. Check `field`. Also returned with `401` for a missing or invalid token. | | `UNAUTHORIZED` | 401 | Token rejected. Some `401`s carry `VALIDATION_ERROR` instead — branch on the status. | | `FORBIDDEN` | 403 | Missing scope, or a tenant that isn't yours. | | `NOT_FOUND` | 404 | The resource or tenant doesn't exist. | | `CONFLICT` | 409 | The request conflicts with the resource's current state. | | `IDEMPOTENCY_CONFLICT` | 409 | Idempotency key reused with a different body, or still in flight. | | `RATE_LIMITED` | 429 | Too many requests — see below. | | `REPLAY_EXPIRED` | 403 | A webhook delivery is too old to replay. | | `SERVICE_UNAVAILABLE` | 503 | Temporary problem on our side. Retry with backoff. | | `INTERNAL_ERROR` | 500 | Unexpected error. Retry with backoff; contact us if it persists. | Some endpoints return more specific codes; the [API reference](/developers/api-reference) lists them. ## Retrying Retry when `retryable` is `true`, on `429`, and on `5xx`. Use exponential backoff with jitter and keep the same `X-Idempotency-Key` so a retry never creates a duplicate. ## Rate limits Limits apply per tenant, separately for reads and writes per hour, plus a per-second burst limit. Your limits depend on your agreement; ask us if you need them raised. Rate-limited responses tell you where you stand (token and health endpoints don't carry these headers): | Header | Meaning | | --- | --- | | `X-RateLimit-Limit` | Requests allowed in the current window | | `X-RateLimit-Remaining` | Requests left in the window | | `X-RateLimit-Reset` | Seconds until the window resets | The headers can occasionally be missing; treat them as advisory and rely on `429` + `Retry-After`. When you exceed a limit you get `429 RATE_LIMITED` with a `Retry-After` header (seconds). Wait that long before retrying. --- # Sandbox Source: https://aiviue.com/developers/guides/sandbox Build and test your integration in the **partner test environment** (`partner_test`) before going live. Test data is kept separate from production, so you can create jobs, apply as candidates and reset as often as you like. ## Using the test environment Use a test client secret (prefix `aiv_test_`) against the same base URL, `https://api.aiviue.com`. The token you get is bound to `partner_test`, and every call made with it reads and writes test data only. Switch to an `aiv_live_` secret to go live — no other change to your code is needed. ## Applying as a candidate Each partner gets a test job board listing its published test jobs. Open it, apply to your own job as a candidate, and watch the screening events arrive at your webhook endpoint. Ask us for your board's link. ## Resetting test data `POST /v1/admin/reset` (scope `partner_test:reset`) deletes the jobs, applications and related data of one test tenant so you can start fresh. Your OAuth client and the tenant itself are kept. - Send the tenant in both `X-Tenant-ID` and the body (`tenant_id`); they must match, otherwise `400`. - It only works with a test token. In production it returns `409 CONFLICT`. ## Going live checklist - Credentials: request an `aiv_live_` secret and store it in your secret manager. - Tenants: map each customer to its production Aiviue tenant ID. - Webhooks: subscribe your production endpoint and verify signatures. - Retries: idempotency keys on every write and backoff on `429`/`5xx`. - Monitoring: log `meta.requestId` from every response. Email [partners@aiviue.com](mailto:partners@aiviue.com) when you are ready to go live. --- # Versioning and changelog Source: https://aiviue.com/developers/guides/versioning ## Versions The major version is in the path: every endpoint lives under `/v1`. The current specification is **Partner API 1.1.0** — download it as [OpenAPI JSON](/developers/partner-openapi.json) and use it with Postman, Insomnia or a client generator. ## What we change without a new version Within `/v1` we only make additive changes: - new endpoints, event types or optional request fields - new fields in responses and webhook payloads - new error codes on existing endpoints Write your client to ignore fields it doesn't know and to handle unknown enum values, event types and error codes gracefully. ## Breaking changes Removing or renaming fields, changing types, or changing behaviour your integration relies on happens only in a new major version. When that happens we email the technical contact on your partner account in advance and publish a migration guide here. ## Staying up to date The [API reference](/developers/api-reference) and the OpenAPI file on this site are generated from the API code. The live spec is also served by the API at `https://api.aiviue.com/v1/openapi.json`.