For developers

API Reference

Everything in the AI interviewing workflow, available over plain REST and JSON. Create interviews, invite candidates at scale, read scored results, and get webhooks the moment a report is ready.

Base URLhttps://api.airbasehq.com/v1

Getting started

The API is a versioned REST surface under /v1. Requests and responses are JSON. Resources are addressed by short numeric codes, never internal ids, so everything you see in a response is safe to store in your own systems.

You need one thing to start: an API key. Create it in the app under Workspace, then API Keys. The full key is shown once at creation, so copy it right away and store it like a password.

Your first request
curl https://api.airbasehq.com/v1/me \
  -H "Authorization: Bearer $API_KEY"
Response 200
{
  "company": { "name": "Acme Inc." },
  "apiKey": {
    "name": "Production automation",
    "permissions": {
      "canManageInterviews": true,
      "canManageApplications": true
    }
  }
}

Authentication

Every request carries your API key as a bearer token. Keys are scoped to your workspace, so no company id ever appears in a path.

Keys look like cuee_<keyId>_<secret>. Only a hash of the secret is stored on our side, which is why the key is shown a single time when you create it. Keys can be given an expiry date and can be revoked at any time from the same page.

Auth header
Authorization: Bearer cuee_0123456789abcdef0123456789abcdef_<64 hex chars>
Content-Type: application/json
  • A missing or malformed key returns 401 authentication_required. A revoked or expired key returns 401 with invalid_api_key or api_key_expired.
  • A valid key that lacks a required permission returns 403 insufficient_permissions.

Permissions

When you create a key you choose what it can do. Grant only what your integration needs. These three scopes cover the whole public surface today:

ScopeLabel in the appWhat it unlocks
canManageInterviewsInterviewsCreate and manage AI interviews, invite candidates, review interview results.
canManageApplicationsTalent PoolView and manage candidates, applicants, and related application records.
canViewApplicationsView ApplicationsRead application and candidate records. Also required for managing webhooks.
  • Keys created in AI Interviewer workspaces automatically carry this bundle, no picking required.
  • Endpoints list their required scopes below. Where two scopes are listed, the key needs both.

Rate limits

Each API key may make 200 requests per 60 seconds across the whole /v1 surface. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Exceeding the limit returns HTTP 429 with a Retry-After header, so a well behaved client can simply wait and retry.

Batch endpoints exist so you rarely need many requests: a single invitation call accepts up to 500 candidates.

Errors

Errors use one stable envelope everywhere. The code field is a machine readable constant that will never be renamed, so branch on it rather than on the message.

Error envelope
{
  "error": {
    "code": "ai_interview_not_published",
    "message": "Publish the interview before inviting candidates."
  }
}
HTTPCodesMeaning
400invalid_request, validation_failedThe request body or query failed validation.
401authentication_required, invalid_api_key, api_key_expiredThe key is missing, malformed, revoked, or expired.
403insufficient_permissions, ai_interview_not_publishedThe key lacks a scope, or the interview cannot accept candidates yet.
404not_found, ai_interview_not_found, candidate_not_foundThe resource does not exist in your workspace.
429rate limitedToo many requests. Honor the Retry-After header.
500operation_failedSomething failed on our side. Safe to retry with backoff.

Workspace

Identify the workspace behind a key and inspect what the key may do. Useful as a connection test.

Who am I

GET/v1/me

Returns the company name and the permissions of the calling key. Works with any valid key, no scope required. Integration platforms use this to label the connected account.

Request
curl https://api.airbasehq.com/v1/me \
  -H "Authorization: Bearer $API_KEY"
Response 200
{
  "company": { "name": "Acme Inc." },
  "apiKey": {
    "name": "Zapier",
    "permissions": {
      "canManageInterviews": true,
      "canManageApplications": true
    }
  }
}

AI Interviews

An AI interview is a reusable screen: a title, optional role context, and questions. Publish it, then invite any number of candidates. Each candidate gets a personal link and the AI conducts, records, and scores the conversation.

Create an AI interview

POST/v1/ai-interviewsInterviews

Creates a standalone AI interview, for example one per role in your ATS. Leave questions empty to configure them later in the app, or pass up to 50.

Body

titlestringRequired1 to 255 chars

Name of the interview, usually the role it screens for.

contentstringOptionalup to 10,000 chars

Role context the AI interviewer uses to steer the conversation.

questionsstring[]Optionalup to 50 items

The questions to ask. Each 1 to 1,000 chars.

rubricTemplateCodenumberOptional

Scorecard to grade against. Defaults to your workspace scorecard.

status"draft" | "published"Optionaldefault draft

Only published interviews can receive candidates.

Request
curl -X POST https://api.airbasehq.com/v1/ai-interviews \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Backend Engineer AI Screen",
    "content": "Screen for Node.js and distributed systems experience.",
    "questions": ["Tell me about a service you built and scaled."],
    "status": "published"
  }'
Response 201
{
  "interview": {
    "code": 27,
    "title": "Backend Engineer AI Screen",
    "content": "Screen for Node.js and distributed systems experience.",
    "status": "published",
    "createdAt": "2026-07-01T09:12:00.000Z",
    "updatedAt": "2026-07-01T09:12:00.000Z"
  }
}

List AI interviews

GET/v1/ai-interviewsInterviews

Pageable list of your interviews with candidate counts. Filter by status or search by title.

Query

pageintegerOptionaldefault 1

Page number.

perPageintegerOptional1 to 100, default 20

Results per page.

statusstringOptional

One of draft, published, paused, archived.

searchstringOptional2 to 500 chars

Title search.

Request
curl "https://api.airbasehq.com/v1/ai-interviews?status=published&perPage=20" \
  -H "Authorization: Bearer $API_KEY"
Response 200
{
  "interviews": [
    {
      "code": 27,
      "title": "Backend Engineer AI Screen",
      "status": "published",
      "candidateCount": 42,
      "createdAt": "2026-07-01T09:12:00.000Z",
      "updatedAt": "2026-07-02T14:03:00.000Z"
    }
  ],
  "pagination": { "page": 1, "perPage": 20, "total": 1, "totalPages": 1 }
}

Get an AI interview

GET/v1/ai-interviews/:codeInterviews

Full detail for one interview, including its questions, channel, duration, and the scorecard it grades against. Fields not yet configured come back as null.

Path

codeintegerRequired

The interview code from create or list responses.

Response 200
{
  "interview": {
    "code": 27,
    "title": "Backend Engineer AI Screen",
    "content": "Screen for Node.js and distributed systems experience.",
    "status": "published",
    "questions": ["Tell me about a service you built and scaled."],
    "channel": "audio",
    "duration": 15,
    "scorecard": { "code": 12, "name": "Backend Engineer Scorecard" },
    "createdAt": "2026-07-01T09:12:00.000Z",
    "updatedAt": "2026-07-02T14:03:00.000Z"
  }
}

Invite candidates

POST/v1/ai-interviews/:code/invitationsInterviewsTalent Pool

Invites up to 500 candidates in one call and returns a personal interview link for each. Fully idempotent: re-inviting someone returns their existing link with status skipped, so retries are always safe. The interview must be published.

Body

candidatesobject[]Required1 to 500 items

Candidate objects, see fields below.

Candidate object (main fields)

emailstringOptionalvalid email

Required unless phone is given.

phonestringOptionalE.164 preferred

Required unless email is given. Required for phone interviews.

firstName / lastNamestringOptional

A name is required, either split or as fullName.

fullNamestringOptional

Alternative to split names. Split automatically.

externalIdstringOptional

Your own id for the candidate, echoed back on results.

preferredCommunicationChannel"email" | "sms"Optional

How the invitation is delivered.

country, city, locationstringOptional

Location hints.

linkedIn, portfolio, websitestringOptional

Profile links.

currentTitle, currentCompanystringOptional

Current position.

resumeTextstringOptionalup to 200,000 chars

Raw resume text. Parsed into the candidate profile.

source, referralSourcestringOptional

Attribution. referralSource wins if both are set.

experiences, educations, skills, certifications, languagesobject[]Optional

Structured profile data. Pass what you have, everything is optional.

Request
curl -X POST https://api.airbasehq.com/v1/ai-interviews/27/invitations \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "candidates": [
      {
        "fullName": "Ada Lovelace",
        "email": "[email protected]",
        "phone": "+14155552671",
        "externalId": "ats-4711"
      }
    ]
  }'
Response 200
{
  "summary": {
    "invitedCount": 1,
    "failedCount": 0,
    "skippedCount": 0,
    "phoneDroppedCount": 0
  },
  "candidates": [
    {
      "row": 1,
      "status": "invited",
      "applicationCode": 3,
      "candidateCode": 18,
      "email": "[email protected]",
      "phone": "+14155552671",
      "interviewUrl": "https://interviewer.cuee.ai/<token>",
      "warnings": [],
      "error": null
    }
  ]
}
  • Row status is invited, skipped, or failed. Skipped means the candidate was already invited and the same link is returned.
  • Interview links expire after roughly 30 days.
  • If the interview delivery setting is "Skip, deliver the link yourself", nothing is sent by us and you deliver interviewUrl through your own channel.

List candidates of an interview

GET/v1/ai-interviews/:code/candidatesTalent Pool

Pageable candidate list with current stage, processing status, and the latest score. The recently_scored sort returns only scored candidates, newest score first, which makes polling for fresh results a one liner.

Query

pageintegerOptionaldefault 1

Page number.

perPageintegerOptional1 to 100, default 50

Results per page.

sortstringOptionalnewest | recently_scored

Default newest. Use recently_scored to poll for results.

Request
curl "https://api.airbasehq.com/v1/ai-interviews/27/candidates?sort=recently_scored" \
  -H "Authorization: Bearer $API_KEY"
Response 200
{
  "candidates": [
    {
      "applicationCode": 3,
      "candidateCode": 18,
      "name": "Ada Lovelace",
      "email": "[email protected]",
      "phone": "+14155552671",
      "currentStage": { "name": "AI Interview", "category": "interview", "orderIndex": 2 },
      "isReady": true,
      "processingStatus": "completed",
      "appliedAt": "2026-07-02T10:00:00.000Z",
      "latestScore": { "overallScore": 82, "scoredAt": "2026-07-02T11:05:00.000Z" },
      "latestInterview": { "interviewStatus": "completed", "channel": "phone" }
    }
  ],
  "pagination": { "page": 1, "perPage": 50, "total": 1, "totalPages": 1 }
}

Get a candidate result

GET/v1/ai-interviews/:code/candidates/:applicationCodeTalent Pool

The full report for one candidate: profile, per criterion scores with evidence, question and answer pairs, interview attempts, and links to the recording and transcript.

Path

codeintegerRequired

The interview code.

applicationCodeintegerRequired

From invitation responses, candidate lists, or webhooks.

Response 200 (abridged)
{
  "candidate": {
    "applicationCode": 3,
    "candidateCode": 18,
    "name": "Ada Lovelace",
    "evaluation": {
      "overallScore": 82,
      "summary": "Strong communication and relevant systems experience.",
      "criteria": [
        {
          "criterionName": "Technical depth",
          "score": 85,
          "evidenceCommentary": "Described sharding a Postgres cluster in detail."
        }
      ]
    },
    "interviews": [
      {
        "attemptNumber": 1,
        "interviewStatus": "completed",
        "channel": "phone",
        "cheatStatus": "no_suspicion",
        "recordingUrl": "https://…signed…",
        "transcriptUrl": "https://…signed…"
      }
    ]
  }
}
  • recordingUrl and transcriptUrl are signed links that expire after about 4 hours. Fetch and store them promptly if you archive results.
  • Both are null until post interview processing finishes. Wait for the interview.scored webhook to know the report is ready.

Talent Pool

Your talent pool is every candidate your workspace has ever seen, searchable with AI. Add candidates directly, without scheduling anything, and find them later with natural language.

Search the talent pool

GET/v1/candidatesView Applications

One search box, two behaviors. An email, name, or phone number matches directly. Anything else runs semantic search over resumes, experience, skills, and past evaluations, so "senior React developer in Berlin" just works.

Query

searchstringOptional2 to 500 chars

Email, name, phone, or a natural language query. Omit to list newest first.

pageintegerOptionaldefault 1

Page number.

perPageintegerOptional1 to 100, default 20

Results per page.

Request
curl "https://api.airbasehq.com/v1/candidates?search=senior+react+developer+in+berlin" \
  -H "Authorization: Bearer $API_KEY"
Response 200
{
  "candidates": [
    {
      "candidateCode": 18,
      "name": "Ada Lovelace",
      "email": "[email protected]",
      "profileSummary": "Senior engineer, 8 years across fintech and infra.",
      "applicationCount": 2,
      "applications": [
        {
          "applicationCode": 3,
          "jobTitle": "Backend Engineer",
          "stageName": "AI Interview",
          "averageScore": 82
        }
      ],
      "match": {
        "distance": 0.18,
        "type": "semantic",
        "text": "Led a 6 person platform team building React tooling in Berlin."
      }
    }
  ],
  "pagination": { "page": 1, "perPage": 20, "total": 1, "totalPages": 1 }
}
  • match appears only on semantic hits. distance is cosine distance, lower means closer.

Add a candidate

POST/v1/candidatesTalent Pool

Adds one candidate to the talent pool and makes them searchable immediately. Deduped by email and phone: re-adding someone tops up missing fields and never overwrites what exists, so syncing from another system is safe.

Body

…candidate fieldsobjectOptional

Same candidate object as invitations: name plus email or phone required, everything else optional.

aboutCandidatestringOptionalup to 10,000 chars

Free notes stored on the profile.

Request
curl -X POST https://api.airbasehq.com/v1/candidates \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fullName": "Grace Hopper",
    "email": "[email protected]",
    "currentTitle": "Staff Engineer",
    "resumeText": "…"
  }'
Response 201
{
  "candidate": {
    "candidateCode": 19,
    "status": "created",
    "name": "Grace Hopper",
    "email": "[email protected]",
    "phone": null
  }
}
  • Returns 201 with status created for new candidates, or 200 with status existing when deduped.

Webhooks

Webhooks push events to your server the moment they happen, so you never poll. Subscribe an https URL to the events you care about and verify each delivery with its signature.

The headline event is interview.scored: it fires when the report is actually ready, scores and summary included, not merely when the call ends.

EventFires when
interview.scoredThe AI report is ready: overall score, summary, and criteria are available.
interview.completedThe interview session ended. Scoring may still be running.
interview.startedThe candidate began the interview.
interview.cancelledThe interview was cancelled.
candidate.appliedA candidate self registered through a shared interview link.
candidate.invitedA candidate was invited, via API or by a recruiter.

Create a webhook

POST/v1/webhooksView Applications

Subscribes an https endpoint to one or more events. The signing secret is returned once, on creation only. You can hold up to 10 active webhooks.

Body

urlstringRequiredhttps only

Your endpoint. Private and internal addresses are rejected.

eventsstring[]Required1 to 6 items

Events from the catalog above.

Request
curl -X POST https://api.airbasehq.com/v1/webhooks \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/airbase",
    "events": ["interview.scored", "candidate.applied"]
  }'
Response 201
{
  "webhook": {
    "id": "3e1f1a3e-1111-2222-3333-444455556666",
    "url": "https://example.com/hooks/airbase",
    "events": ["interview.scored", "candidate.applied"],
    "status": "active",
    "secret": "whsec_<64 hex chars>",
    "createdAt": "2026-07-01T09:12:00.000Z"
  }
}

List webhooks

GET/v1/webhooksView Applications

All subscriptions with delivery health: consecutive failures and the last success and failure. Secrets are never returned again.

Response 200
{
  "webhooks": [
    {
      "id": "3e1f1a3e-1111-2222-3333-444455556666",
      "url": "https://example.com/hooks/airbase",
      "events": ["interview.scored"],
      "status": "active",
      "consecutiveFailures": 0,
      "lastSuccessAt": "2026-07-02T11:05:03.000Z",
      "lastFailureAt": null,
      "lastFailureReason": null,
      "createdAt": "2026-07-01T09:12:00.000Z"
    }
  ]
}

Delete a webhook

DELETE/v1/webhooks/:webhookIdView Applications

Unsubscribes the endpoint. Deliveries stop immediately.

Path

webhookIduuidRequired

The webhook id from create or list.

Response 200
{ "deleted": true }

Receiving deliveries

Each delivery is a POST to your URL with the event name in the X-Cuee-Event header and a signed body. Respond with any 2xx within 10 seconds. Anything else is retried with exponential backoff, and after 20 consecutive failures the subscription is disabled, which you can see in the webhook list.

Delivery for interview.scored
POST /hooks/airbase HTTP/1.1
Content-Type: application/json
User-Agent: Cuee-Webhooks/1.0
X-Cuee-Event: interview.scored
X-Cuee-Delivery: 8b7f4c2a-9d3e-4f5a-b6c7-d8e9f0a1b2c3
X-Cuee-Signature: t=1751449503,v1=<hex hmac>

{
  "id": "8b7f4c2a-9d3e-4f5a-b6c7-d8e9f0a1b2c3",
  "event": "interview.scored",
  "createdAt": "2026-07-02T11:05:03.000Z",
  "data": {
    "interviewCode": 27,
    "jobTitle": "Backend Engineer AI Screen",
    "applicationCode": 3,
    "candidateCode": 18,
    "name": "Ada Lovelace",
    "email": "[email protected]",
    "phone": "+14155552671",
    "interviewStatus": "completed",
    "channel": "phone",
    "completedAt": "2026-07-02T10:58:41.000Z",
    "cheatStatus": "no_suspicion",
    "overallScore": 82,
    "summary": "Strong communication and relevant systems experience.",
    "summaryBullets": ["8 years backend experience", "Clear system design answers"],
    "scoredAt": "2026-07-02T11:05:00.000Z"
  }
}
Verify the signature (Node.js)
import { createHmac, timingSafeEqual } from 'node:crypto'

function verify(rawBody, signatureHeader, secret) {
  const { t, v1 } = Object.fromEntries(
    signatureHeader.split(',').map((part) => part.split('='))
  )
  // Reject replays older than 5 minutes
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false

  const expected = createHmac('sha256', secret)
    .update(t + '.' + rawBody)
    .digest('hex')
  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}
  • The signature is an HMAC SHA-256 of "<timestamp>.<raw body>" with your whsec secret. Always verify against the raw request body, before any JSON parsing.
  • Payloads contain numeric codes only, never internal ids. Use applicationCode with the candidate result endpoint to pull the full report.