Search results, candidate records, outreach sequences, inbox threads, and interviews — read from and written to the same data your recruiters see in the app.
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.
https://api.thefasthire.com/v1Authenticate 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"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).| Convention | Detail |
|---|---|
| Identifiers | All object ids are opaque strings — plain cuids, not prefixed by type. Don’t parse them or rely on a prefix. |
| Field casing | Response bodies use camelCase (e.g. matchScore, createdAt), matching the app’s own data model exactly. |
| Timestamps | ISO 8601, UTC, e.g. 2026-08-20T09:30:00Z. Fields ending in At follow this format. |
| Money | Salary 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 Planned | Planned: 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 Planned | Planned: pass ?expand=field to inline a related object instead of just its id. Not implemented yet. |
Errors return a 4xx/5xx status and a flat JSON body describing what went wrong.
{
"error": "criteria.mandatory is required for sourceType=MANUAL"
}| Status | Meaning |
|---|---|
400 | Malformed body, or a field failed validation. |
401 | Missing, malformed, or revoked API key. |
403 | The team member who created this key has been deactivated. |
404 | Object doesn't exist, or belongs to a different org. |
409 Planned | Reserved for state-conflict errors once they exist — nothing returns this today. |
429 Planned | Reserved for rate limiting once it ships — there is no request cap today. |
500 | Something failed on FastHire's side. Safe to retry. |
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.
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window. |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Unix timestamp when the window resets. |
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.
List searches for the org, most recent first.
| Field | Type | Description |
|---|---|---|
| statusoptionalPlanned | string | Not implemented yet — every search is returned today. DRAFTRUNNINGCOMPLETEFAILED |
| owner_idoptionalPlanned | string | Not implemented yet. |
{
"searches": [
{
"id": "cknq7x2z10001...",
"name": "Senior Backend Engineer — Sydney",
"sourceType": "JOB_DESCRIPTION",
"createdAt": "2026-08-12T04:11:00Z",
"updatedAt": "2026-08-12T04:19:00Z"
}
]
}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.
| Field | Type | Description |
|---|---|---|
| sourceTyperequired | string | NATURAL_LANGUAGEJOB_DESCRIPTIONMANUAL |
| criteriarequired | object | Structured search criteria — see the request example below for the full shape (searchName, roles, companies, languages, education, location). |
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" }]
}
}
}'{ "id": "cknq7x2z10001..." }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.Retrieve one search, including its full criteria.
{
"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"
}List a search’s matched candidates, ranked by matchScore.
{
"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.
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.
Retrieve a candidate’s full profile, including intake if they’ve replied.
{
"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 */
}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:
| Field | Type | Description |
|---|---|---|
| status | string | REQUESTEDCOMPLETED |
| resume | string | Free text or a link — however the candidate provided it. No file storage. |
| contactNumber | string | Self-reported. |
| totalExperience | string | Self-reported. |
| currentSalary | string | Self-reported, free text (see Conventions above). |
| expectedSalary | string | Self-reported, free text. |
| noticePeriod | string | Self-reported. |
| educationalQualification | string | Self-reported. |
| interviewAvailability | string | Self-reported. |
| location | string | Self-reported. |
| requestedAt | timestamp | When FastHire asked this candidate for intake. |
| respondedAt | timestamp | When the candidate completed it. |
Lists are recruiter-curated candidate sets, independent of any one search — the unit sequences target for outreach.
List the org’s lists.
{
"lists": [
{
"id": "cknqa1k3b0002...",
"name": "Q3 backend shortlist",
"createdAt": "2026-08-12T04:11:00Z",
"candidateCount": 18
}
]
}| Field | Type | Description |
|---|---|---|
| namerequired | string | List name. |
{ "id": "cknqa1k3b0002..." }Doesn’t exist today — candidates can only be added to a list from the app.
| Field | Type | Description |
|---|---|---|
| candidate_idsrequired | array | Up to 100 candidate ids per call. |
Doesn’t exist today — removing a candidate from a list is app-only.
A sequence is an outreach campaign: an ordered set of LinkedIn, email, and in-app steps run against one or more target lists.
| Field | Type | Description |
|---|---|---|
| statusoptionalPlanned | string | Not implemented yet — every sequence is returned today. DRAFTACTIVEPAUSEDARCHIVED |
{
"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"
}
]
}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.
| Field | Type | Description |
|---|---|---|
| namerequired | string | Campaign name. |
| channelStrategyrequired | string | LINKEDIN_FIRSTINMAIL_FIRSTEMAIL_FIRSTFASTHIRE_FIRST |
| listIdsrequired | array | Lists to enroll candidates from. Active sequences pick up candidates added later, too. |
| stepsrequired | array | Ordered steps: channel LINKEDINEMAILIN_APP , stepType CONNECTMESSAGEFOLLOW_UP , subject (email only), templateBody. |
{ "id": "cknqb7h4c0003..." }Retrieve one sequence, including its steps and target lists.
{
"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"
}Doesn’t exist today. Once shipped, this will be the way to activate or pause a campaign via API.
curl https://api.thefasthire.com/v1/sequences/cknqb7h4c0003... \
-X PATCH -H "Authorization: Bearer $FASTHIRE_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "ACTIVE" }'Doesn’t exist today. Once shipped, this will list enrolled candidates and where each stands.
| Field | Type | Description |
|---|---|---|
| status | string | NOT_STARTEDIN_PROGRESSREPLIEDBOUNCEDCOMPLETEDPAUSED |
| next_action_at | timestamp | When this candidate's next step is due to send. |
Every send and reply across LinkedIn, email, and WhatsApp lands in one per-candidate thread.
List conversations, most recently active first.
| Field | Type | Description |
|---|---|---|
| unreadoptionalPlanned | boolean | Not implemented yet. |
{
"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"
}
]
}Retrieve one candidate’s full thread across every channel, plus their sequence context and intake.
{
"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
}Doesn’t exist today — sending a reply into a thread is app-only.
| Field | Type | Description |
|---|---|---|
| bodyrequired | string | Message text. |
| channeloptional | string | Defaults to the channel of the candidate's last inbound message. |
Interviews link a candidate, a recruiter, and a time — optionally backed by a real Calendar connection.
| Field | Type | Description |
|---|---|---|
| statusoptionalPlanned | string | Not implemented yet. CONFIRMEDCANCELLEDCOMPLETED |
{
"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" }
}
]
}The recruiter on the interview is always whoever created the API key — there’s no recruiter_id field to set it explicitly.
| Field | Type | Description |
|---|---|---|
| candidateIdrequired | string | |
| scheduledAtrequired | timestamp | Stored as UTC. |
| timezoneoptional | string | IANA zone for display, e.g. Asia/Kolkata. Defaults to UTC. |
| durationMinutesoptional | integer | 15–240. Defaults to 30. |
| meetingLinkoptional | string | Skip auto-generation and set one manually. If omitted, FastHire generates one via Google Meet, Teams, or Calendly — whichever the recruiter has connected. |
{
"interviewId": "cknqc2m5d0005...",
"calendarConnected": true,
"calendarProvider": "GOOGLE",
"calendlyUsed": false,
"whatsappSent": true,
"emailSent": true
}Doesn’t exist today. Once shipped, this will reschedule an interview or update its status.
Doesn’t exist today.
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.
| Event | Fires when |
|---|---|
search.completed | An agent finishes scoring and its status moves to COMPLETE or FAILED. |
candidate.replied | An inbound message arrives on any channel. |
candidate.intake_completed | All intake fields are captured. |
sequence.candidate_completed | A candidate finishes, bounces, or pauses in a sequence. |
interview.scheduled | An interview is created or confirmed. |
{
"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.
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.
fh_live_… key; endpoints marked Planned are design intent — see Status & roadmap.