Create a call

View as Markdown
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: 1. The agent exists and is not paused (404 / 409 `AGENT_PAUSED`). 2. The destination resolves to a valid E.164 number (400). 3. **Compliance** (403 `COMPLIANCE_BLOCKED`): outside the allowed calling hours for the destination's country, in the workspace time zone (Settings → Compliance; `America/Bogota` when 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. 4. **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. 5. **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 (403 `TRIAL_ABUSE_DETECTED`). 6. **Minutes** (403 `BILLING_ERROR`): no active plan, trial minutes (15) used up, or the paid plan's pay-as-you-go ceiling reached. 7. The agent finished provisioning (409 `AGENT_PROVISIONING` with `Retry-After: 5`), and a paying workspace has its dedicated number (403 `PHONE_NUMBER_REQUIRED`). 8. **Concurrency** (429 `MAX_CONCURRENT_CALLS_REACHED`): the campaign's own `max_concurrent_calls` (message `Campaign concurrency limit reached`), then the workspace's plan cap on live calls (Starter 3, Growth 10, Business 25), message `Max 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` and the error is returned (400 `NUMBER_UNVERIFIED` or 502); no webhook event is sent for that row. **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.** When the call ends, `call.completed` (answered, voicemail and other analysed outcomes) or `call.failed` (technical failure, or a stale call closed by the reconciler as failed / no-answer) is delivered; when post-call analysis is still pending the event is held until it arrives. A campaign call can also trigger `campaign.completed`. Mutations also emit `audit.log_recorded` to subscribers. See [Webhooks](/webhooks). **Access** - **Required scope:** `write`. - **Rate limit:** Outbound — 20 requests/min per workspace, shared by every `/api/calls` endpoint. See [Rate limits](/rate-limits). - **Plan:** Available on every plan, including trial; requires an active subscription or remaining trial minutes. Concurrent calls are capped per plan.

Authentication

AuthorizationBearer
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. | Scope | GET / HEAD | POST / PUT / PATCH / DELETE | Admin-only routes | | --- | --- | --- | --- | | `read` | Yes | No | No | | `write` | Yes | Yes | No | | `full` | Yes | Yes | Yes | 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](/authentication).

Request

This endpoint expects an object.
agentIdstringRequiredformat: "uuid"
The agent that talks. Must exist in this workspace, not be paused, and be provisioned with the voice provider.
phoneNumberstringOptionalformat: "^\+[1-9]\d{1,14}$"

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.

campaignIdstringOptionalformat: "uuid"

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.

contactIdstringOptionalformat: "uuid"

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.

promptVariantIdstringOptionalformat: "uuid"

A/B test prompt variant this call belongs to; stored on the call as prompt_variant_id for reporting.

promptOverridestringOptional<=5000 characters

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.

firstMessageOverridestringOptional<=500 characters

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.

dataobject

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 omits transcript, 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_bytes and dnc_scanned_at.
  • POST /api/calls returns 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.

Errors

400
Bad Request Error
401
Unauthorized Error
403
Forbidden Error
404
Not Found Error
409
Conflict Error
429
Too Many Requests Error
502
Bad Gateway Error
503
Service Unavailable Error