Skip to main content
FastHire
API Reference — v1

Build on FastHire.

Search results, candidate records, outreach sequences, inbox threads, and interviews — read from and written to the same data your recruiters see in the app.

Part of this API is live today. Any Org Admin can generate a real fh_live_… key from Settings → Developer and call every endpoint marked Live below right now. Endpoints marked Planned aren’t implemented yet — their shapes here are current design intent, not something you can call today. See the full breakdown in Status & roadmap.

Introduction

The FastHire API is organized around the same objects recruiters work with in the app: searches (an AI sourcing run against a role), the candidates it surfaces, lists recruiters curate from results, sequences that run outreach across LinkedIn, email and in-app steps, the inbox those sequences populate, and interviews scheduled from a reply. Every object is scoped to one organization — your API key determines which org’s data you see.

The API accepts and returns application/json, uses standard HTTP response codes, and is versioned in the URL.

Base URL
https://api.thefasthire.com/v1

Authentication

Authenticate by sending a secret API key as a bearer token. Generate keys from Settings → Developer in the FastHire app — each key is scoped to one org. Planned Role-scoped keys (an ORG_ADMIN key that can manage sequences and connections, a RECRUITER key that can’t) are planned but not enforced today — right now, any key for an org can read and write everything that org’s data allows, regardless of which role generated it.

curl https://api.thefasthire.com/v1/searches \
  -H "Authorization: Bearer fh_live_51H8x...cQ2k"
Keep keys server-side. A key carries full read/write access to the org’s candidate and outreach data — never embed one in client-side code. Use fh_test_… keys against the sandbox org while integrating; they behave identically but never send real LinkedIn, email, or WhatsApp messages. Two guarantees hold for every key, live or test: a sequence can only ever be created as a DRAFT through the API — activation is app-only, so no key can trigger real outreach this way — and an interview created with an fh_test_… key skips its confirmation message and reminders entirely (the interview record itself is still created, so you can verify the full flow end to end).

Conventions

ConventionDetail
IdentifiersAll object ids are opaque strings — plain cuids, not prefixed by type. Don’t parse them or rely on a prefix.
Field casingResponse bodies use camelCase (e.g. matchScore, createdAt), matching the app’s own data model exactly.
TimestampsISO 8601, UTC, e.g. 2026-08-20T09:30:00Z. Fields ending in At follow this format.
MoneySalary fields are free-text strings (currentSalary, expectedSalary) — candidates self-report these in whatever format they reply with, so the API does not coerce them into a number/currency pair.
Idempotency PlannedPlanned: POST requests that create an object will accept an Idempotency-Key header, replaying the same key returning the original response instead of creating a duplicate. Not implemented yet — a repeated POST creates a duplicate object today.
Expansion PlannedPlanned: pass ?expand=field to inline a related object instead of just its id. Not implemented yet.

Errors

Errors return a 4xx/5xx status and a flat JSON body describing what went wrong.

{
  "error": "criteria.mandatory is required for sourceType=MANUAL"
}
StatusMeaning
400Malformed body, or a field failed validation.
401Missing, malformed, or revoked API key.
403The team member who created this key has been deactivated.
404Object doesn't exist, or belongs to a different org.
409 PlannedReserved for state-conflict errors once they exist — nothing returns this today.
429 PlannedReserved for rate limiting once it ships — there is no request cap today.
500Something failed on FastHire's side. Safe to retry.

Pagination & rate limits Planned

Not implemented yet. List endpoints return their full result set as a plain array in one response today — no envelope, and no limit/cursor params.

Planned shape, once cursor pagination ships:

{
  "data": [ /* … */ ],
  "has_more": true,
  "next_cursor": "eyJpZCI6ImNuZF85..."
}

There is also no rate limiting today — no enforced requests-per-minute cap and no X-RateLimit-* headers. The table below documents the planned headers.

HeaderMeaning
X-RateLimit-LimitRequests allowed in the current window.
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetUnix timestamp when the window resets.

Searches

A search is one AI sourcing run for a role — a natural-language brief, a pasted job description, or a manually-built set of structured criteria. Activating one spins up an agent that scores candidates against that criteria.

GET/v1/searchesLive

List searches for the org, most recent first.

FieldTypeDescription
statusoptionalPlannedstringNot implemented yet — every search is returned today.
DRAFTRUNNINGCOMPLETEFAILED
owner_idoptionalPlannedstringNot implemented yet.
Response
{
  "searches": [
    {
      "id": "cknq7x2z10001...",
      "name": "Senior Backend Engineer — Sydney",
      "sourceType": "JOB_DESCRIPTION",
      "createdAt": "2026-08-12T04:11:00Z",
      "updatedAt": "2026-08-12T04:19:00Z"
    }
  ]
}
POST/v1/searchesLive

Create a search from a structured criteria object — the same shape the app’s own search builder submits. Returns 201 with the new search’s id immediately; poll GET /v1/searches/:id for status.

FieldTypeDescription
sourceTyperequiredstring
NATURAL_LANGUAGEJOB_DESCRIPTIONMANUAL
criteriarequiredobjectStructured search criteria — see the request example below for the full shape (searchName, roles, companies, languages, education, location).
Request
curl https://api.thefasthire.com/v1/searches \
  -X POST -H "Authorization: Bearer $FASTHIRE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sourceType": "MANUAL",
    "criteria": {
      "searchName": "Senior Backend Engineer — Sydney",
      "roles": ["Senior Backend Engineer", "Staff Software Engineer"],
      "companies": [],
      "languages": ["English"],
      "education": {
        "fieldsOfStudy": ["Computer Science"],
        "degreeTypes": ["Bachelors"],
        "institutions": [],
        "excludedInstitutions": [],
        "minGraduationYear": null,
        "maxGraduationYear": null,
        "minEducationLevel": "Bachelors"
      },
      "location": {
        "radius": 50,
        "entries": [{ "city": "Sydney", "country": "Australia" }]
      }
    }
  }'
Response
{ "id": "cknq7x2z10001..." }
Planned A simplified shorthand — criteria.mandatory / criteria.desirable string arrays and a candidate_cap to stop sourcing early — is planned as an alternative to the full structured object above, but isn’t accepted today.
GET/v1/searches/:idLive

Retrieve one search, including its full criteria.

Response
{
  "id": "cknq7x2z10001...",
  "name": "Senior Backend Engineer — Sydney",
  "sourceType": "JOB_DESCRIPTION",
  "criteria": { /* the structured criteria object, as submitted */ },
  "createdAt": "2026-08-12T04:11:00Z",
  "updatedAt": "2026-08-12T04:19:00Z"
}
GET/v1/searches/:id/candidatesLive

List a search’s matched candidates, ranked by matchScore.

Response
{
  "candidates": [
    {
      "resultId": "cknq8f4h20007...",
      "candidateId": "cknq7z1a10004...",
      "name": "Priya Nair",
      "headline": "Staff Engineer at Canva",
      "location": "Sydney, Australia",
      "phone": null,
      "email": "priya.nair@example.com",
      "skills": ["Go", "Kubernetes", "PostgreSQL"],
      "experience": [ { "company": "Canva", "title": "Staff Engineer" } ],
      "education": [ { "school": "UNSW", "degree": "BSc Computer Science" } ],
      "matchScore": 94,
      "matchReasoning": "7 yrs backend, distributed systems at scale, AU-based.",
      "percentile": 98
    }
  ]
}

matchScore is 0–100. percentile is this candidate’s rank among the search’s results. There is no match-tier field.

Candidates

A candidate record is deduplicated per org by source profile, and accumulates results, list memberships, sequence history, and interviews as it moves through your pipeline.

GET/v1/candidates/:idLive

Retrieve a candidate’s full profile, including intake if they’ve replied.

Response
{
  "id": "cknq7z1a10004...",
  "name": "Priya Nair",
  "headline": "Staff Engineer at Canva",
  "email": "priya.nair@example.com",
  "phone": null,
  "linkedinUrl": "https://linkedin.com/in/priyanair",
  "location": "Sydney, Australia",
  "skills": ["Go", "Kubernetes", "PostgreSQL"],
  "experience": [ { "company": "Canva", "title": "Staff Engineer", "years": 3 } ],
  "education": [ { "school": "UNSW", "degree": "BSc Computer Science" } ],
  "intake": null /* populated once the candidate replies — see below */
}
GET/v1/candidates/:id/intakePlanned

A standalone intake endpoint doesn’t exist today — the same data is embedded on the candidate response above as intake (null until they reply), and on a conversation thread in the Inbox below. The fields, once populated:

FieldTypeDescription
statusstring
REQUESTEDCOMPLETED
resumestringFree text or a link — however the candidate provided it. No file storage.
contactNumberstringSelf-reported.
totalExperiencestringSelf-reported.
currentSalarystringSelf-reported, free text (see Conventions above).
expectedSalarystringSelf-reported, free text.
noticePeriodstringSelf-reported.
educationalQualificationstringSelf-reported.
interviewAvailabilitystringSelf-reported.
locationstringSelf-reported.
requestedAttimestampWhen FastHire asked this candidate for intake.
respondedAttimestampWhen the candidate completed it.

Lists

Lists are recruiter-curated candidate sets, independent of any one search — the unit sequences target for outreach.

GET/v1/listsLive

List the org’s lists.

Response
{
  "lists": [
    {
      "id": "cknqa1k3b0002...",
      "name": "Q3 backend shortlist",
      "createdAt": "2026-08-12T04:11:00Z",
      "candidateCount": 18
    }
  ]
}
POST/v1/listsLive
FieldTypeDescription
namerequiredstringList name.
Response
{ "id": "cknqa1k3b0002..." }
POST/v1/lists/:id/candidatesPlanned

Doesn’t exist today — candidates can only be added to a list from the app.

FieldTypeDescription
candidate_idsrequiredarrayUp to 100 candidate ids per call.
DELETE/v1/lists/:id/candidates/:candidate_idPlanned

Doesn’t exist today — removing a candidate from a list is app-only.

Sequences

A sequence is an outreach campaign: an ordered set of LinkedIn, email, and in-app steps run against one or more target lists.

GET/v1/sequencesLive
FieldTypeDescription
statusoptionalPlannedstringNot implemented yet — every sequence is returned today.
DRAFTACTIVEPAUSEDARCHIVED
Response
{
  "sequences": [
    {
      "id": "cknqb7h4c0003...",
      "name": "Backend eng outreach — Sydney",
      "status": "DRAFT",
      "channelStrategy": "LINKEDIN_FIRST",
      "createdAt": "2026-08-12T04:11:00Z",
      "updatedAt": "2026-08-12T04:19:00Z"
    }
  ]
}
POST/v1/sequencesLive

Create a sequence — it’s always created as DRAFT, whatever you pass. Sending "activate": true is rejected with a 400: activation is app-only, so no API key — fh_live_… or fh_test_… — can ever trigger real outreach through this endpoint.

FieldTypeDescription
namerequiredstringCampaign name.
channelStrategyrequiredstring
LINKEDIN_FIRSTINMAIL_FIRSTEMAIL_FIRSTFASTHIRE_FIRST
listIdsrequiredarrayLists to enroll candidates from. Active sequences pick up candidates added later, too.
stepsrequiredarrayOrdered steps: channel
LINKEDINEMAILIN_APP
, stepType
CONNECTMESSAGEFOLLOW_UP
, subject (email only), templateBody.
Response
{ "id": "cknqb7h4c0003..." }
GET/v1/sequences/:idLive

Retrieve one sequence, including its steps and target lists.

Response
{
  "id": "cknqb7h4c0003...",
  "name": "Backend eng outreach — Sydney",
  "status": "DRAFT",
  "channelStrategy": "LINKEDIN_FIRST",
  "context": "Prioritize distributed-systems experience and AU right-to-work." /* free-text brief used to personalize outreach copy */,
  "listIds": ["cknqa1k3b0002..."],
  "steps": [ /* … */ ],
  "createdAt": "2026-08-12T04:11:00Z",
  "updatedAt": "2026-08-12T04:19:00Z"
}
PATCH/v1/sequences/:idPlanned

Doesn’t exist today. Once shipped, this will be the way to activate or pause a campaign via API.

Request
curl https://api.thefasthire.com/v1/sequences/cknqb7h4c0003... \
  -X PATCH -H "Authorization: Bearer $FASTHIRE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "ACTIVE" }'
GET/v1/sequences/:id/candidatesPlanned

Doesn’t exist today. Once shipped, this will list enrolled candidates and where each stands.

FieldTypeDescription
statusstring
NOT_STARTEDIN_PROGRESSREPLIEDBOUNCEDCOMPLETEDPAUSED
next_action_attimestampWhen this candidate's next step is due to send.

Inbox & messages

Every send and reply across LinkedIn, email, and WhatsApp lands in one per-candidate thread.

GET/v1/inbox/conversationsLive

List conversations, most recently active first.

FieldTypeDescription
unreadoptionalPlannedbooleanNot implemented yet.
Response
{
  "conversations": [
    {
      "candidateId": "cknq7z1a10004...",
      "candidateName": "Priya Nair",
      "lastMessageBody": "Thanks for reaching out — happy to chat this week.",
      "lastMessageAt": "2026-08-19T02:04:00Z",
      "lastMessageDirection": "INBOUND",
      "channels": ["EMAIL", "LINKEDIN"],
      "hasUnread": true,
      "hasReplied": true,
      "connectionFailureReason": null,
      "intakeStatus": "COMPLETED"
    }
  ]
}
GET/v1/inbox/conversations/:candidate_idLive

Retrieve one candidate’s full thread across every channel, plus their sequence context and intake.

Response
{
  "candidateId": "cknq7z1a10004...",
  "candidateName": "Priya Nair",
  "headline": "Staff Engineer at Canva",
  "messages": [
    {
      "channel": "EMAIL",
      "direction": "INBOUND",
      "body": "Thanks for reaching out — happy to chat this week.",
      "sentAt": "2026-08-19T02:04:00Z"
    }
  ],
  "sequenceContext": { /* which sequence/step produced this thread, if any */ },
  "connectionFailureReason": null,
  "intake": null
}
POST/v1/inbox/conversations/:candidate_id/replyPlanned

Doesn’t exist today — sending a reply into a thread is app-only.

FieldTypeDescription
bodyrequiredstringMessage text.
channeloptionalstringDefaults to the channel of the candidate's last inbound message.

Interviews

Interviews link a candidate, a recruiter, and a time — optionally backed by a real Calendar connection.

GET/v1/interviewsLive
FieldTypeDescription
statusoptionalPlannedstringNot implemented yet.
CONFIRMEDCANCELLEDCOMPLETED
Response
{
  "interviews": [
    {
      "id": "cknqc2m5d0005...",
      "scheduledAt": "2026-08-22T05:30:00Z",
      "timezone": "Australia/Sydney",
      "status": "CONFIRMED",
      "meetingLink": "https://meet.google.com/abc-defg-hij",
      "whatsappReminderSentAt": null,
      "emailReminderSentAt": null,
      "createdAt": "2026-08-19T02:10:00Z",
      "candidate": { "id": "cknq7z1a10004...", "name": "Priya Nair", "email": "priya.nair@example.com" },
      "recruiter": { "id": "cknqzz1x90000...", "name": "Alex Chen", "email": "alex@example.com" }
    }
  ]
}
POST/v1/interviewsLive

The recruiter on the interview is always whoever created the API key — there’s no recruiter_id field to set it explicitly.

FieldTypeDescription
candidateIdrequiredstring
scheduledAtrequiredtimestampStored as UTC.
timezoneoptionalstringIANA zone for display, e.g. Asia/Kolkata. Defaults to UTC.
durationMinutesoptionalinteger15–240. Defaults to 30.
meetingLinkoptionalstringSkip auto-generation and set one manually. If omitted, FastHire generates one via Google Meet, Teams, or Calendly — whichever the recruiter has connected.
Response
{
  "interviewId": "cknqc2m5d0005...",
  "calendarConnected": true,
  "calendarProvider": "GOOGLE",
  "calendlyUsed": false,
  "whatsappSent": true,
  "emailSent": true
}
PATCH/v1/interviews/:idPlanned

Doesn’t exist today. Once shipped, this will reschedule an interview or update its status.

DELETE/v1/interviews/:idPlanned

Doesn’t exist today.

Webhooks Planned

None of this exists yet — no subscription UI, no signing, no event delivery system. Polling the relevant GET endpoint above is the only option until this ships. The event list and payload shape below are design intent, kept here so integrators can plan for the eventual shape.

EventFires when
search.completedAn agent finishes scoring and its status moves to COMPLETE or FAILED.
candidate.repliedAn inbound message arrives on any channel.
candidate.intake_completedAll intake fields are captured.
sequence.candidate_completedA candidate finishes, bounces, or pauses in a sequence.
interview.scheduledAn interview is created or confirmed.
Payload envelope
{
  "id": "evt_8pXeC...",
  "type": "candidate.replied",
  "created_at": "2026-08-19T02:04:00Z",
  "data": { /* the object, shaped like its REST response */ }
}

Each request would carry an X-FastHire-Signature header — an HMAC-SHA256 of the raw body using your endpoint’s signing secret — to verify before trusting the payload, with a 2xx response required within 10 seconds or FastHire retrying with backoff for up to 24 hours.

Status & roadmap

Reviewed against the deployed API on September 19, 2026.

Live today: reading and creating searches, reading candidates (including intake), reading and creating lists, reading and creating sequences (drafts only), reading conversations and threads, and reading and scheduling interviews.

Planned: cursor pagination, rate limiting, structured error codes, role-scoped keys, an Idempotency-Key header, response expansion, sequence activation/pause plus enrolled-candidate listing, adding/removing list members via API, sending inbox replies via API, rescheduling/cancelling interviews via API, a standalone candidate intake endpoint, and webhooks entirely.

FastHire API Reference · v1 · Endpoints marked Live are callable today with a real fh_live_… key; endpoints marked Planned are design intent — see Status & roadmap.