diff --git a/.github/screenshots/calls-sdk-release-desktop.png b/.github/screenshots/calls-sdk-release-desktop.png
new file mode 100644
index 0000000..2b16ce5
Binary files /dev/null and b/.github/screenshots/calls-sdk-release-desktop.png differ
diff --git a/.github/screenshots/calls-sdk-release-mobile.png b/.github/screenshots/calls-sdk-release-mobile.png
new file mode 100644
index 0000000..6027c27
Binary files /dev/null and b/.github/screenshots/calls-sdk-release-mobile.png differ
diff --git a/content/guides/calls.mdx b/content/guides/calls.mdx
index de00205..555d504 100644
--- a/content/guides/calls.mdx
+++ b/content/guides/calls.mdx
@@ -1,692 +1,313 @@
---
title: Calls
-description: Understand the call creation contract and event flow.
+description: Single-target calls, immutable requests, result readiness, and execution boundaries.
---
-Examples on this page target the [Developer API 0.7.0 contract](/openapi/calle.openapi.yaml)
-and server SDKs `@call-e/calle@0.7.0` and `calle-ai==0.7.0` where used.
-See [API versions and migration](/changelog#api-versions-and-migration) before
-adapting an older example.
+Create one phone task and receive a structured result. Start with the
+[Quickstart](/quickstart) for a production example.
-Use call tasks to turn a structured workflow step into one or more real phone interactions.
+Check [Regions & languages](/regions) for supported destinations and languages.
-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.
+## Availability
-## Call inputs
+Calls are available on the production API and in SDK 1.0. See the
+[migration guide](/migration) when upgrading an existing call-task integration.
+Calls and Goal Runs include `call_outcome`, `result_status` and `transcript`.
+Wait while `result_status` is `pending`, not until result or error becomes non-null.
-`task` is the natural-language instruction for the call task. Keep it specific and outcome-oriented.
+## Call ID and Billing
-`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 [outbound-number requirement for batch calls](#batch-calls-and-account-limits) before using multiple targets.
+The response adds a fixed `call_id` field for matching a telephone call to the
+**Call ID** shown in Billing.
-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.
+```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).
-
-```ts title="TypeScript"
-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 title="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
-
-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`.
+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.
-A non-null result confirms that an object was returned, not that the recipient
-answered or supplied useful evidence. A required string can be empty, and an
-`unknown` enum value can satisfy the schema. Check the business answer and its
-evidence against the transcript before treating the result as success.
+## Result schema
-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.
+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.
-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.
+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.
-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`.
+### Result outcomes
-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,
-}
-```
-
-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,
+```json
+{
+ "status": "completed",
+ "call_outcome": "busy",
+ "result_status": "unavailable",
+ "transcript": [],
+ "result": null,
+ "error": null
}
```
-### 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,
-}
-```
+`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.
-### 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,
-}
-```
+## Transcript
-### 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.
-
-## Batch calls and account limits
-
-Shared platform outbound lines support **one phone number per task**, counted
-across all recipients, including targets inferred from `task`. To call multiple
-numbers, select an eligible purchased number as the account's default outbound
-number. Otherwise, creation returns `422 call_not_ready`.
-
-Account task concurrency defaults to 1 on shared platform lines and 10 on
-eligible purchased numbers. Configured account limits take precedence; selecting
-a purchased number as the default does not turn it into a shared platform line.
-Account concurrency and LLM usage checks can reject creation with `429` or `503`;
-see [account-control recovery](/errors#account-controls).
+`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.
-
-### Recover after a restart or lost response
+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.
-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.
+
-| Situation | Recovery action |
+| Situation | Response / next action |
| --- | --- |
-| Call ID saved | 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. |
-| Confirmed creation rejection; cause resolved | Save the request with a new key before submitting an intentionally new attempt. Keep that request and key unchanged for subsequent retries. |
-| Acceptance uncertain; original request and key saved, but no Call ID | Repeat the create request with the same key and unchanged body. If the original request was accepted, save the returned Call 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.
-
-If the original creation is still in progress, an unchanged retry can return
-`409 idempotency_conflict` with `details.reason_code` set to
-`creation_in_progress`. Back off and retry with the **same key and unchanged
-body**; do not submit a replacement operation.
-
-A persisted creation failure replays its original HTTP status and error body.
-An uncertain response alone is not evidence of a rejection.
-
-Keep local workflow identifiers in `metadata` for correlation; they do not
-replace the `Idempotency-Key` header.
-
-### Correct a missing-information rejection
-
-When `POST /v1/calls` returns HTTP `422` with `call_not_ready` and asks for
-missing task information, such as the company or sender's name, review the
-message and `details.questions`. Once you have confirmed this is a creation
-rejection for missing information, correct the task and use a **new**
-idempotency key for that corrected request.
-
-The original key remains bound to the rejected request. Sending its unchanged
-body replays the rejection; changing the body while keeping that key returns
-`409 idempotency_conflict`. Not receiving a Call ID does not mean the server
-created no internal record.
-
-`call_not_ready` alone is not enough to choose this correction path. Check
-which operation failed and what its error details say. This example does not
-handle an accepted call's failure or other reasons a plan was rejected.
-
-#### Run the correction example
-
-The [recovery helper](https://github.com/CALLE-AI/calle-docs/blob/main/examples/recover_create.py)
-uses Python 3.11+ and `calle-ai==0.7.0`, with the same environment setup as the
-[complete Python example](/quickstart#run-a-complete-example). It reads that
-example's private run-directory format: `request.json` contains the saved
-SDK create arguments, including `idempotency_key`; `error.json` contains
-`status_code`, `code`, `message`, and `details`. Keep the original files.
-
-Read `error.json` privately and put the full corrected task in a UTF-8 file,
-`corrected-task.txt`. Supply the missing facts yourself; do not invent them.
-Then prepare the correction:
-
-```bash
-python examples/recover_create.py correct ../rejected-run \
- --task-file corrected-task.txt --confirm-missing-information
-```
-
-This sends no request. It requires the saved `422 call_not_ready` error and
-your confirmation that it was a missing-information creation rejection, and
-refuses a directory with a saved Call ID. It saves a new key and corrected
-task in `../rejected-run/corrected/request.json`, preserving the other request
-fields. Running `correct` again refuses to overwrite that directory or
-generate another saved key.
-
-Review the corrected request, keep the same API key and `CALLE_BASE_URL`, and
-submit it explicitly. This can place a real, billed call to the saved recipient:
-
-```bash
-python examples/recover_create.py submit ../rejected-run/corrected
-```
-
-The helper saves the Call ID, retrieves the result, and makes no automatic
-create retry. If interrupted, rerun the same command with the same directory:
-with a saved ID it only retrieves that call; without one it resubmits the
-unchanged saved request and key. Do not edit or delete the saved files to retry.
-An exit code of zero means a terminal result was retrieved, not that the
-business task succeeded. Stopping the script does not cancel an accepted call.
-
-## 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.
-
-### Read a saved result
-
-The [Python result reader](https://github.com/CALLE-AI/calle-docs/blob/main/examples/read_results.py)
-displays task-level and recipient-level results separately, followed by the
-transcript labeled by recipient and attempt. Run it with Python 3.11+ on the
-`result.json` saved by either [complete Calls example](/quickstart#run-a-complete-example):
-
-```bash
-python examples/read_results.py ../calle-run/result.json
-```
-
-This command makes no API requests and leaves the file unchanged. Its output
-may contain private results and transcript text; keep it private. A zero exit
-code means the file was read, not that the call or business task succeeded.
-
-`result_schema` supplies the task-level `structured_result`;
-`recipient_result_schema` supplies each `recipients[].structured_result`, even
-for a single recipient. A null task-level result does not imply that recipient
-results or transcripts are missing. The reader preserves `null`, `unknown`,
-`false`, and zero rather than treating them as success or substituting one
-recipient's result for the whole task. Attempt transcript counts show when an
-attempt has no transcript turns.
-
-## 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).
-
-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.
-
-Saved transcripts are not automatically deleted. An empty `transcript_turns`
-array means no transcript is available for that attempt; it is not an
-expiration marker.
-
-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.
-
-**Audio recordings:** Saved recordings remain available in Dashboard call
-details, where you can download them. The Calls API does not return audio
-recordings, playback or download URLs, or recording availability and expiration
-fields. Recording retrieval through MCP and the SDKs is tracked separately in
-[#753](https://github.com/CALLE-AI/awesome-phone-call-agents/issues/753).
+`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
-```ts title="TypeScript"
-const completed = await client.calls.waitForResult(call.id, {
- timeoutMs: 120_000,
- intervalMs: 2_000,
-});
+- 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.
+- Billing correlation is available through `call_id`; detailed provider attempt records are not exposed.
+- 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.
-const events = await client.calls.listEvents(call.id, { limit: 50 });
-```
+## Failures and uncertain submissions
-```python title="Python"
-completed = client.calls.wait_for_result(
- call["id"],
- timeout_seconds=120,
- interval_seconds=2,
-)
+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`.
-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 a6cce19..ea31cc3 100644
--- a/content/guides/changelog.mdx
+++ b/content/guides/changelog.mdx
@@ -3,31 +3,52 @@ title: What's New
description: Track CALL-E Developer API and SDK product updates.
---
+## September 23, 2026 - Calls and SDK 1.0
+
+Single-target Calls are available through the production API and the TypeScript
+and Python 1.0.0 SDKs. Start with [Quickstart](/quickstart) or [SDKs](/sdks).
+
+- Send one phone and task with a required result schema and idempotency key.
+- Region and language hints are optional; ambiguous or conflicting input returns
+ actionable validation errors instead of silently changing the target.
+- Read `call_outcome`, `result_status` and a fixed `transcript` array on Calls
+ and Goal Runs. A final unavailable result may have both result and error null.
+- Use `call_id` to match the telephone call to Billing; keep `id` for API requests.
+- Read detailed lifecycle, ASR and speech events, or receive a finalized webhook.
+- Calls execute immediately. Cancellation is available only before submission.
+
+**Upgrade note:** SDK 1.0 changes the Calls input and response shapes. Old SDK
+Goal Run wait helpers can time out on final unavailable results. Upgrade those
+helpers or poll REST using `result_status`. See [Migration](/migration).
+Legacy call tasks remain available during migration and are planned for retirement
+at the end of 2026. See [Retirement](/retirement).
+
+
## API versions and migration
-The current [OpenAPI snapshot](/openapi/calle.openapi.yaml) identifies the
-Developer API contract as `info.version: 0.7.0`. This is distinct from:
+The current [OpenAPI snapshot](/openapi/calle.openapi.yaml) describes the released
+Calls and Goal Runs contracts. Its `info.version` is distinct from:
-- `/v1`, the URL prefix used by both Calls and Goal Runs;
+- the URL prefixes used by Calls, Goal Runs and legacy call tasks;
- `openapi: 3.1.0`, the format of the schema document;
- the installed SDK package version;
- `run_spec.version`, the published workflow version pinned by a Goal Run.
-These guides target contract 0.7.0 and server SDKs
-`@call-e/calle@0.7.0` / `calle-ai==0.7.0`. Updating an SDK does not select a
-historical server contract. Keep the contract snapshot and package lockfile
+Current guides target SDK 1.0.0; [legacy examples](/legacy-sdks) retain SDK 0.7.x.
+Updating an SDK does not select a historical server contract.
+Keep the contract snapshot and package lockfile
used to validate your integration; compare request and response schemas when
upgrading. The linked historical snapshots below are references, not endpoints
for executing old versions.
-### Calls: correct an older recipient example
+### Legacy Calls: correct an older recipient example
An example using a singular `recipient` object should be corrected to
`recipients[].phones[]`. This is an example correction, not a documented API
version transition: the [published 0.2.0 contract](https://github.com/CALLE-AI/calle-docs/blob/29084a5/openapi/calle.openapi.yaml)
already used `recipients`. Do not infer that the singular form was supported.
-The explicit-recipient part of a current request is:
+The explicit-recipient part of a legacy call-task request is:
```json
{
@@ -37,11 +58,11 @@ The explicit-recipient part of a current request is:
This is a field excerpt, not a complete create request: `task` is required.
A recipient's `region` and `locale` are optional; check supported destinations
-before supplying them. Use the [complete Python or Ruby example](/quickstart#run-a-complete-example)
+before supplying them. Use the [complete Python or Ruby example](/legacy-quickstart#run-a-complete-example)
for creation, durable request storage, and result retrieval. The Python command
pins SDK 0.7.0; Ruby uses HTTP directly without an SDK.
-REST and Python use `result_schema` and return `structured_result`. TypeScript
+Legacy REST and Python use `result_schema` and return `structured_result`. TypeScript
uses `resultSchema` and returns `structuredResult`. These are SDK naming
conventions, not different server API versions. Keep property names inside
user-defined schemas unchanged.
@@ -51,7 +72,7 @@ user-defined schemas unchanged.
The July 22 release replaced the preview target wrapper and per-Run voice
settings with a top-level `phone` and `variables`. The
[published 0.6.0 contract](https://github.com/CALLE-AI/calle-docs/blob/adb5106/openapi/calle.openapi.yaml)
-and current 0.7.0 contract use this shape:
+and the current Goal Runs contract use this shape:
```json
{
@@ -95,17 +116,17 @@ when `result` or `error` is non-null. The create request remains `phone` plus
## September 22, 2026
-**Saved call results:** Added a [Python reader](/calls#read-a-saved-result) that
+**Saved call results:** Added a [Python reader](/legacy-calls#read-a-saved-result) that
displays task and recipient results separately alongside the call transcript.
-**Ruby Calls example:** Added a [standard-library HTTP quickstart](/quickstart#run-a-complete-example)
+**Ruby Calls example:** Added a [standard-library HTTP quickstart](/legacy-quickstart#run-a-complete-example)
that saves the request and Call ID, retrieves the result, and resumes without
placing another call.
**Calls API documentation:** Clarified the purchased-number requirement for batch
calls, account-control errors and recovery, and how to retry an idempotent
request while its original creation is still in progress.
-Also clarified [transcript retention and recording access](/calls#read-transcript-turns):
+Also clarified [transcript retention and recording access](/legacy-calls#read-transcript-turns):
saved transcripts are not automatically deleted, and recordings are available
through Dashboard call details rather than the Calls API.
diff --git a/content/guides/errors.mdx b/content/guides/errors.mdx
index 7583832..472a23a 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`
@@ -59,34 +60,50 @@ 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`. |
+| Calls and Goal Runs | Poll while `result_status` is `pending`. `call_outcome` separates ordinary telephone outcomes from technical errors; an unavailable result may have both result and error null. |
+| Legacy call tasks | Lifecycle `status` is stable. `failure_code` is a nullable string without a published enum; `failure_message` is nullable human-readable context. |
-Goal Run error codes do not define Calls API `failure_code` values. Preserve
-the raw failure code and message for support, but do not use undocumented
-strings to decide retries, reporting, or analytics. A generic `failed`
-status does not establish no answer or decline; keep the business outcome
-unresolved when the documented fields do not distinguish it.
+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 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
+a generic `failed` state or an undocumented failure message, and do not
+automatically retry based only on an undocumented failure string.
## Recovery guidance
### 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 call_not_ready` from `POST /v1/calls`, with a request for missing task information | Review the missing information, then [save a corrected request with a new key](/calls#correct-a-missing-information-rejection). Keep that corrected request and key for its subsequent retries. |
+| `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. |
+| `422 call_not_ready` from `POST /v1/calls`, with a request for missing task information | Review the missing information, then [save a corrected request with a new key](/legacy-calls#correct-a-missing-information-rejection). Keep that corrected request and key for its subsequent retries. |
| `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
@@ -105,7 +122,7 @@ For `POST /v1/calls`, account controls can reject creation before planning:
Preserve the original request and idempotency key when recovering an uncertain
creation. A persisted creation rejection replays its original error; after
resolving a confirmed rejection, use a new key for an intentionally new attempt.
-See [Calls recovery](/calls#recover-after-a-restart-or-lost-response).
+See [Legacy Calls recovery](/legacy-calls#recover-after-a-restart-or-lost-response).
### Code-specific guidance
@@ -121,6 +138,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.
@@ -131,18 +155,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` can mean the same key was reused with a different request
-body. Restore the original request for a retry of that operation. On Calls API
-creation, it can also mean the original creation is still in progress:
-`details.reason_code` is `creation_in_progress`. In that case, back off and retry
-with the same key and unchanged body.
+`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.
@@ -157,12 +186,17 @@ For a lost Calls API create response or an application restart, follow
Check the operation and error details for `call_not_ready`. On
`POST /v1/calls`, planning can reject a request with this code before execution.
For a creation rejection asking for missing task
-information, follow [the correction example](/calls#correct-a-missing-information-rejection).
+information, follow [the correction example](/legacy-calls#correct-a-missing-information-rejection).
A batch request without an eligible purchased default outbound number also
returns `422 call_not_ready`; check the [batch calling
-requirements](/calls#batch-calls-and-account-limits).
+requirements](/legacy-calls#batch-calls-and-account-limits).
Do not treat every occurrence of this code as permission to create another call.
-`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 e235e7f..2e5d431 100644
--- a/content/guides/goal-runs.mdx
+++ b/content/guides/goal-runs.mdx
@@ -5,10 +5,10 @@ description: Start outbound calls from a published Goal with one phone number an
Start outbound calls from a published Goal when your application repeats the same validated phone workflow for different targets.
-**API 0.7.0** · **Server-side** · **Asynchronous**
+**Server-side** · **Asynchronous**
-Examples target the [Developer API 0.7.0 contract](/openapi/calle.openapi.yaml)
-and server SDKs `@call-e/calle@0.7.0` and `calle-ai==0.7.0`.
+Examples target the [current Developer API contract](/openapi/calle.openapi.yaml)
+and server SDKs `@call-e/calle@1.0.0` and `calle-ai==1.0.0`.
See [API versions and migration](/changelog#api-versions-and-migration) for
request and response changes.
@@ -16,7 +16,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).
@@ -31,7 +31,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.
@@ -229,6 +229,9 @@ The first accepted request and an exact idempotent replay both return `201 Creat
"run_id": "run_delivery_ord_8472",
"call_id": null,
"status": "queued",
+ "call_outcome": null,
+ "result_status": "pending",
+ "transcript": [],
"run_spec": {
"id": "rspec_delivery_v4",
"version": 4
@@ -261,7 +264,7 @@ 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.
+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
{
@@ -271,6 +274,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
@@ -285,21 +291,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 `[]`.
@@ -345,8 +356,9 @@ See [Errors](/errors) for the HTTP error catalog. HTTP errors mean the API reque
## SDK examples
-The following methods are available in the stable TypeScript and Python `0.7.0`
-packages.
+Use TypeScript and Python SDK `1.0.0` for these examples. SDK 0.7.x wait helpers
+wait for a non-null result or error and can time out on a final unavailable result.
+Upgrade or poll the REST response using `result_status`; do not redial on timeout.
REST responses and Python Goal dictionaries use snake_case. The TypeScript SDK
maps these fields to camelCase:
@@ -460,4 +472,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..7f46017
--- /dev/null
+++ b/content/guides/legacy-calls.mdx
@@ -0,0 +1,699 @@
+---
+title: Legacy Calls
+description: Understand the call creation contract and event flow.
+---
+
+:::note{title="Legacy API"}
+These examples use the legacy call-task API and SDK 0.7.x. Legacy Calls are
+planned for retirement at the end of 2026. Use [Calls](/calls) and
+[SDK 1.0](/sdks) for new integrations; see the [migration guide](/migration).
+:::
+
+
+Examples on this page target the [Developer API 0.7.0 contract](/openapi/calle.openapi.yaml)
+and server SDKs `@call-e/calle@0.7.0` and `calle-ai==0.7.0` where used.
+See [API versions and migration](/changelog#api-versions-and-migration) before
+adapting an older example.
+
+Use call tasks to turn a structured workflow step into one or more real phone interactions.
+
+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 [outbound-number requirement for batch calls](#batch-calls-and-account-limits) before using multiple targets.
+
+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).
+
+```ts title="TypeScript"
+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 title="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`.
+
+A non-null result confirms that an object was returned, not that the recipient
+answered or supplied useful evidence. A required string can be empty, and an
+`unknown` enum value can satisfy the schema. Check the business answer and its
+evidence against the transcript before treating the result as success.
+
+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.
+
+## Batch calls and account limits
+
+Shared platform outbound lines support **one phone number per task**, counted
+across all recipients, including targets inferred from `task`. To call multiple
+numbers, select an eligible purchased number as the account's default outbound
+number. Otherwise, creation returns `422 call_not_ready`.
+
+Account task concurrency defaults to 1 on shared platform lines and 10 on
+eligible purchased numbers. Configured account limits take precedence; selecting
+a purchased number as the default does not turn it into a shared platform line.
+Account concurrency and LLM usage checks can reject creation with `429` or `503`;
+see [account-control recovery](/errors#account-controls).
+
+## 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.
+
+| Situation | Recovery action |
+| --- | --- |
+| Call ID saved | 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. |
+| Confirmed creation rejection; cause resolved | Save the request with a new key before submitting an intentionally new attempt. Keep that request and key unchanged for subsequent retries. |
+| Acceptance uncertain; original request and key saved, but no Call ID | Repeat the create request with the same key and unchanged body. If the original request was accepted, save the returned Call 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.
+
+If the original creation is still in progress, an unchanged retry can return
+`409 idempotency_conflict` with `details.reason_code` set to
+`creation_in_progress`. Back off and retry with the **same key and unchanged
+body**; do not submit a replacement operation.
+
+A persisted creation failure replays its original HTTP status and error body.
+An uncertain response alone is not evidence of a rejection.
+
+Keep local workflow identifiers in `metadata` for correlation; they do not
+replace the `Idempotency-Key` header.
+
+### Correct a missing-information rejection
+
+When `POST /v1/calls` returns HTTP `422` with `call_not_ready` and asks for
+missing task information, such as the company or sender's name, review the
+message and `details.questions`. Once you have confirmed this is a creation
+rejection for missing information, correct the task and use a **new**
+idempotency key for that corrected request.
+
+The original key remains bound to the rejected request. Sending its unchanged
+body replays the rejection; changing the body while keeping that key returns
+`409 idempotency_conflict`. Not receiving a Call ID does not mean the server
+created no internal record.
+
+`call_not_ready` alone is not enough to choose this correction path. Check
+which operation failed and what its error details say. This example does not
+handle an accepted call's failure or other reasons a plan was rejected.
+
+#### Run the correction example
+
+The [recovery helper](https://github.com/CALLE-AI/calle-docs/blob/main/examples/recover_create.py)
+uses Python 3.11+ and `calle-ai==0.7.0`, with the same environment setup as the
+[complete Python example](/legacy-quickstart#run-a-complete-example). It reads that
+example's private run-directory format: `request.json` contains the saved
+SDK create arguments, including `idempotency_key`; `error.json` contains
+`status_code`, `code`, `message`, and `details`. Keep the original files.
+
+Read `error.json` privately and put the full corrected task in a UTF-8 file,
+`corrected-task.txt`. Supply the missing facts yourself; do not invent them.
+Then prepare the correction:
+
+```bash
+python examples/recover_create.py correct ../rejected-run \
+ --task-file corrected-task.txt --confirm-missing-information
+```
+
+This sends no request. It requires the saved `422 call_not_ready` error and
+your confirmation that it was a missing-information creation rejection, and
+refuses a directory with a saved Call ID. It saves a new key and corrected
+task in `../rejected-run/corrected/request.json`, preserving the other request
+fields. Running `correct` again refuses to overwrite that directory or
+generate another saved key.
+
+Review the corrected request, keep the same API key and `CALLE_BASE_URL`, and
+submit it explicitly. This can place a real, billed call to the saved recipient:
+
+```bash
+python examples/recover_create.py submit ../rejected-run/corrected
+```
+
+The helper saves the Call ID, retrieves the result, and makes no automatic
+create retry. If interrupted, rerun the same command with the same directory:
+with a saved ID it only retrieves that call; without one it resubmits the
+unchanged saved request and key. Do not edit or delete the saved files to retry.
+An exit code of zero means a terminal result was retrieved, not that the
+business task succeeded. Stopping the script does not cancel an accepted call.
+
+## 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.
+
+### Read a saved result
+
+The [Python result reader](https://github.com/CALLE-AI/calle-docs/blob/main/examples/read_results.py)
+displays task-level and recipient-level results separately, followed by the
+transcript labeled by recipient and attempt. Run it with Python 3.11+ on the
+`result.json` saved by either [complete Calls example](/legacy-quickstart#run-a-complete-example):
+
+```bash
+python examples/read_results.py ../calle-run/result.json
+```
+
+This command makes no API requests and leaves the file unchanged. Its output
+may contain private results and transcript text; keep it private. A zero exit
+code means the file was read, not that the call or business task succeeded.
+
+`result_schema` supplies the task-level `structured_result`;
+`recipient_result_schema` supplies each `recipients[].structured_result`, even
+for a single recipient. A null task-level result does not imply that recipient
+results or transcripts are missing. The reader preserves `null`, `unknown`,
+`false`, and zero rather than treating them as success or substituting one
+recipient's result for the whole task. Attempt transcript counts show when an
+attempt has no transcript turns.
+
+## 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).
+
+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.
+
+Saved transcripts are not automatically deleted. An empty `transcript_turns`
+array means no transcript is available for that attempt; it is not an
+expiration marker.
+
+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.
+
+**Audio recordings:** Saved recordings remain available in Dashboard call
+details, where you can download them. The Calls API does not return audio
+recordings, playback or download URLs, or recording availability and expiration
+fields. Recording retrieval through MCP and the SDKs is tracked separately in
+[#753](https://github.com/CALLE-AI/awesome-phone-call-agents/issues/753).
+
+### Poll for results
+
+
+```ts title="TypeScript"
+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 title="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..dfdd2a8
--- /dev/null
+++ b/content/guides/legacy-quickstart.mdx
@@ -0,0 +1,268 @@
+---
+title: Legacy Quickstart
+description: Create one call and read the structured result.
+---
+
+:::note{title="Legacy API"}
+These examples use the legacy call-task API and SDK 0.7.x. Legacy Calls are
+planned for retirement at the end of 2026. Use [Calls](/calls) and
+[SDK 1.0](/sdks) for new integrations; see the [migration guide](/migration).
+:::
+
+
+Examples on this page target the [Developer API 0.7.0 contract](/openapi/calle.openapi.yaml)
+and server SDKs `@call-e/calle@0.7.0` and `calle-ai==0.7.0` where used.
+See [API versions and migration](/changelog#api-versions-and-migration) before
+adapting an older example.
+
+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
+
+
+
+
+
+```ts
+import { CalleClient } from "@call-e/calle";
+
+const client = new CalleClient({
+ apiKey: process.env.CALLE_API_KEY!,
+});
+```
+
+
+
+
+
+```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
+
+
+
+
+
+```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
+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`.
+
+A present result may still contain `unknown` or empty strings. Check its values
+and the transcript; presence alone does not establish that anyone answered or
+that the task succeeded.
+
+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`.
+
+
+
+
+
+```ts
+console.log(call.status);
+console.log(call.structuredResult);
+console.log(call.taskCompleted, call.completionConfidence, call.evidence);
+```
+
+```json title="Example output"
+{
+ "status": "completed",
+ "taskCompleted": true,
+ "completionConfidence": {"score": 0.92, "label": "high"},
+ "evidence": ["The recipient clearly answered yes."],
+ "structuredResult": {
+ "can_hear_clearly": "yes"
+ }
+}
+```
+
+
+
+
+
+```python
+print(call["status"])
+print(call["structured_result"])
+print(call["task_completed"], call["completion_confidence"], call["evidence"])
+```
+
+```json title="Example output"
+{
+ "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)
+and [Ruby example](https://github.com/CALLE-AI/calle-docs/blob/main/examples/calls.rb)
+create one real US English test call, save its request and Call ID, wait for
+the terminal result, and write the full response to a private local directory.
+They listen to a greeting and then ask the agent to end the call.
+
+Python requires version 3.11 or later and the CALL-E SDK. Ruby uses the standard
+library with no SDK or extra gems; it was tested with Ruby 4.0.7 on macOS.
+Use 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.
+
+```bash
+git clone https://github.com/CALLE-AI/calle-docs.git
+cd calle-docs
+export CALLE_API_KEY=""
+export CALLE_TEST_PHONE=""
+```
+
+Choose a language and a new private run directory outside the repository:
+
+
+
+```bash title="Python"
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install calle-ai==0.7.0
+# Create one authorized call and save its result
+python examples/calls.py start ../calle-run --phone "$CALLE_TEST_PHONE"
+# Retrieve the same call after a restart or completed run
+python examples/calls.py resume ../calle-run
+```
+
+```bash title="Ruby"
+# Preview without sending a request
+ruby examples/calls.rb start ../calle-ruby-run
+# Create one authorized call and save its result
+ruby examples/calls.rb start ../calle-ruby-run --execute --confirm-authorized-recipient
+# Retrieve the same call after a restart or completed run
+ruby examples/calls.rb resume ../calle-ruby-run
+```
+
+
+
+`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.
+
+`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 example retries a failed
+create request automatically. An HTTP error is saved in `error.json`; see
+[Choose the next action](/errors#choose-the-next-action).
+
+For an executed `start` or a `resume`, 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..e3d6232
--- /dev/null
+++ b/content/guides/legacy-sdks.mdx
@@ -0,0 +1,224 @@
+---
+title: Legacy SDKs
+description: Official and community SDKs.
+---
+
+:::note{title="Legacy API"}
+These examples use the legacy call-task API and SDK 0.7.x. Legacy Calls are
+planned for retirement at the end of 2026. Use [Calls](/calls) and
+[SDK 1.0](/sdks) for new integrations; see the [migration guide](/migration).
+:::
+
+
+Examples on this page target the [historical Developer API 0.7.0 contract](https://github.com/CALLE-AI/calle-docs/blob/b387e01b7ab943a5712bd5f1bb4cfe3699944748/openapi/calle.openapi.yaml)
+and server SDKs `@call-e/calle@0.7.0` and `calle-ai==0.7.0` where used.
+See [API versions and migration](/changelog#api-versions-and-migration) before
+adapting an older example.
+
+## Official SDKs
+
+- [Python](https://github.com/CALLE-AI/server-sdk-python)
+- [TypeScript](https://github.com/CALLE-AI/server-sdk-typescript)
+
+CALL-E provides server SDKs for trusted backend services, workers, and automation systems that create and monitor call tasks.
+
+### 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 legacy examples below use:
+
+- TypeScript: `@call-e/calle@0.7.0`
+- Python: `calle-ai==0.7.0`
+
+These packages support the legacy call-task API. For current Calls and Goal
+Runs, use [SDK 1.0](/sdks). Legacy Goal Run wait helpers can time out when a
+final unavailable result has both `result` and `error` null.
+
+### 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.
+
+```bash title="TypeScript"
+git clone https://github.com/CALLE-AI/server-sdk-typescript.git
+cd server-sdk-typescript
+git checkout v0.7.0
+pnpm install
+pnpm run example:create-and-wait
+```
+
+### Calls API
+
+The two-recipient examples below require an eligible purchased number selected
+as the account's default outbound number. See [batch calls and account
+limits](/legacy-calls#batch-calls-and-account-limits).
+
+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.
+
+```ts title="TypeScript"
+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 title="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 pinned legacy 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@0.7.0
+pip install calle-ai==0.7.0
+```
+
+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
+
+The official TypeScript and Python SDKs do 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
+
+## Community SDKs
+
+- Android / Wear OS — [calle-android-sdk](https://github.com/Baklolman69/calle-android-sdk) (community-maintained).
diff --git a/content/guides/legacy-webhooks.mdx b/content/guides/legacy-webhooks.mdx
new file mode 100644
index 0000000..60154ce
--- /dev/null
+++ b/content/guides/legacy-webhooks.mdx
@@ -0,0 +1,198 @@
+---
+title: Legacy Webhooks
+description: Receive terminal webhook events and process them safely.
+---
+
+:::note{title="Legacy API"}
+These examples use the legacy call-task API and SDK 0.7.x. Legacy Calls are
+planned for retirement at the end of 2026. Use [Calls](/calls) and
+[SDK 1.0](/sdks) for new integrations; see the [migration guide](/migration).
+:::
+
+
+Use webhooks when your application needs to react to terminal call task results without polling.
+
+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` includes the call results and available
+transcripts. Timestamp values may differ from those in later
+`GET /v1/calls/{call_id}` responses.
+
+## 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`.
+
+```ts title="TypeScript"
+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 title="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.
+
+```ts title="TypeScript"
+if (await eventStore.has(event.id)) {
+ return new Response("duplicate", { status: 200 });
+}
+
+await eventStore.insert(event.id);
+await handleCallEvent(event);
+```
+
+```python title="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..52803e8
--- /dev/null
+++ b/content/guides/migration.mdx
@@ -0,0 +1,110 @@
+---
+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 Run endpoints
+and request shapes are unchanged; their result readiness rule has changed. See the [retirement notice](/retirement) for the
+legacy interface's planned support deadline.
+
+Calls and SDK 1.0 are available in production. Install the new SDK only after
+updating Calls inputs and response handling. Legacy `calls` methods in SDK 0.7.x
+use the old interface and cannot read new Call IDs.
+
+**Goal Run users also need the new wait helper.** SDK 0.7.x waits for result or
+error to become non-null, which can time out on a final unavailable result.
+Upgrade to 1.0, or poll the REST resource until `result_status` is not `pending`.
+An unavailable result is not a technical error and must not trigger an automatic redial.
+
+## 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` | 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` | Fixed top-level `transcript` | Read the recorded turns independently of business result availability. |
+| 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. Install SDK 1.0 and update the request builder and readiness rule to use `result_status` and `call_outcome`.
+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:** use the fixed `transcript` field. 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.
+Post-retirement access and export arrangements will be announced separately;
+no new archive endpoint is currently available.
diff --git a/content/guides/quickstart.mdx b/content/guides/quickstart.mdx
index 1cc8354..c09d568 100644
--- a/content/guides/quickstart.mdx
+++ b/content/guides/quickstart.mdx
@@ -1,261 +1,114 @@
---
title: Quickstart
-description: Create one call and read the structured result.
+description: Make a phone call and read its structured result.
---
-Examples on this page target the [Developer API 0.7.0 contract](/openapi/calle.openapi.yaml)
-and server SDKs `@call-e/calle@0.7.0` and `calle-ai==0.7.0` where used.
-See [API versions and migration](/changelog#api-versions-and-migration) before
-adapting an older example.
+Create one phone call, keep its ID, and wait for a structured result.
-Create a CALL-E call task, wait for the terminal result, and read the structured output.
+These examples use the production Calls API. For SDK examples, install
+[TypeScript or Python SDK 1.0](/sdks).
-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:
+## Connect to the API
```bash
+export CALLE_BASE_URL="https://api.heycall-e.com"
export CALLE_API_KEY=""
```
-You can view your API keys in the [CALL-E dashboard](https://dashboard.heycall-e.com/account/api-keys).
+Get a key from the [dashboard](https://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.
-See [Authentication](/authentication) for API key handling and server-only usage.
-
-
-
-## Create a client
-
-
-
-
-
-```ts
-import { CalleClient } from "@call-e/calle";
-
-const client = new CalleClient({
- apiKey: process.env.CALLE_API_KEY!,
-});
-```
+## Create one call
-
+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.
-
-
-```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
-
-
-
-
-
-```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
-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
-
-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 present result may still contain `unknown` or empty strings. Check its values
-and the transcript; presence alone does not establish that anyone answered or
-that the task succeeded.
-
-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`.
-
-
-
-
-
-```ts
-console.log(call.status);
-console.log(call.structuredResult);
-console.log(call.taskCompleted, call.completionConfidence, call.evidence);
-```
+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.
-```json title="Example output"
-{
- "status": "completed",
- "taskCompleted": true,
- "completionConfidence": {"score": 0.92, "label": "high"},
- "evidence": ["The recipient clearly answered yes."],
- "structuredResult": {
- "can_hear_clearly": "yes"
- }
-}
-```
+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.
-
+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.
-
+## Wait for the result
-```python
-print(call["status"])
-print(call["structured_result"])
-print(call["task_completed"], call["completion_confidence"], call["evidence"])
-```
+```bash
+export CALLE_CALL_ID=""
-```json title="Example output"
-{
- "status": "completed",
- "task_completed": true,
- "completion_confidence": {"score": 0.92, "label": "high"},
- "evidence": ["The recipient clearly answered yes."],
- "structured_result": {
- "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"
```
-
-
-
-
-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.
-
-
-
-
+Poll at a reasonable interval, such as every two seconds:
-## Run a complete example
+| Response | What to do |
+| --- | --- |
+| `result_status == "pending"` | Keep waiting, including when `status` is `completed`. |
+| `result_status == "available"` | Consume the validated business result. An empty object is also ready. |
+| `result_status == "unavailable"` | Stop waiting. No schema-valid business result is available; both result and error may be null. |
+| `result_status == "not_applicable"` | Stop waiting. Inspect cancellation or the technical error. |
-The [Python example](https://github.com/CALLE-AI/calle-docs/blob/main/examples/calls.py)
-and [Ruby example](https://github.com/CALLE-AI/calle-docs/blob/main/examples/calls.rb)
-create one real US English test call, save its request and Call ID, wait for
-the terminal result, and write the full response to a private local directory.
-They listen to a greeting and then ask the agent to end the call.
+An illustrative ready result is `{"greeting_delivered": true, "summary": "The recipient replied hello."}`.
+Use [Webhooks](/webhooks) to receive the result without polling.
-Python requires version 3.11 or later and the CALL-E SDK. Ruby uses the standard
-library with no SDK or extra gems; it was tested with Ruby 4.0.7 on macOS.
-Use 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.
+Every response includes `transcript`, which may be empty, and a nullable `call_id`
+for Billing lookup. `status: completed` alone is not proof of business success
+or result readiness. Keep the API resource `id` for reads, events and cancellation.
-```bash
-git clone https://github.com/CALLE-AI/calle-docs.git
-cd calle-docs
-export CALLE_API_KEY=""
-export CALLE_TEST_PHONE=""
-```
-
-Choose a language and a new private run directory outside the repository:
-
-
+## Use the SDKs
-```bash title="Python"
-python3 -m venv .venv
-source .venv/bin/activate
-python -m pip install calle-ai==0.7.0
-# Create one authorized call and save its result
-python examples/calls.py start ../calle-run --phone "$CALLE_TEST_PHONE"
-# Retrieve the same call after a restart or completed run
-python examples/calls.py resume ../calle-run
-```
-
-```bash title="Ruby"
-# Preview without sending a request
-ruby examples/calls.rb start ../calle-ruby-run
-# Create one authorized call and save its result
-ruby examples/calls.rb start ../calle-ruby-run --execute --confirm-authorized-recipient
-# Retrieve the same call after a restart or completed run
-ruby examples/calls.rb resume ../calle-ruby-run
-```
+The [SDK guide](/sdks) includes installation and complete TypeScript and Python examples.
+For tests, explicitly set `CALLE_BASE_URL=https://test-api.heycall-e.com` and use
+a test-compatible key.
-
+## 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`.
-`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.
+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.
-Stopping this process or reaching its five-minute polling timeout does not
-cancel a call already accepted by CALL-E. Neither example retries a failed
-create request automatically. An HTTP error is saved in `error.json`; see
-[Choose the next action](/errors#choose-the-next-action).
+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.
-For an executed `start` or a `resume`, 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..03321a1
--- /dev/null
+++ b/content/guides/retirement.mdx
@@ -0,0 +1,41 @@
+---
+title: Legacy API retirement
+description: Legacy Calls retirement deadline and migration guidance.
+---
+
+Legacy Calls are planned for retirement at the end of 2026. New integrations
+should use [Calls](/calls) and [SDK 1.0](/sdks).
+
+## Scope
+
+| Surface | Support status |
+| --- | --- |
+| Single-target Calls | Available in production, including reads, events and cancellation before submission. |
+| Legacy call tasks | Supported during migration; planned for retirement at the deadline below. |
+| Goals and Goal Runs | Not being retired. Upgrade SDK wait helpers for the current result-readiness contract. |
+| SDK 1.0 | Supports single-target Calls and current Goal Run results. |
+| SDK 0.7.x | Retain only for legacy call-task integrations while migrating. |
+
+## Planned deadline
+
+**2026-12-31 at 16:00 UTC**, equivalent to **2027-01-01 at 00:00 Asia/Shanghai**.
+
+The affected legacy routes are `POST /v1/calls`, `GET /v1/calls/{call_id}`
+and `GET /v1/calls/{call_id}/events`. Goal endpoints are not included.
+
+The cutoff is not active today. Publishing this notice does not stop accepted
+calls, delete stored records or convert legacy IDs to the new interface.
+Historical-data access and export arrangements after retirement will be
+announced separately; no new archive endpoint or read-only grace period is
+currently promised.
+
+## Migrate before retirement
+
+1. Follow the [migration guide](/migration) to update request and result handling.
+2. Install SDK 1.0 for new Calls and Goal Run wait helpers.
+3. Keep each call ID with the interface that created it. Use a legacy client for old call-task IDs.
+4. Continue handling callbacks for already accepted calls; do not create replacements because a client timed out.
+
+The migration guide and [changelog](/changelog) will carry further retirement
+updates. Contact CALL-E support for workflows that depend on legacy batch
+behavior or historical access.
diff --git a/content/guides/sdks.mdx b/content/guides/sdks.mdx
index c038d8f..30dc8e8 100644
--- a/content/guides/sdks.mdx
+++ b/content/guides/sdks.mdx
@@ -1,215 +1,125 @@
---
title: SDKs
-description: Official and community SDKs.
+description: Install SDK 1.0 for single-target Calls and published Goal Runs.
---
-Examples on this page target the [Developer API 0.7.0 contract](/openapi/calle.openapi.yaml)
-and server SDKs `@call-e/calle@0.7.0` and `calle-ai==0.7.0` where used.
-See [API versions and migration](/changelog#api-versions-and-migration) before
-adapting an older example.
+The official server SDKs support single-target Calls and published Goal Runs.
+Use SDK **1.0.0** with the production API. Legacy call-task integrations must
+[migrate their inputs and result handling](/migration) before upgrading.
-## Official SDKs
+## Install
-- [Python](https://github.com/CALLE-AI/server-sdk-python)
-- [TypeScript](https://github.com/CALLE-AI/server-sdk-typescript)
-
-CALL-E provides server SDKs for trusted backend services, workers, and automation systems that create and monitor call tasks.
-
-### 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
+```bash
+pnpm add @call-e/calle@1.0.0
+pip install calle-ai==1.0.0
```
-### Package status
-
-The current stable server SDK packages are:
-
-- TypeScript: `@call-e/calle@0.7.0`
-- Python: `calle-ai==0.7.0`
+Python requires 3.11 or later. Keep API keys in trusted server environments.
+Source: [TypeScript](https://github.com/CALLE-AI/server-sdk-typescript) and
+[Python](https://github.com/CALLE-AI/server-sdk-python).
-These stable packages support both the request-scoped Calls API and the Goal
-Runs SDK surface described below.
+## Configuration
-### 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.
-
-```bash title="TypeScript"
-git clone https://github.com/CALLE-AI/server-sdk-typescript.git
-cd server-sdk-typescript
-pnpm install
-pnpm run example:create-and-wait
+```bash
+export CALLE_BASE_URL="https://api.heycall-e.com"
+export CALLE_API_KEY=""
+export CALLE_TEST_PHONE=""
+export CALLE_IDEMPOTENCY_KEY="hello-001"
```
-### Calls API
-
-The two-recipient examples below require an eligible purchased number selected
-as the account's default outbound number. See [batch calls and account
-limits](/calls#batch-calls-and-account-limits).
+For the test environment, set `CALLE_BASE_URL=https://test-api.heycall-e.com`
+and use a test-compatible key. These examples make a real call. Omitted region
+and locale are inferred from the phone and task; conflicting or ambiguous input
+returns `422 input_incomplete` for correction.
-The SDKs expose `context` as a reserved input for future SDK-side workflow data. The current SDKs do not send `context` to the API.
+## TypeScript
-Examples use phone placeholders such as `` and ``. Replace them with phone numbers you own or are authorized to call.
+```ts title="TypeScript"
+import { CalleClient } from "@call-e/calle";
-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.
+const client = new CalleClient({
+ apiKey: process.env.CALLE_API_KEY!,
+ baseUrl: process.env.CALLE_BASE_URL!,
+});
-```ts title="TypeScript"
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 title="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"}},
+ "additionalProperties": False,
+ "properties": {"summary": {"type": "string"}},
+ "required": ["summary"],
},
- 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,
+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)
+print(ready["result"], ready["error"])
+print(ready["transcript"])
```
-### 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.
+The client defaults to production. Wait helpers stop when `result_status` is
+not `pending` (`resultStatus` in TypeScript), including unavailable results with
+both result and error null. A timeout does not cancel or replace the Call.
+Python exposes the Billing identifier as `call_id`; TypeScript uses `callId`.
+Use the separate resource `id` for API requests.
-```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
-```
+## 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.
-The official TypeScript and Python SDKs do 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 endpoints and request shapes remain unchanged, but the result readiness
+rule has changed. SDK 0.7.x Goal wait helpers can time out on unavailable results;
+upgrade to 1.0 or poll REST using `result_status`.
+HTTP/Python use `result_schema` and `error.detail_code`; TypeScript uses
+`resultSchema` and `error.detailCode`. See the [migration guide](/migration).
## Community SDKs
-- Android / Wear OS — [calle-android-sdk](https://github.com/Baklolman69/calle-android-sdk) (community-maintained).
+[Android / Wear OS](https://github.com/Baklolman69/calle-android-sdk) is community-maintained.
+Check its documented API compatibility independently of the server SDK releases.
diff --git a/content/guides/webhooks.mdx b/content/guides/webhooks.mdx
index a1451dc..6727e91 100644
--- a/content/guides/webhooks.mdx
+++ b/content/guides/webhooks.mdx
@@ -1,191 +1,86 @@
---
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. Delivery includes the final result
+snapshot, not just the moment 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.
+Terminal 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` includes the call results and available
-transcripts. Timestamp values may differ from those in later
-`GET /v1/calls/{call_id}` responses.
+## 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:
+`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.call_outcome`, `data.result_status`, `data.result` and `data.error`. |
+| `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:
+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
+## Receiver behavior
-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`.
+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.
-```ts title="TypeScript"
-const event = JSON.parse(rawBody.toString("utf8"));
-const eventId = request.headers.get("CALL-E-Event-Id");
+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.
-if (!eventId || eventId !== event.id) {
- return new Response("invalid event id", { status: 400 });
-}
+`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.
-if (event.type === "call.completed") {
- console.log(event.data.id, event.data.recipients);
-}
-```
+## Existing receivers
-```python title="Python"
-event = json.loads(raw_body)
-event_id = request.headers.get("CALL-E-Event-Id")
+When migrating, use separate receiver paths or dispatch on `data.object`:
-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.
-
-```ts title="TypeScript"
-if (await eventStore.has(event.id)) {
- return new Response("duplicate", { status: 200 });
+```ts
+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 title="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 53073d8..9df3972 100644
--- a/openapi/calle.openapi.yaml
+++ b/openapi/calle.openapi.yaml
@@ -1,19 +1,250 @@
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.
+ Calls and Goal Runs expose call_outcome, result_status and a fixed transcript
+ array. SDK 1.0 supports the single-target Calls API. Legacy call tasks remain
+ available during the migration period; see the public migration guide.
servers:
- url: https://api.heycall-e.com
- description: Placeholder developer API base URL.
+ description: Production developer API.
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. 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:
@@ -200,7 +431,8 @@ paths:
get:
operationId: getCall
tags:
- - calls
+ - legacy-calls
+ deprecated: true
summary: Get Call
description: Get a call by id.
parameters:
@@ -276,8 +508,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"
@@ -546,6 +779,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":
@@ -581,10 +817,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"
@@ -617,6 +852,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
@@ -626,6 +863,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:
@@ -634,17 +872,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":
@@ -665,7 +903,7 @@ paths:
tags:
- webhooks
summary: Server Message
- description: CALL-E sends this request after a call reaches a terminal state and its post-call outcome and requested structured results are finalized. Configure your receiver URL with `webhook_url` when creating a call.
+ description: CALL-E sends this request after a call reaches a terminal state and its post-call outcome and requested structured results are finalized. Configure this URL with `webhook_url` on create call or through project-level webhook settings.
security: []
servers:
- url: https://{yourserver}
@@ -681,7 +919,7 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/WebhookEvent"
+ $ref: "#/components/schemas/TerminalWebhookEvent"
examples:
callCompleted:
summary: Completed US Friday lunch webhook.
@@ -834,7 +1072,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
@@ -863,6 +1101,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: |-
@@ -1009,9 +1396,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
@@ -1021,6 +1407,9 @@ components:
- call_id
- run_spec
- status
+ - call_outcome
+ - result_status
+ - transcript
- result
- error
- created_at
@@ -1058,19 +1447,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"
@@ -1103,18 +1501,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
@@ -1125,12 +1531,8 @@ components:
type: string
enum:
- call_failed
- - no_answer
- - declined
- timed_out
- - canceled
- result_invalid
- - result_unavailable
- result_failed
message:
type: string
@@ -1184,8 +1586,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`.
@@ -1198,7 +1598,7 @@ components:
additionalProperties: true
webhook_url:
type: string
- description: Optional per-request HTTPS webhook URL. When provided, CALL-E sends terminal call events to this URL after result finalization.
+ description: Optional per-request HTTPS webhook URL. When provided, CALL-E sends terminal call events to this URL in addition to project-level webhook delivery.
format: uri
CallTaskRecipientRequest:
type: object
@@ -1343,15 +1743,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
@@ -1432,7 +1829,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.
@@ -1486,10 +1883,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
@@ -1603,8 +1997,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.
@@ -1632,7 +2025,9 @@ components:
code:
type: string
enum:
+ - call_cannot_cancel
- invalid_request
+ - input_incomplete
- unauthorized
- forbidden
- rate_limit_exceeded
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 d977831..9f6531f 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -226,14 +226,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;
@@ -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"]
)
> span {
width: 100%;
@@ -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"]
)::after {
color: var(--muted-foreground);
font-size: 0.75rem;
@@ -284,14 +284,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,
@@ -301,37 +301,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: "Official and community SDKs.";
}
}
@@ -399,12 +399,19 @@ nav[class*="overflow-y-auto"][class*="shrink-0"] a[aria-current="page"] {
}
.typography code.inline {
+ overflow-wrap: anywhere;
+ white-space: normal;
border-color: transparent;
border-radius: 0.25rem;
background: var(--muted);
color: var(--foreground);
}
+.comparison-table {
+ max-width: 100%;
+ overflow-x: auto;
+}
+
.typography .code-block-wrapper,
.typography .sdk-example {
overflow: hidden;
@@ -649,6 +656,12 @@ aside[class*="overflow-y-auto"] a:hover {
padding: 0.75rem;
font-size: 0.8125rem;
}
+
+ /* 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 d6ec77c..413da82 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://api.heycall-e.com"' }).first(),
).toBeVisible();
await expect(page.locator("code.shiki.not-inline").first()).toHaveCSS(
"background-color",
@@ -311,7 +317,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");
@@ -430,7 +436,7 @@ test("renders every migrated guide from its file route", async ({ page }) => {
await expect(
page.locator("h1").filter({ hasText: guide.heading }),
).toBeVisible();
- if (["/authentication", "/calls", "/sdks", "/webhooks"].includes(guide.path)) {
+ if (["/authentication", "/legacy-calls", "/sdks", "/legacy-webhooks"].includes(guide.path)) {
for (const language of ["Python", "TypeScript"]) {
await expect(page.locator(".code-block-wrapper > div:first-child")
.filter({ hasText: new RegExp(`${language}$`) }).first()).toBeVisible();
@@ -438,7 +444,7 @@ test("renders every migrated guide from its file route", async ({ page }) => {
await expect(page.locator("p")
.filter({ hasText: /^(Python|TypeScript|Example output):$/ })).toHaveCount(0);
}
- if (guide.path === "/quickstart") {
+ if (guide.path === "/legacy-quickstart") {
await expect(page.locator(".code-block-wrapper > div:first-child")
.filter({ hasText: /Example output$/ })).toHaveCount(1);
}
@@ -521,22 +527,21 @@ test("uses the CALL-E Web palette for docs chrome", async ({ page }) => {
test("keeps quickstart requests minimal and safe to copy", async ({ page }) => {
await page.goto("/quickstart");
- await expect(page.locator("pre").filter({ hasText: "pnpm add @call-e/calle" }).first()
- .locator(".code-block-wrapper > div:first-child")).toHaveText(/Terminal$/);
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);
await expect(page.getByRole("heading", { name: "Ruby HTTP example" })).toHaveCount(0);
- await expect(page.getByRole("link", { name: "Ruby example", exact: true })).toHaveAttribute(
- "href", "https://github.com/CALLE-AI/calle-docs/blob/main/examples/calls.rb",
- );
+ await expect(page.locator("main")).toContainText('result_status == "unavailable"');
+ await expect(page.locator("main")).toContainText("call_id");
});
@@ -548,7 +553,7 @@ test("disables code tabs until their interaction is ready", async ({ page }) =>
await route.continue();
});
try {
- await page.goto("/quickstart#run-a-complete-example", { waitUntil: "commit" });
+ await page.goto("/legacy-quickstart#run-a-complete-example", { waitUntil: "commit" });
const ruby = page.getByRole("tab", { name: "Ruby", exact: true });
const complete = page.locator("fieldset").filter({ has: page.getByRole("tab", { name: "Ruby", exact: true }) });
const python = complete.getByRole("tab", { name: "Python", exact: true });
@@ -585,7 +590,7 @@ test("keeps SDK language, code, and output together across quickstart steps", as
for (const colorScheme of ["light", "dark"] as const) {
await page.setViewportSize({ width, height: 900 });
await page.emulateMedia({ colorScheme });
- await page.goto("/quickstart#read-the-result");
+ await page.goto("/legacy-quickstart#read-the-result");
const client = page.getByRole("group", { name: "Create a client", exact: true });
const result = page.getByRole("group", { name: "Read the result", exact: true });
await expect(result.getByRole("tab", { name: "TypeScript", exact: true })).toHaveAttribute("aria-selected", "true");
@@ -631,7 +636,7 @@ test("switches complete examples without a separate Ruby contents entry", async
await page.setViewportSize({ width, height: 900 });
await page.emulateMedia({ colorScheme });
const dark = colorScheme === "dark";
- await page.goto("/quickstart#run-a-complete-example");
+ await page.goto("/legacy-quickstart#run-a-complete-example");
const complete = page.locator("fieldset").filter({ has: page.getByRole("tab", { name: "Ruby", exact: true }) });
const python = complete.getByRole("tab", { name: "Python", exact: true });
const ruby = page.getByRole("tab", { name: "Ruby", exact: true });
@@ -690,9 +695,9 @@ test("switches complete examples without a separate Ruby contents entry", async
await codeTabs.scrollIntoViewIfNeeded();
await page.screenshot({ path: testInfo.outputPath(`example-tabs-${width}-${colorScheme}.png`) });
}
- await page.goto("/quickstart#ruby-http-example");
+ await page.goto("/legacy-quickstart#ruby-http-example");
await expect(page.locator("#ruby-http-example")).toHaveCount(1);
- for (const route of ["/quickstart.md", "/llms-full.txt"]) {
+ for (const route of ["/legacy-quickstart.md", "/llms-full.txt"]) {
const result = await request.get(route);
expect(result.ok()).toBe(true);
const text = await result.text();
@@ -714,7 +719,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"/,
@@ -726,7 +731,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(
@@ -740,20 +745,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(
@@ -816,7 +821,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",
@@ -829,17 +834,18 @@ 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"]').first().click();
- await expect(page).toHaveURL(/\/calls#task-completion$/);
+ await expect(page.getByTestId("theme-menu-trigger")).toHaveAttribute("aria-haspopup", "menu");
+ await page.locator('p a[href="/legacy-calls#task-completion"]').first().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();
@@ -892,12 +898,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"),
@@ -944,3 +961,43 @@ 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);
+});
+
+
+test("publishes production SDK installation and readiness guidance", async ({ page }) => {
+ await page.goto("/sdks");
+ await expect(page.locator("pre").filter({ hasText: "pnpm add @call-e/calle@1.0.0" })).toBeVisible();
+ await expect(page.locator("pre").filter({ hasText: "pip install calle-ai==1.0.0" })).toBeVisible();
+ await expect(page.locator("main")).toContainText("resultStatus");
+ await expect(page.locator("main")).not.toContainText("These packages have not been published");
+ await page.goto("/migration");
+ await expect(page.locator("main")).toContainText("Goal Run users also need the new wait helper");
+ await page.goto("/calls");
+ await expect(page.locator("main")).toContainText("call_id");
+ await expect(page.locator("main")).not.toContainText("unreleased response additions");
+});
diff --git a/zudoku.config.tsx b/zudoku.config.tsx
index 76a62e1..1c8f57c 100644
--- a/zudoku.config.tsx
+++ b/zudoku.config.tsx
@@ -5,9 +5,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}\`;
@@ -26,7 +30,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;
}
@@ -38,19 +42,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",
@@ -60,6 +65,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",
@@ -140,8 +157,8 @@ const config = {
contract.