Screening API

Send a candidate, a job and a use case. Our voice agent phones the candidate, runs a structured screen, and posts back a verdict, the captured fields, a timestamped transcript, the full call log and a recording link. You can follow the transcript live while the call runs. The agent introduces itself under your recruiter name and your company. The candidate never hears ours.

Base URLhttps://fury.humanbit.ai/v1
FormatJSON in, JSON out. Timestamps are ISO 8601 UTC. Phone numbers are E.164.
AuthAuthorization: Bearer hb_live_… or hb_test_…
DeliverySigned webhooks to the URL on your account, plus GET as a fallback.

What the job-screening agent does on every call (the language-freelancer agent is described under Use cases):

  1. It opens with: “Hi Ravi, this is Priya from Acme Talent. I'm calling about a Backend Engineer role at Acme. Do you have two minutes?”
  2. If the candidate is busy, it books a callback time. If they're not interested, it accepts the first no. If it's the wrong person, it apologises. If they ask to be removed, it confirms and flags it.
  3. It covers the deal-breakers first: notice period, current and expected CTC, location and work mode, competing offers, experience. A firm mismatch ends the call politely.
  4. Only then does it pitch the role, ask the role-specific questions it writes from your job brief, and ask your custom questions.
  5. After the call, a separate review pass reads the transcript and produces the result. The verdict rules are mechanical, so an optimistic “Proceed” never reaches you on a candidate who is over budget or over notice.

Quickstart

1. Place a call with your test key. Nobody is dialled. You get the whole lifecycle and every webhook within about 10 seconds.

curl https://fury.humanbit.ai/v1/calls \
  -H "Authorization: Bearer $HB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cand-881-backend-attempt-1" \
  -d '{
    "candidate": { "name": "Ravi Kumar", "phone": "+919876543210" },
    "job": {
      "role_name": "Backend Engineer",
      "client_company": "Acme",
      "role_location": "Bangalore",
      "work_mode": "hybrid",
      "experience_range": "3 to 6 years",
      "ctc_min": "18 lakhs per annum",
      "ctc_max": "30 lakhs per annum",
      "max_notice_period": "60 days",
      "role_description": "Python, Kafka, Postgres; owns the payments ledger service."
    },
    "agent": { "name": "Priya", "company_name": "Acme Talent" },
    "custom_fields": [
      { "key": "open_to_night_shift", "question": "Are you open to a night shift once a month?" }
    ],
    "external_ref": "cand-881",
    "metadata": { "crm_candidate_id": 881, "requisition": "REQ-204" }
  }'

2. Receive the webhooks. You'll get call.status four times (dialing, ringing, in_progress, processing), then one call.completed carrying everything. Verify the signature, store the payload, and answer 2xx.

3. Switch to your live key. Same request, real call. Download the recording before retention.delete_after.

Check your webhook endpoint at any time with POST /v1/webhooks/test. It sends a signed webhook.test event and tells you what your server answered.

Authentication & test mode

Every account has two keys. They differ only in what they do:

KeyEffect
hb_live_…Real phone calls, inside calling hours, within your concurrency limit.
hb_test_…No call is placed. The full status sequence runs in about 10 seconds, and call.completed carries a realistic sample result built from your job brief, with "test": true. Calling hours don't apply.

Calls made with one key can't be seen with the other. Keys are secrets for your server only. Never put them in a browser or mobile app. If a key leaks, ask us to rotate it. The old key stops working immediately.

Your data, not ours

We are a pipe, not a database. Everything a call produces is delivered to you, and you are the system of record.

Download the recording. recording.url is a time-limited link. Fetch the MP3 into your own storage when you receive call.completed. Don't store our link.

Use cases (agents)

Each use case is a separate agent with its own script, its own result schema and its own time limit. Choose one per call with "use_case". If you leave it out, it defaults to job-screening. GET /v1/use-cases returns this table as JSON, so your integration can read the required fields instead of hardcoding them.

use_caseForCallVerdicts
job-screeningFull-time, salaried roles. Deal-breakers first (notice, current and expected CTC, location and mode, other offers, experience), then the pitch and role-specific probes the agent writes from your brief.Up to 12 min, voice saaviproceed hold reject callback_scheduled not_interested
language-freelancerRemote freelance work in Indian languages: annotation, transcription review, translation, voice data. Five checks: laptop, English, languages spoken and written, hours per day, current earnings. Then it states that the work is freelance and gives your next step.About 3 min, at most 7, voice monikaqualified not_qualified incomplete callback_scheduled not_interested wrong_person voicemail

Need a different kind of call, such as campus hiring, blue-collar roles, or reference checks? Each one is a new agent with its own slug. Ask your account manager.

job for job-screening

See the job brief under POST /v1/calls.

job for language-freelancer

The agent says only what is written here. Every value is something it may say aloud, so write it as spoken English.

FieldUsed for
role_namerequired“It's a Kannada transcription reviewer role, fully remote.”
work_descriptionrequiredWhat the work actually is, in one line, e.g. “listen to short audio clips and correct the AI's transcript”.
languagesrequiredThe target languages, e.g. “Kannada or Telugu”. The candidate must speak and read/write at least one of them to qualify.
pay_rangerequiredSpoken pay, e.g. “fifteen to twenty-five thousand rupees a month”. The agent never quotes any other figure.
next_steprequiredWhat happens after the call, e.g. “The team will send a short form on WhatsApp today.” The agent never names a channel that isn't written here.
company_descriptionrequiredOne or two sentences about your company. This is also the answer to “Is this a scam?”, so make it concrete.
company_locationoptionalCity, for “Where are you based?”. Default: “India”.
payment_termsoptionalThe answer to “How will I get paid?”. Default: the team explains it during onboarding. The agent never collects bank details.
profile_sourceoptionalThe answer to “How did you get my number?”, e.g. "I came across your profile on Naukri."
additional_detailsoptionalAnything else it may say if asked: training, experience needed, minimum hours.
{
  "use_case": "language-freelancer",
  "candidate": { "name": "Asha Rao", "phone": "+919876500001" },
  "job": {
    "role_name": "Kannada transcription reviewer",
    "work_description": "listen to short audio clips and correct the AI's transcript",
    "languages": "Kannada or Telugu",
    "pay_range": "fifteen to twenty-five thousand rupees a month",
    "next_step": "The team will send a short form on WhatsApp today.",
    "company_description": "Acme Talent places freelancers with AI companies; about two thousand people work with us today.",
    "profile_source": "I came across your profile on Naukri."
  },
  "agent": { "name": "Meera", "company_name": "Acme Talent" }
}

Result fields for language-freelancer

fields: candidate_name, has_laptop, english_communication (Good / Basic / Poor), languages_spoken, languages_read_written, hours_per_day, current_monthly_earnings, current_occupation, told_freelance, told_next_step, callback_time, not_interested_reason, asked_to_be_removed, unanswered_questions.

structured: has_laptop (bool), english_ok (bool), target_languages_written (array, lowercase), hours_per_day (number), monthly_earnings_inr (number). risk_flags: scam_concern, phone_only, speaks_but_cannot_write, low_english, very_low_hours.

Verdict rules, in order: voicemail if no human answered. Then callback_scheduled, not_interested or wrong_person if the call ended that way. Then not_qualified if the candidate firmly has no laptop, speaks and writes none of the languages, or can't hold a basic English conversation. Then qualified if they have a laptop, can communicate in English, and speak and write at least one of the languages. Otherwise incomplete.

Call lifecycle

queued→dialing→ringing→in_progress→processing→ended

A call that is never answered goes from ringing straight to ended. If you asked for retries, it goes back to queued instead and keeps the same call id. attempt counts the dials. processing is the 10–60 seconds after hang-up while the result and recording are produced.

Outcomes

Every ended call has exactly one outcome:

outcomeMeaningHas result?
answeredA person picked up and the call ran. See end_reason and result.verdict for how it went.Yes (null if nobody spoke)
voicemailAn answering machine or IVR picked up. The agent hung up without leaving a message.Usually no
no_answerRang for 45 s unanswered, or was rejected or unreachable, and no retries were left.No
failedThe call could not be placed. error says why.No
cancelledYou cancelled it.Only if it had already been answered
suppressedThe number was added to your do-not-call list after this call was queued, so it was never dialled.No

end_reason on answered calls

completed · not_interested · callback · wrong_person · removed · time_cap · voicemail · candidate_hung_up

Real-time transcript

You can follow a call while it happens, for example to show a recruiter the conversation live in your CRM. Each finished turn (one person's sentence or two, once they stop talking) is available about 1–2 seconds after it's spoken. There are three ways to get it, and you can use any of them:

WayBest forHow
Webhook per turnSystems that already take our webhooksSet "realtime_transcript": true on POST /v1/calls. Each turn arrives as a call.transcript event at your webhook URL.
Event streamA live view in your appGET /v1/calls/{id}/stream streams server-sent events: status, transcript, then one completed, after which the connection closes. Works for any call and needs no flag.
PollingSimple scriptsGET /v1/calls/{id} while status is in_progress or processing includes live_transcript: every finished turn so far.

A turn looks the same in all three:

{ "speaker": "candidate", "text": "Sixty days, but buyout is possible.", "at_s": 41.3, "interrupted": false }
// speaker: "agent" | "candidate"   at_s: seconds since the agent joined the call
// interrupted: true when the agent was cut off mid-sentence by the candidate

The call.transcript webhook carries data: {id, external_ref, metadata, index, turn}. index starts at 0 and has no gaps. Webhooks can arrive out of order, so sort by index. The authoritative transcript, with per-turn latency and speech-recognition confidence, is the one in call.completed.

Only finished turns are sent, never half-recognised words, so text you've already shown never changes. Your API key must stay on your server, so to show a live transcript in a browser, open the stream from your backend and relay it to the page.

Calling rules

These rules are enforced for every account. They protect the candidates and your sender reputation.

RuleDetail
NumbersIndian mobile numbers only: 10 digits, or +91 followed by 10 digits, starting 6–9.
Calling hours09:30–20:30 IST by default (adjustable per account). Calls queued outside the window wait for it. They are not dropped.
ConcurrencyEach account has a limit on simultaneous calls. Extra calls wait in the queue in schedule_at order. GET /v1/account shows the limit and the current queue.
One open call per numberA second call to a number that already has a queued or live call is refused with 409 call_in_progress.
Do-not-callA number on your suppression list is refused with 409 do_not_call. Candidates who ask to be removed on a call are added automatically.
Ringing45 seconds, then no_answer.
Lengthjob-screening: the agent starts wrapping up at 10 minutes and the call ends at 12. language-freelancer: wraps up at 5 minutes and ends at 7.
AI disclosureIf a candidate asks whether they're speaking to an AI, the agent says yes. It always does, and this can't be configured.
Fixed behaviourThe voice rules, safety rules (never asks for bank or payment details, never states a salary or offer that is not in your brief, never asks about religion, caste, marital status, health, age or other protected topics) and verdict rules are fixed. You shape what is asked. You can't change how the agent behaves.

Endpoints

POST

/v1/calls

Queue a screening call. Returns 202 with the call object in status queued.

Headers

HeaderDescription
Idempotency-KeyrecommendedAny string up to 200 characters, unique per intended call. Repeating a request with the same key returns the original call with 200 and never dials twice. Use it on every retry of a network error.

Body

FieldDescription
use_caseoptionalWhich agent makes the call: job-screening (default) or language-freelancer. See Use cases.
candidate.namerequiredFull name. The agent uses the first name only.
candidate.phonerequiredIndian mobile number.
candidate.variablesoptionalA string→string map of extra facts about this candidate that the agent may use, e.g. {"current_company": "Infosys"}.
jobrequiredThe role brief. Its fields depend on use_case: job-screening or language-freelancer. Unknown keys are rejected so a typo can't silently drop a fact. Each value can be up to 3,000 characters.
realtime_transcriptoptionaltrue sends each finished turn as a call.transcript webhook while the call runs. Default false. See Real-time transcript.
agent.nameoptionalThe recruiter name the agent uses, e.g. "Priya". Defaults to your account's setting.
agent.company_nameoptional*The company the agent says it calls from (usually your agency). *Required unless your account has a default.
agent.voiceoptionalA voice id from GET /v1/voices.
custom_fieldsoptionalUp to 8 extra questions, each {"key", "question"}. The agent asks them once the deal-breakers are covered, and the answers come back in result.custom[key] ("Not captured" if not asked or not answered). key is lowercase snake_case, up to 40 characters.
external_refoptionalYour id for this candidate or application. It is echoed on every event and can be filtered on with GET /v1/calls?external_ref=.
metadataoptionalAny JSON object up to 4 KB, echoed back unchanged on every event.
schedule_atoptionalISO 8601 with a timezone, up to 30 days ahead. Use it for a callback the candidate asked for. It is still subject to calling hours.
retry_no_answeroptional0–3 (default 0). Extra dials if nobody answers. Same call id, and attempt increments.
retry_after_minutesoptional10–1440 (default 120). Gap between retries.

The job brief (job-screening)

Write values the way a recruiter would say them aloud: "28 lakhs per annum", not 28. The agent reads them on the phone.

FieldUsed for
role_namerequiredThe opener and the pitch.
role_locationrequiredThe location deal-breaker.
work_moderequiredoffice, hybrid, remote, or a phrase such as “3 days in office”.
experience_rangerequiredRelevance check.
ctc_maxrequiredThe budget ceiling. It is disclosed only if the candidate's current or expected CTC is above it.
max_notice_periodrequiredThe notice deal-breaker.
ctc_minoptionalBottom of the band. It is never spoken first.
client_companyoptionalThe hiring company, if you are an agency. The agent says it is recruiting for them.
company_descriptionoptionalWhat the hiring company does. Used in the two-sentence pitch.
role_descriptionoptionalStack, scope and must-haves. The agent writes its role-specific probes and green and red flags from this.
funding_stage, employee_size, reporting_tooptionalAnswers to the questions candidates commonly ask.
additional_detailsoptionalBenefits, interview process, anything else the agent may say if asked.
extra_contextoptionalPrivate instructions, e.g. “If asked where we found their profile, say Naukri.”

The agent never invents a salary, benefit or technology that is not in the brief. A thin brief makes for a shorter screen, not a made-up one.

Responses

202Queued. Body: the call object.
200Same Idempotency-Key as an earlier request. Body: that call.
409call_in_progress or do_not_call.
422invalid_request, invalid_use_case, invalid_phone or invalid_voice. The message lists every problem at once.
GET

/v1/calls/{id}/stream

A live feed of server-sent events (text/event-stream) for one call. On connect you get the current status and every turn so far, then each new one as it happens. A : keep-alive comment is sent about every second. After completed (whose data is the full call object) the server closes the connection. If you disconnect, reconnect and skip the indexes you already have.

event: status
data: {"id": "call_…", "status": "in_progress", "sequence": 4}

event: transcript
data: {"id": "call_…", "index": 0, "turn": {"speaker": "agent", "text": "Hi Ravi, this is Priya from Acme Talent. …", "at_s": 0.0, "interrupted": false}}

event: completed
data: { …the call object… }
import json, httpx

with httpx.stream("GET", f"https://fury.humanbit.ai/v1/calls/{call_id}/stream",
                  headers={"Authorization": f"Bearer {KEY}"}, timeout=None) as r:
    event = None
    for line in r.iter_lines():
        if line.startswith("event: "):
            event = line[7:]
        elif line.startswith("data: "):
            data = json.loads(line[6:])
            if event == "transcript":
                print(data["turn"]["speaker"], ":", data["turn"]["text"])
            elif event == "completed":
                save(data)   # the full call object, same as the call.completed webhook
GET

/v1/calls/{id}

The call as it stands. Once status is ended, this returns the same body as the call.completed webhook (with a fresh recording link) until the data is deleted. Use it to reconcile if you missed a webhook. Don't poll it in a tight loop. Webhooks are the primary channel.

GET

/v1/calls

Summaries, newest first. Query: status, external_ref, limit (1–200, default 50), before (a call id, for paging). The response is {"data": [...], "has_more", "next_before"}. Summaries don't include results. Fetch a call by id for those.

POST

/v1/calls/{id}/cancel

A queued call ends at once with outcome: cancelled. A ringing call is hung up. A call in progress is hung up mid-conversation. That's abrupt for the candidate, so use it only when you must. If the call was already answered, the result is still produced and delivered. Returns 409 already_ended if it has already ended.

POST

/v1/calls/{id}/redeliver

Queue the call.completed webhook again, for example after fixing a bug in your handler. Only works before the data is deleted.

GET

/v1/calls/{id}/recording?exp=…&sig=…

The stereo MP3, 96 kbps: left channel = candidate, right channel = agent, on one aligned timeline. The link is self-authenticating, so don't send your API key with it. Get it from recording.url. It returns 410 after recording.expires_at.

POST

/v1/suppressions

Add a number to your account's do-not-call list. Body: {"phone": "+919876543210", "reason": "optional"}. Queued calls to that number end as suppressed and are never dialled. We store only a salted hash.

DELETE

/v1/suppressions/{phone}

Remove a number, for example when a candidate opts back in. Returns {"removed": true|false}.

GET

/v1/account

Your settings (webhook URL, concurrency, calling hours, retention, agent defaults) and a live count of open calls by status, for the key's mode.

GET

/v1/use-cases

The agents you can choose with use_case: title, summary, time limit, default voice, required and optional job fields, result field keys, and verdicts.

GET

/v1/voices

The voices you can pass as agent.voice: saavi (warm, professional, the default for job-screening) and monika (casual and young, the default for language-freelancer). Both are female, Indian-English voices that have been used on real Indian phone calls.

GET

/v1/health

Needs no authentication. Returns {"ok": true}.

POST

/v1/webhooks/test

Sends a signed webhook.test event to your webhook URL now. Returns {"delivered": bool, "status_code"}.

GET

/v1/deliveries

The webhook delivery log: each event, its state (pending, delivered, failed), attempts, the last error your endpoint returned, and the next retry time. Query: state, limit.

Webhooks

We POST JSON to the webhook URL on your account. Every delivery has the same envelope:

{
  "id": "dlv_79279b748a53b857745c",       // unique per event: use it to de-duplicate
  "type": "call.completed",
  "created_at": "2026-10-16T09:24:57.069597Z",
  "data": { ... }
}
EventWhendata
call.statusEach move to dialing, ringing, in_progress, processing, or back to queued for a retry.{id, status, sequence, external_ref, metadata, at}
call.transcriptEach finished turn during the call, only when the call was created with "realtime_transcript": true.{id, index, turn, external_ref, metadata}. See Real-time transcript.
call.completedOnce per call, when it ends with any outcome.The full call object: result, transcript, log, recording, usage.
webhook.testOnly when you call POST /v1/webhooks/test.{"hello": "world"}

Headers on every delivery:

X-Webhook-IdSame as the envelope id.
X-Webhook-EventSame as type.
X-Webhook-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>

Verifying signatures

Compute HMAC-SHA256 with your whsec_… secret over the string "{t}.{raw body}", compare it in constant time with v1, and reject a t more than 5 minutes old. Use the raw request body. Re-serialised JSON won't match.

import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, sig = parts["t"], parts["v1"]
    if abs(time.time() - int(t)) > 300:
        return False
    want = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, sig)

# Flask:   verify(request.get_data(), request.headers["X-Webhook-Signature"], WHSEC)
# FastAPI: verify(await request.body(), request.headers["x-webhook-signature"], WHSEC)

Retries & ordering

The call object

This is what call.completed carries, and what GET /v1/calls/{id} returns once a call has ended. While a call is open, GET returns the first block of fields only.

{
  "id": "call_66e7c041787915e90c13",
  "object": "call",
  "use_case": "job-screening",
  "mode": "live",                          // "test" for test-key calls (and "test": true)
  "status": "ended",
  "outcome": "answered",                   // see Outcomes
  "external_ref": "cand-881",
  "attempt": 1,
  "metadata": { "crm_candidate_id": 881, "requisition": "REQ-204" },
  "candidate": { "name": "Ravi Kumar", "phone": "+919876543210" },
  "job": { ...exactly as you sent it... },
  "agent": { "name": "Priya", "company_name": "Acme Talent", "voice": "saavi" },
  "timeline": {
    "created_at":  "2026-10-16T09:24:46.618Z",
    "dialed_at":   "2026-10-16T09:24:47.933Z",
    "answered_at": "2026-10-16T09:24:51.986Z",
    "ended_at":    "2026-10-16T09:31:02.014Z"
  },
  "duration_s": 370.0,                     // answered → hang-up
  "billable_minutes": 7,                   // ceil(duration_s / 60); 0 if unanswered
  "end_reason": "completed",
  "result": { ...see The result... },      // null when there was no conversation
  "transcript": [
    { "speaker": "agent", "text": "Hi Ravi, this is Priya from Acme Talent. …", "at_s": 0.0, "interrupted": false },
    { "speaker": "candidate", "text": "Yes, go ahead.", "at_s": 5.2, "interrupted": false, "stt_confidence": 0.94 },
    { "speaker": "agent", "text": "What's your notice period?", "at_s": 6.9, "interrupted": false,
      "latency_s": { "e2e_latency": 1.1, "end_of_turn_delay": 0.4, "llm_node_ttft": 0.5, "tts_node_ttfb": 0.2 } }
  ],
  "transcript_text": "Agent: Hi Ravi, …\nCandidate: Yes, go ahead.\n…",
  "recording": {
    "url": "https://fury.humanbit.ai/v1/calls/call_66e7…/recording?exp=…&sig=…",
    "expires_at": "2026-10-17T09:31:02Z",
    "format": "mp3",
    "channels": { "left": "candidate", "right": "agent" }
  },                                       // null if there is no audio
  "quality": {
    "candidate_audio_mean_db": -31.4,
    "candidate_audio_silent": false,       // true = the line carried no audio from their side
    "agent_reply_latency_p50_s": 1.2,
    "agent_turns": 18, "candidate_turns": 17, "interruptions": 2
  },
  "usage": {                               // raw model usage for this call
    "llm_prompt_tokens": 41230, "llm_completion_tokens": 1630,
    "tts_characters_count": 2210, "tts_audio_duration": 152.3, "stt_audio_duration": 365.0
  },
  "retention": { "delete_after": "2026-10-17T09:31:02Z" },
  "error": null,
  "events": [
    { "status": "queued", "at": "…" }, { "status": "dialing", "at": "…" },
    { "status": "ringing", "at": "…" }, { "status": "in_progress", "at": "…" },
    { "status": "processing", "at": "…" }, { "status": "ended", "at": "…", "outcome": "answered" }
  ]
}

The result

This section describes job-screening. For language-freelancer, see its result fields. Both share verdict, summary, fields, structured, risk_flags, custom and do_not_call.

"result": {
  "verdict": "hold",
  "summary": "3–5 lines on fit, motivation, communication and red flags.",
  "fields": { "notice_period": "60 days", "expected_ctc": "26 lakhs per annum", ... },
  "structured": { "current_ctc_lpa": 18, "expected_ctc_lpa": 26, "notice_days": 60, "has_other_offers": false },
  "risk_flags": ["expected_above_budget"],
  "must_capture": { "relevance": "…", "budget_fit": "…", "notice_fit": "…", "location_fit": "…" },
  "custom": { "open_to_night_shift": "Yes, once a month is fine" },
  "do_not_call": false
}

verdict

The verdict is applied in this order, mechanically, so you can act on it without listening to the call:

verdictRule
callback_scheduled / not_interestedThe call ended that way. For a callback, the time is in fields.callback_time.
rejectA firm mismatch: irrelevant experience; expected CTC above budget and the candidate didn't accept the budget; notice above the maximum and not negotiable; location or work mode refused.
holdA soft mismatch (“negotiable”, buyout possible), any must-capture item not captured, or a competing offer.
proceedNone of the above: relevant, within budget or accepted it, notice within the maximum, location and mode accepted, and everything captured.

fields

All values are strings, and anything not covered is exactly "Not captured". Keys: candidate_name, current_company_and_role, total_experience, key_responsibilities, reason_for_leaving, desired_next_role, current_ctc, expected_ctc, expected_ctc_basis, budget_disclosed_and_reaction, notice_period, notice_negotiable_or_buyout, last_working_day, gap_reason, current_location, open_to_location_and_mode, relocation_timeline, callback_time, not_interested_reason, asked_to_be_removed, open_questions, other_offers.

structured

Numbers for writing straight into your ATS: CTC in lakhs per annum and notice in days. A value is null if it wasn't said. It is never guessed from a job title.

risk_flags

Zero or more of: expected_above_budget, competing_offer, notice_above_max, location_or_mode_mismatch, skills_depth_unclear.

do_not_call

If this is true, the candidate asked to be removed. We have already suppressed the number on your account. Mark them in your own system too, and stop all other outreach to them.

Errors

Every error on /v1 has the same shape:

{ "error": { "code": "invalid_request", "message": "job: job is missing required fields: ctc_max" } }
HTTPcodeMeaning
401unauthorizedMissing, malformed or revoked key.
404not_foundNo such call for this key, including calls made with your other key.
409call_in_progressThis number already has an open call.
409do_not_callThe number is on your suppression list.
409already_ended, not_available, no_webhookThe action doesn't fit the call's (or the account's) current state.
410expired, deletedThe recording is past its retention window.
422invalid_request, invalid_use_case, invalid_phone, invalid_voiceThe body failed validation. The message lists every problem.
5xx—Our fault. Retry with the same Idempotency-Key. It's safe.

Your responsibilities

Questions or a key rotation: contact your HumanBit account manager.