Skip to content

AnswerReel docs: the REST API and webhooks

Send interview invites and read scores from your own systems, and get a webhook when a candidate submits, is scored or gets a label.

By The AnswerReel team. Updated .

Before you start

The API and webhooks are included on the Starter, Team and Agency plans, from $39 a month (see pricing). On Free, every request gets a 403 with plan_required, and no webhooks are sent. Your keys and webhook settings stay put if you move down a plan.

Only the Owner of an organization can create API keys. In AnswerReel, open Settings, then API and webhooks, and click Create key. The key is shown once, so copy it straight into your secrets manager. You can have 5 keys at a time and revoke any of them on the same page; requests with a revoked key get a 401. Revoking a key doesn't remove the webhooks it subscribed, so turn those off in Settings too.

Every request goes to https://answerreel.com/api/v1 over HTTPS and sends the key as a bearer token. Keys start with ar_live_.

List your jobs
curl https://answerreel.com/api/v1/jobs \
  -H "Authorization: Bearer ar_live_YOUR_API_KEY"

Requests, errors and limits

Request and response bodies are JSON. When something goes wrong you get an error code and a sentence written for people:

An error response (409)
{
  "error": "job_closed",
  "message": "This job is closed. Copy it to start a new one."
}

Check error in your code. The codes stay the same; the messages may be reworded.

Idempotency keys

Every POST and DELETE needs an Idempotency-Key header: a string of up to 200 characters that you make up, such as a UUID. Send the same key when you retry, and the action happens once. For 24 hours a repeat gets the first request's status code back.

  • We keep only the status code and the ID of what was created, and build the response again from current data. A repeat can show newer values than the first response did, and data is null if the candidate has since been deleted.
  • Idempotency keys are shared by all of your organization's API keys. Using one for a different request (another method, path or body) returns 422 idempotency_key_reused.
  • If the first request is still running, a repeat gets 409 request_in_progress. Try again a few seconds later.
  • If the first request got a 4xx, repeats get the same status with replayed_error. Fix the request and send it with a new key.
  • If it got a 500, retry with the same key after a minute. If the action went through, you get its result; if not, it runs again.

Rate limit and pagination

Each API key can make 60 requests per clock minute. Past that you get 429 rate_limited with a Retry-After header in seconds. Nothing is paginated: the job list comes back whole.

Error codes

API error codes
Code and status
invalid_json400The body isn't a JSON object.
idempotency_key_required400A POST or DELETE came without an Idempotency-Key header, or with one over 200 characters.
invalid_key401The API key is missing, mistyped or revoked.
plan_required403Your plan doesn't include the API.
quota_answers_used403You've used this month's answers.
quota_period_invites403You've reached this month's limit on new invites.
quota_daily_invites403You've reached today's limit on new invites.
quota_open_invites403Too many invites are still open.
job_not_found404No such job in your organization.
candidate_not_found404No such candidate in your organization, or the candidate was deleted.
webhook_not_found404No such webhook, or it was added in Settings rather than through the API.
job_closed409The job is closed.
job_needs_a_question409The job has no questions yet.
publish_not_checked409The job is a draft whose questions haven't passed our checks. Open it in AnswerReel and save it.
request_in_progress409A request with the same Idempotency-Key is still running.
invalid_email422The email address isn't valid.
invalid_name422The name is longer than 120 characters.
invalid_event422The event isn't one of the four listed under Webhooks.
invalid_url422The webhook address breaks a rule. The message says which.
idempotency_key_reused422This Idempotency-Key was already used for a different request.
replayed_error4xxA repeat of a request that failed. The status is the one the first request got.
rate_limited429More than 60 requests this minute. Wait the number of seconds in Retry-After.
server_error500Something failed on our side. Retry with the same Idempotency-Key after a minute.

Endpoints

List jobs

GET /api/v1/jobs

Every job except drafts, newest first. status is published or closed, and client is the client's name or null.

Response (200)
{
  "data": [
    {
      "id": "c2a4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81",
      "title": "Customer support lead",
      "client": "Harbor Logistics",
      "status": "published",
      "created_at": "2026-10-20T09:12:44.31+00:00"
    }
  ]
}

Send an invite

POST /api/v1/jobs/{job_id}/invites

Send email (required) and name (optional). It works like inviting one person from the job page: the same limits apply, it counts toward your new invites, and we email the candidate their own link. If the job is still a draft, the first invite publishes it, as it does in the app.

If that email already has an invite for this job that hasn't expired, bounced or been revoked, no second email goes out. You get 200, "existing": true and that candidate. A new invite returns 201. Its status starts as pending_send and becomes invited once the email is sent.

Request
curl -X POST \
  https://answerreel.com/api/v1/jobs/c2a4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81/invites \
  -H "Authorization: Bearer ar_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invite-maya-ruiz-support-lead" \
  -d '{"email": "maya.ruiz@example.com", "name": "Maya Ruiz"}'
Response (201)
{
  "data": {
    "job": {
      "id": "c2a4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81",
      "title": "Customer support lead"
    },
    "name": "Maya Ruiz",
    "email": "maya.ruiz@example.com",
    "label": null,
    "client": {
      "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "name": "Harbor Logistics"
    },
    "status": "pending_send",
    "ai_status": "none",
    "ai_version": 0,
    "total_score": null,
    "candidate_id": "3d1c7a52-9e64-4b8f-a0c2-7f5e1b9d4a26",
    "data_version": 1,
    "submitted_at": null,
    "scored_questions": null,
    "scorable_questions": null,
    "review_url": "https://answerreel.com/app/jobs/c2a4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81?candidate=3d1c7a52-9e64-4b8f-a0c2-7f5e1b9d4a26"
  },
  "existing": false
}

Get a candidate

GET /api/v1/candidates/{candidate_id}

Status, scores and label for one candidate, in the shape described under the candidate object. A deleted candidate returns 404 candidate_not_found.

Request
curl https://answerreel.com/api/v1/candidates/3d1c7a52-9e64-4b8f-a0c2-7f5e1b9d4a26 \
  -H "Authorization: Bearer ar_live_YOUR_API_KEY"
Response (200)
{
  "data": {
    "job": {
      "id": "c2a4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81",
      "title": "Customer support lead"
    },
    "name": "Maya Ruiz",
    "email": "maya.ruiz@example.com",
    "label": "next_round",
    "client": {
      "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "name": "Harbor Logistics"
    },
    "status": "submitted",
    "ai_status": "done",
    "ai_version": 1,
    "total_score": 4.2,
    "candidate_id": "3d1c7a52-9e64-4b8f-a0c2-7f5e1b9d4a26",
    "data_version": 9,
    "submitted_at": "2026-10-29T14:05:12.48+00:00",
    "scored_questions": 2,
    "scorable_questions": 2,
    "review_url": "https://answerreel.com/app/jobs/c2a4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81?candidate=3d1c7a52-9e64-4b8f-a0c2-7f5e1b9d4a26"
  }
}

Subscribe a webhook

POST /api/v1/webhooks

Send a url and one event. For several events, subscribe once per event. The response has the webhook's ID and its signing secret. The secret only comes back here, and on a repeat with the same Idempotency-Key, so store it. The address rules are under Webhooks; an address that breaks one gets 422 invalid_url.

Request
curl -X POST https://answerreel.com/api/v1/webhooks \
  -H "Authorization: Bearer ar_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: subscribe-scores-ready-1" \
  -d '{"url": "https://example.com/hooks/answerreel", "event": "candidate.ai_completed"}'
Response (201)
{
  "data": {
    "id": "5b0d3c1a-7f2e-4a9b-8c6d-1e3f5a7b9c20",
    "url": "https://example.com/hooks/answerreel",
    "event": "candidate.ai_completed"
  },
  "secret": "whsec_YOUR_SIGNING_SECRET"
}

Delete a webhook

DELETE /api/v1/webhooks/{webhook_id}

Removes a webhook that was added through the API, along with any deliveries still waiting for it. Webhooks added in Settings are turned off there instead; this endpoint returns 404 webhook_not_found for them. It needs an Idempotency-Key like any other DELETE.

Request
curl -X DELETE https://answerreel.com/api/v1/webhooks/5b0d3c1a-7f2e-4a9b-8c6d-1e3f5a7b9c20 \
  -H "Authorization: Bearer ar_live_YOUR_API_KEY" \
  -H "Idempotency-Key: unsubscribe-5b0d3c1a-7f2e-4a9b-8c6d-1e3f5a7b9c20"
Response (200)
{
  "data": {
    "id": "5b0d3c1a-7f2e-4a9b-8c6d-1e3f5a7b9c20",
    "deleted": true
  }
}

The candidate object

Invites, candidate lookups and webhooks all describe a candidate the same way. It never includes links to videos, audio or files.

Candidate fields
Field
candidate_idstringThe candidate's ID.
namestring or nullThe name from the invite or the apply form, if one was given.
emailstringAlways lowercase.
statusstringWhere the invite stands. The values are listed below.
submitted_atstring or nullWhen the candidate clicked Submit, as an ISO 8601 timestamp.
jobobjectThe job's id and title.
clientobject or nullThe id and name of the client the job is for, if you've set one.
total_scorenumber or nullFrom 1.0 to 5.0, with one decimal: the average of the questions that got a score (scored_questions). A question that couldn't be scored is left out, not counted as zero. null until scoring is done, or when no question got a score.
scored_questionsinteger or nullHow many questions got a score.
scorable_questionsinteger or nullHow many questions could have been scored. Short factual questions and file uploads are never scored.
ai_statusstringHow far the AI has got. The values are listed below.
ai_versionintegerGoes up each time the AI results are replaced, for example after you retry a recording that failed.
labelstring or nullYour team's label: next_round, maybe or not_fit.
data_versionintegerGoes up whenever anything in this object changes. Keep whichever copy has the higher number.
review_urlstringOpens the candidate in AnswerReel. Only people in your organization can open it.

status

Candidate status values
pending_sendThe invite exists, and the email hasn't gone out yet.
send_failedThe email couldn't be sent after several tries.
undeliverableThe first invite email bounced.
invitedThe email went out. The deadline counts from here.
in_progressThe candidate has started.
graceThe deadline passed while an upload or a follow-up the candidate started in time was unfinished. Lasts up to an hour.
submittingThe candidate clicked Submit and some recordings are still processing.
submittedSubmitted, with every answer saved.
expiredThe deadline passed before they submitted.
revokedSomeone on your team revoked the invite.

ai_status

AI status values
noneNothing to process yet: the candidate hasn't submitted.
waiting or processingTranscribing, summarizing and scoring.
doneFinished.
partialFinished, but at least one recording couldn't be transcribed. You can retry it from the candidate's page.
failedProcessing failed. You can retry it from the candidate's page.
offThe AI isn't used for this candidate: they asked for a person to review them, or AI is off for the job.

Webhooks

Add a webhook in Settings, under API and webhooks, where you can pick several events at once, or through the API above. Either way the address has to use https on port 443, must not contain a user name or password, and has to point at a public address on the internet. We look the domain up again before every delivery and connect to the address we checked.

Webhook events
Event
candidate.submittedA candidate submits and all of their answers are saved.
candidate.ai_completedThe AI has finished with a candidate's answers: transcripts, summary and, if the job is scored, scores. Sent again with a higher ai_version when the results are redone.
candidate.label_changedSomeone on your team sets, changes or clears a candidate's label.
candidate.deletedA candidate's data was deleted. data carries only candidate_id and deleted: true.

What a delivery looks like

Each delivery is a POST with a JSON body. data is the candidate object.

Body of a candidate.ai_completed delivery
{
  "id": "8f6f9a3e-5b1c-4f0e-9d7a-2c4b6e8a0f13",
  "type": "candidate.ai_completed",
  "sequence": 3,
  "data": {
    "job": {
      "id": "c2a4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81",
      "title": "Customer support lead"
    },
    "name": "Maya Ruiz",
    "email": "maya.ruiz@example.com",
    "label": "next_round",
    "client": {
      "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "name": "Harbor Logistics"
    },
    "status": "submitted",
    "ai_status": "done",
    "ai_version": 1,
    "total_score": 4.2,
    "candidate_id": "3d1c7a52-9e64-4b8f-a0c2-7f5e1b9d4a26",
    "data_version": 9,
    "submitted_at": "2026-10-29T14:05:12.48+00:00",
    "scored_questions": 2,
    "scorable_questions": 2,
    "review_url": "https://answerreel.com/app/jobs/c2a4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81?candidate=3d1c7a52-9e64-4b8f-a0c2-7f5e1b9d4a26"
  }
}
Headers
Content-Type: application/json
User-Agent: AnswerReel-Webhooks/1.0
AnswerReel-Event-Id: 8f6f9a3e-5b1c-4f0e-9d7a-2c4b6e8a0f13
AnswerReel-Event-Type: candidate.ai_completed
AnswerReel-Signature: t=1793282800,v1=<64 hex characters>
  • id is the event ID. The same event can reach you more than once, because we retry whenever we don't get a 2xx in time, so skip IDs you've already handled. A delivery you resend by hand from Settings gets a new ID.
  • sequence puts one candidate's events in order: a later event has a higher number. It can skip numbers.
  • A webhook tells you the current state, not a snapshot. We build data when we send it, so a retry can carry newer data than the first try did. Compare data.data_version with the copy you have and keep the higher one, whatever order deliveries arrive in.
  • Once a candidate is deleted, their earlier events that hadn't gone out yet are dropped and only candidate.deleted is sent. Deleting a candidate in AnswerReel doesn't delete copies you've made elsewhere; this event is your cue to do that.

Timing and retries

Deliveries go out once a minute. Reply with any 2xx within 10 seconds and do slow work after that. A different status, a timeout or a redirect counts as a failure; we don't follow redirects.

After a failure we try again 1 minute later, then after 10 minutes, after an hour, and then every 6 hours. If a webhook keeps failing for 3 days, we turn it off, drop what's still waiting and email the Owner. When you turn it back on in Settings, only new events are sent. The 50 most recent deliveries are listed there, and you can resend any of them.

Checking signatures

Every delivery is signed with the webhook's secret. Webhooks added in Settings show the secret once, when you add them; API subscriptions return it in the response. Check the signature before you trust a delivery:

  1. Read the raw request body as it arrived. Parsing and re-serializing the JSON changes the bytes, and the signature won't match.
  2. Split the AnswerReel-Signature header into t, the Unix time in seconds when we signed it, and v1, the signature.
  3. Compute the HMAC-SHA256 of t, a period and the raw body, using the whole secret (including whsec_) as the key, and hex-encode it.
  4. Compare it with v1 in constant time, and reject timestamps more than 5 minutes from your clock.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody is the request body exactly as it arrived, before any JSON parsing
export function verifyAnswerReelSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(String(header ?? "").split(",").map((part) => part.split("=", 2)));
  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest();
  const received = Buffer.from(parts.v1 ?? "", "hex");
  return received.length === expected.length && timingSafeEqual(received, expected);
}
Python
import hashlib
import hmac
import time


def verify_answerreel_signature(raw_body: bytes, header: str, secret: str) -> bool:
    """raw_body is the request body exactly as it arrived, before any JSON parsing."""
    try:
        parts = dict(part.split("=", 1) for part in (header or "").split(","))
        timestamp = int(parts["t"])
        if abs(time.time() - timestamp) > 300:
            return False
    except (KeyError, ValueError, OverflowError):
        return False
    signed = str(timestamp).encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected.encode(), parts.get("v1", "").encode())

Zapier and help

The AnswerReel Zapier integration runs on this same API: its three triggers are the first three events above, and its action sends an invite. If something here doesn't match what you see, write to support@answerreel.com.

Set up a job, then connect it

Start from our screening interview questions, or build a scorecard first. API keys, webhooks and Zapier come with Starter, $39 a month, and every plan above it.