VoxScreen Public API

Invite candidates to a one-way video interview from your own systems, and get AI-scored results back the moment they are ready. Every endpoint, payload and event is documented on this page.

No sales call, no access request. Create a key in your account settings and start in about a minute.

Base URL

https://voxscreen.com/api/integrations/v1

This is a published contract. Fields get added, never removed or renamed. A breaking change would arrive as a new version at /api/integrations/v2.

Quickstart

Four steps from nothing to a scored candidate arriving at your own endpoint.

  1. 1

    Create an API key

    In VoxScreen, open Settings and then Integrations. The key is shown once, so copy it before you close the dialog. Every plan has API access, the free one included.

  2. 2

    Check that it works

    If this returns your account name, you are connected.

    curl
    curl https://voxscreen.com/api/integrations/v1/me \
      -H "Authorization: Bearer vs_live_YOUR_KEY"
  3. 3

    Invite a candidate

    Use an interview id from GET /interviews. The candidate gets the invitation email straight away.

    curl
    curl -X POST https://voxscreen.com/api/integrations/v1/candidates/invite \
      -H "Authorization: Bearer vs_live_YOUR_KEY" \
      -H "Content-Type: application/json" \
      -d '{
            "interview_id": "YOUR_INTERVIEW_ID",
            "email": "alex.silva@example.com",
            "full_name": "Alex Silva"
          }'
  4. 4

    Get the result back

    Subscribe to submission.scored and we POST to your endpoint as soon as the answers are transcribed and scored. Keep the secret from the response: you need it to verify deliveries.

    curl
    curl -X POST https://voxscreen.com/api/integrations/v1/hooks \
      -H "Authorization: Bearer vs_live_YOUR_KEY" \
      -H "Content-Type: application/json" \
      -d '{
            "event": "submission.scored",
            "target_url": "https://example.com/webhooks/voxscreen"
          }'

Authentication

Every request needs an API key, sent in either header. Keys start with vs_live_ followed by 43 random characters.

curl
curl https://voxscreen.com/api/integrations/v1/me \
  -H "Authorization: Bearer vs_live_YOUR_KEY"

# X-API-Key works too, if that suits your client better
curl https://voxscreen.com/api/integrations/v1/me \
  -H "X-API-Key: vs_live_YOUR_KEY"

A key belongs to an account, not to a person, so removing an employee's login does not break the automations they set up.

API access is included on every plan. A 402 means the subscription has lapsed, not that the plan is too small.

Endpoints

All paths are relative to the base URL above. Responses are JSON.

GET/me

Identify the account

Returns the account behind the key. Useful as a connection test, which is exactly what Zapier and Make use it for.

Response

JSON
{
  "account_id": "3f2b8c14-0000-4000-8000-000000000001",
  "account_name": "Acme Inc.",
  "plan": "starter"
}
GET/interviews

List interviews

Newest first. Most integrations use this to populate a dropdown so a user can pick which interview candidates should be invited to.

Query parameters

statusstringdraft, active, closed or archived.
limitinteger1 to 200, default 100.

Response

JSON
[
  {
    "id": "9c1a0f10-0000-4000-8000-000000000001",
    "title": "Backend Tech Lead",
    "description": "Screening for a backend tech lead role.",
    "status": "active",
    "language": "en",
    "time_limit_seconds": 1800,
    "expires_at": "2026-09-01T23:59:00Z",
    "created_at": "2026-08-01T12:00:00Z"
  }
]
POST/candidates/invite

Invite a candidate

Creates the candidate if the email is new to your account and reuses the existing record otherwise, so re-running the same call does not duplicate anyone. Sends the invitation email and counts against your monthly candidate quota.

The interview must be active. Returns 400 if it is not, if the email is disposable, or if the quota is exhausted.

Request

JSON
{
  "interview_id": "9c1a0f10-0000-4000-8000-000000000001",
  "email": "alex.silva@example.com",
  "full_name": "Alex Silva"
}

Response

JSON
{
  "invitation_id": "44be0000-0000-4000-8000-000000000001",
  "candidate_id": "7d900000-0000-4000-8000-000000000001",
  "interview_id": "9c1a0f10-0000-4000-8000-000000000001",
  "status": "sent"
}
GET/submissions

List submissions

Newest finished first, so a poller can stop reading once it recognises an id. Poll with status=scored and since set to your last run to pick up new results.

Every submission carries results_url, a link to the transcript, scores and recording in VoxScreen. Pass it along in a Slack message or an ATS note so whoever reads it can open the result in one click. Preview submissions, which are recruiters testing their own interview, are never included.

Query parameters

statusstringpending, in_progress, completed, scoring, scored or expired.
interview_iduuidRestrict to a single interview.
sinceISO 8601Only submissions completed at or after this time.
limitinteger1 to 100, default 50.

Response

JSON
[
  {
    "id": "d46e6040-0000-4000-8000-000000000001",
    "interview_id": "9c1a0f10-0000-4000-8000-000000000001",
    "candidate_id": "7d900000-0000-4000-8000-000000000001",
    "status": "scored",
    "results_url": "https://voxscreen.com/submissions/d46e6040-0000-4000-8000-000000000001",
    "started_at": "2026-08-07T16:01:15Z",
    "completed_at": "2026-08-07T16:08:40Z",
    "overall_score": 4.2,
    "percentile": 78.0,
    "language_proficiency": "B2",
    "language_proficiency_reasoning": "Fluent, occasional hesitation.",
    "ai_summary": "Strong communication and relevant backend experience.",
    "created_at": "2026-08-07T16:00:02Z",
    "interview_title": "Backend Tech Lead",
    "candidate_name": "Alex Silva",
    "candidate_email": "alex.silva@example.com"
  }
]
GET/submissions/{id}

Get one submission

Same shape as a list entry. Returns 404 for a submission belonging to another account, never 403.

Response

JSON
{
  "id": "d46e6040-0000-4000-8000-000000000001",
  "status": "scored",
  "results_url": "https://voxscreen.com/submissions/d46e6040-0000-4000-8000-000000000001",
  "overall_score": 4.2,
  "language_proficiency": "B2",
  "ai_summary": "Strong communication and relevant backend experience.",
  "candidate_name": "Alex Silva",
  "candidate_email": "alex.silva@example.com"
}
POST/hooks

Subscribe to an event

Registers a URL we POST to when the event fires. The signing secret is in this response and nowhere else.

target_url must be https. Subscribing the same key to the same event and URL twice returns the existing subscription instead of creating a duplicate, so a retried call is safe.

Request

JSON
{
  "event": "submission.scored",
  "target_url": "https://example.com/webhooks/voxscreen"
}

Response

JSON
{
  "id": "b17c0000-0000-4000-8000-000000000001",
  "event": "submission.scored",
  "target_url": "https://example.com/webhooks/voxscreen",
  "is_active": true,
  "api_key_id": "a1110000-0000-4000-8000-000000000001",
  "created_at": "2026-08-08T02:00:00Z",
  "secret": "o6aZJTxJsC9fj18nzONu8K964MRVdAfQ"
}
GET/hooks

List subscriptions

Only the subscriptions belonging to the calling key. Subscriptions you created in the dashboard are not listed here.

Response

JSON
[
  {
    "id": "b17c0000-0000-4000-8000-000000000001",
    "event": "submission.scored",
    "target_url": "https://example.com/webhooks/voxscreen",
    "is_active": true,
    "api_key_id": "a1110000-0000-4000-8000-000000000001",
    "created_at": "2026-08-08T02:00:00Z"
  }
]
DELETE/hooks/{id}

Unsubscribe

Removes the subscription and stops all further deliveries.

Response

204 No Content

Webhooks

Rather than polling, subscribe to an event and we POST to your URL as it happens. This is the pattern Zapier calls a REST Hook.

Events

submission.scored
Answers have been transcribed and scored. Carries the overall score, CEFR language level and AI summary. This is the one most integrations want.
submission.completed
The candidate finished recording, before scoring runs. Useful for acknowledgements, where waiting for the AI pipeline would add a needless delay.
candidate.invited
A candidate was invited to an interview.

What a delivery looks like

HTTP
POST /webhooks/voxscreen HTTP/1.1
Content-Type: application/json
User-Agent: VoxScreen-Webhooks/1
X-VoxScreen-Event: submission.scored
X-VoxScreen-Delivery: 18dbc0a7-b565-4edd-9696-0f730472d626
X-VoxScreen-Signature: sha256=0ed8eb269ad62155c2b7e60a1260f81f0…
JSON
{
  "event": "submission.scored",
  "created_at": "2026-08-08T02:09:04.713338+00:00",
  "data": {
    "submission_id": "d46e6040-0000-4000-8000-000000000001",
    "results_url": "https://voxscreen.com/submissions/d46e6040-0000-4000-8000-000000000001",
    "status": "scored",
    "overall_score": 4.2,
    "percentile": 78.0,
    "language_proficiency": "B2",
    "ai_summary": "Strong communication and relevant backend experience.",
    "started_at": "2026-08-07T16:01:15Z",
    "completed_at": "2026-08-07T16:08:40Z",
    "interview": {
      "id": "9c1a0f10-0000-4000-8000-000000000001",
      "title": "Backend Tech Lead"
    },
    "candidate": {
      "id": "7d900000-0000-4000-8000-000000000001",
      "full_name": "Alex Silva",
      "email": "alex.silva@example.com"
    }
  }
}

candidate.invited carries invitation_id, status, sent_at, interview and candidate instead.

X-VoxScreen-Delivery is unique per attempt. Use it to make your handler idempotent, since a retry repeats the payload.

Verifying a delivery

Compute HMAC-SHA256 over the raw request body using your subscription secret, and compare it in constant time.

Python

Python
import hashlib
import hmac

def is_valid(secret: str, raw_body: bytes, signature_header: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

Node.js

JavaScript
const crypto = require("crypto");

function isValid(secret, rawBody, signatureHeader) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Verify against the bytes you received, not a re-serialised object. Any difference in whitespace or key order changes the signature.

Retries and failures

Any non-2xx response is retried five times with exponential backoff, so a single event is attempted up to six times in all. Respond 2xx quickly and do the work asynchronously: deliveries time out after 10 seconds.

  • ·30s · 60s · 120s · 240s · 480s
  • ·Return 410 Gone to unsubscribe immediately. This is how a consumer signals that an endpoint is retired, and it stops all further deliveries.
  • ·Attempts are counted one by one, retries included, and a subscription switches off after 10 failures in a row. Because each event is attempted six times, that is under two failed events, or roughly 20 minutes of an endpoint being down. Any 2xx resets the count to zero. A disabled subscription stays visible in your settings, so alert on your own endpoint rather than relying on us to keep trying.

Errors and limits

Errors return {"detail": "..."}. Validation errors return an array under detail naming each offending field.

401Missing, malformed, unknown or revoked key.
402Valid key, but the subscription has lapsed. Plan does not matter; every plan has API access. The body carries current_plan.
404The resource does not exist, or belongs to another account. Deliberately indistinguishable, so the API never confirms an id exists.
422Validation failure. The body lists the offending fields.
429Rate limit exceeded.

120 requests per minute, per key, per endpoint. Zapier and Make poll well inside this.

Prefer no code?

You do not have to write any of this. Zapier and Make connect VoxScreen to thousands of tools, and you can add a webhook straight from Settings and then Integrations without touching an API key.

See the integrations