Skip to navigation

Calls

Place outbound calls, follow their lifecycle, read outcomes and transcripts, stream recordings and hand live calls to a person.
View as Markdown

A call is one phone conversation between one of your agents and one phone number. Jelliu creates a call record for every outbound call it places (from POST /api/calls, a campaign or the MCP server) and for every inbound call that reaches one of your numbers. The record follows the call from the moment it is queued until it ends, and then collects what happened on it: status, outcome, duration, summary, transcript, structured analysis and cost telemetry.

This page covers placing a call, the checks every outbound call must pass, the status lifecycle, outcomes, durations and time limits, recordings, live transfers, hanging up, and how inbound calls are routed.

How it works

Every outbound call, whether you place it directly or a campaign dials it, goes through the same sequence of checks before the phone rings. The first check that fails stops the call and returns an error; no call record is created for checks that run before the record is written.

no yes no yes yes no blocked ok no yes blocked ok no yes no yes accepted rejected POST /api/calls Plan allows calls? 403 BILLING_ERROR Body valid? 400 VALIDATION_FAILED Agent paused? 409 AGENT_PAUSED Resolve destination and contact Compliance: hours, opt-out, DNC,attempts 403 COMPLIANCE_BLOCKED Destination country allowed? Spam and trial-abuse checks 429 CALL_SPAM_DETECTED / 403TRIAL_ABUSE_DETECTED Agent provisioned? Number available? 409 AGENT_PROVISIONING / 403PHONE_NUMBER_REQUIRED Concurrency slot free? 429 MAX_CONCURRENT_CALLS_REACHED Call record created: queued Voice provider dials 201 Created, status ringing Record marked failed, error returned

After the provider accepts the call, the rest of the lifecycle is asynchronous. When the conversation ends, the voice provider reports the transcript and analysis to Jelliu, which closes the record, computes the outcome and sends your webhooks. A background reconciler runs every minute and closes any call whose end was not reported, so a record never stays open forever.

The response to POST /api/calls means the provider accepted the call, not that anyone answered. Track the result with GET /api/calls/{callId} or, better, subscribe to call.completed and call.failed.

The call object

GET /api/calls and GET /api/calls/{callId} return the same shape. The list omits the heavier fields marked detail only. POST /api/calls returns the raw record right after dialing, without the computed fields analysis, has_recording, agent_name, campaign_name and contact_name.

Internal provider identifiers (the carrier call SID and the voice provider’s conversation id) are never returned.

Identity and routing

FieldTypeNullableDescription
idstring (uuid)NoCall id.
tenant_idstring (uuid)NoYour workspace id.
agent_idstring (uuid)YesThe agent that handled the call. Null on very old records.
agent_namestringYesThe agent’s name. Not included in the POST response.
campaign_idstring (uuid)YesThe campaign the call belongs to. Null for standalone and inbound calls.
campaign_namestringYesThe campaign’s name. Not included in the POST response.
contact_idstring (uuid)YesThe contact the call is filed under. See Contacts.
contact_namestringYesThe contact’s name. Not included in the POST response.
phone_numberstringNoThe other party in E.164: the number dialed on outbound calls, the caller on inbound calls.
call_directionstringNooutbound or inbound.
prompt_variant_idstring (uuid)YesThe A/B prompt variant used, if any.

Lifecycle

FieldTypeNullableDescription
statusstringNoOne of queued, ringing, in-progress, completed, failed, no-answer, busy, canceled. See Status lifecycle.
outcomestringYesWhat the call achieved, for example appointment_booked. Null until analysis is available. See Outcomes.
duration_secondsintegerYesLength of the conversation in whole seconds. Null until reported.
started_atstring (date-time)YesWhen the conversation started.
ended_atstring (date-time)YesWhen the record was closed.
termination_reasonstringYesWhy the call ended, as reported by the voice provider (free text). For calls that never connected it starts with initiation_failure:, for example initiation_failure:busy.
has_recordingbooleanNotrue when audio can be requested from /audio. Not included in the POST response.
created_atstring (date-time)NoWhen the record was created. Lists are ordered by this field, newest first.
updated_atstring (date-time)NoLast change to the record.
deleted_atstring (date-time)YesAlways null in API responses: deleted calls are not returned.

Analysis

FieldTypeNullableDescription
summarystringYesShort summary of the conversation. Often arrives a little after the call ends.
sentiment_scorenumberYesAverage caller sentiment from -1 to 1. Null means not measured, which is different from 0.
sentiment_lownumberYesThe most negative caller turn, -1 to 1.
frustration_scorenumberYes0 to 1. Null means not measured; 0 means nobody was frustrated.
call_success_scorenumberYesThe provider’s 0 to 100 confidence that the call met its goal.
objectionsstring[]YesObjections raised by the other party.
evaluation_criteria_resultsobjectNoMap of criterion id to { result, rationale }, where result is success, failure or unknown. {} when none.
data_collection_resultsobjectNoMap of field id to { value, rationale } with the data the agent collected. {} when none.
analysisobjectYesConvenience view built from the fields above. Null when the call produced no analysis signal at all. Not included in the POST response.
analysis.summarystringNosummary, or an empty string.
analysis.outcomestringYesSame as outcome.
analysis.sentimentstringYespositive (score above 0.2), negative (below -0.2), neutral, or null when there is no score.
analysis.sentiment_scorenumberYesSame as sentiment_score.
analysis.key_topicsstring[]NoAlways an empty array today.
analysis.qualificationobjectYesLead qualification derived from collected data: score (0 to 100 or null), interest (high, medium, low or null), answered (boolean or null), next_step, objections, reasons. Null when there is nothing to qualify.
kpi_dataobjectYesDetail only. KPI values computed for the call’s campaign category.
transcriptarrayYesDetail only. Ordered turns. See Transcripts.
sentiment_timelinearrayYesDetail only. Per-turn sentiment as { t, s, f } items.

Telemetry

All telemetry fields are null when the provider did not report them.

FieldTypeNullableDescription
main_languagestringYesLanguage detected on the call, for example es.
initiation_sourcestringYesHow the conversation was started: for example phone for a call over a number bought through Jelliu, sip_trunk for a call over your own SIP trunk, whatsapp, or js_sdk for the web widget. Other values may appear; treat unknown ones as opaque.
agent_talk_seconds, user_talk_secondsnumberYesSeconds of agent and caller audio.
agent_turn_count, user_turn_countintegerYesNumber of turns per side.
interruption_countintegerYesAgent turns the caller cut off.
avg_response_latency_ms, max_response_latency_msintegerYesAgent response latency, average and worst turn.
cost_creditsnumberYesVoice provider credits charged for the conversation.
cost_usd, llm_cost_usdstringYesCost in USD as a decimal string, for example "0.0412".
llm_input_tokens, llm_output_tokensintegerYesDetail only. Model tokens used.
tool_call_countintegerYesDetail only. Tools the agent invoked. 0 means measured and none used.
mcp_call_countintegerYesDetail only. Tool calls that reached external software (MCP servers and webhooks).
rag_lookup_countintegerYesDetail only. Knowledge base lookups.
tool_result_bytesstringYesDetail only. Characters returned by tool results, as a string.
telemetry_fetched_atstring (date-time)YesDetail only. When telemetry was last fetched.
dnc_scanned_atstring (date-time)YesDetail only. When the transcript was last scanned for an opt-out request.
first_response_msintegerYesDetail only. Reserved; not currently populated.

Transfer

FieldTypeNullableDescription
transferred_tostringYesDetail only. E.164 number of the person the call was transferred to.
transferred_atstring (date-time)YesDetail only. When the warm transfer started.
transfer_call_sidstringYesDetail only. Carrier identifier of the leg dialed to that person. Cleared if the transfer is canceled.

Status lifecycle

StatusTerminalSet when
queuedNoThe call passed every check and its record was created, just before the provider is asked to dial.
ringingNoThe provider accepted the call and it has not been answered yet.
in-progressNoThe call was answered. Inbound calls are created in this status. For outbound calls, Jelliu checks live calls about once a minute, so the change can lag the moment the person picks up by up to a minute or so.
completedYesThe provider reported the conversation as done; you hung up a call that had been answered; or the reconciler found it finished.
failedYesThe provider reported a failed conversation or an unknown dial failure; the dial was rejected before connecting; or a record stayed queued for 15 minutes without ever reaching the provider.
no-answerYesThe call rang out.
busyYesThe line was busy.
canceledYesYou hung up before the call was answered, or hung up a call that never reached the carrier; or the carrier reported it canceled.

Terminal statuses never move back to a live status: a late or duplicated event cannot resurrect a closed call. The one upgrade that does happen is to completed: if a call was closed as failed, no-answer, busy or canceled and the provider later delivers a non-empty transcript for it, the status becomes completed because a conversation evidently took place.

An answered outbound call moves from ringing to in-progress, usually within a minute of being answered. A very short call can end before that check runs and go straight from ringing to a terminal status, so decide whether a call connected from the terminal status, duration_seconds and outcome, not from having seen in-progress.

Status values use hyphens (in-progress, no-answer); outcome values use underscores (no_answer). They are different fields with different vocabularies.

Outcomes

outcome says what the call accomplished, independently of how it ended technically. It is null until analysis is available, which can be seconds or minutes after the call ends.

GroupValues
Salessale_closed, callback_scheduled, rejected
Reachabilityno_answer, voicemail, failed
Handoffescalated_to_human
Supportissue_resolved, ticket_created, faq_answered
Schedulingappointment_booked, appointment_rescheduled, appointment_canceled
Generalinfo_provided, follow_up_needed
Collectionspayment_promised, payment_collected, payment_refused
Retentioncustomer_retained, customer_reactivated, churned
Notificationsmessage_delivered, message_acknowledged
Interviewscandidate_evaluated, candidate_unqualified
Language assessmentlevel_assessed

A call counts as successful when its outcome is a success outcome for its category:

CategorySuccess outcomes
salessale_closed
supportissue_resolved, faq_answered
schedulingappointment_booked, appointment_rescheduled
surveysinfo_provided, issue_resolved, callback_scheduled, faq_answered
collectionspayment_promised, payment_collected
retentioncustomer_retained, customer_reactivated
notificationsmessage_delivered, message_acknowledged
interviewcandidate_evaluated
language_assessmentlevel_assessed
personalinfo_provided
generalinfo_provided, sale_closed, issue_resolved, appointment_booked, faq_answered

Two outcomes are written outside the analysis:

  • no_answer when a call ended without a transcript and never connected (status no-answer, busy, canceled or failed, or zero duration). A call that did connect but whose transcript is late keeps outcome: null until the analysis arrives.
  • escalated_to_human on an escalation — see below.

no_effective_contact is a reporting bucket, not a stored outcome. A call fails the conversation threshold when its rounded duration is below 30 seconds, its transcript has fewer than 8 entries, or either side has fewer than 3 counted speech turns. Legacy rows with missing speech counts use the duration/entry-count fallback until backfilled. The bucket is derived when an aggregate is read: the call’s own outcome still says whatever the analysis decided, so no_effective_contact is absent from GET /api/calls, a call.completed webhook and an export’s outcome filter. Speech counts themselves are stored; their migration and backfill can change historical figures. See Counting and coverage.

Escalation

An escalation is a call handed to a person. Since 2026-09-15 there are two ways it happens, and both record the same outcome and fire the same event:

Who handed it overWhenescalatedBytransferredTo
The agent, during the callIt uses its transfer-to-a-person tool, dialling the number configured on the agent"agent"null — this path never learns which number was dialled and will not invent one
An admin, through the API or dashboardPOST /api/calls/{callId}/transfer/complete finalises a warm transferabsent — treat an absent escalatedBy as the admin pathThe destination number

escalated_to_human used to be rare, because only the admin warm transfer wrote it and almost nobody uses that path. It is now common. If you branch on outcome, expect to see it on ordinary calls.

It never overwrites a more specific verdict. If the analysis already decided sale_closed, issue_resolved, rejected or any other outcome, that value stays: a transfer is the outcome of the AI’s part of the call, not of the call. The escalation is still announced on the call.escalated event either way. Retrying the admin completion is safe for the same reason.

Handing the conversation to another agent of your workspace is deliberately not an escalation: no person picked up, so the outcome is not written and no event fires. Both kinds of hand-off do appear in the agent action ledger, as transfer_to_number and transfer_to_agent.

An internal notification tool (the agent filing a note asking a person to look at something) is also not an escalation: nobody was handed the conversation.

Durations and time limits

  • duration_seconds comes from the voice provider when the conversation ends, rounded to whole seconds. If you hang up a call whose carrier leg had already ended, the carrier’s duration is recorded right away; if you hang up a live call, the duration arrives with the post-call report a few seconds later.
  • started_at is the conversation start reported by the provider (for inbound calls, the moment the record is created). ended_at is when Jelliu closed the record.

Every voice call has a hard ceiling after which the platform ends it with a short goodbye. The ceiling comes from the agent’s category, and the agent’s own maximum call duration setting (30 to 3600 seconds, see Agents) can only shorten it:

Agent categoryTarget length the agent paces towardHard ceiling
notifications60 s240 s
interview, language_assessment600 s900 s
Every other category120 s480 s

During a free trial, each call is additionally capped at the trial minutes you have left (never less than 60 seconds).

If a call ends sooner than expected, read termination_reason: it distinguishes a call that hit the maximum duration from one where the agent ended the call itself.

Transcripts

The detail endpoint returns transcript as an array of turns in order:

[
{ "role": "agent", "text": "Hola, ¿hablo con Ana?", "timestamp": "0s", "latency_ms": 820 },
{ "role": "user", "text": "Sí, ella habla.", "timestamp": "3s", "sentiment": 0.1 },
{
"role": "agent",
"text": "Perfecto, le agendo para el jueves a las 10.",
"timestamp": "41s",
"tools": [
{ "name": "book_meeting", "type": "webhook", "ok": true, "action_id": "3f7a2c10-8b4e-4d61-9a0c-5e2f1b7d9c44" }
]
}
]
Turn fieldDescription
roleagent or user.
textWhat was said.
timestampOffset from the start of the call, as a string with an s suffix.
interruptedPresent and true when the turn was cut off.
latency_msAgent turns only: response latency, when measured.
sentimentUser turns only: -1 to 1, when measured.
toolsTools the agent called in that turn: name, type, ok, and action_id for tools that reached external systems.

Transcripts and summaries are kept for your plan’s retention window (7 days on Starter, 30 on Growth, 90 on Business, unlimited on Enterprise), or less if you set a shorter window. Messages from other channels live in Conversations.

Common tasks

Place an outbound call

1

Pick an agent and a destination

You need the id of a provisioned agent (see Agents) and either a phone number in E.164 format or the id of an existing contact.

2

Create the call

curl -sS -X POST "https://api.jelliu.co/api/calls" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
"phoneNumber": "+573001234567"
}'

A successful request returns 201 Created with the new record (abridged):

{
"data": {
"id": "5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10",
"tenant_id": "e2a4c6d8-1b3f-4a5c-9e7d-0f2b4d6a8c1e",
"campaign_id": null,
"contact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"agent_id": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
"phone_number": "+573001234567",
"status": "ringing",
"outcome": null,
"duration_seconds": null,
"transcript": null,
"sentiment_score": null,
"summary": null,
"started_at": null,
"ended_at": null,
"call_direction": "outbound",
"prompt_variant_id": null,
"evaluation_criteria_results": {},
"data_collection_results": {},
"created_at": "2026-09-14T15:39:02.118Z",
"updated_at": "2026-09-14T15:39:03.460Z",
"deleted_at": null
}
}
3

Wait for the result

Subscribe to call.completed and call.failed webhooks, or poll GET /api/calls/{callId} until status is terminal.

Request body

agentId
string (uuid)Required

The agent that places the call. It must belong to your workspace, must not be paused, and must have finished provisioning.

phoneNumber
string

Destination in E.164: +, a country code that does not start with 0, and up to 15 digits in total, for example +573001234567. Required unless you send contactId. When both are sent, phoneNumber is the number dialed.

contactId
string (uuid)

An existing contact to call. Its phone number is used when phoneNumber is omitted.

campaignId
string (uuid)

Files the call under a campaign: it counts toward the campaign’s concurrency limit, feeds its reporting, and gives the agent the campaign’s context (product_context, target_audience, campaign name). See Campaigns.

promptVariantId
string (uuid)

A/B testing: the prompt variant to attribute this call to.

promptOverride
string

Replaces the agent’s prompt for this call only. Up to 5000 characters. Text that looks like a prompt injection (for example “ignore previous instructions” or “actúa como”) is rejected.

firstMessageOverride
string

Replaces the agent’s opening line for this call only. Up to 500 characters, same injection screening.

What Jelliu does with the destination

  • Contact filing. The call is always filed under the contact that owns the dialed number. If you omit contactId, Jelliu reuses the oldest existing contact with that number, or creates a contact without a campaign. If you send a contactId whose number differs from phoneNumber, the call is filed under the contact at phoneNumber instead.
  • Campaign consistency. With a campaignId and a phoneNumber that differs from the named contact’s, the number must itself be a contact of that campaign; otherwise the call is refused with 400 VALIDATION_FAILED: El número de destino no pertenece a esta campaña. Agrégalo como contacto de la campaña o haz la llamada sin campaña.
  • Attempt counting. The contact’s call_attempts increases only after the provider accepts the call, so failures before dialing do not use up compliance attempts.
  • Context. The agent receives the contact’s name and what earlier interactions with this person captured, on any channel, so it does not greet a returning customer as a stranger.
  • Disclosures. When the destination requires it, a recording notice is added to the agent’s greeting. A greeting that already announces recording is left as it is.
  • Caller ID. Calls are placed from your workspace’s own number when one is provisioned. During a trial without one, a shared platform number is used. On an active paid plan with no provisioned number the call fails with PHONE_NUMBER_REQUIRED; see Phone numbers.

Compliance checks

Every outbound call, including campaign calls, is checked against the compliance rules for the destination’s country. The country is read from the number’s prefix (+57 Colombia, +52 Mexico, +55 Brazil, +1 United States, +54 Argentina, +56 Chile, +51 Peru; anything else uses the default profile).

CheckDefaultRefusal message
Allowed calling hours08:00 to 20:00 (21:00 for Mexico and the United States)Outside allowed call hours (08:00-20:00 America/Bogota)
Blocked prefixes you configuredNonePhone number matches blocked prefix: +5760
Opt-out suppression listAlways onPhone number is on the opt-out suppression list
Contact marked do-not-callAlways on when a contact is resolvedContact is on the Do Not Call list
Number marked do-not-call on any contactOnPhone number is on the Do Not Call list
Prior opt-in on recordOff unless you enable require_opt_in for the countryContact has not granted opt-in consent for voice calls
Attempts to the contact today3 per dayMax daily call attempts reached (3/day)
Total attempts to the contact10 (8 for Brazil, 15 for the United States)Max total call attempts reached (10 total)

All of them return 403 COMPLIANCE_BLOCKED. You can change the per-country values in Settings → Compliance or through /api/compliance/configs.

Campaign calls. When the campaign dialer is refused, what happens to the contact depends on which rule refused it:

RefusalContact
Outside allowed calling hours, or the daily attempt limitStays pending and the dial is rescheduled for when the window opens again (the next allowed hour, or the next day). After 14 reschedules it is marked failed.
Total attempt limitfailed
Blocked prefix, or a destination that is not allowed (see below)invalid
No prior opt-in on record, when you require oneinvalid — the campaign keeps running through its other contacts
Opt-out suppression list, or do-not-calldnc

Only an opt-out or a do-not-call refusal marks the contact dnc. A refusal that does not match any of these rules marks it failed, never dnc. A missing opt-in is neither: nobody said no, so it is not dnc, and no call was attempted, so it is not failed — writing a consent record makes the contact dialable again. See Compliance.

The calling-hours window is evaluated in your workspace’s time zone when one is set in your settings, not in the time zone of the country being called. A workspace in Bogotá calling Mexico City is checked against 08:00 to 21:00 Bogotá time. Only when no workspace time zone is set does the country’s own zone apply (Bogotá for numbers outside the modeled countries).

Destination countries. Independently of the person being called, the destination itself must be allowed:

  • Premium-rate and high-risk ranges (for example +1900, +53, satellite +881 to +883, and many Caribbean +1 area codes) are always refused: Calls to premium-rate or high-risk numbers are blocked (toll fraud protection).
  • If you configured allowed calling destinations in Settings → Compliance, only those are reachable.
  • Otherwise the allowed set is Colombia, Mexico, Brazil, United States and Canada (+1), Argentina, Chile, Peru and Spain, plus the country you chose at onboarding and the countries of your own voice numbers. Anything else is refused with This destination is outside the countries this workspace calls. Add it to the allowed calling destinations in Settings → Compliance.

Each refusal is written to your audit log with the masked number.

Abuse protection. A workspace can call the same number at most 5 times per hour (429 CALL_SPAM_DETECTED, Phone number called 6 times in the last hour (max 5)), and place at most 500 calls per hour in total. Trial workspaces calling a number already called by other trial workspaces can be refused with 403 TRIAL_ABUSE_DETECTED.

An opt-out is total. When a person says they do not want to be called again, Jelliu adds their number to the suppression list, marks the contact do-not-call and never dials them again, from any campaign or contact record. See Contacts.

List calls

GET /api/calls uses cursor pagination: pass nextCursor back unchanged as cursor. Filter with agentId and campaignId.

curl -sS "https://api.jelliu.co/api/calls?limit=50&campaignId=0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90" \
-H "Authorization: Bearer $JELLIU_API_KEY"
{
"data": {
"calls": [
{
"id": "5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10",
"agent_id": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
"agent_name": "Valentina",
"campaign_id": "0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90",
"campaign_name": "Webinar follow-up",
"contact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"contact_name": "Ana Gómez",
"phone_number": "+573001234567",
"call_direction": "outbound",
"status": "completed",
"outcome": "callback_scheduled",
"duration_seconds": 184,
"sentiment_score": 0.6,
"summary": "La clienta pidió que la llamen el jueves en la mañana.",
"termination_reason": "end_call tool was called.",
"main_language": "es",
"cost_usd": "0.3912",
"has_recording": true,
"analysis": {
"summary": "La clienta pidió que la llamen el jueves en la mañana.",
"outcome": "callback_scheduled",
"sentiment": "positive",
"sentiment_score": 0.6,
"key_topics": [],
"qualification": {
"score": 85,
"interest": "high",
"answered": true,
"next_step": "Llamar el jueves a las 10:00",
"objections": [],
"reasons": ["Interes high.", "Quedo un siguiente paso concreto: Llamar el jueves a las 10:00."]
}
},
"started_at": "2026-09-14T15:39:05.000Z",
"ended_at": "2026-09-14T15:42:09.771Z",
"created_at": "2026-09-14T15:39:02.118Z",
"updated_at": "2026-09-14T15:42:11.034Z"
}
],
"total": 1284,
"limit": 50,
"offset": 0,
"nextCursor": "MjAyNi0wOS0xNFQxNTozOTowMi4xMTgwMDBafDViMGQyZjdlLTlhNDEtNGMzZS04ZjBhLTJjNmQxZTdiOWExMA"
}
}

List responses are cached for up to 60 seconds and call details for up to 5 minutes. Status changes Jelliu applies itself (hang-ups, post-call reports, the reconciler) clear the cache, so a freshly ended call normally shows up right away.

Get one call

curl -sS "https://api.jelliu.co/api/calls/5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10" \
-H "Authorization: Bearer $JELLIU_API_KEY"

The response is { "data": { ... } } with every field in the call object, including transcript.

Download the recording

GET /api/calls/{callId}/audio streams the recording as binary audio (Content-Type from the provider, audio/mpeg by default). Check has_recording first: it is true once the call is over and has audio to play.

curl -sS -f -o call.mp3 \
"https://api.jelliu.co/api/calls/5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10/audio" \
-H "Authorization: Bearer $JELLIU_API_KEY"

A missing recording is always a 404 with code CALL_NOT_FOUND. The X-Recording-Status response header tells the two common cases apart:

X-Recording-StatusMessageWhat to do
processingRecording is still processing — try again in a minute.Retry shortly. Recordings become available a few minutes after the call.
expiredThis recording is no longer available — recordings are kept by the voice provider for a limited time and this call is past that window.Nothing: the audio has been deleted at the source.
(absent)Call has no recording (no conversation_id)The call never started a conversation.

Audio is retained by the voice provider for your plan’s window: 7 days on Starter, 30 on Growth, 90 on Business, and until you delete it on Enterprise. Download recordings you need to keep. Responses carry Cache-Control: private, max-age=300.

Hang up a call

POST /api/calls/{callId}/hangup ends a live call. It is idempotent: calling it on a call that has already ended does nothing and still returns 200.

curl -sS -X POST "https://api.jelliu.co/api/calls/5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10/hangup" \
-H "Authorization: Bearer $JELLIU_API_KEY"
{
"data": { "success": true }
}

Jelliu first asks the carrier for the call’s real state, then records the right terminal status:

Carrier state at hang-upRecorded status
Already ended (for example it rang out)The carrier’s own status and duration, for example no-answer.
Answered and talkingcompleted. Duration, transcript and analysis follow with the post-call report.
Not yet answeredcanceled.
The call never reached the carriercanceled.

Hanging up ends the telephone line. The concurrency slot is released immediately; the post-call report still arrives and completes the record.

Read live concurrency

GET /api/calls/concurrency (added 2026-09-15) tells you how many voice calls your workspace has up right now and the ceiling the next call is refused at. Use it before dialing a burst, or to render a capacity indicator.

curl -sS "https://api.jelliu.co/api/calls/concurrency" \
-H "Authorization: Bearer $JELLIU_API_KEY"
{
"data": { "active": 2, "max": 5 }
}
  • active is the live counter the admission gate itself reads, so this number and the one that decides whether your next POST /api/calls connects can never disagree.
  • max is the effective ceiling: the lower of the platform ceiling and your plan’s concurrency limit. That is the value a call is actually refused at with 429 MAX_CONCURRENT_CALLS_REACHED; reporting your plan limit alone would promise capacity the gate does not grant.
  • The workspace comes from your credential. There is no tenant parameter, and one workspace cannot read another’s.
  • read scope, and available on every plan — it is deliberately not behind the live-monitor feature, because every workspace is entitled to see its own capacity.

GET /health never carried this. It is a platform probe with no workspace attached, so anything reading an activeCalls field from it was reading a number that did not exist. Read this route instead.

Transfer a live call to a person

A warm transfer brings a person into a live call. The caller is moved into a conference, Jelliu dials the person you name from your workspace’s number, and both end up on the line. You then either complete the transfer (the person keeps the call) or cancel it (the person’s leg is hung up and the AI agent picks the conversation back up).

These three endpoints are admin operations and require a key with the full scope.

1

Start the transfer

curl -sS -X POST "https://api.jelliu.co/api/calls/5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10/transfer" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "transfer_to": "+5716012345" }'
{
"data": {
"transferId": "CA8f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c",
"conferenceId": "CF1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
}
}

transfer_to must be an E.164 number and passes the same destination rules as outbound calls. The call must be live on the carrier: Jelliu checks the carrier directly, not the stored status.

2

Complete or cancel it

# The person keeps the call
curl -sS -X POST "https://api.jelliu.co/api/calls/5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10/transfer/complete" \
-H "Authorization: Bearer $JELLIU_API_KEY"
# Or: hang up the person's leg and give the call back to the agent
curl -sS -X POST "https://api.jelliu.co/api/calls/5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10/transfer/cancel" \
-H "Authorization: Bearer $JELLIU_API_KEY"

These two endpoints answer without a data wrapper:

{ "success": true, "message": "Transfer completed — AI agent disconnected" }
{ "success": true, "message": "Transfer canceled — AI agent resumed" }

What each step does to the call:

  • Complete does not hang up anyone. It sets outcome to escalated_to_human if the call had no outcome yet, sends a call.escalated event with transferredTo set to the destination and no escalatedBy key, and the record closes normally when the caller hangs up.
  • Cancel hangs up the person’s leg, starts a new agent conversation on the caller’s line and clears transferred_to, transferred_at and transfer_call_sid. It fails if the call’s agent is paused or no longer provisioned.

Agents can also hand calls to a person on their own, through a transfer tool configured on the agent. That transfer runs during the conversation; the destination number is checked against the same destination rules when you save it on the agent. Since 2026-09-15 it is recorded like this one — outcome: escalated_to_human and a call.escalated event, with escalatedBy: "agent" and transferredTo: null. See Escalation.

Inbound calls

When someone calls one of your numbers, Jelliu admits the call only if your workspace is not suspended, your plan allows calls and a concurrency slot is free. Then it picks the agent:

  1. The agent assigned to the number. If that agent is paused, the call is not passed to another agent: pausing the assigned agent stops that number from answering.
  2. Routing among your agents, choosing the least busy provisioned agent that is not paused.
  3. Your oldest provisioned, unpaused agent, as a last resort.

Numbers that were removed (soft-deleted) never route calls. An IVR menu, when enabled on the number, is played before routing. Inbound call records are created with call_direction: "inbound", status in-progress and no campaign. Assign agents and configure IVR on the number; see Phone numbers.

Retries

Standalone calls placed with POST /api/calls are never retried. Calls that belong to a campaign and end no-answer, busy, or without connecting are re-dialed according to the campaign’s maxRetryAttempts and retryIntervalMinutes, and each retry passes all the checks on this page again. See Campaigns.

Errors

All errors use the standard error envelope. Codes you can receive from the calls endpoints:

CodeStatusWhen
VALIDATION_FAILED400The body failed validation (Invalid call input, with details.fieldErrors); neither phoneNumber nor contactId was sent; the number is not E.164 (Invalid or missing phone number (E.164 required)); the destination is not a contact of the named campaign; a callId is not a UUID (Invalid call ID); list parameters are out of range (Invalid calls query).
VALIDATION_FAILED400Transfers: Invalid transfer input, Call has no active session — cannot transfer, Call must be in-progress to initiate a transfer, No active transfer for this call, No active transfer to cancel.
VALIDATION_FAILED409Transfers: Could not confirm the call is still live — try again in a moment.
UNAUTHORIZED401Missing or invalid API key. See Authentication.
FORBIDDEN403The key lacks write (placing calls, hanging up) or full (transfers).
BILLING_ERROR403No active plan, trial minutes used up, or the plan’s overage ceiling was reached.
COMPLIANCE_BLOCKED403Outside calling hours, opt-out, do-not-call, attempt limits, blocked prefix, or a destination that is not allowed. The message names the rule.
TRIAL_ABUSE_DETECTED403A trial workspace called a number already used by other trial workspaces.
PHONE_NUMBER_REQUIRED403An active paid plan has no provisioned phone number to call from.
NOT_FOUND404contactId does not exist and no phoneNumber was sent (Contact not found).
AGENT_NOT_FOUND404The agent does not exist in your workspace, or was deleted.
CALL_NOT_FOUND404The call does not exist in your workspace, or its recording is unavailable (see Download the recording).
AGENT_PAUSED409The agent is paused. 503 when its state could not be read; the call is refused as a precaution.
AGENT_PROVISIONING409The agent is still being set up. Retry after Retry-After (5 seconds).
NUMBER_UNVERIFIED400The telephony account cannot dial this destination (trial telephony account or a region not enabled). The record is marked failed.
CALL_SPAM_DETECTED429More than 5 calls to the same number in an hour, or more than 500 calls per hour for the workspace.
MAX_CONCURRENT_CALLS_REACHED429Your plan’s concurrent-call limit, or the campaign’s maxConcurrentCalls, is in use. No Retry-After header; retry when a call ends.
RATE_LIMIT_EXCEEDED429More than 20 outbound requests per minute (placing calls and starting transfers, shared with email sends and WhatsApp), or the general API limit of your plan. Honor Retry-After.
CALL_INITIATION_FAILED502The voice provider rejected the call. The body carries only Internal server error; the record is marked failed and can be inspected with GET /api/calls.
TELEPHONY_ERROR502The carrier failed while starting, completing or canceling a transfer (also when canceling is refused because the agent is paused). The body carries Internal server error.

Do not retry CALL_INITIATION_FAILED or a timeout on POST /api/calls blindly. If the provider accepted the call but a later bookkeeping step failed, Jelliu still returns 201 for the live call; a network timeout on your side may hide a call that is already ringing. List recent calls for the number before dialing again.

Limits

LimitValue
Request ratePOST /api/calls and POST /api/calls/{callId}/transfer: 20 requests per minute per workspace, one budget shared with email sends and WhatsApp, and this limit fails closed. Every route under /api/calls, those two included, also counts against the general per-plan limit. Listing calls, reading one, downloading audio, hanging up and completing or canceling a transfer use only the general limit. See Rate limits.
Concurrent callsStarter 3, Growth 10, Business 25, Enterprise custom (subject to a platform-wide ceiling). Inbound and outbound calls share the same slots. A campaign’s maxConcurrentCalls further caps calls filed under that campaign.
Calls to one number5 per hour per workspace.
Calls per workspace500 per hour.
MinutesTrial: 15 minutes in total, and each call is capped at the minutes left. Paid plans: calls are refused once usage reaches 150% of the plan’s included minutes plus any minute packs bought, until the period renews, a pack is bought or the plan is upgraded.
Call length240 to 900 seconds depending on the agent’s category; see Durations and time limits.
Recording retention7, 30 or 90 days on Starter, Growth and Business; until deleted on Enterprise.
EndpointScope
GET /api/calls, GET /api/calls/{callId}, GET /api/calls/{callId}/audio, GET /api/calls/concurrencyread
POST /api/calls, POST /api/calls/{callId}/hangupwrite
POST /api/calls/{callId}/transfer, .../transfer/complete, .../transfer/cancelfull

Webhooks

EventSent when
call.completedA call ended and its analysis is available. When the analysis is still being computed, the event waits for it so that outcome is filled in.
call.failedA call ended without a conversation. reason is no_answer, busy, failed, canceled, voicemail or error. Sent instead of call.completed, never both for one call.
contact.convertedThe call converted its contact. Carries call_id and source: "call".
crm_sync.completed, crm_sync.failedThe call was written, or failed to be written, to a connected CRM.
agent_action.recordedThe agent invoked a tool during the call. Carries call_id.

call.started, call.answered, call.transferred and call.analysis_updated are also available; the catalog describes their fields. The transcript is not included in any event: retrieve it with GET /api/calls/{callId}. See Webhooks for payloads, signatures and retries.

In call.completed, sentiment_score is null when the call was not scored, the same as in the API. 0 is a real neutral score.