Hirevoice Docs

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

sequenceDiagram autonumber actor Client as Client (ATS/Backend) participant API as Hirevoice API participant Pos as Position Augmentation participant Int as Interview Orchestrator participant WH as Your Webhook Endpoint Note over Client,API: Step 1: API key created in Dashboard Client->>API: Step 2: POST /positions API-->>Client: 201 Position created API->>WH: position.created API->>WH: position.augmentation.idle API->>Pos: Start augmentation (status=idle) Pos->>WH: position.augmentation.processing Client->>API: Step 3: POST /candidates API-->>Client: 201 Candidate + Interview(status=queued) API->>WH: candidate.created Note over API,Int: Interview remains queued while position != done alt Position augmentation done Pos->>WH: position.augmentation.done Pos->>Int: Position ready signal Int->>WH: interview.processing Int->>WH: interview.preprocessing Int->>WH: interview.ready Int->>WH: interview.postprocessing Int->>WH: interview.done API-->>Client: Poll candidates/interviews => done else Position augmentation error Pos->>WH: position.augmentation.error Int->>WH: interview.error API-->>Client: Poll candidates/interviews => error end

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

FieldTypeRequiredDefaultNotes
namestringyesPosition title
job_descriptionstringyesFull job description text or HTML
company_position_uuidstringyesYour external position ID (unique per company)
interview_typestringno"white-collar""white-collar" (Google Meet) or "blue-collar" (phone call)
interview_channelstringno"chat:google_meet"Interview channel type
locationstringnonullFree-text location label
job_modestringnonullremote, hybrid, onsite, or unspecified. Also accepts Remote, Hibird, Presential
languagestringno"en"Interview language: en, es, or ca
durationintegernonullInterview duration in minutes (min: 1)
challenge_levelintegerno5AI challenge intensity, 1–10
depth_levelintegerno5Question depth, 1–10
auto_outreachbooleannofalseAuto-send invitation when interview is ready (email for white-collar, WhatsApp for blue-collar)
evaluation_criteriastring[]no[]Criteria labels for AI evaluation
extract_dataobject[]no[]Fields to extract from the call. Each: { "key": "...", "description": "..." }
white_labelobjectnonullPer-position branding override (see White Label Configuration)

Notes:

  • company_position_uuid must be unique per company.
  • duration is expressed in minutes and converted to seconds internally.
  • extract_data specifies 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 idle or processing, 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:

FieldNotes
namePosition name
job_descriptionChanges here trigger position re-augmentation
locationFree-text location label
job_moderemote / hybrid / onsite
languageen / es / ca
durationMinutes
challenge_level1..10
depth_level1..10
evaluation_criteriaReplaces all criteria; triggers re-augmentation
extract_dataReplaces all extract-data fields; triggers re-augmentation
white_labelPer-position branding override
auto_outreachToggle automatic outreach
update_cascadetrue 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

FieldTypeRequiredDefaultNotes
company_candidate_uuidstringyesYour external candidate ID (unique per company)
first_namestringyesCandidate first name
last_namestringyesCandidate last name
position_idUUIDone of†Internal Hirevoice position ID
company_position_uuidstringone of†Your external position ID
cv_urlURLnonullPublic URL to candidate CV (PDF). Mutually exclusive with candidate_data
candidate_dataobjectnonullPre-parsed candidate timeline (see below). Mutually exclusive with cv_url
emailstringnonullCandidate email
phonestringnonullCandidate phone number (required for blue-collar / phone interviews)
interview_languagestringnonullOverride 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_uuid must be unique per company.
  • company_candidate_uuid and company_position_uuid are your external identifiers; use them in your system so you do not need to store Hirevoice resource IDs.
  • cv_url and candidate_data are mutually exclusive — passing both returns 422.
  • cv_url must 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_id
  • company_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_uuid
  • position_id
  • company_position_uuid

Soft Delete

Admin-scoped API keys can soft-delete operational resources through the Integration API.

EndpointEffect
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

FieldTypeRequiredNotes
company_namestringyesBrand name shown to candidates (must not be empty)
brand_assetsobjectnoContainer for visual branding
brand_assets.logo_urlURLnoHTTPS URL to brand logo
brand_assets.colorsobjectnoIf provided, both foreground and background are required
brand_assets.colors.foregroundobjectyes†{ "type": "hex", "value": "#1a1a2e" }
brand_assets.colors.backgroundobjectyes†{ "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:

  1. white_label.company_name → company name
  2. white_label.brand_assets.logo_url → company logo
  3. white_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:

  • queued
  • processing
  • preprocessing
  • ready
  • in_progress
  • postprocessing
  • done
  • error

Mapping from internal status:

Internal StatusExternal Status
idle + status_detail.phase=awaiting_position_readyqueued
idleprocessing
pre_processingpreprocessing
readyready
in_progressin_progress
completedpostprocessing
post_processingpostprocessing
post_processing_retrypostprocessing
donedone
errorerror

Each interview payload can include:

  • interview_url
  • result (when done)
  • 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 via POST /positions
  • position.augmentation.idle — augmentation enqueued
  • position.augmentation.processing — augmentation running
  • position.augmentation.done — augmentation finished; criteria, prompts and applied settings are now populated
  • position.augmentation.error — augmentation failed
  • position.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 via POST /candidates

Interview lifecycle events (status transitions)

EventWhen it fires
interview.processingInterview created (status idle)
interview.preprocessingPrep pipeline claimed the interview
interview.readyPrep complete; interview_url available
interview.in_progressCandidate started the interview call
interview.postprocessingCall ended; LLM evaluation running
interview.donePostprocessing finished; full result included in payload (see below)
interview.errorInterview 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.

EventWhen it fires
interview.candidate_page_visitedCandidate opened the interview link
interview.candidate_terms_acceptedCandidate accepted terms before starting
interview.candidate_startedCandidate clicked Start
interview.startedCall connected (correlates with the click)
interview.endedCandidate ended the call
interview.outreach_sentOutreach was accepted by the delivery provider. Payload data.channel is "whatsapp" or "email"
interview.outreach_failedOutreach delivery failed. This can be emitted immediately when a send fails, or later when the email provider reports a terminal delivery event
interview.processing_warningA 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_id
  • candidate_id
  • company_candidate_uuid
  • position_id
  • company_position_uuid
  • status ("outreach_sent" or "outreach_failed")

interview.outreach_sent adds channel-specific metadata:

FieldNotes
channel"whatsapp" or "email"
providerPresent for email sends, for example "our_email", "resend", or "smtp"
template_keyPresent for templated email sends, for example "invite_white_collar_v1"
localePresent for templated email sends, for example "en", "es", or "ca"
provider_message_idPresent when the email provider returns a message ID
deliveryPresent 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:

FieldNotes
channel"email", "email_meet", or "whatsapp"
providerPresent for email sends, for example "our_email", "resend", or "smtp"
template_keyPresent for templated email sends
localePresent for templated email sends
errorProvider or delivery error string when available
status_codeEmail provider HTTP status code when available
reasonTrigger/runtime failure reason when the send could not be started
provider_message_idEmail provider message ID when available
resend_event_typeResend terminal event type, for example "email.bounced", "email.failed", or "email.suppressed"
resend_webhook_idResend/Svix webhook delivery ID used for idempotency
toRecipient 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_id
  • company_position_uuid
  • name
  • status

Delivery headers:

  • X-Hirevoice-Event-Id
  • X-Hirevoice-Event-Type
  • X-Hirevoice-Event-Time
  • X-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_id in signature input is header X-Hirevoice-Event-Id (UUID), not payload field id (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).

BucketVerbsLimit
readGET100 requests / minute
writePOST, PATCH, PUT, DELETE30 requests / minute

Every authenticated response includes these headers so you can track usage proactively:

HeaderMeaning
X-RateLimit-LimitMax requests allowed in the current window for this bucket
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUTC 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 both cv_url and candidate_data).
  • 429 Too Many Requests: rate limit exceeded. Respect the Retry-After header.

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_url is 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.

On this page