Create a call
Places a real outbound phone call in which the agent agentId talks to the destination. Give either a raw phoneNumber (E.164) or a contactId; when both are present phoneNumber wins. The call is dialed synchronously: a 201 means the voice provider accepted it and the phone is ringing (status: ringing). Everything else about the conversation (transcript, outcome, duration) arrives after it ends; follow it with webhooks or GET /api/calls/{callId}.
The call is always filed under a contact. With a contactId whose number matches the destination, that contact is used; otherwise the oldest contact of the workspace with the destination number is used, and a campaign-less contact is created when none exists. If phoneNumber differs from the named contact’s number, the named contact is ignored. With a campaignId, such a different destination must itself be a contact of that campaign, or the call is refused with 400.
Checks before dialing, in order. Each refusal happens before any money is spent:
- The agent exists and is not paused (404 / 409
AGENT_PAUSED). - The destination resolves to a valid E.164 number (400).
- Compliance (403
COMPLIANCE_BLOCKED): outside the allowed calling hours for the destination’s country, in the workspace time zone (Settings → Compliance;America/Bogotawhen none is set); a blocked prefix; the number is on the opt-out suppression list; the contact or number is marked Do Not Call; the contact reached its daily or lifetime attempt cap. - Destination policy (403
COMPLIANCE_BLOCKED): premium-rate / high-risk (toll-fraud) prefixes, the workspace blocklist, and the allowed calling countries. Every refusal here writes an audit entry naming the masked number. - Abuse guards: more than 5 calls to the same number in an hour or 500 calls per hour in the workspace (429
CALL_SPAM_DETECTED); a number already called by another trial workspace (403TRIAL_ABUSE_DETECTED). - Minutes (403
BILLING_ERROR): no active plan, trial minutes (15) used up, or the paid plan’s pay-as-you-go ceiling reached. - The agent finished provisioning (409
AGENT_PROVISIONINGwithRetry-After: 5), and a paying workspace has its dedicated number (403PHONE_NUMBER_REQUIRED). - Concurrency (429
MAX_CONCURRENT_CALLS_REACHED): the campaign’s ownmax_concurrent_calls(messageCampaign concurrency limit reached), then the workspace’s plan cap on live calls (Starter 3, Growth 10, Business 25), messageMax concurrent calls reached. A slot is freed when a call ends.
Side effects. Dials through the voice engine (over the telephony carrier, or the workspace’s SIP trunk) using the workspace’s own number for the agent as caller ID. For destinations that require it, a recording notice is prepended to the agent’s greeting. The contact’s call_attempts and last_called_at are incremented once the provider accepts the call. Talk time is billed when the call ends, rounded up to whole minutes per call, against included minutes and then overage; a trial call is cut at the remaining trial seconds. Writes an audit entry. If the provider refuses the call, the row is kept with status: failed, the error is returned (400 NUMBER_UNVERIFIED or 502), and call.failed is published for it with reason: error.
Idempotency. Not idempotent and no idempotency key is honoured: retrying after a timeout or a network error can place a second real call to the same person. Check GET /api/calls?agentId=... for a recent row before retrying. Each attempt also counts towards the 5-calls-per-number-per-hour abuse guard.
Webhook events. Exactly one terminal event per call: call.completed ({ call_id, campaign_id, contact_id, agent_id, phone_number, outcome, sentiment_score, duration, summary, data_collection, kpi_data }) when the call had a conversation, sent once its post-call analysis is in, or call.failed ({ call_id, campaign_id, contact_id, agent_id, phone_number, outcome, reason, duration }, reason one of no_answer, busy, failed, canceled, voicemail, error) when it did not, including a call the provider refused and a stale call closed by the reconciler. A campaign call can also trigger campaign.completed. The call also emits call.started, call.answered (when it was answered), call.transferred (when it was transferred) and call.analysis_updated (a later re-analysis). Mutations also emit audit.log_recorded to subscribers. See Webhooks.
Access
- Required scope:
write. - Rate limit: Outbound — 20 requests/min per workspace, fail-closed, one budget shared with transfers, email sends and WhatsApp; the request also counts against the General API limit (120 to 600 requests/min by plan). Listing and reading calls do not spend the outbound budget. See Rate limits.
- Plan: Available on every plan, including trial; requires an active subscription or remaining trial minutes. Concurrent calls are capped per plan.
Authentication
Workspace API key: jl_ followed by 64 lowercase hex characters, created by the workspace owner in the
dashboard (Settings → API Keys) and sent as Authorization: Bearer jl_.... The plaintext is shown once,
at creation; Jelliu stores only a SHA-256 hash. A workspace can hold up to 25 active keys.
Operations restricted to admins or owners reject keys without the full scope with 403, and say so in
their description. No key, whatever its scope, can mint or revoke API keys or rotate a webhook secret —
that requires a signed-in owner session. A revoked key stops authenticating within about 10 seconds.
See Authentication.
Request
Destination in E.164 (Must be E.164 format otherwise). When omitted, the contact’s number is used. When it differs from the contact’s number, the call is filed under the contact that owns this number instead.
File the call under this campaign of the workspace (its name, product context and target audience are passed to the agent, and its max_concurrent_calls applies). If phoneNumber overrides the contact’s number, the destination must be one of the campaign’s contacts.
Contact to call. Its name and earlier conversations on any channel are given to the agent. Ignored when it does not exist and phoneNumber is present.
A/B test prompt variant this call belongs to; stored on the call as prompt_variant_id for reporting.
Replaces the agent’s prompt for this call only. Rejected when it contains prompt-injection patterns (Prompt contains disallowed injection patterns), e.g. “ignore previous instructions”, “actúa como”, sistema: or zero-width characters. Its use is logged.
Replaces the agent’s opening line for this call. Same injection check as promptOverride. A legally required recording notice is still prepended when the destination needs one.
Response
The provider accepted the call and it is ringing. The body is the stored row without the derived analysis, has_recording, agent_name, campaign_name and contact_name.
A call as the API serializes it. Field names are snake_case (raw database columns); timestamps are ISO-8601 strings. Vendor identifiers (carrier call SID, voice-provider conversation id) are never returned.
Which fields are present depends on the endpoint:
GET /api/calls/{callId}returns every field below.GET /api/calls(list) returns a slimmer row: it omitstranscript,first_response_ms,transfer_call_sid,transferred_at,transferred_to,kpi_data,llm_cost_usd,llm_input_tokens,llm_output_tokens,sentiment_timeline,telemetry_fetched_at,tool_call_count,mcp_call_count,rag_lookup_count,tool_result_bytesanddnc_scanned_at.POST /api/callsreturns the stored row without the derived fields (analysis,has_recording,agent_name,campaign_name,contact_name).
Telemetry fields are null when not measured, which is not the same as 0. Every field is server-generated; calls cannot be edited through the API.

