From 0e4be13332b173fd084dcb6019f0bccaa0bfcf9e Mon Sep 17 00:00:00 2001 From: "lin.si" Date: Wed, 23 Sep 2026 17:02:19 +0800 Subject: [PATCH] docs: prepare Calls migration and result contract --- content/guides/calls.mdx | 784 +++++++++------------------ content/guides/changelog.mdx | 52 ++ content/guides/errors.mdx | 57 +- content/guides/goal-runs.mdx | 39 +- content/guides/legacy-calls.mdx | 589 ++++++++++++++++++++ content/guides/legacy-quickstart.mdx | 214 ++++++++ content/guides/legacy-sdks.mdx | 208 +++++++ content/guides/legacy-webhooks.mdx | 201 +++++++ content/guides/migration.mdx | 106 ++++ content/guides/quickstart.mdx | 257 +++------ content/guides/retirement.mdx | 65 +++ content/guides/sdks.mdx | 239 +++----- content/guides/webhooks.mdx | 224 ++------ openapi/calle.openapi.yaml | 523 ++++++++++++++++-- playwright.config.ts | 8 +- scripts/augment-llms.mjs | 9 +- scripts/verify-dist.mjs | 17 +- src/styles.css | 95 ++-- tests/docs-site.spec.ts | 81 ++- zudoku.config.tsx | 56 +- 20 files changed, 2651 insertions(+), 1173 deletions(-) create mode 100644 content/guides/legacy-calls.mdx create mode 100644 content/guides/legacy-quickstart.mdx create mode 100644 content/guides/legacy-sdks.mdx create mode 100644 content/guides/legacy-webhooks.mdx create mode 100644 content/guides/migration.mdx create mode 100644 content/guides/retirement.mdx diff --git a/content/guides/calls.mdx b/content/guides/calls.mdx index 7735d93..6a2646a 100644 --- a/content/guides/calls.mdx +++ b/content/guides/calls.mdx @@ -1,583 +1,325 @@ --- title: Calls -description: Understand the call creation contract and event flow. +description: Single-target calls, immutable requests, result readiness, and execution boundaries. --- -Use call tasks to turn a structured workflow step into one or more real phone interactions. +Create one phone task and receive a structured result. Start with the +[Quickstart](/quickstart) to try it in the test environment. -Use the [Calls API Reference](/api-reference/calls) for the exact HTTP request -and response schemas. See the [error handling guide](/errors) for retry -behavior and the [terminal webhooks guide](/webhooks) for asynchronous -completion. +Check [Regions & languages](/regions) for supported destinations and languages. -## Call inputs +## Preview availability -`task` is the natural-language instruction for the call task. Keep it specific and outcome-oriented. +This is a preview contract, not a production availability announcement. +Optional target hints, input validation, idempotent creation and detailed events +were verified in the test preview on September 22, 2026. Shared test deployments +can change; confirm the deployed contract before integrating. -`recipients` is optional. When it is omitted, include the phone target in `task` and CALL-E will infer it. Use `recipients` for explicit batch targets; each recipient contains a `phones` array of E.164 numbers. +The `call_outcome`, `result_status` and fixed `transcript` fields below are +**unreleased response additions**, not part of that verified deployment. +Until they are deployed, follow the [Quickstart polling rule](/quickstart#wait-for-the-result): +wait for a non-null `result` or `error`. The result-outcome and transcript sections +describe the intended contract, not a guarantee about the current test response. -Check the [supported regions and languages](/regions) -before choosing a recipient's `region` and `locale`. A valid E.164 number does -not establish that its destination is supported. +## Call ID and Billing -Examples use phone placeholders such as `` and ``. Replace them with phone numbers you own or are authorized to call. +The response adds a fixed `call_id` field for matching a telephone call to the +**Call ID** shown in Billing. This field is a local preview addition and has not +yet been deployed. -`result_schema` is a JSON Schema object for the whole call task. CALL-E validates the structured result against it before returning the terminal call task state. Object schemas are strict by default, so fields not declared in `properties` are rejected. - -`recipient_result_schema` is an optional JSON Schema object for each recipient result. It uses the same strict object behavior. - -`metadata` is copied through to the call task and webhook payload. Use it for workflow identifiers, user ids, or reconciliation fields. +```json +{ + "id": "call_example", + "call_id": "0123456789abcdef0123456789abcdef" +} +``` -`webhook_url` is an optional request-level endpoint for terminal webhooks. +- `id` identifies the API resource. Keep using it for get, cancel and event requests. +- `call_id` identifies the actual telephone call. Use it to find the corresponding + Billing entry. It is `null` until the provider ID is recorded, including cancellation + before dialing; never substitute `id` when it is null. -The server SDKs also reserve a `context` input for future SDK-side workflow data. It is not sent to the API yet. +The field does not depend on a successful business result. GET, exact creation +replay and terminal webhook `data.call_id` use the same recorded identity, including +existing Calls with saved provider evidence. An ID does not mean settlement is +complete; the Billing charge may appear later. Event envelopes keep their existing +`call_id` as the API resource ID. In the TypeScript wrapper the new response field +is `call.callId`; Python and raw JSON use `call["call_id"]`. -## Direct HTTP with curl +## Request contract -Set your API key, then create a call with a stable idempotency key. Replace -`` with a phone number you own or are authorized to call. +`POST /v2/calls` prepares one phone task, then returns `202` after durable +acceptance. The request waits for preparation; dialing runs in the background +using the saved instructions. -```bash -export CALLE_API_KEY="" +| Input | Required | Meaning | +| --- | --- | --- | +| `task` | Yes | Complete instructions and facts needed for the conversation. | +| `phone` | Yes | One E.164 number, including `+` and country code. | +| `region` | No | Two-letter uppercase destination hint, such as `US`. Inferred from the phone when omitted. | +| `locale` | No | Spoken language, such as `en-US`. Inferred from the task and available regional languages when omitted. | +| `result_schema` | Yes | A closed object containing at most 32 scalar properties. | +| `Idempotency-Key` header | Yes | A stable key for this logical call, 1-255 characters after trimming. See [Idempotency](#idempotency). | +| `metadata` | No | Application-owned correlation data, returned with the Call. | +| `webhook_url` | No | HTTPS receiver for the final result or error. | + +If `region` and `locale` pass format validation but have no supported calling +configuration, creation returns `422 unsupported_region` or +`422 unsupported_language`. The error identifies the field to correct and +includes the rejected region and locale, whether supplied or inferred: -curl --fail-with-body --silent --show-error \ - --request POST "https://api.heycall-e.com/v1/calls" \ - --header "Authorization: Bearer $CALLE_API_KEY" \ - --header "Content-Type: application/json" \ - --header "Idempotency-Key: wf_123_hearing_check" \ - --data '{ - "task": "Call and ask whether they can hear clearly.", - "result_schema": { - "type": "object", - "required": ["can_hear_clearly"], - "properties": { - "can_hear_clearly": { - "type": "string", - "enum": ["yes", "no", "unknown"] - } - }, - "additionalProperties": false - }, - "metadata": { - "workflow_run_id": "wf_123" +```json +{ + "error": { + "code": "unsupported_language", + "message": "Locale 'xx-XX' is not supported for region 'CN'. Choose a supported locale for this region.", + "details": { + "field": "locale", + "region": "CN", + "locale": "xx-XX" } - }' -``` - -The response contains the call task `id`. Use it to read the current state or -the ordered lifecycle events: - -```bash -export CALLE_CALL_ID="" - -curl --fail-with-body --silent --show-error \ - --header "Authorization: Bearer $CALLE_API_KEY" \ - "https://api.heycall-e.com/v1/calls/$CALLE_CALL_ID" - -curl --fail-with-body --silent --show-error \ - --header "Authorization: Bearer $CALLE_API_KEY" \ - "https://api.heycall-e.com/v1/calls/$CALLE_CALL_ID/events?limit=50" + } +} ``` -## Call identifiers - -- **Calls API `call_id`:** use the top-level `id` (`call_...`) in HTTP and - Python, or `call.id` in TypeScript. -- **Dashboard Call Record ID:** use - `recipients[].attempts[].provider_call_id` in HTTP and Python, or - `call.recipients[i].attempts[j].providerCallId` in TypeScript. - -Use `call_id` with `GET /v1/calls/{call_id}` and -`GET /v1/calls/{call_id}/events`. Do not use `provider_call_id` as `call_id`; -it identifies one attempt and may be `null`. - -Event-list items expose the CallTask ID as `call_id`. Terminal webhooks expose -it as `data.id`; the webhook's top-level `id` identifies the event. - -Persist the returned Call ID with your workflow record. The Calls API does not -provide a list endpoint: `GET /v1/calls` cannot recover IDs you did not save. -See [Recover after a restart or lost response](#recover-after-a-restart-or-lost-response). - -TypeScript: - -```ts -const call = await client.calls.create( - { - task: "Call each recipient and ask whether they can attend Friday lunch in San Francisco.", - recipients: [ - { phones: [""] }, - { phones: [""] }, - ], - resultSchema: { - type: "object", - required: ["attending_count"], - properties: { - attending_count: { type: "integer" }, - }, - }, - recipientResultSchema: { - type: "object", - required: ["can_attend"], - properties: { - can_attend: { type: "string", enum: ["yes", "no", "unknown"] }, - }, - }, - metadata: { - workflow_run_id: "wf_123", - }, - webhookUrl: "https://example.com/calle/webhook", - }, - { - idempotencyKey: "wf_123_friday_lunch", - }, -); -``` +Choose a supported [region and language](/regions) and resubmit with the same +idempotency key. Explicit unsupported targets are rejected before model +preparation; an inferred locale is checked after inference. Neither rejection +creates a Call or outbound telephone task. Configuration service failures still return +`503 provider_unavailable`. + +Omitting a hint or sending `null` requests inference. Explicit spoken-language +instructions in `task` take precedence over the language used to write the task. +CALL-E returns the resolved `region` and `locale` on acceptance and keeps them +fixed for execution and retries. An explicit region conflicting with the phone, +or a language that cannot be determined unambiguously, returns +`422 input_incomplete` with `details.missing_inputs`. Correct the phone/region +or specify the spoken language before resubmitting. A supplied locale is never +silently replaced with another language. + +For a phone/region conflict, `details.fields` identifies `phone` and `region`, +`details.region` is the supplied region, and `details.inferred_region` is the +region resolved from the number. Display `details.missing_inputs` to the user; +do not fix the conflict by silently changing their destination. + +If essential information is missing or conflicting, creation returns +`422 input_incomplete` with the specific missing inputs. No Call is created, +no phone is dialed, and no terminal webhook is sent for the rejected request. -Python: - -```python -call = client.calls.create( - task="Call each recipient and ask whether they can attend Friday lunch in San Francisco.", - recipients=[ - {"phones": [""]}, - {"phones": [""]}, - ], - result_schema={ - "type": "object", - "required": ["attending_count"], - "properties": {"attending_count": {"type": "integer"}}, - }, - recipient_result_schema={ - "type": "object", - "required": ["can_attend"], - "properties": { - "can_attend": {"type": "string", "enum": ["yes", "no", "unknown"]}, - }, - }, - metadata={"workflow_run_id": "wf_123"}, - webhook_url="https://example.com/calle/webhook", - idempotency_key="wf_123_friday_lunch", -) +```json +{ + "error": { + "code": "input_incomplete", + "message": "The task lacks information required to make this call.", + "details": { + "missing_inputs": [ + "Provide the appointment date.", + "Provide the appointment time." + ] + } + } +} ``` -## Structured results +Collect the missing facts in your application, update `task`, and submit again. +Information the call is meant to collect from the recipient is not itself a +missing prerequisite. The API does not ask clarification questions interactively. +Preparation is bounded to 25 seconds. A preparation timeout or model failure +returns `503 provider_unavailable`; it is not an incomplete-input response. -Structured results let you turn the terminal call evidence into a stable JSON object for your workflow. The schema is an extraction contract: the SDK sends the schema to CALL-E, CALL-E extracts a result from the completed call evidence, and the service validates the result before returning it. +## Result schema -The extraction model uses the call transcript, ASR, and Calling facts as primary evidence. It uses the post-call summary and outcome as supporting context. If CALL-E cannot produce a schema-valid result from the evidence, the public `structured_result` is `null`. +Use `type: "object"` and `additionalProperties: false`. Property values can be +strings, booleans, integers or numbers. Nested objects, arrays, null values and +schema combinators are unsupported. Use an explicit scalar such as `"unknown"` +when an answer can be unknown. The request and schema are frozen for execution. -Use `result_schema` for one result object that describes the whole call task. Use `recipient_result_schema` when each recipient needs an independent result, especially for batch calls. TypeScript uses `resultSchema` and `recipientResultSchema`; Python uses `result_schema` and `recipient_result_schema`. The JSON Schema object itself is the same shape. +For example, `"type": ["string", "null"]` is not supported. It returns +`400 result_schema_invalid` before a Call is created. Use `"type": "string"` +and explicitly describe an allowed `"unknown"` fallback in the task instead. -For `recipient_result_schema`, avoid reserved recipient response field names such as `summary`, `status`, `transcript`, `call_id`, and timing fields. Use custom names such as `customer_summary`, `notes`, or `reason` instead. +### Planned result outcomes -Descriptions are passed to the extraction model. Use `description` to explain what each field means and how enum values should be selected. Descriptions guide extraction, but they are not hard validation rules. Hard validation comes from `type`, `required`, `enum`, and `additionalProperties`. +The following outcome/readiness behavior belongs to the +[unreleased response additions](#preview-availability). -Supported schema features: +The response uses the same result model as Goal Runs. `call_outcome` describes +the telephone outcome: `completed`, `no_answer`, `busy`, or `declined`. A no-answer, +busy or declined call finishes with `status: "completed"` and is not a technical error. -- `type`: `object`, `string`, `number`, `integer`, `boolean`, or `array` -- `properties` -- `required` -- `enum` -- nested `object` fields -- simple `array.items` -- `description` -- `additionalProperties: false` +Poll while `result_status` is `pending`. `available` includes a validated empty +object; `unavailable` means the evidence did not support a schema-valid business +result; `not_applicable` covers cancellation and technical execution failure. +Both `result` and `error` can be null in a final unavailable result. -Unsupported schema features include `$ref`, `oneOf`, `anyOf`, `allOf`, recursive schemas, complex format validation, and `additionalProperties: true`. +Include explicit missing-answer and unknown rules in `task`, consistent with +`result_schema`. CALL-E preserves those rules during extraction. Missing answers +do not automatically become `false`. Without a permitted fallback, the business +result remains unavailable and `error` stays null. -For business decisions, prefer string enums over booleans when the answer can be unclear. Include an `unknown` value when the call may not provide enough evidence. - -```ts -resultSchema: { - type: "object", - required: ["answer", "evidence"], - properties: { - answer: { - type: "string", - enum: ["yes", "no", "unknown"], - description: - "The answer to the task question. Use unknown if the recipient did not answer, avoided the question, or the evidence is ambiguous.", - }, - evidence: { - type: "string", - description: - "A short quote or paraphrase from the call that supports the answer.", - }, - }, - additionalProperties: false, -} -``` - -When a result drives automation, add an evidence field so your system can inspect why CALL-E made the classification. - -### Classify the final endpoint - -The Calls API does not return a built-in AMD disposition or `answered_by` field. Define the classification with a per-recipient Structured Result. You control the property name and enum values. - -```ts -recipientResultSchema: { - type: "object", - required: ["answered_by"], - properties: { - answered_by: { - type: "string", - enum: ["human", "ivr", "voicemail", "unknown"], - description: - "Classify the final endpoint. If an IVR transfers the call to a person, use human.", - }, - }, - additionalProperties: false, +```json +{ + "status": "completed", + "call_outcome": "busy", + "result_status": "unavailable", + "transcript": [], + "result": null, + "error": null } ``` -Read `call.recipients[i].structuredResult` in TypeScript, `call["recipients"][i]["structured_result"]` in Python, or `recipients[i].structured_result` over HTTP. CALL-E returns `null` when it cannot produce a schema-valid recipient result or when the request omits `recipient_result_schema`. The example uses `unknown` as a schema-valid fallback. - -### Sales handoff - -Use this pattern when a prospect should be routed to a human if they ask for help or show strong interest. - -```ts -resultSchema: { - type: "object", - required: [ - "human_assistance_requested", - "interest_level", - "handoff_recommended", - "evidence_summary", - ], - properties: { - human_assistance_requested: { - type: "string", - enum: ["yes", "no", "unknown"], - description: - "Whether the prospect explicitly asked to speak with a human, sales representative, specialist, manager, or requested a callback from a person. Use unknown if the evidence is unclear.", - }, - interest_level: { - type: "string", - enum: ["strong", "moderate", "low", "not_interested", "unknown"], - description: - "Use strong when the prospect asks about pricing, demos, next steps, availability, implementation, purchase process, or clearly wants follow-up. Use moderate for curiosity without a concrete next step. Use low for minimal engagement. Use not_interested when they clearly decline. Use unknown when evidence is insufficient.", - }, - handoff_recommended: { - type: "string", - enum: ["yes", "no", "unknown"], - description: - "Use yes if human_assistance_requested is yes or interest_level is strong. Use no when the prospect is low interest or not interested. Use unknown when the evidence is insufficient.", - }, - evidence_summary: { - type: "string", - description: - "One concise sentence citing the prospect's words or behavior that supports the handoff decision.", - }, - }, - additionalProperties: false, -} -``` +`status: "completed"` means telephone execution ended; it does not mean the +business goal succeeded or that the result is ready. `completed_at` is the +execution completion time. Evaluate your own schema fields to decide business +success. Put a summary or completion flag in the schema if your application needs it. -### Appointment confirmation - -Use this pattern when calling a business to confirm, reschedule, or cancel an appointment. - -```ts -resultSchema: { - type: "object", - required: ["appointment_status", "confirmed_time", "confirmation_code"], - properties: { - appointment_status: { - type: "string", - enum: ["confirmed", "rescheduled", "canceled", "not_found", "unknown"], - description: - "The final appointment outcome. Use confirmed only when the business clearly confirms the appointment. Use rescheduled if a new time was agreed. Use not_found if the business cannot find the appointment. Use unknown when the evidence is unclear.", - }, - confirmed_time: { - type: "string", - description: - "The confirmed appointment time as stated in the call, or an empty string if no time was confirmed.", - }, - confirmation_code: { - type: "string", - description: - "The confirmation number or booking reference provided by the business, or an empty string if none was provided.", - }, - }, - additionalProperties: false, -} -``` +## Transcript -### Batch recipient result - -Use `recipientResultSchema` when each recipient should have their own answer. - -```ts -recipientResultSchema: { - type: "object", - required: ["can_attend", "dietary_notes"], - properties: { - can_attend: { - type: "string", - enum: ["yes", "no", "maybe", "unknown"], - description: - "Whether this recipient can attend. Use maybe only when they express uncertainty. Use unknown if the call did not reach the recipient or no clear answer was given.", - }, - dietary_notes: { - type: "string", - description: - "Any dietary restrictions or preferences mentioned by this recipient, or an empty string if none were mentioned.", - }, - }, - additionalProperties: false, -} -``` +This field belongs to the [unreleased response additions](#preview-availability). -### Support triage - -Use this pattern when a call should determine whether an issue was resolved or needs follow-up. - -```ts -resultSchema: { - type: "object", - required: ["issue_resolved", "requires_follow_up", "priority", "summary"], - properties: { - issue_resolved: { - type: "string", - enum: ["yes", "no", "partial", "unknown"], - description: - "Use yes when the issue was fully resolved during the call. Use partial when some progress was made but another action remains. Use no when the issue was not resolved. Use unknown if the outcome is unclear.", - }, - requires_follow_up: { - type: "string", - enum: ["yes", "no", "unknown"], - description: - "Whether another human or system action is required after the call. Use unknown if the call evidence is insufficient.", - }, - priority: { - type: "string", - enum: ["urgent", "normal", "low", "unknown"], - description: - "Use urgent for time-sensitive issues, service outages, billing blockers, or explicit escalation requests. Use normal for standard follow-up. Use low for informational or non-urgent cases.", - }, - summary: { - type: "string", - description: - "A concise summary of the issue, outcome, and any next action.", - }, - }, - additionalProperties: false, -} -``` +Every Call includes a top-level `transcript` array, separate from `result` and +`result_schema`. After execution ends, it contains the recorded conversation in +order, even while the business result is pending, unavailable, or failed. +Before execution ends or when no transcript is available, it is `[]`. -### Pricing or quote request - -Use this pattern when a prospect may ask about pricing, quotes, discounts, or budget. - -```ts -resultSchema: { - type: "object", - required: ["pricing_requested", "budget_mentioned", "next_step"], - properties: { - pricing_requested: { - type: "string", - enum: ["yes", "no", "unknown"], - description: - "Whether the prospect asked for pricing, a quote, discount information, plan details, or cost comparison. Use unknown if the evidence is unclear.", - }, - budget_mentioned: { - type: "string", - description: - "Any budget, price range, or cost constraint mentioned by the prospect, or an empty string if none was mentioned.", - }, - next_step: { - type: "string", - enum: ["send_pricing", "schedule_demo", "human_callback", "no_action", "unknown"], - description: - "The most appropriate next step based on the prospect's request. Use human_callback if they ask to speak with a person. Use no_action if they clearly decline or no follow-up is needed. Use unknown if the evidence is insufficient.", - }, - }, - additionalProperties: false, +```json +{ + "transcript": [ + {"speaker": "bot", "offset_seconds": 0, "text": "Hello."}, + {"speaker": "user", "offset_seconds": 2, "text": "Goodbye."} + ] } ``` -### Best practices - -- Keep schemas focused. A small schema with clear fields is more reliable than a large schema with many optional fields. -- Put enum selection rules in the field `description`. -- Include `unknown` when the call may not contain enough evidence. -- Use `required` for fields your workflow always expects. -- Use `additionalProperties: false` to prevent extra fields from being returned. -- Add an evidence or summary field when the result triggers workflow automation. -- Do not rely on `description` for validation. Use schema constraints for enforceable behavior. - -## Call status - -The call task's `status` has exactly five values: - -| Status | Terminal? | Meaning | -| --- | --- | --- | -| `queued` | No | The call task is queued. | -| `in_progress` | No | The call task is running, including post-call finalization. | -| `completed` | Yes | The call task completed. Check its results for the business outcome. | -| `failed` | Yes | The call task failed. Keep the failure fields as diagnostic context. | -| `canceled` | Yes | The call task was canceled. | - -`no_answer`, `busy`, and `voicemail` are not Calls API lifecycle statuses. -Recipient and attempt objects have their own status enums; do not substitute -them for the top-level call status. A `completed` state does not establish -that a person answered or that your business objective succeeded. See -[Task completion](#task-completion) for interpreting the business result. - -## Parallel and quorum-based dispatch - -The Calls API does not expose an operation for clients to cancel a call after -it has been created. A call that is already in flight may therefore continue -to completion even when your application no longer needs its result. The -`canceled` resource status does not imply that clients can request -cancellation. - -For workflows that need only a target number of confirmations, dispatch calls -in controlled waves instead of starting every call at once. Count terminal -results through polling or webhooks, and stop creating subsequent waves after -the confirmation target is reached. Choose a wave size that balances response -speed against the number of calls that may still be in flight when the target -is met. +`speaker` is `bot` (CALL-E), `user` (the remote party, including automated prompts), or `unknown`. +`offset_seconds` is a nonnegative offset in seconds, or `null` when unavailable. +`text` preserves the recorded words. Missing roles or times are not inferred. +GET and terminal webhooks use the same transcript snapshot. This field does not +provide live ASR streaming, recordings, or a generated summary. ## Idempotency -Pass an idempotency key when a workflow step might retry. The key maps to the `Idempotency-Key` HTTP header and prevents duplicate call creation for the same external operation. - -Use a stable workflow key, not a random UUID generated at each retry. +Persist the original request and one stable `Idempotency-Key` before submission. +Reuse them to recover the **same logical call**, not to dial again. Replay requires +the same project and authenticated owner; a key is not a global call identifier. -### Recover after a restart or lost response +
-Save the idempotency key and original request with your workflow record before -sending `POST /v1/calls`. Save the returned Call ID as soon as the response -arrives. Keep that record across application restarts. - -| What you saved | Recovery action | +| Situation | Response / next action | | --- | --- | -| Call ID | Read `GET /v1/calls/{call_id}` or resume SDK polling with that ID. Do not create another call to learn the existing call's outcome. | -| Original request and idempotency key, but no Call ID | Repeat the create request with the same key and unchanged body. The documented idempotency contract returns the original call instead of creating a duplicate. Save its ID. | -| Neither the Call ID nor the original request and key | Reconcile the original operation before submitting a replacement. A lost response does not prove that the first request was rejected. | - -For an idempotent replay, preserve the entire request, including `metadata`, -schemas, and `webhook_url`. Rebuilding it with a new timestamp or other changed -value can produce `idempotency_conflict`. Check the saved request if this occurs; -do not generate a new key just to bypass the conflict. - -A retry that recovers an existing operation and an intentionally new call are -different actions. Use a new key only for an intentionally new operation after -resolving the prior one. Keep local workflow identifiers in `metadata` for -correlation; they do not replace the `Idempotency-Key` header. - -## Task completion - -`task_completed` is CALL-E's post-call judgment of whether the task reached -a clear end state for the user. `completion_confidence` is confidence in that -judgment, and `evidence` supports it. These fields do not require a custom -result schema. - -Read execution, task completion, and the business answer separately: - -| Field | Question it answers | How to use it | +| Same key and unchanged input after acceptance | `202` with the same Call ID and its current saved state, not necessarily the original response body. Preparation and dialing are not repeated. | +| Original Call is completed, failed or canceled | Same replay behavior. A new key is required for an intentionally new call, even to the same phone. | +| Same key while the first create request is still running | `409 idempotency_conflict`, with `details.reason_code: "creation_in_progress"`. Back off and retry the original key and body. | +| Same key but changed input | `409 idempotency_conflict`. Restore the original request to recover it, or use a new key only for a deliberate new call. | +| Definitive validation rejection before acceptance | Correct the input and reuse the key. For example: `400 result_schema_invalid`, `422 input_incomplete`, `unsupported_region` or `unsupported_language`. No Call was created. | +| Timeout, lost response or an unclassified server error | Acceptance is uncertain. Retry the same key and unchanged body; do not generate another key to bypass the error. | + +
+ +The input comparison includes `task`, `phone`, `region`, `locale`, `result_schema`, +`metadata` and `webhook_url`. JSON object-key order is not significant, but +changing a field value is. Keep your original body rather than reconstructing +it from the response: if you omitted `region` or `locale`, continue omitting +them on retries. Copying the inferred response values into a retry changes the +request and returns 409, even when those values match the chosen destination. + +Use a durable business key such as `order-8472-confirmation-attempt-1`. Reuse it +after a timeout; use `order-8472-confirmation-attempt-2` only when your application +has intentionally decided to place another call. Store the returned Call ID and +use GET for routine status polling. These rules apply to Calls, not automatically +to [legacy creation failures](/legacy-calls#idempotency) or [Goal Run keys](/goal-runs#idempotency). + +## Read, events and cancellation + +| Method | Path | Behavior | | --- | --- | --- | -| `status` | Has the call task finished executing? | Use the [lifecycle states](#call-status) to decide whether to keep waiting. `completed` alone does not establish task or business success. | -| `task_completed` | Did CALL-E judge that the requested task reached a clear end state? | Read it with `completion_confidence` and `evidence`. Confidence applies to this judgment, not to the likelihood of a favorable business answer. | -| `structured_result` | What business answer was extracted? | Check the fields defined by your result schema and the supporting transcript before taking a business action. | - -A `true` value or high confidence does not establish that a person answered -or that your business objective was met. Check the business answer in -`structured_result` against the call transcript. Use the -[custom answered_by example](#classify-the-final-endpoint) to extract an -endpoint classification alongside your business result. Keep `unknown` -answers unresolved. - -### Example: an answered question with an unfavorable result - -Suppose the task is: "Ask whether a table for two is available at 7 p.m. -Do not make a reservation." The restaurant says no tables are available. -With a caller-defined `table_available` result field, an illustrative terminal -response excerpt is: +| GET | `/v2/calls/{call_id}` | Reads committed state without dialing or running inference. | +| GET | `/v2/calls/{call_id}/events` | Lists persisted lifecycle events and observed speech updates. | +| POST | `/v2/calls/{call_id}/cancel` | Cancels before provider submission. | + +Events support `cursor` and `limit` (1-100). Continue with `next_cursor` to read +remaining available pages. To poll for new events, retain the last returned +event's `id` and pass it as `cursor`, even when `next_cursor` is null. An empty +page does not mean the call ended. Cursors belong to one Call; event IDs remain +stable across retries. Events follow persistence order, so late observations +remain reachable after the previous cursor. + +This endpoint returns JSON pages, not an SSE connection. Deduplicate by event +`id`, not `turn` or text. A turn can contain several distinct speech updates. +Read until `next_cursor` is null, then keep the last event ID for the next poll; +an empty page must not erase that saved cursor. Invalid or foreign cursors and +out-of-range limits return `400 invalid_request`. + +| Event | Meaning | +| --- | --- | +| `call.accepted` | Instructions prepared and Call accepted. | +| `call.in_progress` | Provider submission started. | +| `call.ringing` / `call.connected` | Observed ringing or connection. | +| `call.asr` | Callee recognition update; may be a partial fragment or a revised hypothesis. | +| `call.speech` | Observed bot speech text. | +| `call.interrupted` / `call.dtmf` | Interruption or keypad input. | +| `call.ended` | Phone execution ended; result processing may still be pending. | +| `call.result_ready` | The structured result is saved and can be read. | +| `call.completed` / `call.failed` / `call.canceled` | The Call's terminal outcome. | + +Speech events preserve the full observed text without truncation. They do not +guarantee a complete sentence or include an `is_final` flag. For example, distinct +ASR events may contain `"my"`, `"my favorite"` and `"my favorite color is green"`. +Do not concatenate every ASR event into a final transcript. One illustrative event: ```json { - "status": "completed", - "task_completed": true, - "evidence": ["The restaurant confirmed that no table for two is available at 7 p.m."], - "structured_result": { - "table_available": "no" + "id": "call_example:event:42", + "type": "call.asr", + "call_id": "call_example", + "created_at": "2026-09-22T10:00:12Z", + "level": "info", + "status": "in_progress", + "message": "Callee speech recognized.", + "details": { + "speaker": "user", + "turn": 3, + "text": "Yes, I will attend with two guests.", + "occurred_at": "2026-09-22T10:00:11Z" } } ``` -The availability question reached a clear answer, even though the answer was -unfavorable. The application should report "No table available", not -"Reservation successful". If the requested task were to make a reservation, -the application would need evidence of a confirmed booking; this example does -not establish that outcome. A `null` result or an `unknown` answer must remain -unresolved rather than becoming a yes or no from `status` alone. - -## Polling and events - -Use `waitForResult` or `wait_for_result` for simple server-side polling. Use events when you need a developer-facing trace of the call lifecycle. - -Before mapping a failed call to no answer or decline, read -[Accepted call execution outcomes](/errors#accepted-call-execution-outcomes). -Calls API `failure_code` values are not a published enum, and Goal Run error -codes do not define the Calls API contract. - -When the terminal `structured_result` is `null`, CALL-E did not produce a schema-valid whole-task result from the available evidence. Recipient-level structured results use the same rule: invalid or unsupported values are returned as `null`. - -Each recipient attempt can include `transcript_turns`, an ordered list of structured transcript turns for that dial attempt. Each turn has `offset_seconds`, `speaker`, and `text`; `speaker` is `bot`, `user`, or `unknown`. The array is empty when no transcript is available. - -### Read transcript turns - -Read `recipients[].attempts[].transcript_turns` in Python or HTTP responses, -and `recipients[].attempts[].transcriptTurns` in the TypeScript SDK. Each turn -keeps the `offset_seconds` field in both SDKs. A null offset means the timestamp -is unavailable; zero means the start of the attempt. - -The runnable [Python reader](https://github.com/CALLE-AI/calle-docs/blob/main/examples/read_transcript.py) -and [TypeScript reader](https://github.com/CALLE-AI/calle-docs/blob/main/examples/read-transcript.ts) -label each recipient and attempt, keep `unknown` separate from `user`, and display -null offsets as `time unavailable` without changing the input. An unknown speaker -is not evidence that the recipient answered. Interpret the result using -[Task completion](#task-completion). - -From a checkout of the docs repository, run the synthetic checks with Python 3.11+ -or Node.js 22.18+ (native TypeScript support): - -```bash -python examples/read_transcript.py -node examples/read-transcript.ts -``` - -These commands make no API requests. They cover bot, user, unknown, zero and null -offsets, and an empty transcript. To read a real result in your application, pass -the completed Python/HTTP call object to `transcript_lines`, or the completed -TypeScript SDK call object to `transcriptLines`, and iterate the returned lines. -Transcript text may contain private data; keep the output private. +`occurred_at` is the provider timestamp when supplied, otherwise null. These +are observed events, not a guarantee that the remote party heard the bot. +Not every call emits every lifecycle event; absence of `call.ringing` or +`call.connected` alone does not establish a telephone outcome. +Historical calls without stored realtime +evidence retain their lifecycle events; missing speech is not synthesized. +Internal agent traces are not exposed. `call.accepted` alone does not prove the +phone rang. Reads never initiate a call, extract a result or persist new events. -### Poll for results +Cancellation after provider submission returns `409 call_cannot_cancel`. +It does not hang up an active call. Repeating cancellation on a terminal Call +returns its stored state. See the [API Reference](/api-reference/calls) for schemas. +## Limits of this preview -TypeScript: +- One target per Call. Recipient lists, batch requests and `recipient_result_schema` are not accepted. +- Calls accept immediate execution only. Scheduling and recurrence are not supported. +- Attempt details and provider identifiers are not exposed by the Calls response. +- Account concurrency, balance, regional access and other account policies still apply. Acceptance may be followed by queued work. +- Existing legacy call-task IDs must be read through their original interface; changing the URL does not convert historical records. -```ts -const completed = await client.calls.waitForResult(call.id, { - timeoutMs: 120_000, - intervalMs: 2_000, -}); +## Failures and uncertain submissions -const events = await client.calls.listEvents(call.id, { limit: 50 }); -``` +HTTP errors reject or fail the request itself. An accepted Call can later expose +an execution error or a result-processing error in `error`. The latter can +coexist with execution status `completed`. -Python: - -```python -completed = client.calls.wait_for_result( - call["id"], - timeout_seconds=120, - interval_seconds=2, -) - -events = client.calls.list_events(call["id"], limit=50) -``` +| Error | Meaning / next step | +| --- | --- | +| `call_failed`, `timed_out` | A technical execution failure; inspect the returned detail. | +| `result_invalid` | The result did not satisfy the schema. | +| `result_failed` | Processing or persisting the result failed. | +| `detail_code: authorization_expired` | Execution authorization became unavailable. After submission, the provider may still complete the call. | +| `detail_code: submission_unknown` | Provider submission could not be confirmed. | + +Do not automatically create a replacement for an uncertain submission. Retain +the original Call ID and reconcile it before making another call. Expiration of +a client wait timeout is also not evidence that no call occurred. diff --git a/content/guides/changelog.mdx b/content/guides/changelog.mdx index 9703ce3..117e1e0 100644 --- a/content/guides/changelog.mdx +++ b/content/guides/changelog.mdx @@ -3,6 +3,58 @@ title: What's New description: Track CALL-E Developer API and SDK product updates. --- +## September 23, 2026 - Billing call ID (unreleased) + +The local Calls response draft adds nullable `call_id`, matching the telephone +Call ID in Billing. The API resource `id` stays unchanged. Reads, exact retries +and terminal webhook data include the same recorded ID; calls without a recorded +telephone ID return `null`. See [Call ID and Billing](/calls#call-id-and-billing). +This addition is not yet deployed; no SDK package is published by this change. + +## September 22, 2026 - Calls preview update + +Calls accept omitted region and locale hints, infer the target from the phone +and task, and return the resolved values. Phone/region conflicts and unresolved +language choices request corrected input. The events endpoint now includes +observed ringing, connection, ASR recognition updates, bot speech, interruptions, call end +and result readiness, with stable IDs and cursor-based incremental reads. +ASR may contain partial or revised text; there is no final-sentence flag. + +These input and event changes were verified in the test preview, not released +as production GA. See [preview availability](/calls#preview-availability) and +[Call retry decisions](/calls#idempotency) before integrating. + +Unsupported Call regions and locales return HTTP 422 with `unsupported_region` +or `unsupported_language`, including the rejected field and target in `details`. +Correct the input before retrying; these requests create no Call. Configuration +service failures remain HTTP 503. + +Call creation now prepares the task before acceptance. Missing or conflicting +essential facts return `422 input_incomplete` with `error.details.missing_inputs`, +without creating a Call. Accepted requests reuse the saved preparation during +background execution. Preparation failures return a service error. See +[Calls](/calls#request-contract) for recovery and idempotency behavior. + +### Unreleased response additions + +The local response draft adds `transcript`, `call_outcome` and `result_status` +to Calls and Goal Runs. These additions were not included in the verified test +deployment. They preserve recorded transcript turns separately from the business +result and distinguish ordinary telephone outcomes from technical errors. +Local preview SDK wait helpers target this newer response draft; they require a +matching deployment. Do not infer availability from their presence in this documentation. + +## September 21, 2026 - Calls preview + +Try single-target calls and structured results with the [Quickstart](/quickstart), +[Calls guide](/calls), [Webhooks](/webhooks) and [SDKs](/sdks). +Preview SDK packages are available locally. + +For existing integrations, see the [migration guide](/migration) and the +[legacy API retirement plan](/retirement) for 2026-12-31 16:00 UTC. +Goal Runs are unaffected. This preview does not announce production GA or +enable the retirement cutoff or send customer notifications. + ## September 19, 2026 **Editable prompt modules:** Customize Facts, Opening, Callee Identity Check, diff --git a/content/guides/errors.mdx b/content/guides/errors.mdx index efd5c47..30c5526 100644 --- a/content/guides/errors.mdx +++ b/content/guides/errors.mdx @@ -22,6 +22,7 @@ SDK methods raise typed SDK errors while preserving the stable API error code an ## Stable error codes - `invalid_request` +- `input_incomplete` - `unauthorized` - `forbidden` - `rate_limit_exceeded` @@ -55,15 +56,16 @@ later reached a terminal state. | Surface | Stable contract | | --- | --- | | Developer API request | `APIError.code` uses the stable values listed above. | -| Calls API call task | Lifecycle `status` is stable. `failure_code` is a nullable string without a published enum; `failure_message` is nullable human-readable context. | -| Goal Runs API | `GoalRunError.code` is a separate enum that includes `no_answer` and `declined`. | +| Verified Calls test preview | Wait for a non-null `result` or `error`, not just terminal `status`. See [preview availability](/calls#preview-availability). | +| Planned Calls and Goal Run response additions | Poll while `result_status` is `pending`. `call_outcome` separates ordinary telephone outcomes from technical errors. These additions are not part of the verified test deployment. | +| Legacy call tasks | Lifecycle `status` is stable. `failure_code` is a nullable string without a published enum; `failure_message` is nullable human-readable context. | -For Calls API resources, treat `failure_code` and `failure_message` as +For legacy call-task resources, treat `failure_code` and `failure_message` as diagnostic context. Preserve the raw values for support, but do not branch retry, reporting, or analytics logic on a particular string. -The Goal Run codes `no_answer` and `declined` do not define Calls API -`failure_code` values. The Calls API currently does not guarantee a distinct +The call outcomes `no_answer`, `busy` and `declined` do not define legacy call-task +`failure_code` values. The legacy API does not guarantee a distinct no-answer or callee-decline value at the call-task, recipient, or attempt level. If the documented Calls fields do not establish that distinction, keep the business outcome unresolved. Do not infer that a recipient declined from @@ -75,19 +77,29 @@ automatically retry based only on an undocumented failure string. ### Choose the next action First distinguish an HTTP request failure from an accepted call's result. -The [complete Python example](/quickstart#run-a-complete-example) saves both -the original request and the returned Call ID for that purpose. +The [Quickstart](/quickstart#retry-without-calling-twice) explains how to retain +the original request, key and Call ID for that purpose. + +
| Observation | What to check and do next | | --- | --- | | `invalid_request`, recipient, phone, or schema validation error | Correct the rejected input using the field-specific guidance below. Preserve the error code and details; do not retry unchanged invalid input. | +| `422 input_incomplete` on Call creation | No Call was created. Read `error.details.missing_inputs`; correct a conflicting phone/region, specify an ambiguous spoken language, or supply missing task facts. Resubmit the corrected input with the same key. | +| `422 unsupported_region` on Call creation | The requested region has no supported calling configuration. Correct `region` using the supported regions and languages, then resubmit with the same key. | +| `422 unsupported_language` on Call creation | The region has a calling configuration, but the requested locale does not match. Correct `locale` for that region, then resubmit with the same key. | +| `409 idempotency_conflict` with `details.reason_code: "creation_in_progress"` | Creation with this key is still running. Back off and retry with the same key and unchanged body. | +| `409 idempotency_conflict` without that reason code | The key belongs to another request or owner. Recover using the original input and identity. Do not automatically switch keys: that can create another phone call. | +| `503 provider_unavailable` during Call preparation | Preparation could not finish. Retry the same request with the same key; do not treat this as missing user information. | | `unauthorized` or `forbidden` | Check the key and its access to the resource. Keep credentials out of logs and support posts. | | `insufficient_balance` | Resolve the account's billing condition before attempting more calls. A provider failure alone does not prove the balance is exhausted. | | `rate_limit_exceeded` | Back off. For a retry of the same operation, retain the original request and idempotency key. Do not replace an uncertain call with a new key. | | `provider_unavailable` on a Goal Run create request | This code applies before durable Goal Run acceptance. Preserve the request and error details; a later accepted Goal Run failure belongs to that run's `error` field. | -| Timeout, malformed response, or server error without a saved Call ID | The response alone may not establish acceptance. Follow [Calls recovery](/calls#recover-after-a-restart-or-lost-response); retain the original request and key. | +| Timeout, malformed response, or server error without a saved Call ID | The response alone may not establish acceptance. Follow [Calls recovery](/quickstart#retry-without-calling-twice); retain the original request and key. | | A saved Call ID, including a polling error or terminal failure | Retrieve that call and inspect its status, failure context, and available transcript. Keep the ID for support. Do not issue another create request to discover its outcome. | +
+ The `provider_unavailable` Goal Run contract does not establish the cause of an arbitrary Calls API `503`. For support, retain the SDK version, UTC timestamp, HTTP status, error code/details, and existing Call ID if available. Redact @@ -107,6 +119,13 @@ See [Authentication](/authentication) for API key setup, server-only usage, and `unsupported_region` or `unsupported_language` means CALL-E could not resolve a supported calling configuration for the request. +On Call creation these errors return HTTP 422 before durable acceptance. +Explicit unsupported targets are rejected before preparation; an inferred locale +is checked after inference. `details.field` identifies `region` or `locale`, while +`details.region` and `details.locale` identify the rejected target. No Call is +created, and the rejected request does not reserve the idempotency key. +Configuration access failures and conflicting profiles retain HTTP 503. + Check the [supported regions and languages](/regions) for the destination and locale you requested. Correct phone formatting alone does not resolve a coverage error. @@ -117,14 +136,23 @@ does not resolve a coverage error. `invalid_phone` means a phone number is not valid E.164 format. Replace placeholders such as `` with a phone number you own or are authorized to call. -`result_schema_invalid` means the whole call task `result_schema` is not a valid supported JSON Schema object. +`result_schema_invalid` returns HTTP 400 when the Calls `result_schema` is outside +the supported [scalar-object profile](/calls#result-schema). For example, +`"type": ["string", "null"]` is unsupported even though it is valid general +JSON Schema. Correct the schema before retrying; do not treat this as a provider outage. `recipient_result_schema_invalid` means the per-recipient `recipient_result_schema` is not a valid supported JSON Schema object. -`idempotency_conflict` means the same idempotency key was reused with a different request body. Reuse keys only for the same external workflow operation. +`idempotency_conflict` has two recovery paths. When +`details.reason_code == "creation_in_progress"`, retry unchanged after backoff. +Otherwise, the key belongs to another request or owner; check the original +payload and identity. Only an intentional new call should get a new key. +See [Call retry decisions](/calls#idempotency), including completed calls and +omitted target hints. Use `error.code` and structured details for branching; +do not parse the English message. For a lost Calls API create response or an application restart, follow -[Recover after a restart or lost response](/calls#recover-after-a-restart-or-lost-response). +[Recover after a restart or lost response](/quickstart#retry-without-calling-twice). `not_found` means a call, Goal, or Goal Run does not exist or is not visible to the current API key. Owner mismatch and hidden Goals use the same code. @@ -138,6 +166,11 @@ For a lost Calls API create response or an application restart, follow `call_not_ready` means the call task has not reached a terminal state. -`provider_unavailable` applies only before durable Goal Run acceptance. After acceptance, dispatch, call, or result-processing failures are reported in the existing Goal Run resource's top-level `error` field. Retry transport failures only when the workflow can preserve the same idempotency key and request. +`provider_unavailable` identifies an unavailable dependency, not necessarily the +telephone provider. Call creation can return it for preparation, authorization, +balance-check or configuration access failures. It does not mean no-answer or busy. +For Goal Run creation it applies before durable acceptance; later execution or +result-processing failures belong to the accepted Run's `error` field. Preserve +the original key and request when retrying an uncertain create. `internal_error` is retryable only when the workflow can safely tolerate retry. diff --git a/content/guides/goal-runs.mdx b/content/guides/goal-runs.mdx index 3d72531..5be9c81 100644 --- a/content/guides/goal-runs.mdx +++ b/content/guides/goal-runs.mdx @@ -11,7 +11,7 @@ A Goal owns its call behavior, voice region and locale, input schema, result sch CALL-E does not currently provide Developer API endpoints for inbound calling. Use the [CALL-E dashboard](https://dashboard.heycall-e.com/) to purchase numbers and configure Inbound Goals. -Use the one-shot [Calls](/calls) API when each request needs new task text or a request-scoped result schema. Goal Runs are a separate contract and do not replace or reinterpret `/v1/calls`. +Use [Calls](/calls) when each request needs new task text or a request-scoped result schema. Goal Runs use a published workflow with reusable inputs. For endpoint schemas and response models, browse the read-only [Goals API Reference](/api-reference/goals) and [Goal Runs API Reference](/api-reference/goal-runs). @@ -26,7 +26,7 @@ Your application then performs the following steps for every order: 1. Store the published `goal_id` in server-side configuration. 2. Read the Goal interface and validate which `variables` the current published version expects. 3. Create one Goal Run using the customer's E.164 phone number, order-specific variables, and the order event's stable idempotency key. -4. Poll until the Goal Run returns either `result` or `error`. +4. Poll until `result_status` is no longer `pending`. 5. Update the fulfillment system directly from `result`, for example by confirming the delivery or opening a rescheduling task. This guide uses that delivery-confirmation workflow throughout. Phone numbers remain placeholders so examples cannot accidentally call a real recipient. @@ -224,6 +224,9 @@ The first accepted request and an exact idempotent replay both return `201 Creat "run_id": "run_delivery_ord_8472", "call_id": null, "status": "in_progress", + "call_outcome": null, + "result_status": "pending", + "transcript": [], "run_spec": { "id": "rspec_delivery_v4", "version": 4 @@ -256,7 +259,11 @@ curl "https://api.heycall-e.com/v1/goals/${CALLE_GOAL_ID}/runs/${GOAL_RUN_ID}" \ `GET /v1/goals/{goal_id}/runs/{goal_run_id}` only reads persisted facts. It does not dispatch work, move the Goal's published pointer, or start result materialization. -The response deliberately has one completion rule: stop polling when either `result` or `error` is non-null. `status` describes the telephone execution, so a completed call can briefly return `result: null, error: null` while CALL-E parses and saves the structured result. +The response examples in this section include the [unreleased response additions](/calls#preview-availability). +Confirm that contract is deployed before using `result_status`, `call_outcome` +or `transcript`. Without those additions, wait for a non-null `result` or `error`. + +With the additions deployed, stop polling when `result_status` is no longer `pending`. `call_outcome` describes the telephone outcome separately. No-answer, busy and declined outcomes use `status: completed` and do not populate `error`. A completed execution can still have a pending business result. ```json { @@ -266,6 +273,9 @@ The response deliberately has one completion rule: stop polling when either `res "run_id": "run_delivery_ord_8472", "call_id": "calling_call_delivery_ord_8472", "status": "completed", + "call_outcome": "completed", + "result_status": "available", + "transcript": [{"speaker": "user", "offset_seconds": 12, "text": "Can you deliver after 5 PM on July 24?"}], "run_spec": { "id": "rspec_delivery_v4", "version": 4 @@ -280,21 +290,26 @@ The response deliberately has one completion rule: stop polling when either `res } ``` -When parsing succeeds, `result` is the exact dynamic object described by the Goal's `result_schema`. When anything prevents a usable result, CALL-E returns one `error` object instead: +When parsing succeeds, `result` is the exact object described by the Goal's `result_schema`, with `result_status: available`. If evidence is insufficient and the published instructions/schema provide no valid fallback, return `result_status: unavailable` and no technical error: ```json { - "status": "failed", + "status": "completed", + "call_outcome": "no_answer", + "result_status": "unavailable", + "transcript": [], "result": null, - "error": { - "code": "no_answer", - "message": "No human answered the call.", - "detail_code": "no_human_answered" - } + "error": null } ``` -Use `error.code` for application logic and `error.message` for logs or operators. The possible Goal Run error codes are `call_failed`, `no_answer`, `declined`, `timed_out`, `canceled`, `result_invalid`, `result_unavailable`, and `result_failed`. +Technical failures use `error.code`: `call_failed`, `timed_out`, `result_invalid`, or `result_failed`. Keep `error.message` for logs or operators. Cancellation has `result_status: not_applicable` and no error. An unavailable business result is final even when both result and error are null. + +Goal Runs also always include a top-level `transcript` array using the same +[turn structure as Calls](/calls#transcript): `speaker`, `offset_seconds`, and +`text`. It is independent of the published result schema and remains available +after execution ends even when the business result is pending, unavailable, or +failed. Before execution ends or when no transcript is available, it is `[]`. @@ -455,4 +470,4 @@ created_and_completed = client.goals.run_and_wait( ) ``` -`waitForResult` and `wait_for_result` return when either `result` or `error` becomes non-null. A terminal failure still returns the complete Goal Run object; polling timeout continues to use each SDK's existing timeout exception. +`waitForResult` and `wait_for_result` return when `result_status` is no longer `pending`, including unavailable results with no error. A technical failure still returns the complete Goal Run object; polling timeout continues to use each SDK's existing timeout exception. diff --git a/content/guides/legacy-calls.mdx b/content/guides/legacy-calls.mdx new file mode 100644 index 0000000..b999d2d --- /dev/null +++ b/content/guides/legacy-calls.mdx @@ -0,0 +1,589 @@ +--- +title: Legacy Calls +description: Understand the call creation contract and event flow. +--- + +Use call tasks to turn a structured workflow step into one or more real phone interactions. + +:::note{title="Legacy API"} +For new integrations, start with [Calls](/calls). +See the [migration guide](/migration) and [retirement notice](/retirement) +when updating an existing integration. +::: + +Use the [Calls API Reference](/api-reference/legacy-calls) for the exact HTTP request +and response schemas. See the [error handling guide](/errors) for retry +behavior and the [terminal webhooks guide](/legacy-webhooks) for asynchronous +completion. + +## Call inputs + +`task` is the natural-language instruction for the call task. Keep it specific and outcome-oriented. + +`recipients` is optional. When it is omitted, include the phone target in `task` and CALL-E will infer it. Use `recipients` for explicit batch targets; each recipient contains a `phones` array of E.164 numbers. + +Check the [supported regions and languages](/regions) +before choosing a recipient's `region` and `locale`. A valid E.164 number does +not establish that its destination is supported. + +Examples use phone placeholders such as `` and ``. Replace them with phone numbers you own or are authorized to call. + +`result_schema` is a JSON Schema object for the whole call task. CALL-E validates the structured result against it before returning the terminal call task state. Object schemas are strict by default, so fields not declared in `properties` are rejected. + +`recipient_result_schema` is an optional JSON Schema object for each recipient result. It uses the same strict object behavior. + +`metadata` is copied through to the call task and webhook payload. Use it for workflow identifiers, user ids, or reconciliation fields. + +`webhook_url` is an optional request-level endpoint for terminal webhooks. + +The server SDKs also reserve a `context` input for future SDK-side workflow data. It is not sent to the API yet. + +## Direct HTTP with curl + +Set your API key, then create a call with a stable idempotency key. Replace +`` with a phone number you own or are authorized to call. + +```bash +export CALLE_API_KEY="" + +curl --fail-with-body --silent --show-error \ + --request POST "https://api.heycall-e.com/v1/calls" \ + --header "Authorization: Bearer $CALLE_API_KEY" \ + --header "Content-Type: application/json" \ + --header "Idempotency-Key: wf_123_hearing_check" \ + --data '{ + "task": "Call and ask whether they can hear clearly.", + "result_schema": { + "type": "object", + "required": ["can_hear_clearly"], + "properties": { + "can_hear_clearly": { + "type": "string", + "enum": ["yes", "no", "unknown"] + } + }, + "additionalProperties": false + }, + "metadata": { + "workflow_run_id": "wf_123" + } + }' +``` + +The response contains the call task `id`. Use it to read the current state or +the ordered lifecycle events: + +```bash +export CALLE_CALL_ID="" + +curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $CALLE_API_KEY" \ + "https://api.heycall-e.com/v1/calls/$CALLE_CALL_ID" + +curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $CALLE_API_KEY" \ + "https://api.heycall-e.com/v1/calls/$CALLE_CALL_ID/events?limit=50" +``` + +## Call identifiers + +- **Calls API `call_id`:** use the top-level `id` (`call_...`) in HTTP and + Python, or `call.id` in TypeScript. +- **Dashboard Call Record ID:** use + `recipients[].attempts[].provider_call_id` in HTTP and Python, or + `call.recipients[i].attempts[j].providerCallId` in TypeScript. + +Use `call_id` with `GET /v1/calls/{call_id}` and +`GET /v1/calls/{call_id}/events`. Do not use `provider_call_id` as `call_id`; +it identifies one attempt and may be `null`. + +Event-list items expose the CallTask ID as `call_id`. Terminal webhooks expose +it as `data.id`; the webhook's top-level `id` identifies the event. + +Persist the returned Call ID with your workflow record. The Calls API does not +provide a list endpoint: `GET /v1/calls` cannot recover IDs you did not save. +See [Recover after a restart or lost response](#recover-after-a-restart-or-lost-response). + +TypeScript: + +```ts +const call = await client.calls.create( + { + task: "Call each recipient and ask whether they can attend Friday lunch in San Francisco.", + recipients: [ + { phones: [""] }, + { phones: [""] }, + ], + resultSchema: { + type: "object", + required: ["attending_count"], + properties: { + attending_count: { type: "integer" }, + }, + }, + recipientResultSchema: { + type: "object", + required: ["can_attend"], + properties: { + can_attend: { type: "string", enum: ["yes", "no", "unknown"] }, + }, + }, + metadata: { + workflow_run_id: "wf_123", + }, + webhookUrl: "https://example.com/calle/webhook", + }, + { + idempotencyKey: "wf_123_friday_lunch", + }, +); +``` + +Python: + +```python +call = client.calls.create( + task="Call each recipient and ask whether they can attend Friday lunch in San Francisco.", + recipients=[ + {"phones": [""]}, + {"phones": [""]}, + ], + result_schema={ + "type": "object", + "required": ["attending_count"], + "properties": {"attending_count": {"type": "integer"}}, + }, + recipient_result_schema={ + "type": "object", + "required": ["can_attend"], + "properties": { + "can_attend": {"type": "string", "enum": ["yes", "no", "unknown"]}, + }, + }, + metadata={"workflow_run_id": "wf_123"}, + webhook_url="https://example.com/calle/webhook", + idempotency_key="wf_123_friday_lunch", +) +``` + +## Structured results + +Structured results let you turn the terminal call evidence into a stable JSON object for your workflow. The schema is an extraction contract: the SDK sends the schema to CALL-E, CALL-E extracts a result from the completed call evidence, and the service validates the result before returning it. + +The extraction model uses the call transcript, ASR, and Calling facts as primary evidence. It uses the post-call summary and outcome as supporting context. If CALL-E cannot produce a schema-valid result from the evidence, the public `structured_result` is `null`. + +Use `result_schema` for one result object that describes the whole call task. Use `recipient_result_schema` when each recipient needs an independent result, especially for batch calls. TypeScript uses `resultSchema` and `recipientResultSchema`; Python uses `result_schema` and `recipient_result_schema`. The JSON Schema object itself is the same shape. + +For `recipient_result_schema`, avoid reserved recipient response field names such as `summary`, `status`, `transcript`, `call_id`, and timing fields. Use custom names such as `customer_summary`, `notes`, or `reason` instead. + +Descriptions are passed to the extraction model. Use `description` to explain what each field means and how enum values should be selected. Descriptions guide extraction, but they are not hard validation rules. Hard validation comes from `type`, `required`, `enum`, and `additionalProperties`. + +Supported schema features: + +- `type`: `object`, `string`, `number`, `integer`, `boolean`, or `array` +- `properties` +- `required` +- `enum` +- nested `object` fields +- simple `array.items` +- `description` +- `additionalProperties: false` + +Unsupported schema features include `$ref`, `oneOf`, `anyOf`, `allOf`, recursive schemas, complex format validation, and `additionalProperties: true`. + +For business decisions, prefer string enums over booleans when the answer can be unclear. Include an `unknown` value when the call may not provide enough evidence. + +```ts +resultSchema: { + type: "object", + required: ["answer", "evidence"], + properties: { + answer: { + type: "string", + enum: ["yes", "no", "unknown"], + description: + "The answer to the task question. Use unknown if the recipient did not answer, avoided the question, or the evidence is ambiguous.", + }, + evidence: { + type: "string", + description: + "A short quote or paraphrase from the call that supports the answer.", + }, + }, + additionalProperties: false, +} +``` + +When a result drives automation, add an evidence field so your system can inspect why CALL-E made the classification. + +### Classify the final endpoint + +The Calls API does not return a built-in AMD disposition or `answered_by` field. Define the classification with a per-recipient Structured Result. You control the property name and enum values. + +```ts +recipientResultSchema: { + type: "object", + required: ["answered_by"], + properties: { + answered_by: { + type: "string", + enum: ["human", "ivr", "voicemail", "unknown"], + description: + "Classify the final endpoint. If an IVR transfers the call to a person, use human.", + }, + }, + additionalProperties: false, +} +``` + +Read `call.recipients[i].structuredResult` in TypeScript, `call["recipients"][i]["structured_result"]` in Python, or `recipients[i].structured_result` over HTTP. CALL-E returns `null` when it cannot produce a schema-valid recipient result or when the request omits `recipient_result_schema`. The example uses `unknown` as a schema-valid fallback. + +### Sales handoff + +Use this pattern when a prospect should be routed to a human if they ask for help or show strong interest. + +```ts +resultSchema: { + type: "object", + required: [ + "human_assistance_requested", + "interest_level", + "handoff_recommended", + "evidence_summary", + ], + properties: { + human_assistance_requested: { + type: "string", + enum: ["yes", "no", "unknown"], + description: + "Whether the prospect explicitly asked to speak with a human, sales representative, specialist, manager, or requested a callback from a person. Use unknown if the evidence is unclear.", + }, + interest_level: { + type: "string", + enum: ["strong", "moderate", "low", "not_interested", "unknown"], + description: + "Use strong when the prospect asks about pricing, demos, next steps, availability, implementation, purchase process, or clearly wants follow-up. Use moderate for curiosity without a concrete next step. Use low for minimal engagement. Use not_interested when they clearly decline. Use unknown when evidence is insufficient.", + }, + handoff_recommended: { + type: "string", + enum: ["yes", "no", "unknown"], + description: + "Use yes if human_assistance_requested is yes or interest_level is strong. Use no when the prospect is low interest or not interested. Use unknown when the evidence is insufficient.", + }, + evidence_summary: { + type: "string", + description: + "One concise sentence citing the prospect's words or behavior that supports the handoff decision.", + }, + }, + additionalProperties: false, +} +``` + +### Appointment confirmation + +Use this pattern when calling a business to confirm, reschedule, or cancel an appointment. + +```ts +resultSchema: { + type: "object", + required: ["appointment_status", "confirmed_time", "confirmation_code"], + properties: { + appointment_status: { + type: "string", + enum: ["confirmed", "rescheduled", "canceled", "not_found", "unknown"], + description: + "The final appointment outcome. Use confirmed only when the business clearly confirms the appointment. Use rescheduled if a new time was agreed. Use not_found if the business cannot find the appointment. Use unknown when the evidence is unclear.", + }, + confirmed_time: { + type: "string", + description: + "The confirmed appointment time as stated in the call, or an empty string if no time was confirmed.", + }, + confirmation_code: { + type: "string", + description: + "The confirmation number or booking reference provided by the business, or an empty string if none was provided.", + }, + }, + additionalProperties: false, +} +``` + +### Batch recipient result + +Use `recipientResultSchema` when each recipient should have their own answer. + +```ts +recipientResultSchema: { + type: "object", + required: ["can_attend", "dietary_notes"], + properties: { + can_attend: { + type: "string", + enum: ["yes", "no", "maybe", "unknown"], + description: + "Whether this recipient can attend. Use maybe only when they express uncertainty. Use unknown if the call did not reach the recipient or no clear answer was given.", + }, + dietary_notes: { + type: "string", + description: + "Any dietary restrictions or preferences mentioned by this recipient, or an empty string if none were mentioned.", + }, + }, + additionalProperties: false, +} +``` + +### Support triage + +Use this pattern when a call should determine whether an issue was resolved or needs follow-up. + +```ts +resultSchema: { + type: "object", + required: ["issue_resolved", "requires_follow_up", "priority", "summary"], + properties: { + issue_resolved: { + type: "string", + enum: ["yes", "no", "partial", "unknown"], + description: + "Use yes when the issue was fully resolved during the call. Use partial when some progress was made but another action remains. Use no when the issue was not resolved. Use unknown if the outcome is unclear.", + }, + requires_follow_up: { + type: "string", + enum: ["yes", "no", "unknown"], + description: + "Whether another human or system action is required after the call. Use unknown if the call evidence is insufficient.", + }, + priority: { + type: "string", + enum: ["urgent", "normal", "low", "unknown"], + description: + "Use urgent for time-sensitive issues, service outages, billing blockers, or explicit escalation requests. Use normal for standard follow-up. Use low for informational or non-urgent cases.", + }, + summary: { + type: "string", + description: + "A concise summary of the issue, outcome, and any next action.", + }, + }, + additionalProperties: false, +} +``` + +### Pricing or quote request + +Use this pattern when a prospect may ask about pricing, quotes, discounts, or budget. + +```ts +resultSchema: { + type: "object", + required: ["pricing_requested", "budget_mentioned", "next_step"], + properties: { + pricing_requested: { + type: "string", + enum: ["yes", "no", "unknown"], + description: + "Whether the prospect asked for pricing, a quote, discount information, plan details, or cost comparison. Use unknown if the evidence is unclear.", + }, + budget_mentioned: { + type: "string", + description: + "Any budget, price range, or cost constraint mentioned by the prospect, or an empty string if none was mentioned.", + }, + next_step: { + type: "string", + enum: ["send_pricing", "schedule_demo", "human_callback", "no_action", "unknown"], + description: + "The most appropriate next step based on the prospect's request. Use human_callback if they ask to speak with a person. Use no_action if they clearly decline or no follow-up is needed. Use unknown if the evidence is insufficient.", + }, + }, + additionalProperties: false, +} +``` + +### Best practices + +- Keep schemas focused. A small schema with clear fields is more reliable than a large schema with many optional fields. +- Put enum selection rules in the field `description`. +- Include `unknown` when the call may not contain enough evidence. +- Use `required` for fields your workflow always expects. +- Use `additionalProperties: false` to prevent extra fields from being returned. +- Add an evidence or summary field when the result triggers workflow automation. +- Do not rely on `description` for validation. Use schema constraints for enforceable behavior. + +## Call status + +The call task's `status` has exactly five values: + +| Status | Terminal? | Meaning | +| --- | --- | --- | +| `queued` | No | The call task is queued. | +| `in_progress` | No | The call task is running, including post-call finalization. | +| `completed` | Yes | The call task completed. Check its results for the business outcome. | +| `failed` | Yes | The call task failed. Keep the failure fields as diagnostic context. | +| `canceled` | Yes | The call task was canceled. | + +`no_answer`, `busy`, and `voicemail` are not Calls API lifecycle statuses. +Recipient and attempt objects have their own status enums; do not substitute +them for the top-level call status. A `completed` state does not establish +that a person answered or that your business objective succeeded. See +[Task completion](#task-completion) for interpreting the business result. + +## Parallel and quorum-based dispatch + +The Calls API does not expose an operation for clients to cancel a call after +it has been created. A call that is already in flight may therefore continue +to completion even when your application no longer needs its result. The +`canceled` resource status does not imply that clients can request +cancellation. + +For workflows that need only a target number of confirmations, dispatch calls +in controlled waves instead of starting every call at once. Count terminal +results through polling or webhooks, and stop creating subsequent waves after +the confirmation target is reached. Choose a wave size that balances response +speed against the number of calls that may still be in flight when the target +is met. + +## Idempotency + +Pass an idempotency key when a workflow step might retry. The key maps to the `Idempotency-Key` HTTP header and prevents duplicate call creation for the same external operation. + +Use a stable workflow key, not a random UUID generated at each retry. + +### Recover after a restart or lost response + +Save the idempotency key and original request with your workflow record before +sending `POST /v1/calls`. Save the returned Call ID as soon as the response +arrives. Keep that record across application restarts. + +| What you saved | Recovery action | +| --- | --- | +| Call ID | Read `GET /v1/calls/{call_id}` or resume SDK polling with that ID. Do not create another call to learn the existing call's outcome. | +| Original request and idempotency key, but no Call ID | Repeat the create request with the same key and unchanged body. The documented idempotency contract returns the original call instead of creating a duplicate. Save its ID. | +| Neither the Call ID nor the original request and key | Reconcile the original operation before submitting a replacement. A lost response does not prove that the first request was rejected. | + +For an idempotent replay, preserve the entire request, including `metadata`, +schemas, and `webhook_url`. Rebuilding it with a new timestamp or other changed +value can produce `idempotency_conflict`. Check the saved request if this occurs; +do not generate a new key just to bypass the conflict. + +A retry that recovers an existing operation and an intentionally new call are +different actions. Use a new key only for an intentionally new operation after +resolving the prior one. Keep local workflow identifiers in `metadata` for +correlation; they do not replace the `Idempotency-Key` header. + +## Task completion + +`task_completed` is CALL-E's post-call judgment of whether the task reached +a clear end state for the user. `completion_confidence` is confidence in that +judgment, and `evidence` supports it. These fields do not require a custom +result schema. + +Read execution, task completion, and the business answer separately: + +| Field | Question it answers | How to use it | +| --- | --- | --- | +| `status` | Has the call task finished executing? | Use the [lifecycle states](#call-status) to decide whether to keep waiting. `completed` alone does not establish task or business success. | +| `task_completed` | Did CALL-E judge that the requested task reached a clear end state? | Read it with `completion_confidence` and `evidence`. Confidence applies to this judgment, not to the likelihood of a favorable business answer. | +| `structured_result` | What business answer was extracted? | Check the fields defined by your result schema and the supporting transcript before taking a business action. | + +A `true` value or high confidence does not establish that a person answered +or that your business objective was met. Check the business answer in +`structured_result` against the call transcript. Use the +[custom answered_by example](#classify-the-final-endpoint) to extract an +endpoint classification alongside your business result. Keep `unknown` +answers unresolved. + +### Example: an answered question with an unfavorable result + +Suppose the task is: "Ask whether a table for two is available at 7 p.m. +Do not make a reservation." The restaurant says no tables are available. +With a caller-defined `table_available` result field, an illustrative terminal +response excerpt is: + +```json +{ + "status": "completed", + "task_completed": true, + "evidence": ["The restaurant confirmed that no table for two is available at 7 p.m."], + "structured_result": { + "table_available": "no" + } +} +``` + +The availability question reached a clear answer, even though the answer was +unfavorable. The application should report "No table available", not +"Reservation successful". If the requested task were to make a reservation, +the application would need evidence of a confirmed booking; this example does +not establish that outcome. A `null` result or an `unknown` answer must remain +unresolved rather than becoming a yes or no from `status` alone. + +## Polling and events + +Use `waitForResult` or `wait_for_result` for simple server-side polling. Use events when you need a developer-facing trace of the call lifecycle. + +Before mapping a failed call to no answer or decline, read +[Accepted call execution outcomes](/errors#accepted-call-execution-outcomes). +Calls API `failure_code` values are not a published enum, and Goal Run error +codes do not define the Calls API contract. + +When the terminal `structured_result` is `null`, CALL-E did not produce a schema-valid whole-task result from the available evidence. Recipient-level structured results use the same rule: invalid or unsupported values are returned as `null`. + +Each recipient attempt can include `transcript_turns`, an ordered list of structured transcript turns for that dial attempt. Each turn has `offset_seconds`, `speaker`, and `text`; `speaker` is `bot`, `user`, or `unknown`. The array is empty when no transcript is available. + +### Read transcript turns + +Read `recipients[].attempts[].transcript_turns` in Python or HTTP responses, +and `recipients[].attempts[].transcriptTurns` in the TypeScript SDK. Each turn +keeps the `offset_seconds` field in both SDKs. A null offset means the timestamp +is unavailable; zero means the start of the attempt. + +The runnable [Python reader](https://github.com/CALLE-AI/calle-docs/blob/main/examples/read_transcript.py) +and [TypeScript reader](https://github.com/CALLE-AI/calle-docs/blob/main/examples/read-transcript.ts) +label each recipient and attempt, keep `unknown` separate from `user`, and display +null offsets as `time unavailable` without changing the input. An unknown speaker +is not evidence that the recipient answered. Interpret the result using +[Task completion](#task-completion). + +From a checkout of the docs repository, run the synthetic checks with Python 3.11+ +or Node.js 22.18+ (native TypeScript support): + +```bash +python examples/read_transcript.py +node examples/read-transcript.ts +``` + +These commands make no API requests. They cover bot, user, unknown, zero and null +offsets, and an empty transcript. To read a real result in your application, pass +the completed Python/HTTP call object to `transcript_lines`, or the completed +TypeScript SDK call object to `transcriptLines`, and iterate the returned lines. +Transcript text may contain private data; keep the output private. + +### Poll for results + + +TypeScript: + +```ts +const completed = await client.calls.waitForResult(call.id, { + timeoutMs: 120_000, + intervalMs: 2_000, +}); + +const events = await client.calls.listEvents(call.id, { limit: 50 }); +``` + +Python: + +```python +completed = client.calls.wait_for_result( + call["id"], + timeout_seconds=120, + interval_seconds=2, +) + +events = client.calls.list_events(call["id"], limit=50) +``` diff --git a/content/guides/legacy-quickstart.mdx b/content/guides/legacy-quickstart.mdx new file mode 100644 index 0000000..83e7524 --- /dev/null +++ b/content/guides/legacy-quickstart.mdx @@ -0,0 +1,214 @@ +--- +title: Legacy Quickstart +description: Create one call and read the structured result. +--- + +This quickstart is for existing call-task integrations. For new integrations, +use the [Quickstart](/quickstart). The [retirement plan](/retirement) covers +the legacy Calls API only; Goal Runs are unaffected. + +Create a CALL-E call task, wait for the terminal result, and read the structured output. + +This quickstart uses the one-shot Calls API. If your application repeats a workflow that has already been authored and published in CALL-E, start with [Goal Runs](/goal-runs) instead. + +Outbound calling is not available in every country or region. Check the current +[supported regions and languages](/regions) +before building an integration. A phone number can be valid E.164 and still be +rejected with `unsupported_region`; see [Errors](/errors) for recovery guidance. + +**API key** · **TypeScript** · **Python** + +## Install + +Install the server SDK package for your runtime. The Python SDK requires +Python 3.11 or later; Python 3.9 and 3.10 are not supported. + +```bash +pnpm add @call-e/calle +pip install calle-ai +``` + +Set your API key before running the examples. Replace `` +with the complete key from the dashboard; the placeholder is not a working +credential: + +```bash +export CALLE_API_KEY="" +``` + +You can view your API keys in the [CALL-E dashboard](https://dashboard.heycall-e.com/account/api-keys). + +See [Authentication](/authentication) for API key handling and server-only usage. + +## Create a client + +TypeScript: + +```ts +import { CalleClient } from "@call-e/calle"; + +const client = new CalleClient({ + apiKey: process.env.CALLE_API_KEY!, +}); +``` + +Python: + +```python +import os +from calle import CalleClient + +client = CalleClient(api_key=os.environ["CALLE_API_KEY"]) +``` + +## Minimum request + +The minimum create request is task-only. Include the phone number directly in the task when CALL-E should infer the recipient from the instruction. Replace `` with a phone number you own or are authorized to call. + +```json +{ + "task": "Call and ask whether they can hear clearly." +} +``` + +## Create and wait + +TypeScript: + +```ts +const call = await client.calls.createAndWait({ + task: "Call and ask whether they can hear clearly.", + resultSchema: { + type: "object", + required: ["can_hear_clearly"], + properties: { + can_hear_clearly: { type: "string", enum: ["yes", "no", "unknown"] }, + }, + }, +}); +``` + +Python: + +```python +call = client.calls.create_and_wait( + task="Call and ask whether they can hear clearly.", + result_schema={ + "type": "object", + "required": ["can_hear_clearly"], + "properties": { + "can_hear_clearly": {"type": "string", "enum": ["yes", "no", "unknown"]}, + }, + }, +) +``` + +## Read the result + +The terminal call task includes a stable status, a schema-valid structured result, and task-level outcome fields from the post-call summary. When CALL-E cannot produce a schema-valid result from the evidence, `structured_result` is `null`. + +See [Call status](/legacy-calls#call-status) for lifecycle and terminal states, and +[Task completion](/legacy-calls#task-completion) for how to interpret +`task_completed` (`taskCompleted` in TypeScript) and `completion_confidence`. + +TypeScript: + +```ts +console.log(call.status); +console.log(call.structuredResult); +console.log(call.taskCompleted, call.completionConfidence, call.evidence); +``` + +Example output: + +```json +{ + "status": "completed", + "taskCompleted": true, + "completionConfidence": {"score": 0.92, "label": "high"}, + "evidence": ["The recipient clearly answered yes."], + "structuredResult": { + "can_hear_clearly": "yes" + } +} +``` + +Python: + +```python +print(call["status"]) +print(call["structured_result"]) +print(call["task_completed"], call["completion_confidence"], call["evidence"]) +``` + +Example output: + +```json +{ + "status": "completed", + "task_completed": true, + "completion_confidence": {"score": 0.92, "label": "high"}, + "evidence": ["The recipient clearly answered yes."], + "structured_result": { + "can_hear_clearly": "yes" + } +} +``` + +Before adding automatic retries, follow +[Recover after a restart or lost response](/legacy-calls#recover-after-a-restart-or-lost-response) +to persist the original request, idempotency key, and Call ID. + +## Run a complete example + +The [Python example](https://github.com/CALLE-AI/calle-docs/blob/main/examples/calls.py) +creates one real US English test call, saves its request and Call ID, waits for +the terminal result, and writes the full response to a private local directory. +It listens to a greeting and then asks the agent to end the call. This is a +real-service integration check, not a simulated business transaction. + +Use Python 3.11 or later and a number you own or are authorized to test. If you +need a destination, follow the [official US English testing hotline announcement](https://discord.com/channels/1493880186826133504/1495622983253889054/1546414916515401788). +Calls use your real account and may consume credits. The hotline is a test +recipient, not a promise of free sandbox credentials or deterministic answers. + +```bash +git clone https://github.com/CALLE-AI/calle-docs.git +cd calle-docs +python3 -m venv .venv +source .venv/bin/activate +python -m pip install calle-ai==0.7.0 +export CALLE_API_KEY="" +export CALLE_TEST_PHONE="" +python examples/calls.py start ../calle-run --phone "$CALLE_TEST_PHONE" +``` + +`start` requires a new run directory and submits at most one create request. +The directory holds `request.json` (including the original idempotency key), +`call-id.json`, and, once available, `result.json`. Keep it private: it contains +the destination and call transcript. The API key is not written there. On Unix, +the directory is created with owner-only access; on Windows, use a private +directory protected by your account's file permissions. + +After a polling timeout, application restart, or successful run, read the same +call again with: + +```bash +python examples/calls.py resume ../calle-run +``` + +`resume` only retrieves the saved Call ID; it never creates a call. If a create +response was lost and no ID was saved, the script stops. Keep `request.json` +and follow [Calls recovery](/legacy-calls#recover-after-a-restart-or-lost-response) +before starting a replacement. Do not delete a run directory to bypass this check. + +Stopping this process or reaching its five-minute polling timeout does not +cancel a call already accepted by CALL-E. Neither command retries a failed +create request automatically. An HTTP error is saved in `error.json`; see +[Choose the next action](/errors#choose-the-next-action). + +An exit code of zero means a terminal response was retrieved. Read `status`, +`task_completed`, and `structured_result` separately and check the transcript +in `result.json`. A `null` structured result or `heard_greeting: "unknown"` +does not establish that a greeting was heard. A terminal `failed` or `canceled` +response is still a readable outcome, not a successful business task. diff --git a/content/guides/legacy-sdks.mdx b/content/guides/legacy-sdks.mdx new file mode 100644 index 0000000..b37b1ea --- /dev/null +++ b/content/guides/legacy-sdks.mdx @@ -0,0 +1,208 @@ +--- +title: Legacy SDKs +description: Install the stable TypeScript and Python server SDKs. +--- + +CALL-E provides server SDKs for trusted backend services, workers, and automation systems that create and monitor call tasks. + +:::note{title="Legacy packages"} +The packages documented here use the legacy call-task contract. Follow the +[SDK guide](/sdks) for the current test preview. +::: + +## Packages + +TypeScript package name: + +```text +@call-e/calle +``` + +Python distribution name: + +```text +calle-ai +``` + +Python imports the package as `calle`: + +```python +from calle import CalleClient +``` + +## Package status + +The current stable server SDK packages are: + +- TypeScript: `@call-e/calle@0.7.0` +- Python: `calle-ai==0.7.0` + +These stable packages support both the request-scoped Calls API and the Goal +Runs SDK surface described below. + +## Goal Runs + +The Goal-based SDK surface keeps published contracts separate from the +one-shot Calls API: + +- TypeScript: `client.goals.list(...)`, `get(...)`, `run(...)`, `getRun(...)`, + `waitForResult(...)`, and `runAndWait(...)` +- Python: `client.goals.list(...)`, `get(...)`, `run(...)`, `get_run(...)`, + `wait_for_result(...)`, and `run_and_wait(...)` + +Run requests contain Goal identity, one phone number, per-Run variables, and an +idempotency key. They do not accept request-scoped task text, prompts, +`input_schema`, or `result_schema`; those fields are owned by the published +RunSpec. See the [Goal Runs guide](/goal-runs) for the API contract and SDK +examples. + +## Source code + +Both SDKs are maintained in public repositories. + +| Runtime | Package | Repository | +| --- | --- | --- | +| TypeScript | `@call-e/calle` | [CALLE-AI/server-sdk-typescript](https://github.com/CALLE-AI/server-sdk-typescript) | +| Python | `calle-ai` | [CALLE-AI/server-sdk-python](https://github.com/CALLE-AI/server-sdk-python) | + +The TypeScript repository includes public source code, examples, and security +guidance: + +- [TypeScript security policy](https://github.com/CALLE-AI/server-sdk-typescript/blob/main/SECURITY.md) + +## Local examples + +The TypeScript SDK repository includes runnable examples for the server-side +one-shot call task flow. + +TypeScript: + +```bash +git clone https://github.com/CALLE-AI/server-sdk-typescript.git +cd server-sdk-typescript +pnpm install +pnpm run example:create-and-wait +``` + +## Calls API + +The SDKs expose `context` as a reserved input for future SDK-side workflow data. The current SDKs do not send `context` to the API. + +Examples use phone placeholders such as `` and ``. Replace them with phone numbers you own or are authorized to call. + +The SDKs accept structured result schemas as plain JSON objects. Use field `description` values to explain how CALL-E should interpret enum values, and use `type`, `required`, `enum`, and `additionalProperties` for hard validation. See the Calls guide for structured result design patterns and examples. + +TypeScript: + +```ts +const created = await client.calls.create( + { + task: "Call each recipient and ask whether they can attend Friday lunch in San Francisco.", + recipients: [ + { phones: [""] }, + { phones: [""] }, + ], + resultSchema: { + type: "object", + required: ["attending_count"], + properties: { + attending_count: { type: "integer" }, + }, + }, + recipientResultSchema: { + type: "object", + required: ["can_attend"], + properties: { + can_attend: { type: "string", enum: ["yes", "no", "unknown"] }, + }, + }, + }, + { idempotencyKey: "wf_123_friday_lunch" }, +); + +const fetched = await client.calls.get(created.id); +const completed = await client.calls.waitForResult(fetched.id); +const createdAndCompleted = await client.calls.createAndWait( + { + task: "Call and confirm their preferred appointment time.", + resultSchema: { + type: "object", + required: ["preferred_time"], + properties: { + preferred_time: { type: "string" }, + }, + }, + }, + { timeoutMs: 120_000, intervalMs: 2_000 }, +); +const events = await client.calls.listEvents(createdAndCompleted.id, { + limit: 50, +}); +``` + +Python: + +```python +created = client.calls.create( + task="Call each recipient and ask whether they can attend Friday lunch in San Francisco.", + recipients=[ + {"phones": [""]}, + {"phones": [""]}, + ], + result_schema={ + "type": "object", + "required": ["attending_count"], + "properties": {"attending_count": {"type": "integer"}}, + }, + recipient_result_schema={ + "type": "object", + "required": ["can_attend"], + "properties": { + "can_attend": {"type": "string", "enum": ["yes", "no", "unknown"]}, + }, + }, + idempotency_key="wf_123_friday_lunch", +) + +fetched = client.calls.get(created["id"]) +completed = client.calls.wait_for_result(fetched["id"]) +created_and_completed = client.calls.create_and_wait( + task="Call and confirm their preferred appointment time.", + result_schema={ + "type": "object", + "required": ["preferred_time"], + "properties": {"preferred_time": {"type": "string"}}, + }, + timeout_seconds=120, + interval_seconds=2, +) +events = client.calls.list_events(created_and_completed["id"], limit=50) +``` + +## Availability + +Install the stable server SDK package for your runtime. The Python SDK requires +Python 3.11 or later; Python 3.9 and 3.10 are not supported. + +```bash +pnpm add @call-e/calle +pip install calle-ai +``` + +Use pinned versions when your deployment process requires exact package reproducibility: + +```bash +pnpm add @call-e/calle@0.7.0 +pip install calle-ai==0.7.0 +``` + +## Supported scope + +This release does not include: + +- Python async client support +- Project-level webhook management +- Client-initiated cancellation of in-flight calls +- Recurring or scheduled calls +- Zod result schema helpers +- Pydantic result schema helpers diff --git a/content/guides/legacy-webhooks.mdx b/content/guides/legacy-webhooks.mdx new file mode 100644 index 0000000..bb230b5 --- /dev/null +++ b/content/guides/legacy-webhooks.mdx @@ -0,0 +1,201 @@ +--- +title: Legacy Webhooks +description: Receive terminal webhook events and process them safely. +--- + +Use webhooks when your application needs to react to terminal call task results without polling. + +This page covers legacy call-task payloads. New integrations should use +[Webhooks](/webhooks); keep this receiver for existing call tasks. + +Phone values in examples are placeholders. Real webhook payloads echo the phone numbers from your call task. + +CALL-E publishes a terminal event only after the post-call summary, task +completion judgment, confidence, evidence, and any requested structured results +are finalized. The event `data` is the same complete terminal snapshot returned +by `GET /v1/calls/{call_id}`. + +## Terminal events + +CALL-E sends terminal webhook events: + +- `call.completed` +- `call.failed` +- `call.result_validation_failed` + +`call.result_validation_failed` is sent when a completed call task had an internal structured-result validation failure for the task or a recipient. The payload does not expose validation details; invalid or unsupported structured results are returned as `null`. Failed or canceled terminal call tasks use `call.failed`. + +Each event has an `id`, `type`, `created_at`, and a `data` object containing the complete terminal call task fields. + +Recipient-level structured results appear inside the `recipients` array. They do not create separate recipient webhook events. + +Attempt transcripts appear as `recipients[].attempts[].transcript_turns`. Each turn includes `offset_seconds`, `speaker`, and `text`; the array is empty when no transcript is available. + +See [Task completion](/legacy-calls#task-completion) for how to interpret +`task_completed` and `completion_confidence` in the payload. + +Example payload: + +```json +{ + "id": "evt_123", + "type": "call.completed", + "created_at": "2026-06-08T18:30:00Z", + "data": { + "id": "call_123", + "object": "call_task", + "status": "completed", + "task": "Call each recipient and ask whether they can attend Friday lunch in San Francisco.", + "recipients": [ + { + "id": "rcp_001", + "phones": [""], + "region": "US", + "locale": "en-US", + "status": "completed", + "structured_result": { + "can_attend": "yes" + }, + "summary": "The recipient can attend Friday lunch.", + "attempts": [ + { + "id": "att_001", + "phone": "", + "status": "completed", + "started_at": "2026-06-08T18:21:00Z", + "completed_at": "2026-06-08T18:29:00Z", + "summary": null, + "transcript_turns": [ + { + "offset_seconds": 0, + "speaker": "bot", + "text": "Can you attend Friday lunch in San Francisco?" + }, + { + "offset_seconds": 8, + "speaker": "user", + "text": "Yes, I can attend." + } + ], + "provider_call_id": "provider_001", + "failure_code": null, + "failure_message": null + } + ] + }, + { + "id": "rcp_002", + "phones": [""], + "region": "US", + "locale": "en-US", + "status": "completed", + "structured_result": null, + "summary": "The recipient did not provide a usable answer.", + "attempts": [ + { + "id": "att_002", + "phone": "", + "status": "completed", + "started_at": "2026-06-08T18:22:00Z", + "completed_at": "2026-06-08T18:30:00Z", + "summary": null, + "transcript_turns": [], + "provider_call_id": "provider_002", + "failure_code": null, + "failure_message": null + } + ] + } + ], + "structured_result": { + "attending_count": 1 + }, + "summary": "One recipient can attend Friday lunch.", + "task_completed": true, + "completion_confidence": { + "score": 0.86, + "label": "high" + }, + "evidence": [ + "One recipient confirmed they can attend.", + "The second recipient did not provide a usable answer." + ], + "metadata": { + "workflow_run_id": "wf_123" + }, + "failure_code": null, + "failure_message": null, + "created_at": "2026-06-08T18:20:00Z", + "completed_at": "2026-06-08T18:30:00Z" + } +} +``` + +## Receive events + +Current CALL-E webhook delivery does not use a webhook secret, +`CALL-E-Timestamp`, or `CALL-E-Signature`. Treat the receiver as a public, +untrusted-input boundary: validate the JSON shape, require +`CALL-E-Event-Id`, and reject the request when that header does not match the +body event `id`. + +TypeScript: + +```ts +const event = JSON.parse(rawBody.toString("utf8")); +const eventId = request.headers.get("CALL-E-Event-Id"); + +if (!eventId || eventId !== event.id) { + return new Response("invalid event id", { status: 400 }); +} + +if (event.type === "call.completed") { + console.log(event.data.id, event.data.recipients); +} +``` + +Python: + +```python +event = json.loads(raw_body) +event_id = request.headers.get("CALL-E-Event-Id") + +if not event_id or event_id != event["id"]: + return {"error": "invalid_event_id"}, 400 + +if event["type"] == "call.completed": + print(event["data"]["id"], event["data"]["recipients"]) +``` + +Return a `2xx` response after accepting the event. CALL-E retries delivery when +the receiver returns a non-`2xx` response or the request fails. + +HTTPS and event-id matching do not provide cryptographic proof of the sender. +Before a sensitive side effect that requires origin assurance, fetch +`GET /v1/calls/{call_id}` with your API key and compare its terminal snapshot +with the event. + +## Idempotent handling + +Webhook delivery is at least once. Store the webhook event `id` before processing side effects so duplicate deliveries are ignored safely. + +TypeScript: + +```ts +if (await eventStore.has(event.id)) { + return new Response("duplicate", { status: 200 }); +} + +await eventStore.insert(event.id); +await handleCallEvent(event); +``` + +Python: + +```python +if event_store.has(event["id"]): + return {"ok": True, "duplicate": True} + +event_store.insert(event["id"]) +handle_call_event(event) +``` diff --git a/content/guides/migration.mdx b/content/guides/migration.mdx new file mode 100644 index 0000000..25c4e33 --- /dev/null +++ b/content/guides/migration.mdx @@ -0,0 +1,106 @@ +--- +title: Migration guide +description: Update call inputs, result handling, webhooks, and SDKs without duplicating calls. +--- + +Move an existing call-task integration to the single-target Calls API. +Test the new integration before moving production traffic. Goal Runs keep +their current contract. See the [retirement notice](/retirement) for the +legacy interface's planned support deadline. + +Check [preview availability](/calls#preview-availability) before migrating. +The `call_outcome`, `result_status` and `transcript` changes below are planned +response additions; they were not part of the verified September 22 test deployment. +Do not move a production workflow based on this preview alone. + +## What changes + +
+ +| Legacy call tasks | Calls | Required migration | +| --- | --- | --- | +| `/v1/calls`, HTTP `201` | `/v2/calls`, HTTP `202` | Treat create as acceptance, then wait. | +| Optional inferred target or `recipients[].phones` | Required `phone`; optional `region` and `locale` | Send one destination. Omitted hints are inferred; phone/region conflicts require corrected input. | +| Optional idempotency key | Required `Idempotency-Key` | Save the key and original request before submission. | +| Optional nested/array result schema | Required closed scalar-object schema | Flatten fields; do not silently discard needed information. | +| `structured_result` | `result` | Update response parsing. | +| `failure_code`, `failure_message` | `error.code`, `error.message`, `error.detail_code` | Update error handling and retry decisions. | +| Wait for terminal `status` | Verified preview: wait for non-null `result` or `error`. Planned: wait until `result_status` is not `pending`. | Use the readiness rule for the deployed contract, not just telephone completion. | +| Top-level summary and completion fields | Business fields in `result_schema` | Ask for the summary or business flag explicitly. | +| `recipients[].attempts[].transcript_turns` | Planned top-level `transcript` | Require the response addition to be deployed before migrating a transcript-dependent workflow. | +| Provider attempt details | Not in the Calls response | Resolve this dependency before migrating that workflow. | + +
+ +## Before and after + +The same single-recipient intention has different request shapes. These are +request-body examples; use a stable idempotency header for the new request. + +**Before** + +```json +{ + "task": "Ask in English whether tomorrow's 2 PM to 4 PM delivery window is confirmed.", + "recipients": [{"phones": [""], "region": "US", "locale": "en-US"}], + "result_schema": { + "type": "object", + "properties": {"confirmed": {"type": "boolean"}}, + "required": ["confirmed"], + "additionalProperties": false + } +} +``` + +**After** + +```json +{ + "task": "Ask in English whether tomorrow's 2 PM to 4 PM delivery window is confirmed.", + "phone": "", + "result_schema": { + "type": "object", + "properties": {"confirmed": {"type": "boolean"}}, + "required": ["confirmed"], + "additionalProperties": false + } +} +``` + +Update required inputs and result handling together. Changing the endpoint +alone is insufficient. The [SDK guide](/sdks) contains equivalent examples. + +## Migrate one workflow + +1. Inventory dependencies on batch targets, nested schemas, transcripts and old response fields. +2. Implement the new request builder and the readiness rule for the verified deployment. Adopt `result_status` and `call_outcome` only after those response additions are deployed. +3. Update the receiver using [Webhooks](/webhooks). Keep the legacy receiver for call tasks still finishing. +4. Run authorized test calls against the test base URL; check results, HTTP retries and duplicate webhook handling. +5. Keep each Call ID with the interface used to create it. Retrieve historical records through that interface. +6. Switch new calls for the workflow once its dependencies are covered. Continue polling and receiving callbacks for calls already accepted. + +Never submit the same phone task to both interfaces to compare results: both +requests can place a call. Do not submit through the legacy interface after a timeout. +Preserve the exact original input on recovery, including omitted hints and metadata. +An already completed or failed Call is not redialed by replaying its key. See +[Call retry decisions](/calls#idempotency) before adding automatic retries. + +## Workflows that need a decision + +**Batch calls:** Calls accepts one target. Client-side orchestration and aggregation +may work if the application can respect account concurrency and maintain one key +per logical call. This is not a server-side batch replacement. + +**Nested results:** flatten the business fields or adapt the application's data +model. Arrays and nested objects are not supported by the current schema profile. + +**Transcripts:** wait for the planned fixed `transcript` field to be deployed; +raw ASR updates are not a substitute for a final transcript. **Provider attempts:** the Calls +response does not expose these details; resolve that dependency before migrating. + +**Execution timing:** Calls accept immediate execution only. Scheduled and recurring +execution are outside this interface. + +**Historical calls:** the new interface does not adopt legacy records. Keep the old ID and its original interface. +The post-retirement access/export policy remains a decision in this preview; +there is no new archive endpoint to integrate against yet. diff --git a/content/guides/quickstart.mdx b/content/guides/quickstart.mdx index c5fdcb5..a99ff07 100644 --- a/content/guides/quickstart.mdx +++ b/content/guides/quickstart.mdx @@ -1,210 +1,113 @@ --- title: Quickstart -description: Create one call and read the structured result. +description: Make a phone call and read its structured result. --- -Create a CALL-E call task, wait for the terminal result, and read the structured output. +Create one phone call, keep its ID, and wait for a structured result. -This quickstart uses the one-shot Calls API. If your application repeats a workflow that has already been authored and published in CALL-E, start with [Goal Runs](/goal-runs) instead. +These examples target the [September 22 test preview contract](/calls#preview-availability), +not production availability. The shared test deployment may differ. -Outbound calling is not available in every country or region. Check the current -[supported regions and languages](/regions) -before building an integration. A phone number can be valid E.164 and still be -rejected with `unsupported_region`; see [Errors](/errors) for recovery guidance. - -**API key** · **TypeScript** · **Python** - -## Install - -Install the server SDK package for your runtime. The Python SDK requires -Python 3.11 or later; Python 3.9 and 3.10 are not supported. +## Connect to the test API ```bash -pnpm add @call-e/calle -pip install calle-ai -``` - -Set your API key before running the examples. Replace `` -with the complete key from the dashboard; the placeholder is not a working -credential: - -```bash -export CALLE_API_KEY="" -``` - -You can view your API keys in the [CALL-E dashboard](https://dashboard.heycall-e.com/account/api-keys). - -See [Authentication](/authentication) for API key handling and server-only usage. - -## Create a client - -TypeScript: - -```ts -import { CalleClient } from "@call-e/calle"; - -const client = new CalleClient({ - apiKey: process.env.CALLE_API_KEY!, -}); -``` - -Python: - -```python -import os -from calle import CalleClient - -client = CalleClient(api_key=os.environ["CALLE_API_KEY"]) +export CALLE_BASE_URL="https://test-api.heycall-e.com" +export CALLE_API_KEY="" ``` -## Minimum request - -The minimum create request is task-only. Include the phone number directly in the task when CALL-E should infer the recipient from the instruction. Replace `` with a phone number you own or are authorized to call. +Get a key for the test account from the [test dashboard](https://test-dashboard.heycall-e.com/account/api-keys). +Keep it on your server. See [Authentication](/authentication) for key handling. +Check [Regions & languages](/regions) before choosing a destination; availability +also depends on the account and line configuration. -```json -{ - "task": "Call and ask whether they can hear clearly." -} -``` +## Create one call -## Create and wait +Replace `` with a number you own or are authorized to call. +Running this request can place a real phone call. Persist the request and its +idempotency key before sending it. -TypeScript: - -```ts -const call = await client.calls.createAndWait({ - task: "Call and ask whether they can hear clearly.", - resultSchema: { - type: "object", - required: ["can_hear_clearly"], - properties: { - can_hear_clearly: { type: "string", enum: ["yes", "no", "unknown"] }, - }, - }, -}); -``` - -Python: - -```python -call = client.calls.create_and_wait( - task="Call and ask whether they can hear clearly.", - result_schema={ - "type": "object", - "required": ["can_hear_clearly"], - "properties": { - "can_hear_clearly": {"type": "string", "enum": ["yes", "no", "unknown"]}, - }, +```bash +curl --fail-with-body --silent --show-error \ + --request POST "$CALLE_BASE_URL/v2/calls" \ + --header "Authorization: Bearer $CALLE_API_KEY" \ + --header "Content-Type: application/json" \ + --header "Idempotency-Key: hello-test-001" \ + --data '{ + "task": "Say hello in English, allow a brief reply, then politely end the call.", + "phone": "", + "result_schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "greeting_delivered": {"type": "boolean"}, + "summary": {"type": "string"} + }, + "required": ["greeting_delivered", "summary"] }, -) + "metadata": {"test_case": "hello-test-001"} + }' ``` -## Read the result +This example omits the optional `region` and `locale`: the destination is inferred +from the phone, and the task explicitly requests English. You may supply them +when needed, but they must match your intended destination and language; a phone/region +conflict returns `422 input_incomplete` instead of silently changing the region. -The terminal call task includes a stable status, a schema-valid structured result, and task-level outcome fields from the post-call summary. When CALL-E cannot produce a schema-valid result from the evidence, `structured_result` is `null`. +A successful create returns **202 Accepted** with `id`, `object: "call"`, +`status: "queued"`, `result: null`, and `error: null`. Acceptance does not mean +the phone has connected. Save the returned `call_...` ID. -See [Call status](/calls#call-status) for lifecycle and terminal states, and -[Task completion](/calls#task-completion) for how to interpret -`task_completed` (`taskCompleted` in TypeScript) and `completion_confidence`. +Creation waits for task preparation before returning `202`. If it returns +`422 input_incomplete`, no Call was created: collect the facts listed in +`error.details.missing_inputs`, update the task, and submit again. See +[Calls](/calls#request-contract) for the response and retry behavior. -TypeScript: +## Wait for the result -```ts -console.log(call.status); -console.log(call.structuredResult); -console.log(call.taskCompleted, call.completionConfidence, call.evidence); -``` +```bash +export CALLE_CALL_ID="" -Example output: - -```json -{ - "status": "completed", - "taskCompleted": true, - "completionConfidence": {"score": 0.92, "label": "high"}, - "evidence": ["The recipient clearly answered yes."], - "structuredResult": { - "can_hear_clearly": "yes" - } -} +curl --fail-with-body --silent --show-error \ + --header "Authorization: Bearer $CALLE_API_KEY" \ + "$CALLE_BASE_URL/v2/calls/$CALLE_CALL_ID" ``` -Python: - -```python -print(call["status"]) -print(call["structured_result"]) -print(call["task_completed"], call["completion_confidence"], call["evidence"]) -``` +Poll at a reasonable interval, such as every two seconds: -Example output: - -```json -{ - "status": "completed", - "task_completed": true, - "completion_confidence": {"score": 0.92, "label": "high"}, - "evidence": ["The recipient clearly answered yes."], - "structured_result": { - "can_hear_clearly": "yes" - } -} -``` +| Response | What to do | +| --- | --- | +| `result == null` and `error == null` | Keep waiting, including when `status` is `completed`. | +| `result != null` | Consume the validated business result. An empty object is also ready. | +| `error != null` | Handle the final execution or result-processing error. | -Before adding automatic retries, follow -[Recover after a restart or lost response](/calls#recover-after-a-restart-or-lost-response) -to persist the original request, idempotency key, and Call ID. +An illustrative ready result is `{"greeting_delivered": true, "summary": "The recipient replied hello."}`. +Use [Webhooks](/webhooks) to receive the result without polling. -## Run a complete example +Do not wait for `result_status` or assume `transcript` is present on this verified +test contract. Those are [unreleased response additions](/calls#preview-availability) +with a different readiness rule. `status: completed` alone is not proof of +business success or result readiness. -The [Python example](https://github.com/CALLE-AI/calle-docs/blob/main/examples/calls.py) -creates one real US English test call, saves its request and Call ID, waits for -the terminal result, and writes the full response to a private local directory. -It listens to a greeting and then asks the agent to end the call. This is a -real-service integration check, not a simulated business transaction. +## Try the local SDKs -Use Python 3.11 or later and a number you own or are authorized to test. If you -need a destination, follow the [official US English testing hotline announcement](https://discord.com/channels/1493880186826133504/1495622983253889054/1546414916515401788). -Calls use your real account and may consume credits. The hotline is a test -recipient, not a promise of free sandbox credentials or deterministic answers. +The [SDK guide](/sdks) includes local installation and complete TypeScript +and Python examples for the test environment. -```bash -git clone https://github.com/CALLE-AI/calle-docs.git -cd calle-docs -python3 -m venv .venv -source .venv/bin/activate -python -m pip install calle-ai==0.7.0 -export CALLE_API_KEY="" -export CALLE_TEST_PHONE="" -python examples/calls.py start ../calle-run --phone "$CALLE_TEST_PHONE" -``` +## Retry without calling twice -`start` requires a new run directory and submits at most one create request. -The directory holds `request.json` (including the original idempotency key), -`call-id.json`, and, once available, `result.json`. Keep it private: it contains -the destination and call transcript. The API key is not written there. On Unix, -the directory is created with owner-only access; on Windows, use a private -directory protected by your account's file permissions. +If the create response is lost, retry the **same request with the same key**. +Do not switch interfaces or generate a fresh key because of a timeout. Changing input +under the same key returns `409 idempotency_conflict`. -After a polling timeout, application restart, or successful run, read the same -call again with: +If waiting times out, keep the Call ID and query it again. A polling timeout does +not cancel the call. A new key is for an intentionally new logical call. -```bash -python examples/calls.py resume ../calle-run -``` +Repeated creation returns the same Call's current state, even after completion, +failure or cancellation; it does not redial. If the first request is still being +created, `409 idempotency_conflict` with `details.reason_code: creation_in_progress` +means back off and retry unchanged. A definitive input rejection before acceptance +lets you correct the body and reuse the key. Keep omitted `region/locale` omitted +on retries instead of copying their inferred values from the response. -`resume` only retrieves the saved Call ID; it never creates a call. If a create -response was lost and no ID was saved, the script stops. Keep `request.json` -and follow [Calls recovery](/calls#recover-after-a-restart-or-lost-response) -before starting a replacement. Do not delete a run directory to bypass this check. - -Stopping this process or reaching its five-minute polling timeout does not -cancel a call already accepted by CALL-E. Neither command retries a failed -create request automatically. An HTTP error is saved in `error.json`; see -[Choose the next action](/errors#choose-the-next-action). - -An exit code of zero means a terminal response was retrieved. Read `status`, -`task_completed`, and `structured_result` separately and check the transcript -in `result.json`. A `null` structured result or `heard_greeting: "unknown"` -does not establish that a greeting was heard. A terminal `failed` or `canceled` -response is still a readable outcome, not a successful business task. +Read the [Call retry decisions](/calls#idempotency) for the full contract. If you already have an +integration, use the [migration guide](/migration). diff --git a/content/guides/retirement.mdx b/content/guides/retirement.mdx new file mode 100644 index 0000000..f8cd0b7 --- /dev/null +++ b/content/guides/retirement.mdx @@ -0,0 +1,65 @@ +--- +title: Legacy API retirement +description: Planned support deadline and migration arrangements for legacy call tasks. +--- + +:::note{title="Retirement notice preview"} +This page is a review draft. The test API is available for evaluation; production +GA and the customer notification campaign have not been announced by this preview. +::: + +## Scope + +
+ +| Surface | Status in this preview | +| --- | --- | +| Single-target Calls | Test preview, including reads, events and cancellation before submission. | +| Legacy call tasks | Creation, reads and events are planned for retirement at the deadline below. | +| Goals and Goal Runs | Unaffected by this retirement. | +| Published SDK packages | Continue to serve legacy call tasks during this preview. | +| Local preview packages | Support single-target Calls; not published to npm or PyPI. | + +
+ +See the [migration guide](/migration) for the request and response differences. + +## Planned deadline + +**2026-12-31 at 16:00 UTC**, equivalent to **2027-01-01 at 00:00 Asia/Shanghai**. + +The proposal is to retire legacy call creation, reads and events at this deadline and return +`410 Gone` with migration guidance. That cutoff behavior is not enabled merely +by publishing this preview. Legacy requests are not automatically converted. + +Work already accepted must complete or be reconciled; shutting off new requests +does not cancel provider calls. Existing callbacks must retain their original +version's payload. Retirement does not itself delete stored call records. + +## Before the deadline is announced + +- Publish the production-ready Calls contract, migration guide and supported SDK packages. +- Resolve impacted workflows that need batch targets, nested schemas or transcript access. +- Specify how users can read or export historical call-task data after retirement. The new interface cannot read those IDs. +- Notify affected project owners directly and identify integrations still using the legacy interface. + +The exact historical-data access policy and any read-only grace period remain +open for review. No export API, data-retention duration or grace period is promised +by this draft. + +## Deprecation signals + +The release plan uses `Deprecation`, `Sunset` and a `Link` with +`rel="deprecation"` to point clients to migration guidance. These response +headers are planned; they are not claimed as implemented in this preview. +Deprecation leaves the supported legacy behavior intact until its published retirement date. + +## Migration communications + +The proposed cadence is a GA announcement followed by reminders 60, 30, 14, 7 and +1 day before retirement, targeted to projects still using legacy call tasks. The notice +includes the affected routes, the UTC deadline, SDK versions, migration steps, +historical-data arrangements and the established support channel. + +Start with the [migration guide](/migration). No user notification is +sent as part of viewing this documentation. diff --git a/content/guides/sdks.mdx b/content/guides/sdks.mdx index 4819bb6..f14f9db 100644 --- a/content/guides/sdks.mdx +++ b/content/guides/sdks.mdx @@ -1,203 +1,130 @@ --- title: SDKs -description: Install the stable TypeScript and Python server SDKs. +description: Use the TypeScript and Python SDKs with the test API. --- -CALL-E provides server SDKs for trusted backend services, workers, and automation systems that create and monitor call tasks. +For this preview, install the TypeScript or Python SDK from its local checkout. +These packages have not been published. The [registry packages](/legacy-sdks) +continue to serve existing call-task integrations. -## Packages +These local checkouts also include the [unreleased response additions](/calls#preview-availability). +The wait helpers below require a deployment that returns `result_status`; +do not use them against the September 22 test contract, which lacks that field. +Use the [REST Quickstart](/quickstart#wait-for-the-result) for that contract. -TypeScript package name: +## Install from a local checkout -```text -@call-e/calle -``` - -Python distribution name: +Run these from a separate test project, adjusting the paths to your local checkouts. +The TypeScript checkout must be built before linking it: -```text -calle-ai +```bash +pnpm --dir /path/to/server-sdk-typescript install --frozen-lockfile +pnpm --dir /path/to/server-sdk-typescript run build +pnpm add /path/to/server-sdk-typescript ``` -Python imports the package as `calle`: +For Python 3.11 or later, use an isolated environment: -```python -from calle import CalleClient +```bash +python3 -m venv .venv +.venv/bin/pip install /path/to/server-sdk-python ``` -## Package status - -The current stable server SDK packages are: - -- TypeScript: `@call-e/calle@0.7.0` -- Python: `calle-ai==0.7.0` - -These stable packages support both the request-scoped Calls API and the Goal -Runs SDK surface described below. - -## Goal Runs - -The Goal-based SDK surface keeps published contracts separate from the -one-shot Calls API: - -- TypeScript: `client.goals.list(...)`, `get(...)`, `run(...)`, `getRun(...)`, - `waitForResult(...)`, and `runAndWait(...)` -- Python: `client.goals.list(...)`, `get(...)`, `run(...)`, `get_run(...)`, - `wait_for_result(...)`, and `run_and_wait(...)` +You can also install the local `.tgz` or `.whl` produced by those checkouts. -Run requests contain Goal identity, one phone number, per-Run variables, and an -idempotency key. They do not accept request-scoped task text, prompts, -`input_schema`, or `result_schema`; those fields are owned by the published -RunSpec. See the [Goal Runs guide](/goal-runs) for the API contract and SDK -examples. - -## Source code - -Both SDKs are maintained in public repositories. - -| Runtime | Package | Repository | -| --- | --- | --- | -| TypeScript | `@call-e/calle` | [CALLE-AI/server-sdk-typescript](https://github.com/CALLE-AI/server-sdk-typescript) | -| Python | `calle-ai` | [CALLE-AI/server-sdk-python](https://github.com/CALLE-AI/server-sdk-python) | - -The TypeScript repository includes public source code, examples, and security -guidance: - -- [TypeScript security policy](https://github.com/CALLE-AI/server-sdk-typescript/blob/main/SECURITY.md) - -## Local examples - -The TypeScript SDK repository includes runnable examples for the server-side -one-shot call task flow. - -TypeScript: +## Test environment ```bash -git clone https://github.com/CALLE-AI/server-sdk-typescript.git -cd server-sdk-typescript -pnpm install -pnpm run example:create-and-wait +export CALLE_BASE_URL="https://test-api.heycall-e.com" +export CALLE_API_KEY="" +export CALLE_TEST_PHONE="" +export CALLE_IDEMPOTENCY_KEY="hello-test-001" ``` -## Calls API - -The SDKs expose `context` as a reserved input for future SDK-side workflow data. The current SDKs do not send `context` to the API. +The examples omit `region` and `locale` and ask for English in the task. +You may supply either hint explicitly; a phone/region conflict returns +`422 input_incomplete`. Keep the original input unchanged on retries. +The examples below create a real call when run with valid credentials and a number. -Examples use phone placeholders such as `` and ``. Replace them with phone numbers you own or are authorized to call. +## TypeScript -The SDKs accept structured result schemas as plain JSON objects. Use field `description` values to explain how CALL-E should interpret enum values, and use `type`, `required`, `enum`, and `additionalProperties` for hard validation. See the Calls guide for structured result design patterns and examples. +```ts +import { CalleClient } from "@call-e/calle"; -TypeScript: +const client = new CalleClient({ + apiKey: process.env.CALLE_API_KEY!, + baseUrl: process.env.CALLE_BASE_URL!, +}); -```ts const created = await client.calls.create( { - task: "Call each recipient and ask whether they can attend Friday lunch in San Francisco.", - recipients: [ - { phones: [""] }, - { phones: [""] }, - ], + task: "Say hello in English, allow a brief reply, then politely end the call.", + phone: process.env.CALLE_TEST_PHONE!, resultSchema: { type: "object", - required: ["attending_count"], - properties: { - attending_count: { type: "integer" }, - }, - }, - recipientResultSchema: { - type: "object", - required: ["can_attend"], - properties: { - can_attend: { type: "string", enum: ["yes", "no", "unknown"] }, - }, + additionalProperties: false, + properties: { summary: { type: "string" } }, + required: ["summary"], }, }, - { idempotencyKey: "wf_123_friday_lunch" }, + { idempotencyKey: process.env.CALLE_IDEMPOTENCY_KEY! }, ); +console.log("Keep this Call ID:", created.id); -const fetched = await client.calls.get(created.id); -const completed = await client.calls.waitForResult(fetched.id); -const createdAndCompleted = await client.calls.createAndWait( - { - task: "Call and confirm their preferred appointment time.", - resultSchema: { - type: "object", - required: ["preferred_time"], - properties: { - preferred_time: { type: "string" }, - }, - }, - }, - { timeoutMs: 120_000, intervalMs: 2_000 }, -); -const events = await client.calls.listEvents(createdAndCompleted.id, { - limit: 50, +const ready = await client.calls.waitForResult(created.id, { + intervalMs: 2000, + timeoutMs: 600000, }); +console.log(ready.result, ready.error); +console.log(ready.transcript); ``` -Python: +## Python ```python +import os +from calle import CalleClient + +client = CalleClient( + api_key=os.environ["CALLE_API_KEY"], + base_url=os.environ["CALLE_BASE_URL"], +) created = client.calls.create( - task="Call each recipient and ask whether they can attend Friday lunch in San Francisco.", - recipients=[ - {"phones": [""]}, - {"phones": [""]}, - ], + task="Say hello in English, allow a brief reply, then politely end the call.", + phone=os.environ["CALLE_TEST_PHONE"], + idempotency_key=os.environ["CALLE_IDEMPOTENCY_KEY"], result_schema={ "type": "object", - "required": ["attending_count"], - "properties": {"attending_count": {"type": "integer"}}, - }, - recipient_result_schema={ - "type": "object", - "required": ["can_attend"], - "properties": { - "can_attend": {"type": "string", "enum": ["yes", "no", "unknown"]}, - }, + "additionalProperties": False, + "properties": {"summary": {"type": "string"}}, + "required": ["summary"], }, - idempotency_key="wf_123_friday_lunch", ) - -fetched = client.calls.get(created["id"]) -completed = client.calls.wait_for_result(fetched["id"]) -created_and_completed = client.calls.create_and_wait( - task="Call and confirm their preferred appointment time.", - result_schema={ - "type": "object", - "required": ["preferred_time"], - "properties": {"preferred_time": {"type": "string"}}, - }, - timeout_seconds=120, - interval_seconds=2, +print("Keep this Call ID:", created["id"]) +ready = client.calls.wait_for_result( + created["id"], interval_seconds=2, timeout_seconds=600 ) -events = client.calls.list_events(created_and_completed["id"], limit=50) -``` - -## Availability - -Install the stable server SDK package for your runtime. The Python SDK requires -Python 3.11 or later; Python 3.9 and 3.10 are not supported. - -```bash -pnpm add @call-e/calle -pip install calle-ai +print(ready["result"], ready["error"]) +print(ready["transcript"]) ``` -Use pinned versions when your deployment process requires exact package reproducibility: +The client defaults to the production base URL if you omit the override, so pass +the test URL explicitly. Wait helpers stop when `result_status` is not `pending`; +a timeout does not cancel or replace the Call. -```bash -pnpm add @call-e/calle@0.7.0 -pip install calle-ai==0.7.0 -``` +## Retry safely -## Supported scope +Persist one key and the original request before calling `create`. Retrying the +same logical call uses both unchanged. Do not generate a new key inside a retry +loop or copy resolved `region/locale` from the response into an originally +omitted input. A replay returns the original Call even after failure or completion; +it does not initiate another phone call. Follow [Call retry decisions](/calls#idempotency) +to distinguish `409 idempotency_conflict` with reason `creation_in_progress` +from changed-input conflicts. -This release does not include: +## Existing integrations -- Python async client support -- Project-level webhook management -- Client-initiated cancellation of in-flight calls -- Recurring or scheduled calls -- Zod result schema helpers -- Pydantic result schema helpers +Keep a legacy client for historical call-task IDs. The current SDK's Calls +methods use the single-target interface; Goal methods retain their existing contract. +HTTP/Python use `result_schema` and `error.detail_code`; TypeScript uses +`resultSchema` and `error.detailCode`. See the [migration guide](/migration). diff --git a/content/guides/webhooks.mdx b/content/guides/webhooks.mdx index e136347..c7576a0 100644 --- a/content/guides/webhooks.mdx +++ b/content/guides/webhooks.mdx @@ -1,198 +1,88 @@ --- title: Webhooks -description: Receive terminal webhook events and process them safely. +description: Receive and process final call results. --- -Use webhooks when your application needs to react to terminal call task results without polling. +Set `webhook_url` when creating a Call. The verified test preview sends a final +snapshot once `result` or `error` is non-null, not when the phone first ends. +A rejected creation request does not produce a terminal webhook. -Phone values in examples are placeholders. Real webhook payloads echo the phone numbers from your call task. +The `result_status`, `call_outcome` and `transcript` fields in the example below +belong to the [unreleased response additions](/calls#preview-availability). +With those additions, delivery occurs once `result_status` is no longer `pending`; +an unavailable result can have both `result` and `error` null, and ordinary +no-answer, busy or declined outcomes use `call.completed`. -CALL-E publishes a terminal event only after the post-call summary, task -completion judgment, confidence, evidence, and any requested structured results -are finalized. The event `data` is the same complete terminal snapshot returned -by `GET /v1/calls/{call_id}`. +## Payload and events -## Terminal events +The envelope contains `id`, `type`, `created_at` and `data`. `data` matches the +ready Call returned by GET and has `object: "call"`. -CALL-E sends terminal webhook events: +In that planned contract, `data.transcript` is always an array with the same recorded turns as GET. +Business result errors do not remove it; no available transcript is represented +by `[]`. -- `call.completed` -- `call.failed` -- `call.result_validation_failed` +| Event | Interpretation | +| --- | --- | +| `call.completed` | Telephone execution completed; inspect `data.result` or `data.error` for the outcome. | +| `call.failed` | Execution failed. | +| `call.canceled` | The Call was canceled before provider submission. | -`call.result_validation_failed` is sent when a completed call task had an internal structured-result validation failure for the task or a recipient. The payload does not expose validation details; invalid or unsupported structured results are returned as `null`. Failed or canceled terminal call tasks use `call.failed`. +A completed call with a result-processing error still uses `call.completed`. +The current call contract does not use `call.result_validation_failed`; inspect +`error.code`, such as `result_invalid`. -Each event has an `id`, `type`, `created_at`, and a `data` object containing the complete terminal call task fields. - -Recipient-level structured results appear inside the `recipients` array. They do not create separate recipient webhook events. - -Attempt transcripts appear as `recipients[].attempts[].transcript_turns`. Each turn includes `offset_seconds`, `speaker`, and `text`; the array is empty when no transcript is available. - -See [Task completion](/calls#task-completion) for how to interpret -`task_completed` and `completion_confidence` in the payload. - -Example payload: +Planned response envelope, with an abbreviated `data` object: ```json { - "id": "evt_123", + "id": "evt_example", "type": "call.completed", - "created_at": "2026-06-08T18:30:00Z", + "created_at": "2026-09-21T02:00:00Z", "data": { - "id": "call_123", - "object": "call_task", + "id": "call_example", + "call_id": "0123456789abcdef0123456789abcdef", + "object": "call", "status": "completed", - "task": "Call each recipient and ask whether they can attend Friday lunch in San Francisco.", - "recipients": [ - { - "id": "rcp_001", - "phones": [""], - "region": "US", - "locale": "en-US", - "status": "completed", - "structured_result": { - "can_attend": "yes" - }, - "summary": "The recipient can attend Friday lunch.", - "attempts": [ - { - "id": "att_001", - "phone": "", - "status": "completed", - "started_at": "2026-06-08T18:21:00Z", - "completed_at": "2026-06-08T18:29:00Z", - "summary": null, - "transcript_turns": [ - { - "offset_seconds": 0, - "speaker": "bot", - "text": "Can you attend Friday lunch in San Francisco?" - }, - { - "offset_seconds": 8, - "speaker": "user", - "text": "Yes, I can attend." - } - ], - "provider_call_id": "provider_001", - "failure_code": null, - "failure_message": null - } - ] - }, - { - "id": "rcp_002", - "phones": [""], - "region": "US", - "locale": "en-US", - "status": "completed", - "structured_result": null, - "summary": "The recipient did not provide a usable answer.", - "attempts": [ - { - "id": "att_002", - "phone": "", - "status": "completed", - "started_at": "2026-06-08T18:22:00Z", - "completed_at": "2026-06-08T18:30:00Z", - "summary": null, - "transcript_turns": [], - "provider_call_id": "provider_002", - "failure_code": null, - "failure_message": null - } - ] - } - ], - "structured_result": { - "attending_count": 1 - }, - "summary": "One recipient can attend Friday lunch.", - "task_completed": true, - "completion_confidence": { - "score": 0.86, - "label": "high" - }, - "evidence": [ - "One recipient confirmed they can attend.", - "The second recipient did not provide a usable answer." - ], - "metadata": { - "workflow_run_id": "wf_123" - }, - "failure_code": null, - "failure_message": null, - "created_at": "2026-06-08T18:20:00Z", - "completed_at": "2026-06-08T18:30:00Z" + "call_outcome": "completed", + "result_status": "available", + "transcript": [{"speaker": "bot", "offset_seconds": 0, "text": "Hello."}], + "result": {"greeting_delivered": true}, + "error": null } } ``` -## Receive events - -Current CALL-E webhook delivery does not use a webhook secret, -`CALL-E-Timestamp`, or `CALL-E-Signature`. Treat the receiver as a public, -untrusted-input boundary: validate the JSON shape, require -`CALL-E-Event-Id`, and reject the request when that header does not match the -body event `id`. - -TypeScript: - -```ts -const event = JSON.parse(rawBody.toString("utf8")); -const eventId = request.headers.get("CALL-E-Event-Id"); - -if (!eventId || eventId !== event.id) { - return new Response("invalid event id", { status: 400 }); -} - -if (event.type === "call.completed") { - console.log(event.data.id, event.data.recipients); -} -``` - -Python: +## Receiver behavior -```python -event = json.loads(raw_body) -event_id = request.headers.get("CALL-E-Event-Id") - -if not event_id or event_id != event["id"]: - return {"error": "invalid_event_id"}, 400 - -if event["type"] == "call.completed": - print(event["data"]["id"], event["data"]["recipients"]) -``` +Return a `2xx` response after durably accepting the event. Delivery can repeat; +the sender makes up to six attempts with the same event ID. Use the event ID +for deduplication, not the timestamp or phone number. -Return a `2xx` response after accepting the event. CALL-E retries delivery when -the receiver returns a non-`2xx` response or the request fails. +The webhook event ID is separate from your create request's `Idempotency-Key`. +Process repeated deliveries once; do not create another Call in response to a +delivery retry. ASR and speech updates are read from +[Call events](/calls#read-events-and-cancellation), not pushed to this terminal webhook. -HTTPS and event-id matching do not provide cryptographic proof of the sender. -Before a sensitive side effect that requires origin assurance, fetch -`GET /v1/calls/{call_id}` with your API key and compare its terminal snapshot -with the event. +`CALL-E-Event-Id` must match the envelope `id`. This is a consistency check, +not a cryptographic signature. The current delivery path does not supply +`CALL-E-Signature` or a webhook signing secret. Before a sensitive side effect, +fetch the original Call with your API key and compare its ready snapshot. -## Idempotent handling +## Existing receivers -Webhook delivery is at least once. Store the webhook event `id` before processing side effects so duplicate deliveries are ignored safely. - -TypeScript: +When migrating, use separate receiver paths or dispatch on `data.object`: ```ts -if (await eventStore.has(event.id)) { - return new Response("duplicate", { status: 200 }); +if (event.data.object === "call") { + handleCallResult(event.data.call_outcome, event.data.result_status, event.data.result, event.data.error); +} else if (event.data.object === "call_task") { + handleLegacyResult(event.data.structured_result); } - -await eventStore.insert(event.id); -await handleCallEvent(event); ``` -Python: - -```python -if event_store.has(event["id"]): - return {"ok": True, "duplicate": True} - -event_store.insert(event["id"]) -handle_call_event(event) -``` +`handleCallResult` and `handleLegacyResult` are application handlers, not SDK +exports. Both interfaces can send `call.completed`, so event type alone cannot +identify the payload format. Keep the [legacy receiver](/legacy-webhooks) available for +call tasks accepted before the cutover. Switching new requests does not rewrite +old webhook payloads or call records. diff --git a/openapi/calle.openapi.yaml b/openapi/calle.openapi.yaml index 938e9d7..a550c8a 100644 --- a/openapi/calle.openapi.yaml +++ b/openapi/calle.openapi.yaml @@ -1,21 +1,255 @@ openapi: 3.1.0 info: title: CALL-E Developer API - version: 0.7.0 - description: Developer API contract used by the CALL-E TypeScript and Python SDKs. + version: 1.0.0 + description: >- + Developer API contract used by the CALL-E TypeScript and Python SDKs. + This development contract includes call_outcome, result_status and the fixed + transcript array. Confirm the target environment's deployed contract before + using those fields or local SDK wait helpers. This is not a production + availability announcement. See the Calls guide's preview availability section. servers: - url: https://api.heycall-e.com description: Placeholder developer API base URL. security: - bearerAuth: [] +tags: + - name: calls + x-displayName: Calls + description: Create, track, and read single-target calls. + - name: legacy-calls + x-displayName: Legacy Calls (Deprecated) + description: >- + Deprecated. v1 Calls will retire on December 31, 2026 + (2026-12-31 16:00 UTC / 2027-01-01 00:00 Asia/Shanghai). + See the [migration guide](/migration). Goal APIs are unaffected. paths: + /v2/calls: + post: + operationId: createAgenticCall + tags: + - calls + summary: Create Call + description: >- + Prepare one phone task, then return 202 after durable acceptance. Dialing runs + in the background; acceptance does not prove connection or business success. + Region and locale are optional or null. Infer region from the phone and spoken + locale from task intent and supported regional languages; responses contain + the resolved values. Explicit locale is preserved. Conflicting phone/region + or ambiguous language returns 422 input_incomplete with details.missing_inputs; + phone/region conflicts also include details.fields, region and inferred_region. + Unsupported targets return 422 unsupported_region or unsupported_language with + details.field, region and locale. Explicit targets are checked before preparation; + inferred locale is checked after inference. These rejections create no Call. + Unsupported result_schema types, including nullable unions, return 400 + result_schema_invalid. Preparation is bounded to 25 seconds; configuration access + failures, conflicting profiles, model failures or timeouts return 503 provider_unavailable. + Accepted instructions and input are immutable and reused on replay. Calls accept + one phone and immediate execution only: no batch recipients, scheduling, recurrence + or recipient_result_schema. + parameters: + - name: Idempotency-Key + in: header + required: true + description: >- + Persist one key and the original request per logical call. Same project, + authenticated owner, key and input return 202 with the original Call ID + and its current saved state, including after completion, failure or cancellation. + Replay does not prepare or dial again. An intentionally new call requires + a new key. Changes to task, phone, region, locale, result_schema, metadata + or webhook_url return 409 idempotency_conflict; JSON object-key order is + not significant. Keep omitted region/locale omitted on retries; adding + inferred response values changes the input. Concurrent creation returns + 409 idempotency_conflict with details.reason_code=creation_in_progress: + back off and retry unchanged. A definitive validation rejection before + acceptance creates no Call and permits corrected input with the same key. + After a timeout or uncertain response, reuse the original key and body, + not a new key. Keys must be 1-255 characters after trimming. + schema: + type: string + minLength: 1 + maxLength: 255 + responses: + '202': + description: Task prepared and Call durably accepted, or an exact replay returning the same Call ID and current saved state. No duplicate dialing on replay. + content: + application/json: + schema: + $ref: '#/components/schemas/AgenticCall' + '400': &id001 + $ref: '#/components/responses/ErrorResponse' + '401': *id001 + '402': *id001 + '403': *id001 + '409': + description: Creation is still in progress, or the key belongs to another request or owner. Do not automatically retry with a new key. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + examples: + creation_in_progress: + value: + error: + code: idempotency_conflict + message: Call creation is still in progress. Retry with the same idempotency key and request. + details: + reason_code: creation_in_progress + changed_request: + value: + error: + code: idempotency_conflict + message: Idempotency key belongs to another request. + details: {} + '422': + description: The requested region/locale is unsupported, essential task information is missing, or the request violates calling policy. No Call is created. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + examples: + unsupported_region: + value: + error: + code: unsupported_region + message: Region 'ZZ' is not supported for calls. Choose a supported region and locale. + details: + field: region + region: ZZ + locale: en-US + unsupported_language: + value: + error: + code: unsupported_language + message: Locale 'xx-XX' is not supported for region 'CN'. Choose a supported locale for this region. + details: + field: locale + region: CN + locale: xx-XX + input_incomplete: + value: + error: + code: input_incomplete + message: The task lacks information required to make this call. + details: + missing_inputs: + - Provide the appointment date. + - Provide the appointment time. + '429': *id001 + '503': *id001 + '500': *id001 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAgenticCallRequest' + /v2/calls/{call_id}: + get: + operationId: getAgenticCall + tags: + - calls + summary: Get Call + description: Polling reads persisted state and never performs extraction or initiates a call. + parameters: + - &id002 + name: call_id + in: path + required: true + schema: + type: string + description: Developer Call id returned by v2. + responses: + '200': + description: Committed response. + content: + application/json: + schema: + $ref: '#/components/schemas/AgenticCall' + '400': *id001 + '401': *id001 + '403': *id001 + '404': *id001 + '409': *id001 + '500': *id001 + /v2/calls/{call_id}/cancel: + post: + operationId: cancelAgenticCall + tags: + - calls + summary: Cancel Call + description: Cancellation is idempotent for terminal calls. Once provider submission starts, returns 409 call_cannot_cancel; it does not hang up an active call. + parameters: + - *id002 + responses: + '200': + description: Committed response. + content: + application/json: + schema: + $ref: '#/components/schemas/AgenticCall' + '400': *id001 + '401': *id001 + '403': *id001 + '404': *id001 + '409': *id001 + '500': *id001 + /v2/calls/{call_id}/events: + get: + operationId: listAgenticCallEvents + tags: + - calls + summary: List Events + description: >- + Poll durable JSON event pages, not SSE. Events include call.accepted, + call.in_progress, call.ringing, call.connected, call.asr, call.speech, + call.interrupted, call.dtmf, call.ended, call.result_ready, and + call.completed/failed/canceled. Not every call emits every event type. + Speech details preserve observed text, speaker, turn and optional occurred_at. + ASR may contain partial fragments or revised hypotheses; no is_final flag + or final-sentence guarantee is provided. Do not concatenate every update into + a transcript. Deduplicate by event ID, not turn or text. Persistence order + includes late arrivals; IDs remain stable. Follow next_cursor for additional + available pages. Retain the last event ID for later polls even when next_cursor + is null, and keep it on empty pages. Empty pages do not mean completion. + Existing lifecycle cursors remain valid. Invalid or foreign cursors return + 400 invalid_request. Reads do not dial, infer or persist anything. + parameters: + - *id002 + - name: cursor + in: query + description: Opaque next_cursor or last returned event ID; scoped to this call. Retain the saved cursor on an empty page. + schema: + type: string + - name: limit + in: query + description: Page size, 1-100. Invalid or out-of-range values return 400 invalid_request. + schema: + type: integer + default: 50 + minimum: 1 + maximum: 100 + responses: + '200': + description: Committed response. + content: + application/json: + schema: + $ref: '#/components/schemas/EventList' + '400': *id001 + '401': *id001 + '403': *id001 + '404': *id001 + '409': *id001 + '500': *id001 /v1/calls: post: operationId: createCall tags: - - calls + - legacy-calls + deprecated: true summary: Create Call - description: Create an asynchronous call. Use `result_schema` and `recipient_result_schema` to ask CALL-E to extract structured JSON results from terminal call evidence. + description: Create an asynchronous call. Use `result_schema` and `recipient_result_schema` to ask CALL-E to extract structured JSON results from terminal call evidence. Shared platform outbound lines support one phone number per task. Batch calls require an eligible purchased number selected as the account default outbound number; otherwise creation returns `422 call_not_ready`. Account concurrency and LLM token usage are controlled by the effective account configuration. Task concurrency defaults to 1 on shared platform lines and 10 on eligible dedicated purchased numbers; selecting a purchased number as the account default does not make it a shared platform line. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: @@ -26,7 +260,7 @@ paths: $ref: "#/components/schemas/CreateCallRequest" examples: batchLunch: - summary: Check Friday lunch availability for two recipients. + summary: Check Friday lunch availability for two recipients using an eligible purchased outbound number. value: task: Call each recipient and ask whether they can attend Friday lunch in San Francisco. recipients: @@ -172,14 +406,36 @@ paths: "422": $ref: "#/components/responses/ErrorResponse" "429": - $ref: "#/components/responses/ErrorResponse" + description: An account concurrency limit (`account_concurrency_exceeded`), or LLM token budget (`llm_token_budget_exceeded`) was reached. Account admission is checked before planning. Concurrency errors identify the effective line type and limit. Shared-line errors include KYC and dedicated-number purchase guidance in the message and `details.upgrade`; purchased-line errors ask the user to wait for capacity without another purchase suggestion. Configured limits override the default 1 / 10 values. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" + example: + error: + code: account_concurrency_exceeded + message: Your default shared line (such as us/all) is at its account concurrency limit of 1. This limit is shared across API, MCP, and Dashboard. Wait for an active task to finish, then retry. For up to 10 concurrent tasks, complete identity verification (KYC), purchase a dedicated phone number, and select the activated number as your default outbound number. [Verify identity and buy a number](https://dashboard.heycall-e.com/account/numbers/buy). + details: + line_type: platform_default + max_active_tasks: 1 + upgrade: + max_active_tasks: 10 + kyc_required: true + purchase_url: https://dashboard.heycall-e.com/account/numbers/buy "500": $ref: "#/components/responses/ErrorResponse" + "503": + description: The provider or an account control is unavailable (`provider_unavailable`, `account_concurrency_unavailable`, or `llm_token_budget_unavailable`). + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" /v1/calls/{call_id}: get: operationId: getCall tags: - - calls + - legacy-calls + deprecated: true summary: Get Call description: Get a call by id. parameters: @@ -255,8 +511,9 @@ paths: get: operationId: listCallEvents tags: - - calls - summary: List Call Events + - legacy-calls + deprecated: true + summary: List Events description: List developer-facing call events. parameters: - $ref: "#/components/parameters/CallId" @@ -525,6 +782,9 @@ paths: error: null created_at: "2026-07-22T10:00:00Z" completed_at: null + call_outcome: null + result_status: pending + transcript: [] "400": $ref: "#/components/responses/ErrorResponse" "401": @@ -560,10 +820,9 @@ paths: Goal pointer, dispatch work, or start result materialization. Use the `GoalRun.id` returned by create as `goal_run_id`; the nested telephone `run_id` is not valid in this path. - Poll until either `result` or `error` is non-null. A non-null `result` is the parsed object - validated against the published result schema. A non-null `error` means this Run will not - produce a result. `status: completed` with both fields null means result processing is still - in progress. + Poll while `result_status` is `pending`. A non-null `result` is the parsed object validated + against the published result schema. `unavailable` means evidence did not support a business + result; no-answer, busy and declined calls are ordinary `call_outcome` values, not errors. parameters: - $ref: "#/components/parameters/GoalId" - $ref: "#/components/parameters/GoalRunId" @@ -596,6 +855,8 @@ paths: run_id: run_delivery_ord_8472 call_id: calling_call_delivery_ord_8472 status: completed + call_outcome: completed + result_status: available run_spec: id: rspec_delivery_v4 version: 4 @@ -605,6 +866,7 @@ paths: error: null created_at: "2026-07-22T10:00:00Z" completed_at: "2026-07-22T10:01:12Z" + transcript: [] deliveryConfirmationNoAnswer: summary: No human answered, so no business result is available. value: @@ -613,17 +875,17 @@ paths: goal_id: goal_delivery_confirmation run_id: run_delivery_ord_8472 call_id: null - status: failed + status: completed + call_outcome: no_answer + result_status: unavailable run_spec: id: rspec_delivery_v4 version: 4 result: null - error: - code: no_answer - message: No human answered the call. - detail_code: no_human_answered + error: null created_at: "2026-07-22T10:00:00Z" completed_at: "2026-07-22T10:01:12Z" + transcript: [] "401": $ref: "#/components/responses/ErrorResponse" "403": @@ -660,7 +922,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/WebhookEvent" + $ref: "#/components/schemas/TerminalWebhookEvent" examples: callCompleted: summary: Completed US Friday lunch webhook. @@ -738,7 +1000,7 @@ components: IdempotencyKey: name: Idempotency-Key in: header - description: Stable caller-provided key used to make create-call retries safe. Reusing the same key with the same request returns the original call instead of creating a duplicate. + description: Stable caller-provided key used to make create-call retries safe. Reusing the same key with the same request returns the original call instead of creating a duplicate. A persisted creation failure replays its original HTTP status and error body; use a new key for a new attempt after resolving the error. While creation is still in progress, a duplicate returns `409 idempotency_conflict` with `details.reason_code=creation_in_progress`. A different request using the same key returns `409 idempotency_conflict`. required: false schema: type: string @@ -813,7 +1075,7 @@ components: CallId: name: call_id in: path - description: Top-level CallTask `id` returned by Create Call. It starts with `call_`. + description: Public CALL-E call identifier returned by create call. It starts with `call_` and is safe to store in your workflow records. required: true schema: type: string @@ -842,6 +1104,155 @@ components: schema: $ref: "#/components/schemas/ErrorEnvelope" schemas: + CreateAgenticCallRequest: + additionalProperties: false + properties: + task: + minLength: 1 + type: string + phone: + pattern: ^\+[1-9][0-9]{7,14}$ + type: string + region: + anyOf: + - type: string + pattern: ^[A-Z]{2}$ + - type: 'null' + description: Optional destination region. Inferred from the phone when omitted. + An explicit region conflicting with the phone requires corrected input. + locale: + anyOf: + - type: string + minLength: 2 + maxLength: 64 + - type: 'null' + description: Optional spoken BCP-47 locale. Inferred from task intent and available + regional languages when omitted. + result_schema: + type: object + additionalProperties: true + description: Required closed flat JSON Schema using the same calle.result.scalar-object.v1 + profile as Goal. At most 32 string, boolean, integer or number properties; additionalProperties + must be false. Nested objects, arrays, null values and schema combinators are + unsupported. + metadata: + additionalProperties: true + type: object + webhook_url: + anyOf: + - format: uri + minLength: 1 + type: string + - type: 'null' + required: + - task + - phone + - result_schema + type: object + description: One phone, optional region and spoken locale, and a required Goal-compatible + result schema. + AgenticCall: + additionalProperties: false + properties: + id: + type: string + description: API Call resource ID. Use this ID for API paths and event correlation, not the telephone ID in Billing. + call_id: + type: [string, 'null'] + description: Telephone call ID shown in Billing, using the same identity as Goal Run call_id. Always present; null until recorded, including cancellation before dialing. Independent of business result availability. Does not indicate whether charges have settled. Use id, not this field, for API paths. + object: + const: call + type: string + status: + $ref: '#/components/schemas/GoalRunStatus' + call_outcome: + $ref: '#/components/schemas/CallOutcome' + result_status: + $ref: '#/components/schemas/BusinessResultStatus' + transcript: + type: array + items: + $ref: '#/components/schemas/CallTranscriptTurn' + description: Recorded conversation turns in order, independent of the business result. Always present; empty before terminal execution or when no transcript is available. Never generated from result_schema. + task: + type: string + phone: + type: string + region: + type: string + locale: + type: string + result: + type: + - object + - 'null' + description: Result validated against result_schema and durably persisted. Null while pending, unavailable, not applicable, or on a technical error. Explicit schema-valid task fallbacks are preserved. + additionalProperties: + $ref: '#/components/schemas/GoalScalar' + error: + description: Technical execution or result-processing error, or null. No-answer, busy, declined, cancellation and insufficient business evidence do not populate error. + oneOf: + - $ref: '#/components/schemas/GoalRunError' + - type: 'null' + metadata: + additionalProperties: true + type: object + created_at: + format: date-time + type: string + completed_at: + type: + - string + - 'null' + format: date-time + description: UTC telephone-execution completion time, or `null` while execution is non-terminal. + required: + - id + - call_id + - object + - status + - call_outcome + - result_status + - transcript + - task + - phone + - region + - locale + - result + - error + - metadata + - created_at + - completed_at + type: object + description: Persisted one-shot snapshot sharing execution, call_outcome and result_status with Goal Run. Poll only while result_status is pending. Completed execution does not imply business success. Terminal webhooks are sent when result_status is no longer pending, including unavailable results. + TerminalWebhookEvent: + description: Terminal event for either a legacy v1 call task or an Agentic v2 call. Inspect data.object to distinguish the payload. + anyOf: + - $ref: '#/components/schemas/WebhookEvent' + - $ref: '#/components/schemas/AgenticWebhookEvent' + AgenticWebhookEvent: + type: object + required: + - id + - type + - created_at + - data + additionalProperties: false + properties: + id: + type: string + type: + type: string + enum: + - call.completed + - call.failed + - call.canceled + created_at: + type: string + format: date-time + data: + $ref: '#/components/schemas/AgenticCall' + description: v2 terminal webhook. data is identical to the terminal GET response. A valid call completion can carry a failed custom result. Retries reuse the same event id. GoalList: type: object description: |- @@ -988,9 +1399,8 @@ components: GoalRun: type: object description: |- - Public projection of one phone-specific execution of a published Goal. A non-null `result` - is a successfully parsed and persisted object. A non-null `error` means the Run will not - produce a result. When both are null, continue polling. + Public projection of one phone-specific execution of a published Goal. Execution, telephone + outcome and business result readiness are independent. Poll only while result_status is pending. additionalProperties: false required: - object @@ -1000,6 +1410,9 @@ components: - call_id - run_spec - status + - call_outcome + - result_status + - transcript - result - error - created_at @@ -1037,19 +1450,28 @@ components: $ref: "#/components/schemas/GoalRunSpecSnapshot" status: $ref: "#/components/schemas/GoalRunStatus" + call_outcome: + $ref: '#/components/schemas/CallOutcome' + result_status: + $ref: '#/components/schemas/BusinessResultStatus' + transcript: + type: array + items: + $ref: '#/components/schemas/CallTranscriptTurn' + description: Recorded conversation turns in order, independent of business result readiness or errors. Always present; empty before terminal execution or when no transcript is available. result: type: - object - "null" description: |- Parsed result validated against the published result schema and durably persisted, or - `null` while processing or when the Run has an error. Its keys vary by Goal. + `null` while pending, unavailable, not applicable, or on a technical error. Its keys vary by Goal. additionalProperties: $ref: "#/components/schemas/GoalScalar" error: description: |- - Unified execution or result-processing error, or `null`. Branch on `code`; keep `message` - for logs and operators. A non-null error is final and is mutually exclusive with `result`. + Technical execution or result-processing error, or null. Ordinary telephone outcomes, + cancellation and insufficient business evidence are represented by call_outcome/result_status. oneOf: - $ref: "#/components/schemas/GoalRunError" - type: "null" @@ -1082,18 +1504,26 @@ components: GoalRunStatus: type: string description: |- - Stable telephone execution state. `queued` and `in_progress` are non-terminal; `completed`, - `failed`, and `canceled` are terminal. A completed call can still have `result: null` and - `error: null` briefly while CALL-E parses and saves the result. + Stable execution state. No-answer, busy and declined calls complete execution normally. + Technical execution failures are failed; explicit cancellation is canceled. Completed execution + may still have result_status=pending while its business result is being processed. enum: - queued - in_progress - completed - failed - canceled + CallOutcome: + type: [ string, 'null' ] + enum: [ completed, no_answer, busy, declined, null ] + description: Reported telephone outcome, separate from business success and billing connection evidence. Null before a telephone outcome is known or when execution is canceled or fails technically. + BusinessResultStatus: + type: string + enum: [ pending, available, unavailable, not_applicable ] + description: Pending means keep polling. Available includes an empty result object. Unavailable means no schema-valid business result could be produced; inspect error for technical failures. Not applicable is used for cancellation and technical execution failure. GoalRunError: type: object - description: Unified safe error returned when a Goal Run cannot produce a usable result. + description: Technical execution or result-processing error. Ordinary call outcomes are not errors. additionalProperties: false required: - code @@ -1104,12 +1534,8 @@ components: type: string enum: - call_failed - - no_answer - - declined - timed_out - - canceled - result_invalid - - result_unavailable - result_failed message: type: string @@ -1135,7 +1561,7 @@ components: type: - array - "null" - description: Optional explicit recipients for this call task. Omit it when the task text already contains the phone targets CALL-E should use. + description: Optional explicit recipients for this call task. Omit it when the task text already contains the phone targets CALL-E should use. The default outbound line permits one phone number in total across all recipients. Multiple targets require an eligible purchased outbound number; this also applies to targets inferred from task text. minItems: 1 items: $ref: "#/components/schemas/CallTaskRecipientRequest" @@ -1163,8 +1589,6 @@ components: This is useful for batch calls where each recipient needs their own outcome, such as `can_attend`, `confirmed`, `requested_callback`, or `interest_level`. - Supported schema features are `type`, `properties`, `required`, `enum`, nested `object` fields, simple `array.items`, `description`, and `additionalProperties: false`. Unsupported features include `$ref`, `oneOf`, `anyOf`, `allOf`, recursive schemas, complex format validation, and `additionalProperties: true`. - Do not use reserved recipient response field names such as `summary`, `status`, `transcript`, `call_id`, or timing fields as custom result fields. Use names such as `customer_summary`, `notes`, or `reason` instead. Field `description` values are passed to the extraction model and should explain how enum values should be selected. Descriptions guide extraction but are not hard validation rules. Hard validation comes from `type`, `required`, `enum`, and `additionalProperties`. @@ -1322,15 +1746,12 @@ components: type: - string - "null" - description: Provider identifier for this call attempt. Use it to match Dashboard Call Records. Do not use it as call_id. + description: Provider call identifier for support correlation when available. failure_code: type: - string - "null" - description: |- - Diagnostic failure reason when this attempt failed. No published enum. - - Preserve raw values for support; do not branch retry, reporting, or analytics logic on specific values. See [Accepted call execution outcomes](https://docs.heycall-e.com/errors#accepted-call-execution-outcomes). + description: Machine-readable failure reason when this attempt failed. failure_message: type: - string @@ -1411,7 +1832,7 @@ components: properties: id: type: string - description: CALL-E call task identifier. Use it as call_id to fetch the call and list its events. Terminal webhooks return it as data.id; the webhook's top-level id identifies the event. + description: Public CALL-E call task identifier. Store this id to fetch state, list events, and correlate webhooks. object: type: string description: Always `call_task` for call task responses. @@ -1465,10 +1886,7 @@ components: type: - string - "null" - description: |- - Diagnostic failure reason when `status` is `failed`; otherwise `null`. No published enum. - - Preserve raw values for support; do not branch retry, reporting, or analytics logic on specific values. See [Accepted call execution outcomes](https://docs.heycall-e.com/errors#accepted-call-execution-outcomes). + description: Machine-readable failure reason when `status` is `failed`; otherwise `null`. failure_message: type: - string @@ -1582,8 +2000,7 @@ components: $ref: "#/components/schemas/WebhookCallData" WebhookCallData: description: Complete terminal call task state included in webhook events. This shape is the same stable `call_task` object returned by the calls API after post-call outcome and requested structured-result finalization. - allOf: - - $ref: "#/components/schemas/CallTask" + $ref: "#/components/schemas/CallTask" WebhookAcknowledgement: type: object description: Example acknowledgement response from your webhook receiver. CALL-E treats any 2xx response as delivered and ignores the response body. @@ -1611,10 +2028,16 @@ components: code: type: string enum: + - call_cannot_cancel - invalid_request + - input_incomplete - unauthorized - forbidden - rate_limit_exceeded + - account_concurrency_exceeded + - account_concurrency_unavailable + - llm_token_budget_exceeded + - llm_token_budget_unavailable - insufficient_balance - unsupported_region - unsupported_language diff --git a/playwright.config.ts b/playwright.config.ts index f25d3bb..c74ebb0 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,7 @@ import { defineConfig, devices } from "@playwright/test"; +const port = Number(process.env.DOCS_PREVIEW_PORT ?? 4174); + export default defineConfig({ testDir: "./tests", outputDir: "./artifacts/docs-site-test-results", @@ -8,7 +10,7 @@ export default defineConfig({ timeout: 10_000, }, use: { - baseURL: "http://localhost:4174", + baseURL: `http://localhost:${port}`, trace: "retain-on-failure", }, projects: [ @@ -18,8 +20,8 @@ export default defineConfig({ }, ], webServer: { - command: "pnpm run preview --port 4174", - url: "http://localhost:4174", + command: `pnpm run preview --port ${port}`, + url: `http://localhost:${port}`, env: { ZUDOKU_DISABLE_UPDATE_CHECK: "1", }, diff --git a/scripts/augment-llms.mjs b/scripts/augment-llms.mjs index c58ec16..adbc8b3 100644 --- a/scripts/augment-llms.mjs +++ b/scripts/augment-llms.mjs @@ -1,12 +1,13 @@ import { readFile, writeFile } from "node:fs/promises"; -import { resolve } from "node:path"; +import { join, resolve } from "node:path"; -const llmsPath = resolve("dist/llms.txt"); +const basePath = process.env.ZUDOKU_PUBLIC_BASE_PATH ?? ""; +const llmsPath = resolve(join("dist", basePath, "llms.txt")); const llms = await readFile(llmsPath, "utf8"); const apiSection = `## API Reference -- [API Reference](/api-reference): Browse the read-only CALL-E Developer API reference. -- [OpenAPI Specification](/openapi/calle.openapi.yaml): Read the authoritative OpenAPI 3.1 contract for tools and code generation. +- [API Reference](${basePath}/api-reference): Browse the read-only CALL-E Developer API reference. +- [OpenAPI Specification](${basePath}/openapi/calle.openapi.yaml): Read the authoritative OpenAPI 3.1 contract for tools and code generation. `; if ( diff --git a/scripts/verify-dist.mjs b/scripts/verify-dist.mjs index c85a739..ca18ab1 100644 --- a/scripts/verify-dist.mjs +++ b/scripts/verify-dist.mjs @@ -5,9 +5,16 @@ import { fileURLToPath } from "node:url"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const docsRoot = resolve(scriptDir, ".."); const distRoot = resolve(docsRoot, "dist"); +const docsOrigin = (process.env.ZUDOKU_PUBLIC_DOCS_ORIGIN ?? "https://docs.heycall-e.com").replace(/\/$/, ""); const guides = [ { slug: "quickstart", title: "Quickstart" }, + { slug: "migration", title: "Migration guide" }, + { slug: "retirement", title: "Legacy API retirement" }, + { slug: "legacy-quickstart", title: "Legacy Quickstart" }, + { slug: "legacy-calls", title: "Legacy Calls" }, + { slug: "legacy-webhooks", title: "Legacy Webhooks" }, + { slug: "legacy-sdks", title: "Legacy SDKs" }, { slug: "authentication", title: "Authentication" }, { slug: "calls", title: "Calls" }, { slug: "regions", title: "Regions & languages" }, @@ -52,6 +59,10 @@ const apiCallsHtml = readRequired( "api-reference/calls.html", "Calls API Reference", ); +const apiLegacyCallsHtml = readRequired( + "api-reference/legacy-calls.html", + "Legacy Calls API Reference", +); const apiGoalsHtml = readRequired( "api-reference/goals.html", "Goals API Reference", @@ -114,7 +125,7 @@ for (const guide of guides) { if (!llmsFull.includes(`# ${guide.title}`)) { throw new Error(`llms-full.txt does not contain ${guide.title}.`); } - if (!sitemap.includes(`https://docs.heycall-e.com/${guide.slug}`)) { + if (!sitemap.includes(`${docsOrigin}/${guide.slug}`)) { throw new Error(`sitemap.xml does not contain /${guide.slug}.`); } } @@ -148,7 +159,9 @@ if ( if ( !apiCallsHtml.includes("Create Call") || - !apiCallsHtml.includes("List Call Events") + !apiCallsHtml.includes("List Events") || + !apiCallsHtml.includes("Cancel Call") || + !apiLegacyCallsHtml.includes("deprecated") ) { throw new Error("Calls API Reference is missing prerendered operations."); } diff --git a/src/styles.css b/src/styles.css index 298de3a..86cefed 100644 --- a/src/styles.css +++ b/src/styles.css @@ -167,7 +167,7 @@ header[data-pagefind-ignore="all"] nav[aria-label="Main"] a { header[data-pagefind-ignore="all"] nav[aria-label="Main"] - a[href="https://dashboard.heycall-e.com/"] { + a[href="https://test-dashboard.heycall-e.com/"] { height: 2.25rem; padding-inline: 1rem; border-radius: 999px; @@ -178,7 +178,7 @@ header[data-pagefind-ignore="all"] header[data-pagefind-ignore="all"] nav[aria-label="Main"] - a[href="https://dashboard.heycall-e.com/"]:hover { + a[href="https://test-dashboard.heycall-e.com/"]:hover { background: var(--call-action-hover); color: var(--call-action-foreground); } @@ -209,14 +209,14 @@ nav[class*="overflow-y-auto"][class*="shrink-0"] a[aria-current="page"] { nav[class*="overflow-y-auto"][class*="shrink-0"] :is( - a[href="/quickstart"], - a[href="/authentication"], - a[href="/calls"], - a[href="/regions"], - a[href="/goal-runs"], - a[href="/webhooks"], - a[href="/errors"], - a[href="/sdks"] + a[href$="/quickstart"], + a[href$="/authentication"], + a[href$="/calls"], + a[href$="/regions"], + a[href$="/goal-runs"], + a[href$="/webhooks"], + a[href$="/errors"], + a[href$="/sdks"] ) { display: grid; min-height: 3.5rem; @@ -233,14 +233,14 @@ nav[class*="overflow-y-auto"][class*="shrink-0"] a[aria-current="page"] { nav[class*="overflow-y-auto"][class*="shrink-0"] :is( - a[href="/quickstart"], - a[href="/authentication"], - a[href="/calls"], - a[href="/regions"], - a[href="/goal-runs"], - a[href="/webhooks"], - a[href="/errors"], - a[href="/sdks"] + a[href$="/quickstart"], + a[href$="/authentication"], + a[href$="/calls"], + a[href$="/regions"], + a[href$="/goal-runs"], + a[href$="/webhooks"], + a[href$="/errors"], + a[href$="/sdks"] ) > span { width: 100%; @@ -250,14 +250,14 @@ nav[class*="overflow-y-auto"][class*="shrink-0"] a[aria-current="page"] { nav[class*="overflow-y-auto"][class*="shrink-0"] :is( - a[href="/quickstart"], - a[href="/authentication"], - a[href="/calls"], - a[href="/regions"], - a[href="/goal-runs"], - a[href="/webhooks"], - a[href="/errors"], - a[href="/sdks"] + a[href$="/quickstart"], + a[href$="/authentication"], + a[href$="/calls"], + a[href$="/regions"], + a[href$="/goal-runs"], + a[href$="/webhooks"], + a[href$="/errors"], + a[href$="/sdks"] )::after { color: var(--muted-foreground); font-size: 0.75rem; @@ -267,14 +267,14 @@ nav[class*="overflow-y-auto"][class*="shrink-0"] a[aria-current="page"] { nav[class*="overflow-y-auto"][class*="shrink-0"] :is( - a[href="/quickstart"], - a[href="/authentication"], - a[href="/calls"], - a[href="/regions"], - a[href="/goal-runs"], - a[href="/webhooks"], - a[href="/errors"], - a[href="/sdks"] + a[href$="/quickstart"], + a[href$="/authentication"], + a[href$="/calls"], + a[href$="/regions"], + a[href$="/goal-runs"], + a[href$="/webhooks"], + a[href$="/errors"], + a[href$="/sdks"] )[aria-current="page"] { border-color: color-mix( in srgb, @@ -284,37 +284,37 @@ nav[class*="overflow-y-auto"][class*="shrink-0"] a[aria-current="page"] { } nav[class*="overflow-y-auto"][class*="shrink-0"] - a[href="/quickstart"]::after { + a[href$="/quickstart"]::after { content: "Create your first call."; } nav[class*="overflow-y-auto"][class*="shrink-0"] - a[href="/authentication"]::after { + a[href$="/authentication"]::after { content: "Secure API key setup."; } - nav[class*="overflow-y-auto"][class*="shrink-0"] a[href="/calls"]::after { + nav[class*="overflow-y-auto"][class*="shrink-0"] a[href$="/calls"]::after { content: "Create, track, and read calls."; } - nav[class*="overflow-y-auto"][class*="shrink-0"] a[href="/regions"]::after { + nav[class*="overflow-y-auto"][class*="shrink-0"] a[href$="/regions"]::after { content: "Supported destinations and languages."; } nav[class*="overflow-y-auto"][class*="shrink-0"] - a[href="/goal-runs"]::after { + a[href$="/goal-runs"]::after { content: "Run published phone workflows."; } - nav[class*="overflow-y-auto"][class*="shrink-0"] a[href="/webhooks"]::after { + nav[class*="overflow-y-auto"][class*="shrink-0"] a[href$="/webhooks"]::after { content: "Receive terminal call events."; } - nav[class*="overflow-y-auto"][class*="shrink-0"] a[href="/errors"]::after { + nav[class*="overflow-y-auto"][class*="shrink-0"] a[href$="/errors"]::after { content: "Recover from stable API errors."; } - nav[class*="overflow-y-auto"][class*="shrink-0"] a[href="/sdks"]::after { + nav[class*="overflow-y-auto"][class*="shrink-0"] a[href$="/sdks"]::after { content: "TypeScript and Python packages."; } } @@ -388,6 +388,11 @@ nav[class*="overflow-y-auto"][class*="shrink-0"] a[aria-current="page"] { color: var(--foreground); } +.comparison-table { + max-width: 100%; + overflow-x: auto; +} + .typography pre > .code-block-wrapper { overflow: hidden; border-color: var(--code-border); @@ -525,6 +530,12 @@ aside[class*="overflow-y-auto"] a:hover { .typography .code-block { font-size: 0.75rem; } + + /* Match OpenAPI operation margins to the mobile content gutter. */ + main > div > .px-6.-mx-6 { + margin-inline: -1rem; + padding-inline: 1rem; + } } @media (max-width: 22.5rem) { diff --git a/tests/docs-site.spec.ts b/tests/docs-site.spec.ts index cbf2322..4b23923 100644 --- a/tests/docs-site.spec.ts +++ b/tests/docs-site.spec.ts @@ -3,6 +3,12 @@ import { extractRegions, REGIONS_README_URL } from "../src/regions.mjs"; const docsPages = [ { path: "/quickstart", heading: "Quickstart" }, + { path: "/migration", heading: "Migration guide" }, + { path: "/retirement", heading: "Legacy API retirement" }, + { path: "/legacy-quickstart", heading: "Legacy Quickstart" }, + { path: "/legacy-calls", heading: "Legacy Calls" }, + { path: "/legacy-webhooks", heading: "Legacy Webhooks" }, + { path: "/legacy-sdks", heading: "Legacy SDKs" }, { path: "/authentication", heading: "Authentication" }, { path: "/calls", heading: "Calls" }, { path: "/regions", heading: "Regions & languages" }, @@ -14,7 +20,7 @@ const docsPages = [ ]; const guideNavigationItems = docsPages.filter( - ({ path }) => path !== "/changelog", + ({ path }) => ["/quickstart", "/authentication", "/calls", "/regions", "/goal-runs", "/webhooks", "/errors", "/sdks"].includes(path), ); test("serves prerendered guides on clean URLs", async ({ page, request }) => { @@ -34,14 +40,14 @@ test("serves prerendered guides on clean URLs", async ({ page, request }) => { await expect( page.getByRole("link", { name: "API Reference", exact: true }).first(), ).toHaveAttribute("href", "/api-reference"); - const coverageLink = page.getByRole("link", { name: /^Regions & languages/ }); + const coverageLink = page.locator("main").getByRole("link", { name: "Regions & languages", exact: true }); await expect(coverageLink).toBeVisible(); await expect(coverageLink).toHaveAttribute( "href", "/regions", ); await expect( - page.locator("pre").filter({ hasText: "pnpm add @call-e/calle" }).first(), + page.locator("pre").filter({ hasText: 'CALLE_BASE_URL="https://test-api.heycall-e.com"' }).first(), ).toBeVisible(); await expect(page.locator("code.shiki.not-inline").first()).toHaveCSS( "background-color", @@ -305,7 +311,7 @@ test("publishes non-empty Markdown and LLM discovery files", async ({ expect(markdown.status()).toBe(200); expect(markdown.headers()["content-type"]).toContain("text/markdown"); expect(await markdown.text()).toMatch( - /^# Quickstart[\s\S]+pnpm add @call-e\/calle/, + /^# Quickstart[\s\S]+https:\/\/test-api\.heycall-e\.com/, ); const llms = await request.get("/llms.txt"); @@ -437,7 +443,7 @@ test("preserves CALL-E brand and favicon metadata", async ({ page }) => { .toBeVisible(); await expect( page.getByRole("link", { name: "Dashboard", exact: true }), - ).toHaveAttribute("href", "https://dashboard.heycall-e.com/"); + ).toHaveAttribute("href", "https://test-dashboard.heycall-e.com/"); await expect(page.locator('link[rel="icon"]')).toHaveAttribute( "href", "/favicon.svg", @@ -505,11 +511,13 @@ test("keeps quickstart requests minimal and safe to copy", async ({ page }) => { const minimumRequest = page .locator("pre") - .filter({ hasText: /"task":\s*"[^"]*[^"]*"/ }) + .filter({ hasText: '"phone": ""' }) .first(); await expect(minimumRequest).toBeVisible(); await expect(minimumRequest).not.toContainText('"recipient"'); await expect(minimumRequest).not.toContainText('"recipients"'); + await expect(minimumRequest).toContainText('"result_schema"'); + await expect(minimumRequest).toContainText("Idempotency-Key:"); await expect(page.getByText("+14155550100")).toHaveCount(0); await expect(page.getByText("+8613800000000")).toHaveCount(0); }); @@ -527,7 +535,7 @@ test("preserves authentication, webhook, and SDK guidance", async ({ page.getByRole("link", { name: "Webhooks", exact: true }).first(), ).toHaveAttribute("href", "/webhooks"); - await page.goto("/webhooks"); + await page.goto("/legacy-webhooks"); await expect( page.locator("pre").filter({ hasText: /"recipients"[\s\S]*"attempts"[\s\S]*"provider_call_id"/, @@ -539,7 +547,7 @@ test("preserves authentication, webhook, and SDK guidance", async ({ ).toBeVisible(); await expect(page.getByText(/cryptographic proof of the sender/)).toBeVisible(); - await page.goto("/sdks"); + await page.goto("/legacy-sdks"); await expect( page.getByRole("link", { name: "CALLE-AI/server-sdk-typescript" }), ).toHaveAttribute( @@ -553,20 +561,20 @@ test("preserves authentication, webhook, and SDK guidance", async ({ ).toHaveAttribute("href", "https://github.com/CALLE-AI/server-sdk-python"); }); -test("connects the Calls guide to HTTP and related references", async ({ +test("connects the legacy Calls guide to HTTP and related references", async ({ page, }) => { - await page.goto("/calls"); + await page.goto("/legacy-calls"); const callsBody = page.locator('[data-pagefind-body="true"]'); await expect( - callsBody.locator('p a[href="/api-reference/calls"]'), + callsBody.locator('p a[href="/api-reference/legacy-calls"]'), ).toBeVisible(); await expect( callsBody.locator('p a[href="/errors"]'), ).toBeVisible(); await expect( - callsBody.locator('p a[href="/webhooks"]'), + callsBody.locator('p a[href="/legacy-webhooks"]'), ).toBeVisible(); await expect( @@ -629,7 +637,7 @@ test("connects the Calls guide to HTTP and related references", async ({ page.getByRole("heading", { name: "Accepted call execution outcomes" }), ).toBeVisible(); const callsOutcomeWarning = page.locator("main p").filter({ - hasText: "do not define Calls API", + hasText: "do not define legacy call-task", }); await expect(callsOutcomeWarning).toContainText( "keep the business outcome unresolved", @@ -642,17 +650,17 @@ test("connects the Calls guide to HTTP and related references", async ({ test("links result examples to task completion and endpoint classification", async ({ page, }) => { - for (const route of ["/quickstart", "/webhooks"]) { + for (const route of ["/legacy-quickstart", "/legacy-webhooks"]) { await page.goto(route); - await page.locator('p a[href="/calls#task-completion"]').click(); - await expect(page).toHaveURL(/\/calls#task-completion$/); + await page.locator('p a[href="/legacy-calls#task-completion"]').click(); + await expect(page).toHaveURL(/\/legacy-calls#task-completion$/); await expect( page.getByRole("heading", { name: "Task completion" }), ).toBeVisible(); } await page.getByRole("link", { name: "custom answered_by example" }).click(); - await expect(page).toHaveURL(/\/calls#classify-the-final-endpoint$/); + await expect(page).toHaveURL(/\/legacy-calls#classify-the-final-endpoint$/); await expect( page.getByRole("heading", { name: "Classify the final endpoint" }), ).toBeVisible(); @@ -705,12 +713,23 @@ test("renders a read-only OpenAPI reference", async ({ page }) => { page.locator("h2#create-call"), ).toBeVisible(); await expect( - page.locator("h2#list-call-events"), + page.locator("h2#list-events"), ).toBeVisible(); await expect(page.getByText("POST").first()).toBeVisible(); await expect(page.getByText("Try it", { exact: true })).toHaveCount(0); await expect(page.getByText("Send Request", { exact: true })).toHaveCount(0); + await expect(page.locator("h2#get-call")).toBeVisible(); + await expect(page.locator("h2#cancel-call")).toBeVisible(); + await expect(page.locator("main").getByText("deprecated", { exact: true })).toHaveCount(0); + + await page.goto("/api-reference/legacy-calls"); + await expect(page.locator("main").getByText("deprecated", { exact: true })).toHaveCount(3); + await expect(page.locator('nav[class*="overflow-y-auto"]').getByText("Legacy Calls (Deprecated)", { exact: true })).toBeVisible(); + const retirementLink = page.locator("header").getByRole("link", { name: "View retirement details" }); + await retirementLink.click(); + await expect(page).toHaveURL(/\/retirement$/); + await page.goto("/api-reference/webhooks"); await expect( page.locator("h2#server-message"), @@ -757,3 +776,29 @@ test("keeps the guide usable on a narrow screen", async ({ page }) => { }); await expect(page.getByTestId("scroll-to-top")).toHaveCSS("display", "none"); }); + +test("keeps retry tables and API operations within the mobile viewport", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + + for (const [path, name] of [ + ["/calls#idempotency", "Call retry decisions"], + ["/errors#choose-the-next-action", "Error recovery decisions"], + ]) { + await page.goto(path); + const region = page.getByRole("region", { name }); + await region.scrollIntoViewIfNeeded(); + await expect(region).toBeVisible(); + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390); + + const hasOverflow = await region.evaluate((element) => element.scrollWidth > element.clientWidth); + if (hasOverflow) { + await region.focus(); + await page.keyboard.press("ArrowRight"); + await expect.poll(() => region.evaluate((element) => element.scrollLeft)).toBeGreaterThan(0); + } + } + + await page.goto("/api-reference/calls"); + await expect(page.locator("h2#create-call")).toBeVisible(); + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390); +}); diff --git a/zudoku.config.tsx b/zudoku.config.tsx index 0b547f9..3f279fa 100644 --- a/zudoku.config.tsx +++ b/zudoku.config.tsx @@ -3,9 +3,13 @@ import { ScrollToTop } from "./src/components/ScrollToTop.js"; import { ThemeMenu } from "./src/components/ThemeMenu.js"; import "./src/styles.css"; +const basePath = process.env.ZUDOKU_PUBLIC_BASE_PATH ?? ""; +const docsOrigin = process.env.ZUDOKU_PUBLIC_DOCS_ORIGIN ?? "https://docs.heycall-e.com"; +const docsUrl = (path: string) => `${basePath}${path}`; + const legacyHashRedirect = ` (() => { - if (window.location.pathname !== "/") return; + if (window.location.pathname !== ${JSON.stringify(`${basePath}/`)}) return; const [rawRoute, query = ""] = window.location.hash.slice(1).split("?", 2); const route = rawRoute.startsWith("/") || rawRoute === "" ? rawRoute : \`/\${rawRoute}\`; @@ -24,7 +28,7 @@ const legacyHashRedirect = ` if (guideRoutes.has(route)) { const section = new URLSearchParams(query).get("section"); window.location.replace( - section ? \`\${route}#\${encodeURIComponent(section)}\` : route, + ${JSON.stringify(basePath)} + (section ? \`\${route}#\${encodeURIComponent(section)}\` : route), ); return; } @@ -36,19 +40,20 @@ const legacyHashRedirect = ` route.startsWith("/description/") || route === "/models" ) { - window.location.replace("/api-reference"); + window.location.replace(${JSON.stringify(docsUrl("/api-reference"))}); return; } if (rawRoute === "") { - window.location.replace("/quickstart"); + window.location.replace(${JSON.stringify(docsUrl("/quickstart"))}); } })(); `; const config = { port: 5174, - canonicalUrlOrigin: "https://docs.heycall-e.com", + basePath, + canonicalUrlOrigin: docsOrigin, metadata: { title: "%s | CALL-E Developer Docs", defaultTitle: "CALL-E Developer Docs", @@ -58,6 +63,18 @@ const config = { applicationName: "CALL-E Developer Docs", }, site: { + banner: { + message: ( + <> + v1 Calls will retire on December 31, 2026.{" "} + + View retirement details + + + ), + color: "info", + dismissible: false, + }, logo: { src: { light: "/call-e-logo.svg", @@ -82,7 +99,7 @@ const config = { }, { label: "Dashboard", - to: "https://dashboard.heycall-e.com/", + to: "https://test-dashboard.heycall-e.com/", target: "_blank", }, ], @@ -135,8 +152,8 @@ const config = { contract.

), @@ -154,6 +171,27 @@ const config = { { type: "doc", file: "errors", label: "Errors" }, { type: "doc", file: "sdks", label: "SDKs" }, { type: "doc", file: "regions", label: "Regions & languages" }, + { + type: "category", + label: "Existing integrations", + collapsible: false, + items: [ + { type: "doc", file: "migration", label: "Migration guide" }, + { type: "doc", file: "retirement", label: "Retirement notice" }, + ], + }, + { + type: "category", + label: "Legacy API", + collapsible: true, + collapsed: true, + items: [ + { type: "doc", file: "legacy-quickstart", label: "Legacy Quickstart" }, + { type: "doc", file: "legacy-calls", label: "Legacy Calls" }, + { type: "doc", file: "legacy-webhooks", label: "Legacy Webhooks" }, + { type: "doc", file: "legacy-sdks", label: "Legacy SDKs" }, + ], + }, ], }, { @@ -200,7 +238,7 @@ const config = { }, }, sitemap: { - siteUrl: "https://docs.heycall-e.com", + siteUrl: docsOrigin, }, enableStatusPages: true, plugins: [