API Integration V1
API key setup, external integration endpoints, lifecycle states, and webhooks
Use this guide to integrate an external ATS or backend with Hirevoice.
External integration endpoints
- Base path:
/api/integration/v1 - Header:
X-API-Key: <api_key> - API key authenticated responses include:
X-API-Key-Expires-At: <iso_timestamp | never>
Setup Flow
API Key Management
API keys are generated from the Dashboard in Integrations. Use these endpoints to view, update, and revoke keys programmatically.
https://app.hirevoice.com/en/sign-in
External Integration Endpoints
All endpoints in this section use:
- Base:
/api/integration/v1 - Auth:
X-API-Key
POST /api/integration/v1/positions
Creates a position and binds your external identifier.
Minimum required fields
{
"name": "Senior Backend Engineer",
"job_description": "Build scalable backend systems in Python.",
"company_position_uuid": "ext-pos-001"
}Full example (all optional fields)
{
"name": "Senior Backend Engineer",
"job_description": "Build scalable backend systems in Python.",
"company_position_uuid": "ext-pos-001",
"interview_type": "white-collar",
"interview_channel": "chat:google_meet",
"location": "Barcelona",
"job_mode": "Remote",
"language": "en",
"duration": 30,
"challenge_level": 6,
"depth_level": 7,
"auto_outreach": true,
"evaluation_criteria": ["System design", "Communication"],
"extract_data": [
{ "key": "years_of_experience", "description": "Candidate years of professional experience" },
{ "key": "availability", "description": "Notice period or start availability" },
{ "key": "salary_expectation", "description": "Expected salary amount and currency" },
{ "key": "candidate_location", "description": "Current city/country or relocation preference" }
],
"white_label": {
"company_name": "Acme Corp",
"brand_assets": {
"logo_url": "https://cdn.example.com/acme-logo.png",
"colors": {
"foreground": { "type": "hex", "value": "#1a1a2e" },
"background": { "type": "hex", "value": "#f0f0f5" }
}
}
}
}Field reference
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name | string | yes | — | Position title |
job_description | string | yes | — | Full job description text or HTML |
company_position_uuid | string | yes | — | Your external position ID (unique per company) |
interview_type | string | no | "white-collar" | "white-collar" (Google Meet) or "blue-collar" (phone call) |
interview_channel | string | no | "chat:google_meet" | Interview channel type |
location | string | no | null | Free-text location label |
job_mode | string | no | null | remote, hybrid, onsite, or unspecified. Also accepts Remote, Hibird, Presential |
language | string | no | "en" | Interview language: en, es, or ca |
duration | integer | no | null | Interview duration in minutes (min: 1) |
challenge_level | integer | no | 5 | AI challenge intensity, 1–10 |
depth_level | integer | no | 5 | Question depth, 1–10 |
auto_outreach | boolean | no | false | Auto-send invitation when interview is ready (email for white-collar, WhatsApp for blue-collar) |
evaluation_criteria | string[] | no | [] | Criteria labels for AI evaluation |
extract_data | object[] | no | [] | Fields to extract from the call. Each: { "key": "...", "description": "..." } |
white_label | object | no | null | Per-position branding override (see White Label Configuration) |
Notes:
company_position_uuidmust be unique per company.durationis expressed in minutes and converted to seconds internally.extract_dataspecifies the key information that the AI can capture from the call.- Position augmentation runs asynchronously after creation.
- Position starts as
status=idle.
Example response:
{
"id": "9e36a0df-2bf4-4f95-92f8-e74d8a22d2aa",
"name": "Senior Backend Engineer",
"job_description": "Build scalable backend systems in Python.",
"company_position_uuid": "ext-pos-001"
}GET /api/integration/v1/positions/{position_id}
Returns position details including augmentation status and interview context fields.
{
"id": "9e36a0df-2bf4-4f95-92f8-e74d8a22d2aa",
"name": "Senior Backend Engineer",
"job_description": "Build scalable backend systems in Python.",
"company_position_uuid": "ext-pos-001",
"evaluation_criteria": [],
"extract_data": [
{
"key": "Salary expectation",
"description": "Candidate expected salary range"
}
],
"applied_extract_data": null,
"interview_prompt_persona_role": null,
"interview_prompt_first_message": null,
"white_label": {
"company_name": "Acme Corp",
"brand_assets": {
"colors": {
"foreground": { "type": "hex", "value": "#1a1a2e" },
"background": { "type": "hex", "value": "#f0f0f5" }
},
"logo_url": "https://cdn.example.com/acme-logo.png"
}
},
"interview_type": "white-collar",
"status_detail": {},
"status": "idle"
}Notes:
- While status is
idleorprocessing, generated fields are still empty/null. - Once status becomes
done, criteria/prompt/applied settings are populated.
PATCH /api/integration/v1/positions/{position_id}
Partial update of a position. Only fields present in the request body are updated; omitted fields are left unchanged.
{
"language": "es",
"evaluation_criteria": ["React expertise", "TypeScript", "Testing strategy"],
"duration": 25,
"update_cascade": true
}Editable fields:
| Field | Notes |
|---|---|
name | Position name |
job_description | Changes here trigger position re-augmentation |
location | Free-text location label |
job_mode | remote / hybrid / onsite |
language | en / es / ca |
duration | Minutes |
challenge_level | 1..10 |
depth_level | 1..10 |
evaluation_criteria | Replaces all criteria; triggers re-augmentation |
extract_data | Replaces all extract-data fields; triggers re-augmentation |
white_label | Per-position branding override |
auto_outreach | Toggle automatic outreach |
update_cascade | true resets interviews currently in ready back to idle so they are re-prepared with the new configuration |
update_cascade only affects interviews in ready status. Interviews in in_progress, postprocessing, done or error are never disturbed by a position update. Completed interview reports are immutable.
Returns the same shape as GET /positions/{position_id}.
POST /api/integration/v1/candidates
Creates a candidate and initial interview relation.
Minimum required fields
{
"company_candidate_uuid": "ext-cand-0099",
"company_position_uuid": "ext-pos-001",
"first_name": "Ada",
"last_name": "Lovelace"
}No CV or candidate data is strictly required. When both cv_url and candidate_data are omitted, the interview proceeds without prior candidate context.
Field reference
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
company_candidate_uuid | string | yes | — | Your external candidate ID (unique per company) |
first_name | string | yes | — | Candidate first name |
last_name | string | yes | — | Candidate last name |
position_id | UUID | one of† | — | Internal Hirevoice position ID |
company_position_uuid | string | one of† | — | Your external position ID |
cv_url | URL | no | null | Public URL to candidate CV (PDF). Mutually exclusive with candidate_data |
candidate_data | object | no | null | Pre-parsed candidate timeline (see below). Mutually exclusive with cv_url |
email | string | no | null | Candidate email |
phone | string | no | null | Candidate phone number (required for blue-collar / phone interviews) |
interview_language | string | no | null | Override language: en, es, or ca. Falls back to position language |
† Pass exactly one of position_id or company_position_uuid. Passing both or neither returns 400.
Providing candidate data
You can provide candidate data in one of two mutually-exclusive ways:
{
"company_candidate_uuid": "ext-cand-0099",
"company_position_uuid": "ext-pos-001",
"first_name": "Ada",
"last_name": "Lovelace",
"cv_url": "https://files.example.com/cv/ada-lovelace.pdf",
"interview_language": "es"
}cv_url must be public and return a valid PDF. The CV is parsed asynchronously and the result drives the interview prep.
{
"company_candidate_uuid": "ext-cand-0099",
"company_position_uuid": "ext-pos-001",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com",
"phone": "+34666111222",
"interview_language": "en",
"candidate_data": {
"location": "Madrid, Spain",
"contact": {
"email": "ada@example.com",
"phone_number": "+34666111222",
"name": "Ada Lovelace"
},
"skills": ["Python", "FastAPI", "PostgreSQL"],
"spoken_languages": ["English", "Spanish"],
"highlights": ["5 years at Stripe building payments infra"],
"timeline": [
{
"type": "experience",
"start_date": "2020-01",
"end_date": "2024-12",
"role": "Senior Backend Engineer",
"company": "Stripe",
"description": "Led payments backend team"
}
]
}
}candidate_data is validated directly against the CandidateTimeline schema. If the field names match exactly, no LLM call is made (fast path, ~1s). If field names differ (e.g. city instead of location), the API falls back to an LLM normalization (~2-3s) and persists the normalized result.
contact.phone_number is optional. For white-collar interviews (Google Meet), you can omit it. For blue-collar interviews (phone call), a phone number is required either in contact.phone_number or in the top-level phone field.
Rules:
company_candidate_uuidmust be unique per company.company_candidate_uuidandcompany_position_uuidare your external identifiers; use them in your system so you do not need to store Hirevoice resource IDs.cv_urlandcandidate_dataare mutually exclusive — passing both returns422.cv_urlmust be publicly reachable and point to PDF content.- Candidate creation does not wait for position augmentation; when position is not done the interview is returned as
queued.
Example response:
{
"id": "f3efbe2f-0dc1-424d-a57c-0f75e74fdc34",
"company_candidate_uuid": "ext-cand-0099",
"first_name": "Ada",
"last_name": "Lovelace",
"created_at": "2026-02-22T16:10:00+00:00",
"interviews": [
{
"position_id": "9e36a0df-2bf4-4f95-92f8-e74d8a22d2aa",
"company_position_uuid": "ext-pos-001",
"status": "queued",
"interview_url": null,
"result": null,
"context": {
"criteria_source": "position.interview_evaluation_criteria",
"job_opening_role": "Senior Backend Engineer",
"job_description": "Build scalable backend systems in Python.",
"evaluation_criteria": [],
"applied_extract_data": {
"tools_or_tags": [],
"must_have_questions_added": [],
"max_questions": 5,
"language": "en",
"persona_applied": false,
"first_message_applied": false
},
"extract_data_fields": [
{
"key": "Salary expectation",
"description": "Candidate expected salary range"
}
]
},
"white_label": {
"company_name": "Acme Corp",
"brand_assets": {
"colors": {
"foreground": { "type": "hex", "value": "#1a1a2e" },
"background": { "type": "hex", "value": "#f0f0f5" }
},
"logo_url": "https://cdn.example.com/acme-logo.png"
}
},
"error_detail": null,
"status_detail": {
"phase": "awaiting_position_ready",
"position_status": "idle"
}
}
]
}GET /api/integration/v1/candidates/{candidate_id}
Returns candidate identity plus interview lifecycle/result payloads.
Optional query filters:
position_idcompany_position_uuid
If both are sent, they must resolve to the same position.
GET /api/integration/v1/candidates
Lists candidates with optional filters:
company_candidate_uuidposition_idcompany_position_uuid
Soft Delete
Admin-scoped API keys can soft-delete operational resources through the Integration API.
| Endpoint | Effect |
|---|---|
DELETE /api/integration/v1/candidates/{candidate_id} | Marks the candidate as deleted, marks all of the candidate's interviews as deleted/discarded, and archives related applications |
DELETE /api/integration/v1/positions/{position_id} | Marks the position as deleted, marks all interviews for the position as deleted/discarded, and archives related applications |
Both endpoints return 204 No Content when the delete is accepted. Soft-deleted rows are hidden from normal list/detail reads, including position, candidate, and interview listings. Cross-company IDs return 404; viewer-scoped API keys return 403.
Soft delete does not hard-delete candidate, position, interview, or application records. It stamps deleted_at and deleted_by_company_user_id on the deleted resource; candidate and position deletes also stamp those fields on affected interviews and archive related applications.
White Label Configuration
The white_label object lets you override the branding shown to candidates during interviews and in email invitations. It is set per position and applies to all candidates created under that position.
Where white label is applied
- Email invitations — company name in subject, body, and footer
- Interview welcome screen — company name and logo displayed to the candidate
- Brand colors — foreground and background colors used in the interview UI
Object structure
{
"white_label": {
"company_name": "Acme Corp"
}
}Only overrides the company name. Logo and colors fall back to company-level settings.
{
"white_label": {
"company_name": "Acme Corp",
"brand_assets": {
"logo_url": "https://cdn.example.com/acme-logo.png",
"colors": {
"foreground": { "type": "hex", "value": "#1a1a2e" },
"background": { "type": "hex", "value": "#f0f0f5" }
}
}
}
}Field reference
| Field | Type | Required | Notes |
|---|---|---|---|
company_name | string | yes | Brand name shown to candidates (must not be empty) |
brand_assets | object | no | Container for visual branding |
brand_assets.logo_url | URL | no | HTTPS URL to brand logo |
brand_assets.colors | object | no | If provided, both foreground and background are required |
brand_assets.colors.foreground | object | yes† | { "type": "hex", "value": "#1a1a2e" } |
brand_assets.colors.background | object | yes† | { "type": "hex", "value": "#f0f0f5" } |
† Required only when colors is provided. You cannot specify one color without the other.
Fallback chain
When a white label field is omitted or null, the system falls back to company-level settings configured in the Hirevoice dashboard:
white_label.company_name→ company namewhite_label.brand_assets.logo_url→ company logowhite_label.brand_assets.colors→ company foreground/background colors
Updating white label
Use PATCH /api/integration/v1/positions/{position_id} with the white_label field to update branding after position creation. Set white_label to null to remove the override and revert to company-level branding.
Interview Lifecycle Mapping
External interview status values:
queuedprocessingpreprocessingreadyin_progresspostprocessingdoneerror
Mapping from internal status:
| Internal Status | External Status |
|---|---|
idle + status_detail.phase=awaiting_position_ready | queued |
idle | processing |
pre_processing | preprocessing |
ready | ready |
in_progress | in_progress |
completed | postprocessing |
post_processing | postprocessing |
post_processing_retry | postprocessing |
done | done |
error | error |
Each interview payload can include:
interview_urlresult(whendone)context(criteria + extract-data configuration)error_detail(when error)
Webhooks
Webhook events are queued per active webhook target for active API keys in the company.
Position events
position.created— fired when a position is created viaPOST /positionsposition.augmentation.idle— augmentation enqueuedposition.augmentation.processing— augmentation runningposition.augmentation.done— augmentation finished; criteria, prompts and applied settings are now populatedposition.augmentation.error— augmentation failedposition.augmentation.warning— augmentation has retried more than the warning threshold but has not yet failed
Candidate events
candidate.created— fired when a candidate is created viaPOST /candidates
Interview lifecycle events (status transitions)
| Event | When it fires |
|---|---|
interview.processing | Interview created (status idle) |
interview.preprocessing | Prep pipeline claimed the interview |
interview.ready | Prep complete; interview_url available |
interview.in_progress | Candidate started the interview call |
interview.postprocessing | Call ended; LLM evaluation running |
interview.done | Postprocessing finished; full result included in payload (see below) |
interview.error | Interview failed at any step |
Interview action events (candidate-driven)
These events represent specific candidate actions and are emitted in addition to the status-driven events above. They fire from the interview frontend / call lifecycle.
| Event | When it fires |
|---|---|
interview.candidate_page_visited | Candidate opened the interview link |
interview.candidate_terms_accepted | Candidate accepted terms before starting |
interview.candidate_started | Candidate clicked Start |
interview.started | Call connected (correlates with the click) |
interview.ended | Candidate ended the call |
interview.outreach_sent | Outreach was accepted by the delivery provider. Payload data.channel is "whatsapp" or "email" |
interview.outreach_failed | Outreach delivery failed. This can be emitted immediately when a send fails, or later when the email provider reports a terminal delivery event |
interview.processing_warning | A prep or postprocess step has retried more than the warning threshold |
interview.in_progress is the lifecycle/status event; interview.started is the action event that correlates with the candidate's click. The two are intentionally distinct and you may see both for the same interview.
Outreach action payloads
Both outreach action events use the standard webhook envelope. Their data object always includes:
interview_idcandidate_idcompany_candidate_uuidposition_idcompany_position_uuidstatus("outreach_sent"or"outreach_failed")
interview.outreach_sent adds channel-specific metadata:
| Field | Notes |
|---|---|
channel | "whatsapp" or "email" |
provider | Present for email sends, for example "our_email", "resend", or "smtp" |
template_key | Present for templated email sends, for example "invite_white_collar_v1" |
locale | Present for templated email sends, for example "en", "es", or "ca" |
provider_message_id | Present when the email provider returns a message ID |
delivery | Present for Google Meet autonomous email outreach |
For email, interview.outreach_sent means the provider accepted the message. If the provider later reports a bounce, suppression, or terminal failure, Hirevoice emits a separate interview.outreach_failed event for the same interview.
interview.outreach_failed includes the same resource identifiers plus failure metadata:
| Field | Notes |
|---|---|
channel | "email", "email_meet", or "whatsapp" |
provider | Present for email sends, for example "our_email", "resend", or "smtp" |
template_key | Present for templated email sends |
locale | Present for templated email sends |
error | Provider or delivery error string when available |
status_code | Email provider HTTP status code when available |
reason | Trigger/runtime failure reason when the send could not be started |
provider_message_id | Email provider message ID when available |
resend_event_type | Resend terminal event type, for example "email.bounced", "email.failed", or "email.suppressed" |
resend_webhook_id | Resend/Svix webhook delivery ID used for idempotency |
to | Recipient email addresses included in the provider event |
Email failure example:
{
"id": "evt_0195a5e7-5d29-7f2a-93a8-5da72c9fa123",
"type": "interview.outreach_failed",
"occurred_at": "2026-02-22T14:00:00+00:00",
"api_version": "v1",
"company_id": "22222222-2222-2222-2222-222222222222",
"data": {
"interview_id": "11111111-1111-1111-1111-111111111111",
"candidate_id": "33333333-3333-3333-3333-333333333333",
"company_candidate_uuid": "ext-cand-0099",
"position_id": "44444444-4444-4444-4444-444444444444",
"company_position_uuid": "ext-pos-001",
"status": "outreach_failed",
"channel": "email",
"provider": "resend",
"template_key": "invite_white_collar_v1",
"locale": "es",
"error": "email.bounced:bounced",
"provider_message_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
"resend_event_type": "email.bounced",
"resend_webhook_id": "msg_2YYH8J5uLte4P6w",
"to": ["ada@example.com"]
}
}Position augmentation payload
Position augmentation webhook payload data fields:
position_idcompany_position_uuidnamestatus
Delivery headers:
X-Hirevoice-Event-IdX-Hirevoice-Event-TypeX-Hirevoice-Event-TimeX-Hirevoice-Signature
Signature details:
- Format:
sha256=<hmac_hex> - Signed input:
<event_id>.<occurred_at>.<raw_json_payload> - Algorithm: HMAC-SHA256
- Secret: webhook target
signing_secret event_idin signature input is headerX-Hirevoice-Event-Id(UUID), not payload fieldid(evt_...).
Retry schedule (seconds): 10, 30, 120, 600, 1800, 7200
Example payload envelope:
{
"id": "evt_0195a5e7-5d29-7f2a-93a8-5da72c9fa123",
"type": "candidate.created",
"occurred_at": "2026-02-22T14:00:00+00:00",
"api_version": "v1",
"company_id": "22222222-2222-2222-2222-222222222222",
"data": {}
}interview.done payload (full result)
When postprocessing finishes, interview.done carries the full IntegrationInterviewResult in data.result. You do not need to call GET /candidates/{id} afterwards — the webhook contains everything the dashboard report uses.
{
"id": "evt_06a1439b-d444-74ae-8000-7f2ae84595ec",
"type": "interview.done",
"occurred_at": "2026-05-25T11:59:57.266671+00:00",
"api_version": "v1",
"company_id": "...",
"data": {
"interview_id": "...",
"candidate_id": "...",
"company_candidate_uuid": "...",
"position_id": "...",
"company_position_uuid": "...",
"status": "done",
"result": {
"evaluation": "{...raw JSON string...}",
"evaluation_parsed": [
{ "type": "hard-skill", "title": "...", "level": 3, "max_level": 5, "reason": "..." }
],
"transcript": "AI: Hi...\nUser: ...",
"duration": 415,
"video_url": "https://...",
"audio_url": "https://...",
"summary": "AI-generated executive summary",
"raw_notes": ["bullet 1", "bullet 2"],
"highlights": [
{
"title": "Strong system design",
"description": "...",
"explainability": { "claims": [/* citations */] }
}
],
"warnings": [
{
"title": "Vague on testing strategy",
"severity": "medium",
"reasoning": "...",
"followup_questions": ["..."],
"explainability": { "claims": [/* citations */] }
}
],
"evaluation_detailed": [
{
"type": "hard-skill",
"title": "...",
"level": 2,
"max_level": 5,
"reason": "...",
"confidence_level": "Medium",
"explainability": { "claims": [/* citations */] }
}
],
"transcript_turns": [
{ "role": "agent", "message": "Hi...", "time_in_call_secs": 0.5 },
{ "role": "user", "message": "Yes...", "time_in_call_secs": 3.2 }
],
"extracted_data": [
{ "key": "years_of_experience", "captured": 4 }
]
}
}
}result fields are null while the interview has not yet reached done. Do not rely on them in earlier events (interview.ready, interview.in_progress, interview.postprocessing). The previous interview.completed event has been renamed to interview.done — clients that consumed interview.completed must update to the new event name.
Rate Limits
The Integration API enforces a fixed-window rate limit per API key. Reads and writes have separate budgets so heavy read traffic never starves writes (and vice versa).
| Bucket | Verbs | Limit |
|---|---|---|
read | GET | 100 requests / minute |
write | POST, PATCH, PUT, DELETE | 30 requests / minute |
Every authenticated response includes these headers so you can track usage proactively:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Max requests allowed in the current window for this bucket |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | UTC Unix epoch (seconds) when the window resets |
When the limit is exceeded the API returns 429 Too Many Requests with an additional header:
Retry-After: <seconds until window reset>Implement exponential backoff on retries to avoid hammering the API during traffic spikes. Limits are per API key — different keys for the same company have independent budgets.
Errors and Troubleshooting
Domain-level error mapping:
409 Conflict: duplicate external identifiers.404 Not Found: resource not found in company scope.400 Bad Request: validation failures.401 Unauthorized: invalid/missing/expired/revoked key or invalid JWT.422 Unprocessable Entity: schema-level validation failures (e.g. passing bothcv_urlandcandidate_data).429 Too Many Requests: rate limit exceeded. Respect theRetry-Afterheader.
Runtime error shape:
{
"detail": "Human-readable message"
}Common checks when requests fail:
- Confirm auth header type matches endpoint group.
- Confirm API key is active and not expired.
- Confirm position selector values are valid and in company scope.
- Confirm
cv_urlis publicly reachable and points to PDF content.
Upcoming
A WhatsApp-based interview flow for blue-collar positions, including a built-in document gathering step. See the WhatsApp & Blue-collar preview docs for the agent behavior and the document gathering schema.