From 086da4655ebc90d9a8be4db30cafcf00af8aa026 Mon Sep 17 00:00:00 2001 From: "lin.si" Date: Tue, 28 Jul 2026 13:26:31 +0800 Subject: [PATCH 1/2] feat(docs): document Goal Runs API --- content/guides/changelog.mdx | 36 ++ content/guides/errors.mdx | 21 +- content/guides/goal-runs.mdx | 422 +++++++++++++++++++++ content/guides/quickstart.mdx | 2 + content/guides/sdks.mdx | 29 +- openapi/calle.openapi.yaml | 673 +++++++++++++++++++++++++++++++++- scripts/verify-dist.mjs | 26 +- tests/docs-site.spec.ts | 54 +++ zudoku.config.tsx | 2 + 9 files changed, 1256 insertions(+), 9 deletions(-) create mode 100644 content/guides/goal-runs.mdx diff --git a/content/guides/changelog.mdx b/content/guides/changelog.mdx index 6ce00d6..38da8c1 100644 --- a/content/guides/changelog.mdx +++ b/content/guides/changelog.mdx @@ -5,6 +5,42 @@ description: Track CALL-E Developer API and SDK product updates. Product updates for the CALL-E Developer API and server SDKs. +## July 22, 2026 + +### Phone-only Goal Run requests in API 0.6 + +Create Goal Run requests now use a top-level `phone` plus `variables`. The `target` wrapper and per-Run `region`, `locale`, and `display_name` fields have been removed. Voice region and callee locale are fixed by the published Goal; recipient names needed by the conversation belong in the Goal's input schema and are supplied through `variables`. + +The TypeScript and Python SDK source candidates now implement the matching +`client.goals` surface at version `0.6.0`, including Goal discovery, Run +creation, Run reads, and result-or-error polling. Existing Calls and Webhooks +surfaces remain compatible; registry packages stay on their current stable +versions until the 0.6.0 release is published. + +### Simplified Goal Runs API 0.5 + +1. Useful Goal metadata: Goal list and get responses now include the current published `title` and `description` alongside the input and result schemas. + +2. Smaller response: Goal and Goal Run responses expose RunSpec ids and versions without content fingerprints. + +3. One result contract: a Goal Run returns the parsed object directly in `result`, or one unified `error` when execution or result processing fails. When both are null, clients continue polling. + +4. Goal discovery: `GET /v1/goals` lists the authenticated owner's active, listed, published Goal interfaces with opaque cursor pagination. The API 0.5 SDK design adds `client.goals.list(...)` in both TypeScript and Python while keeping stable 0.2 packages Calls-only until a matching SDK minor is released. + +## July 21, 2026 + +### Goal Runs API preview + +1. Published Goal interface: Read the current normalized input schema, result schema, and RunSpec identity with `GET /v1/goals/{goal_id}`. + +2. Goal Run creation: Create one singleton Run with `POST /v1/goals/{goal_id}/runs`. Requests supply one target, scalar variables, and a required business-stable `Idempotency-Key`; they cannot replace the published schemas or choose a RunSpec version. Initial acceptance and exact replay both return `201`. + +3. Result polling: Read execution and schema-bound results with `GET /v1/goals/{goal_id}/runs/{goal_run_id}`. The Goal Run preserves the exact RunSpec version pinned when CALL-E accepted it. + +4. SDK transition: The Goal Runs guide defines TypeScript and Python parity on `client.goals`, with `get`, `run`, get-Run, wait, and run-and-wait helpers using each language's naming convention. Stable 0.2 SDK packages continue to support the one-shot Calls API until matching next-minor packages are released. + +See [Goal Runs](/goal-runs) for request, polling, version-pinning, and SDK examples. + ## June 8, 2026 ### What's New: Developer API and server SDKs diff --git a/content/guides/errors.mdx b/content/guides/errors.mdx index 707d65a..551096c 100644 --- a/content/guides/errors.mdx +++ b/content/guides/errors.mdx @@ -3,7 +3,7 @@ title: Errors description: Handle stable API errors and recovery paths. --- -CALL-E returns stable error envelopes for call task API failures. +CALL-E returns stable error envelopes for Developer API request failures. ## Error envelope @@ -37,6 +37,11 @@ SDK methods raise typed SDK errors while preserving the stable API error code an - `result_schema_invalid` - `recipient_result_schema_invalid` - `idempotency_conflict` +- `goal_not_published` +- `goal_not_executable` +- `goal_not_ready` +- `schema_override_not_allowed` +- `variables_invalid` - `provider_unavailable` - `internal_error` - `not_found` @@ -67,8 +72,18 @@ See [Authentication](/authentication) for API key setup, server-only usage, and `idempotency_conflict` means the same idempotency key was reused with a different request body. Reuse keys only for the same external workflow operation. +`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. + +`goal_not_published` means an active Goal has no published RunSpec. `goal_not_executable` means the Goal is draft, paused, or retired. Existing Goal Run ids remain readable after a lifecycle change. + +`goal_not_ready` means the exact published RunSpec or provider contract does not currently pass the execution gate. + +`schema_override_not_allowed` means the Goal Run request attempted to supply a task, RunSpec selector, schema, materialization setting, or provider configuration owned by the published Goal. + +`variables_invalid` means the scalar variables do not satisfy the input schema of the RunSpec pinned by the Goal Run. + `call_not_ready` means the call task has not reached a terminal state. -`provider_unavailable` and `internal_error` are retryable only when the workflow can safely tolerate retry. +`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. -`not_found` means the call task id does not exist or is not visible to the current API key. +`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 new file mode 100644 index 0000000..9b07cd6 --- /dev/null +++ b/content/guides/goal-runs.mdx @@ -0,0 +1,422 @@ +--- +title: Goal Runs +description: Run a published Goal with one phone number and schema-validated variables. +--- + +Run a published Goal when your application repeats the same validated phone workflow for different targets. + +**API 0.6** · **Server-side** · **Asynchronous** + +A Goal owns its call behavior, voice region and locale, input schema, result schema, and provider contract. A Goal Run supplies only one E.164 phone number, per-Run variables, and a business-stable idempotency key. + +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`. + +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). + + + +## Example: confirm a delivery window + +Imagine a fulfillment system that needs to call customers before a scheduled delivery. An operations user authors and publishes one reusable Goal in CALL-E Chat: explain the order reference and proposed delivery window, ask the customer to confirm or request another time, and return a structured outcome. + +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`. +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. + +## List published Goals + +Use `GET /v1/goals` to discover the authenticated owner's active, listed Goals that have a published RunSpec: + +```bash +curl "https://api.heycall-e.com/v1/goals?limit=20" \ + -H "Authorization: Bearer $CALLE_API_KEY" +``` + +| Parameter | Location | Required | Description | +|---|---|---:|---| +| `Authorization` | Header | Yes | `Bearer $CALLE_API_KEY`. The API key determines the Goal owner scope; an owner id is never accepted from the request. | +| `limit` | Query | No | Number of Goal interfaces to return. Defaults to `20`; minimum `1`, maximum `100`. | +| `after` | Query | No | Opaque value from the previous response's `next_cursor`. Pass it unchanged and never build it from a Goal id. | + +```json +{ + "object": "list", + "data": [ + { + "object": "goal", + "id": "goal_delivery_confirmation", + "title": "Delivery window confirmation", + "description": "Call a customer to confirm the proposed delivery window or collect a preferred alternative.", + "status": "active", + "published_run_spec": { + "id": "rspec_delivery_v4", + "version": 4, + "input_schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "customer_name": { + "type": "string", + "description": "Name the voice agent may use when greeting the customer." + }, + "order_reference": { + "type": "string", + "description": "Customer-safe order reference to mention during the call." + }, + "delivery_window": { + "type": "string", + "description": "Proposed local delivery date and time window." + } + }, + "required": ["customer_name", "order_reference", "delivery_window"] + }, + "result_schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "delivery_outcome": { + "type": "string", + "enum": ["confirmed", "reschedule_requested", "declined", "unknown"] + }, + "preferred_window": { + "type": "string", + "description": "Alternative window requested by the customer, when stated." + } + }, + "required": ["delivery_outcome"] + } + } + } + ], + "next_cursor": null +} +``` + +When `next_cursor` is non-null, pass it unchanged as `after` on the next request. Cursors are opaque and results use stable Goal identity order. Hidden, draft, paused, retired, unpublished, and other owners' Goals are not returned. + +The list is a discovery and recovery tool, not title search. `title` and `description` help a developer recognize each published workflow, but they are not stable identity. Do not execute `data[0]` blindly: persist the `goal_id` received when the intended Goal is published. + + + +## Read one published interface + +You can also store the `goal_id` returned by a successful publish in CALL-E Chat and read it directly. Goal authoring and publication are not Developer API operations. + +| Parameter | Location | Required | Description | +|---|---|---:|---| +| `goal_id` | Path | Yes | Opaque Goal identity returned by publish success or Goal listing. It is not the nested RunSpec id. | +| `Authorization` | Header | Yes | Developer API key for the same owner that published the Goal. Cross-owner and hidden Goals return `404`. | + +```bash +curl "https://api.heycall-e.com/v1/goals/${CALLE_GOAL_ID}" \ + -H "Authorization: Bearer $CALLE_API_KEY" +``` + +`GET /v1/goals/{goal_id}` returns the current normalized input and result schemas: + +```json +{ + "id": "goal_delivery_confirmation", + "object": "goal", + "title": "Delivery window confirmation", + "description": "Call a customer to confirm the proposed delivery window or collect a preferred alternative.", + "status": "active", + "published_run_spec": { + "id": "rspec_delivery_v4", + "version": 4, + "input_schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "customer_name": { + "type": "string", + "description": "Name the voice agent may use when greeting the customer." + }, + "order_reference": { + "type": "string", + "description": "Customer-safe order reference to mention during the call." + }, + "delivery_window": { + "type": "string", + "description": "Proposed local delivery date and time window." + } + }, + "required": ["customer_name", "order_reference", "delivery_window"] + }, + "result_schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "delivery_outcome": { + "type": "string", + "enum": ["confirmed", "reschedule_requested", "declined", "unknown"] + }, + "preferred_window": { + "type": "string", + "description": "Alternative window requested by the customer, when stated." + } + }, + "required": ["delivery_outcome"] + } + } +} +``` + +The response is an interface, not a version selector. `title` and `description` explain the current published workflow without exposing its prompt. The response never exposes provider configuration, materialization guidance, or authoring history. + + + +## Create a Goal Run + +Send one top-level `phone` to `POST /v1/goals/{goal_id}/runs`. `variables` defaults to `{}` and is validated against the RunSpec pinned during acceptance. + +Request headers: + +| Parameter | Required | Description | +|---|---:|---| +| `Authorization` | Yes | `Bearer $CALLE_API_KEY`. Keep API keys in backend secrets; never send them from browser code. | +| `Idempotency-Key` | Yes | Stable identity for this logical business action, 1–255 characters. Derive it from a durable event such as `delivery:ORD-8472:confirm-window:v1`, store it with the order, and reuse it after timeouts. Do not generate a new UUID for every retry. | +| `Content-Type` | Yes | Use `application/json`. The request is a closed object; unknown fields and contract overrides are rejected. | + +Request body fields: + +| Field | Required | Description | +|---|---:|---| +| `phone` | Yes | Recipient phone in canonical E.164 form: `+`, country code, and subscriber number. Do not include spaces, parentheses, or extensions. Your application must be authorized to call it. | +| `variables` | No | Per-Run scalar business values defined by the published `input_schema`. Defaults to `{}`. | + +Do not send `target`, `region`, `locale`, or `display_name`. Voice region and callee locale belong to the published Goal and cannot be changed per Run. If the workflow needs a customer's name, define a field such as `customer_name` in the Goal's `input_schema` and send it through `variables`. + +`variables` is an object whose allowed keys, required keys, types, defaults, and enum values come from `published_run_spec.input_schema`. Values are limited to finite JSON scalars: strings, numbers, and booleans. Do not send nested objects, arrays, or `null`. In this example, all three delivery variables are required by version 4 of the published interface. + +```bash +curl -X POST "https://api.heycall-e.com/v1/goals/${CALLE_GOAL_ID}/runs" \ + -H "Authorization: Bearer $CALLE_API_KEY" \ + -H "Idempotency-Key: delivery:ORD-8472:confirm-window:v1" \ + -H "Content-Type: application/json" \ + -d '{ + "phone": "", + "variables": { + "customer_name": "Taylor", + "order_reference": "ORD-8472", + "delivery_window": "July 24, 2:00-4:00 PM" + } + }' +``` + +The first accepted request and an exact idempotent replay both return `201 Created`. The response contains the immutable RunSpec snapshot and never echoes the phone or variables: + +```json +{ + "id": "rgrp_delivery_ord_8472", + "object": "goal_run", + "goal_id": "goal_delivery_confirmation", + "run_id": "run_delivery_ord_8472", + "status": "in_progress", + "run_spec": { + "id": "rspec_delivery_v4", + "version": 4 + }, + "result": null, + "error": null, + "created_at": "2026-07-20T10:00:00Z", + "completed_at": null +} +``` + +`201` means CALL-E durably accepted responsibility for the Run. It does not mean the provider accepted the call, the recipient answered, or the structured result is ready. + +Save `GoalRun.id` from the response as `GOAL_RUN_ID`. The `Location` response header contains the same resource path, and `Retry-After` suggests when to begin polling. The response deliberately omits `phone` and `variables`; keep your own correlation between the idempotency key and order record. + +## Poll for results + +Poll the read-only Goal Run resource: + +```bash +curl "https://api.heycall-e.com/v1/goals/${CALLE_GOAL_ID}/runs/${GOAL_RUN_ID}" \ + -H "Authorization: Bearer $CALLE_API_KEY" +``` + +| Parameter | Location | Required | Description | +|---|---|---:|---| +| `goal_id` | Path | Yes | The Goal identity used when creating the Run. This prevents a Run id from being read through the wrong Goal. | +| `goal_run_id` | Path | Yes | `GoalRun.id` returned by `POST`, not the nested telephone `run_id`. | +| `Authorization` | Header | Yes | API key for the owning account. A missing, cross-owner, or mismatched resource returns `404`. | + +`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. + +```json +{ + "id": "rgrp_delivery_ord_8472", + "object": "goal_run", + "goal_id": "goal_delivery_confirmation", + "run_id": "run_delivery_ord_8472", + "status": "completed", + "run_spec": { + "id": "rspec_delivery_v4", + "version": 4 + }, + "result": { + "delivery_outcome": "reschedule_requested", + "preferred_window": "July 24 after 5:00 PM" + }, + "error": null, + "created_at": "2026-07-20T10:00:00Z", + "completed_at": "2026-07-20T10:01:00Z" +} +``` + +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: + +```json +{ + "status": "failed", + "result": null, + "error": { + "code": "no_answer", + "message": "No human answered the call.", + "detail_code": "no_human_answered" + } +} +``` + +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`. + + + +## Schemas belong to the Goal + +A Run request cannot provide task text, prompts, instructions, schemas, RunSpec identity, provider settings, or materialization settings. To change these fields, update the Goal in Chat and publish a new RunSpec version. Existing Runs keep their pinned version. + +Before deploying a variable change, compare the published `version`, `input_schema`, and `result_schema` with the contract expected by your integration. A newly published version affects new idempotency keys only; an exact replay of an existing business command keeps the original pinned RunSpec. + +## Idempotency + +`Idempotency-Key` is required, 1–255 characters after trimming, and scoped to the authenticated owner plus `goal_id`. + +- Retry a lost response with the same key, phone, and variables. +- Reusing a key with an equivalent request returns the same `GoalRun.id` and `run_id` with HTTP `201`. +- Reusing a key with changed input returns `409 idempotency_conflict`. +- Use a new stable business key for each new logical Run. + +The SDK does not generate the key because it must survive network ambiguity and process restarts. + +For example, if the first `POST` for order `ORD-8472` times out after CALL-E accepts it, retry with the same key and identical JSON. If the delivery window changes, create a new logical command and key such as `delivery:ORD-8472:confirm-window:v2`; changing the body while reusing the `v1` key returns `409 idempotency_conflict`. + + + +## Common integration errors + +CALL-E returns a stable JSON error envelope. Use `error.code` for program logic and keep `error.message` for logs or operator diagnostics. + +| HTTP / code | What it usually means | Recommended action | +|---|---|---| +| `400 invalid_request` | The JSON shape, query parameter, cursor, or idempotency header is malformed. | Fix the request. Retrying the same invalid payload will not help. | +| `401 unauthorized` / `403 forbidden` | The API key is missing, invalid, or cannot use the requested capability. | Verify the Bearer header, environment, and project permissions. Never fall back to a key from another tenant. | +| `404 not_found` | The Goal or Goal Run is missing, hidden, owned by another account, or paired with the wrong `goal_id`. | Check the saved `goal_id` and `GoalRun.id`. Do not probe other owner ids or substitute the nested `run_id`. | +| `409 idempotency_conflict` | The key already identifies a Run, but the phone or variables differ. | Retrieve the original workflow record. Use the existing key only for an exact replay; create a new business-version key for an intentionally new call. | +| `409 goal_not_published` / `goal_not_executable` / `goal_not_ready` | The Goal has no published interface, is not active, or its exact published contract cannot currently execute. | Stop automatic retries and ask the Goal owner to publish, activate, or repair the Goal. | +| `422 variables_invalid` | Required variables are missing, an extra key was supplied, or a value violates the pinned input schema. | Refresh `GET /v1/goals/{goal_id}`, compare its version and input schema, then correct the integration payload. | +| `422 invalid_phone` | `phone` is not a valid supported E.164 number. | Normalize and validate the number before calling. Do not send the documentation placeholder. | +| `422 schema_override_not_allowed` | The request tried to supply task text, schemas, RunSpec selectors, or provider/materialization settings owned by the Goal. | Remove those fields. Change the Goal in Chat and publish a new version instead. | +| `429 rate_limit_exceeded` | The caller has reached an API limit. | Honor `Retry-After` when present and retry with backoff while preserving the same idempotency key and request. | +| `502/503 provider_unavailable` | CALL-E could not durably accept the Run because a required upstream dependency was unavailable. | Retry with the same idempotency key and identical body. Once a `201` has been returned, later execution failures appear on the Goal Run resource instead of as POST errors. | + +See [Errors](/errors) for the HTTP error catalog. HTTP errors mean the API request itself was rejected. After a `201`, execution or result-processing problems are returned in the Goal Run's unified `error` field. + +## SDK examples + +The following methods define the API 0.6 SDK surface. The currently stable 0.2 packages remain Calls-only until matching TypeScript and Python minors are released. + +TypeScript uses camelCase on the existing `client.goals` resource: + +```ts +const goals = await client.goals.list({ limit: 20 }); +const nextGoals = goals.nextCursor + ? await client.goals.list({ limit: 20, after: goals.nextCursor }) + : null; + +const goal = await client.goals.get("goal_delivery_confirmation"); + +const run = await client.goals.run({ + goalId: goal.id, + phone: "", + variables: { + customer_name: "Taylor", + order_reference: "ORD-8472", + delivery_window: "July 24, 2:00-4:00 PM", + }, + idempotencyKey: "delivery:ORD-8472:confirm-window:v1", +}); + +const current = await client.goals.getRun(goal.id, run.id); +const completed = await client.goals.waitForResult(goal.id, run.id); +if (completed.error) { + throw new Error(`${completed.error.code}: ${completed.error.message}`); +} +const deliveryResult = completed.result; + +const createdAndCompleted = await client.goals.runAndWait({ + goalId: goal.id, + phone: "", + variables: { + customer_name: "Morgan", + order_reference: "ORD-8473", + delivery_window: "July 25, 9:00-11:00 AM", + }, + idempotencyKey: "delivery:ORD-8473:confirm-window:v1", +}); +``` + +Python keeps the same resource with snake_case methods: + +```python +goals = client.goals.list(limit=20) +next_goals = ( + client.goals.list(limit=20, after=goals["next_cursor"]) + if goals["next_cursor"] is not None + else None +) + +goal = client.goals.get("goal_delivery_confirmation") + +run = client.goals.run( + goal_id=goal["id"], + phone="", + variables={ + "customer_name": "Taylor", + "order_reference": "ORD-8472", + "delivery_window": "July 24, 2:00-4:00 PM", + }, + idempotency_key="delivery:ORD-8472:confirm-window:v1", +) + +current = client.goals.get_run(goal["id"], run["id"]) +completed = client.goals.wait_for_result(goal["id"], run["id"]) +if completed["error"] is not None: + raise RuntimeError( + f'{completed["error"]["code"]}: {completed["error"]["message"]}' + ) +delivery_result = completed["result"] + +created_and_completed = client.goals.run_and_wait( + goal_id=goal["id"], + phone="", + variables={ + "customer_name": "Morgan", + "order_reference": "ORD-8473", + "delivery_window": "July 25, 9:00-11:00 AM", + }, + idempotency_key="delivery:ORD-8473:confirm-window:v1", +) +``` + +`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. diff --git a/content/guides/quickstart.mdx b/content/guides/quickstart.mdx index 6eecb90..6dc5fc5 100644 --- a/content/guides/quickstart.mdx +++ b/content/guides/quickstart.mdx @@ -5,6 +5,8 @@ description: Create one call and read the structured result. 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. + **API key** · **TypeScript** · **Python** ## Install diff --git a/content/guides/sdks.mdx b/content/guides/sdks.mdx index a1445ec..0a05d0f 100644 --- a/content/guides/sdks.mdx +++ b/content/guides/sdks.mdx @@ -29,9 +29,29 @@ from calle import CalleClient The current stable server SDK packages are: -- TypeScript: `@call-e/calle@0.2.0` +- TypeScript: `@call-e/calle@0.2.2` - Python: `calle-ai==0.2.0` +These stable packages support the request-scoped Calls API. The Goal Runs +API and both SDK source candidates are aligned on `0.6.0`; the registry +packages remain Calls-only until that matching SDK release is published. + +## Goal Runs preview + +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 repositories The SDKs are maintained as separate repositories so they can have independent release cadence, CI, package metadata, and language-specific examples. @@ -48,7 +68,8 @@ Each repository includes public source code, examples, and security guidance: ## Local examples -Each SDK repository includes runnable examples for the server-side call task flow. +Each SDK repository currently includes runnable examples for the server-side +one-shot call task flow. TypeScript: @@ -72,7 +93,7 @@ uv run python examples/webhook_server.py The webhook examples listen on `POST /calle/webhook` and verify `CALL-E-Timestamp` plus `CALL-E-Signature` before parsing JSON. -## Supported methods +## Stable 0.2 methods The SDKs expose `context` as a reserved input for future SDK-side workflow data. The current SDKs do not send `context` to the API. @@ -201,7 +222,7 @@ pip install calle-ai Use pinned versions when your deployment process requires exact package reproducibility: ```bash -pnpm add @call-e/calle@0.2.0 +pnpm add @call-e/calle@0.2.2 pip install calle-ai==0.2.0 ``` diff --git a/openapi/calle.openapi.yaml b/openapi/calle.openapi.yaml index b45be32..2026fef 100644 --- a/openapi/calle.openapi.yaml +++ b/openapi/calle.openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: CALL-E Developer API - version: 0.2.0 + version: 0.6.0 description: Developer API contract used by the CALL-E TypeScript and Python SDKs. servers: - url: https://api.heycall-e.com @@ -313,6 +313,332 @@ paths: $ref: "#/components/responses/ErrorResponse" "500": $ref: "#/components/responses/ErrorResponse" + /v1/goals: + get: + operationId: listGoals + tags: + - goals + summary: List Goals + description: |- + List the authenticated owner's active, listed Goals that have a published RunSpec. For + example, a fulfillment service can inspect the published interface for its reusable + delivery-confirmation workflow before creating phone-specific Runs. + + Results are ordered by opaque Goal identity. Use `next_cursor` as the next request's + `after` value; clients must not parse or construct cursor values. `title` and `description` + help operators recognize each published workflow, but integrations should still store the + intended `goal_id` at publish time and must not execute the first list item blindly. + parameters: + - $ref: "#/components/parameters/GoalListLimit" + - $ref: "#/components/parameters/GoalListAfter" + responses: + "200": + description: Page of executable published Goal interfaces. + headers: + Cache-Control: + description: Prevent storage of owner-scoped Goal data. + schema: + type: string + enum: + - no-store + content: + application/json: + schema: + $ref: "#/components/schemas/GoalList" + examples: + deliveryConfirmation: + summary: Published delivery-window confirmation Goal. + value: + object: list + data: + - object: goal + id: goal_delivery_confirmation + title: Delivery window confirmation + description: Call a customer to confirm the proposed delivery window or collect a preferred alternative. + status: active + published_run_spec: + id: rspec_delivery_v4 + version: 4 + input_schema: + type: object + additionalProperties: false + properties: + customer_name: + type: string + description: Name the voice agent may use when greeting the customer. + order_reference: + type: string + description: Customer-safe order reference to mention during the call. + delivery_window: + type: string + description: Proposed local delivery date and time window. + required: + - customer_name + - order_reference + - delivery_window + result_schema: + type: object + additionalProperties: false + properties: + delivery_outcome: + type: string + enum: + - confirmed + - reschedule_requested + - declined + - unknown + preferred_window: + type: string + description: Alternative window requested by the customer, when stated. + required: + - delivery_outcome + next_cursor: null + "400": + $ref: "#/components/responses/ErrorResponse" + "401": + $ref: "#/components/responses/ErrorResponse" + "403": + $ref: "#/components/responses/ErrorResponse" + "409": + $ref: "#/components/responses/ErrorResponse" + "429": + $ref: "#/components/responses/ErrorResponse" + "500": + $ref: "#/components/responses/ErrorResponse" + /v1/goals/{goal_id}: + get: + operationId: getGoal + tags: + - goals + summary: Get Goal + description: |- + Get an owner-scoped active Goal and its currently published immutable RunSpec interface. + + Store the `goal_id` returned by Chat publish success. `title` and `description` explain the + current published workflow. Before sending a delivery-confirmation Run, use `input_schema` + to verify that `customer_name`, `order_reference`, and `delivery_window` match the current + published version, and use `result_schema` to prepare downstream outcome handling. + + This endpoint does not search by title, objective, or recency, and does not expose authoring + instructions, provider bindings, or result materialization guidance. The + server always resolves the current published pointer for a new business key. + parameters: + - $ref: "#/components/parameters/GoalId" + responses: + "200": + description: Active Goal and its currently published RunSpec interface. + headers: + Cache-Control: + description: Prevent storage of owner-scoped Goal data. + schema: + type: string + enum: + - no-store + content: + application/json: + schema: + $ref: "#/components/schemas/Goal" + "401": + $ref: "#/components/responses/ErrorResponse" + "403": + $ref: "#/components/responses/ErrorResponse" + "404": + $ref: "#/components/responses/ErrorResponse" + "409": + $ref: "#/components/responses/ErrorResponse" + "429": + $ref: "#/components/responses/ErrorResponse" + "500": + $ref: "#/components/responses/ErrorResponse" + "502": + $ref: "#/components/responses/ErrorResponse" + "503": + $ref: "#/components/responses/ErrorResponse" + /v1/goals/{goal_id}/runs: + post: + operationId: createGoalRun + tags: + - goal-runs + summary: Create Goal Run + description: |- + Create one singleton Goal Run for a published Goal. + + In a delivery-confirmation integration, `phone` identifies one customer and `variables` + provide that order's reference and proposed window. CALL-E atomically resolves and pins the + published RunSpec, validates the variables, and durably accepts execution. + + The request cannot select, replace, or relax schemas or the materialization contract. The + first accepted request and an exact idempotent replay both return `201` with the same Goal + Run identity. A `201` response means durable acceptance, not that the recipient answered or + that `result` is ready. + parameters: + - $ref: "#/components/parameters/GoalId" + - $ref: "#/components/parameters/GoalRunIdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateGoalRunRequest" + examples: + confirmDeliveryWindow: + summary: Ask one customer to confirm a proposed delivery window. + value: + phone: "" + variables: + customer_name: Taylor + order_reference: ORD-8472 + delivery_window: July 24, 2:00-4:00 PM + responses: + "201": + description: Goal Run durably accepted, or the current projection of an exact idempotent replay. + headers: + Cache-Control: + description: Prevent storage of owner-scoped Goal Run data. + schema: + type: string + enum: + - no-store + Location: + description: Relative URL of the Goal Run resource. + schema: + type: string + Retry-After: + description: Suggested number of seconds before polling the execution. + schema: + type: integer + minimum: 0 + content: + application/json: + schema: + $ref: "#/components/schemas/GoalRun" + examples: + acceptedDeliveryConfirmation: + summary: Delivery confirmation accepted and awaiting execution. + value: + object: goal_run + id: rgrp_delivery_ord_8472 + goal_id: goal_delivery_confirmation + run_id: run_delivery_ord_8472 + status: queued + run_spec: + id: rspec_delivery_v4 + version: 4 + result: null + error: null + created_at: "2026-07-22T10:00:00Z" + completed_at: null + "400": + $ref: "#/components/responses/ErrorResponse" + "401": + $ref: "#/components/responses/ErrorResponse" + "402": + $ref: "#/components/responses/ErrorResponse" + "403": + $ref: "#/components/responses/ErrorResponse" + "404": + $ref: "#/components/responses/ErrorResponse" + "409": + $ref: "#/components/responses/ErrorResponse" + "422": + $ref: "#/components/responses/ErrorResponse" + "429": + $ref: "#/components/responses/ErrorResponse" + "500": + $ref: "#/components/responses/ErrorResponse" + "502": + $ref: "#/components/responses/ErrorResponse" + "503": + $ref: "#/components/responses/ErrorResponse" + /v1/goals/{goal_id}/runs/{goal_run_id}: + get: + operationId: getGoalRun + tags: + - goal-runs + summary: Get Goal Run + description: |- + Get an owner- and Goal-scoped Run and its structured result facts. + + This is a pure read of the immutable execution snapshot. It does not resolve the current + 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. + parameters: + - $ref: "#/components/parameters/GoalId" + - $ref: "#/components/parameters/GoalRunId" + responses: + "200": + description: Current Goal Run execution and result state. + headers: + Cache-Control: + description: Prevent storage of owner-scoped Goal Run data. + schema: + type: string + enum: + - no-store + Retry-After: + description: Suggested number of seconds before polling again when results are pending. + schema: + type: integer + minimum: 0 + content: + application/json: + schema: + $ref: "#/components/schemas/GoalRun" + examples: + deliveryRescheduleRequested: + summary: Customer requested a different delivery window. + value: + object: goal_run + id: rgrp_delivery_ord_8472 + goal_id: goal_delivery_confirmation + run_id: run_delivery_ord_8472 + status: completed + run_spec: + id: rspec_delivery_v4 + version: 4 + result: + delivery_outcome: reschedule_requested + preferred_window: July 24 after 5:00 PM + error: null + created_at: "2026-07-22T10:00:00Z" + completed_at: "2026-07-22T10:01:12Z" + deliveryConfirmationNoAnswer: + summary: No human answered, so no business result is available. + value: + object: goal_run + id: rgrp_delivery_ord_8472 + goal_id: goal_delivery_confirmation + run_id: run_delivery_ord_8472 + status: failed + 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 + created_at: "2026-07-22T10:00:00Z" + completed_at: "2026-07-22T10:01:12Z" + "401": + $ref: "#/components/responses/ErrorResponse" + "403": + $ref: "#/components/responses/ErrorResponse" + "404": + $ref: "#/components/responses/ErrorResponse" + "429": + $ref: "#/components/responses/ErrorResponse" + "500": + $ref: "#/components/responses/ErrorResponse" + "502": + $ref: "#/components/responses/ErrorResponse" + "503": + $ref: "#/components/responses/ErrorResponse" /calle/webhook: post: operationId: receiveWebhookEvent @@ -421,6 +747,72 @@ components: type: string minLength: 1 maxLength: 255 + GoalRunIdempotencyKey: + name: Idempotency-Key + in: header + description: |- + Required business-stable identity for one logical Goal Run, scoped to the authenticated + owner and `goal_id`. Derive it from a durable workflow event, for example + `delivery:ORD-8472:confirm-window:v1`, and persist it before sending the request. + + Retry a timeout with the same key and byte-equivalent logical input. An exact replay returns + the original Goal Run with `201`; changing the phone or variables while reusing the key + returns `409 idempotency_conflict`. Do not generate a new random key for each network retry. + required: true + schema: + type: string + minLength: 1 + maxLength: 255 + example: delivery:ORD-8472:confirm-window:v1 + GoalListLimit: + name: limit + in: query + description: |- + Maximum number of Goal interfaces in this page. Defaults to `20`; values below `1` or above + `100` return `400 invalid_request`. A page can contain fewer items even when the limit is + larger. Continue only when `next_cursor` is non-null. + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + example: 20 + GoalListAfter: + name: after + in: query + description: |- + Opaque cursor returned as `next_cursor` by the immediately preceding list response. Pass it + unchanged together with the desired `limit`. Do not decode it, construct it from a Goal id, + or persist assumptions about its format. Omit this parameter for the first page. + required: false + schema: + type: string + minLength: 1 + example: goalcur_Z29hbF9kZWxpdmVyeV9jb25maXJtYXRpb24 + GoalId: + name: goal_id + in: path + description: |- + Opaque public Goal identity returned by CALL-E Chat publish success or `GET /v1/goals`. + Store it with your integration configuration. It identifies the reusable Goal and is + different from `published_run_spec.id`, `goal_run_id`, and the nested telephone `run_id`. + required: true + schema: + type: string + minLength: 1 + example: goal_delivery_confirmation + GoalRunId: + name: goal_run_id + in: path + description: |- + Opaque `GoalRun.id` returned by `POST /v1/goals/{goal_id}/runs`. Use it together with the + same `goal_id` when polling. Do not substitute the nested telephone `run_id`. + required: true + schema: + type: string + minLength: 1 + example: rgrp_delivery_ord_8472 CallId: name: call_id in: path @@ -457,11 +849,285 @@ components: responses: ErrorResponse: description: Stable API error. + headers: + Cache-Control: + description: Prevent storage of owner-scoped API error details. + schema: + type: string + enum: + - no-store content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" schemas: + GoalList: + type: object + description: |- + Cursor-paginated collection of the authenticated owner's listed, active, published Goal + interfaces. Use this for discovery or recovery of a known Goal id, not as title search. + additionalProperties: false + required: + - object + - data + - next_cursor + properties: + object: + type: string + description: Always `list` for paginated list responses. + enum: + - list + data: + type: array + description: |- + Goal interfaces in stable opaque-id order. Do not assume the first item is the Goal your + workflow should execute; store the intended `goal_id` when it is published. + items: + $ref: "#/components/schemas/Goal" + next_cursor: + type: + - string + - "null" + description: Opaque cursor for the next page. `null` means there are no more Goals. + Goal: + type: object + description: |- + Owner-scoped active Goal and its currently published immutable RunSpec interface. This is an + execution contract, not an authoring record: it includes a developer-facing title and + description but omits prompts, provider settings, and history. + additionalProperties: false + required: + - object + - id + - title + - description + - status + - published_run_spec + properties: + object: + type: string + description: Always `goal` for Goal responses. + enum: + - goal + id: + type: string + description: Opaque Goal identity. This is distinct from the nested RunSpec identity. + minLength: 1 + title: + type: + - string + - "null" + description: Short developer-facing title from the current published RunSpec. It may be null for an untitled Goal. + minLength: 1 + description: + type: string + description: Developer-facing summary of what the current published Goal does. This is not the execution prompt. + minLength: 1 + status: + type: string + description: Always `active`; non-executable Goals are not returned by this surface. + enum: + - active + published_run_spec: + description: |- + Currently published immutable interface used to validate new `variables` and interpret + `result`. New business keys pin this version at acceptance time. + $ref: "#/components/schemas/GoalPublishedRunSpec" + GoalPublishedRunSpec: + type: object + description: |- + Read-only published RunSpec interface for a Goal. Applications should inspect the schemas + before constructing variables and should record the version used by deployments. + additionalProperties: false + required: + - id + - version + - input_schema + - result_schema + properties: + id: + type: string + description: Opaque immutable RunSpec identity. + minLength: 1 + version: + type: integer + description: Monotonic published version within this Goal. A later publish affects only new Runs. + minimum: 1 + input_schema: + type: object + description: |- + Normalized JSON Schema for per-Run `variables`. Respect `required`, property types, enum + values, defaults, and `additionalProperties`; invalid input is rejected before execution. + additionalProperties: true + result_schema: + type: object + description: |- + Normalized JSON Schema for `result`. CALL-E exposes a result only after it is + validated against this exact pinned schema and durably persisted. + additionalProperties: true + CreateGoalRunRequest: + type: object + description: |- + One phone-specific submission against the Goal's currently published RunSpec. The object is + closed: target wrappers, per-Run region/locale/display-name hints, task text, schemas, RunSpec + selectors, provider settings, and unknown fields are not accepted. Region, callee locale, and + runtime profile come from the published Goal. Use the required `Idempotency-Key` header for + retry safety. + additionalProperties: false + required: + - phone + properties: + phone: + type: string + description: |- + Recipient phone in canonical E.164 form: `+`, country code, and subscriber number with + no spaces, punctuation, or extension. The caller must be authorized to contact it. + CALL-E validates it against the published Goal's fixed Voice Target policy. + pattern: "^\\+[1-9]\\d{7,14}$" + variables: + description: |- + Per-Run business context validated against the pinned published `input_schema`. Keys and + required fields vary by Goal. Values must be finite JSON strings, numbers, or booleans; + nested objects, arrays, and null are not supported. Omit for Goals whose schema accepts `{}`. + default: {} + $ref: "#/components/schemas/GoalVariables" + GoalScalar: + type: + - string + - number + - boolean + description: Scalar value supported by published Goal input and result Schema profiles. + GoalVariables: + type: object + description: |- + Dynamic variable map validated by the exact published input schema pinned during acceptance. + Read the Goal interface rather than hard-coding undocumented keys. + additionalProperties: + $ref: "#/components/schemas/GoalScalar" + 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. + additionalProperties: false + required: + - object + - id + - goal_id + - run_id + - run_spec + - status + - result + - error + - created_at + - completed_at + properties: + object: + type: string + enum: + - goal_run + id: + type: string + description: Public Goal Run identity. Persist this value and use it as `goal_run_id` when polling. + minLength: 1 + goal_id: + type: string + description: Goal identity supplied in the create path. + minLength: 1 + run_id: + type: string + description: Internal execution member exposed for correlation; do not use it in the Goal Run polling path. + minLength: 1 + run_spec: + description: Read-only identity and version of the exact RunSpec pinned by this Run. + $ref: "#/components/schemas/GoalRunSpecSnapshot" + status: + $ref: "#/components/schemas/GoalRunStatus" + 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. + 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`. + oneOf: + - $ref: "#/components/schemas/GoalRunError" + - type: "null" + created_at: + type: string + format: date-time + description: UTC time at which CALL-E durably accepted this Goal Run. + completed_at: + type: + - string + - "null" + format: date-time + description: UTC telephone-execution completion time, or `null` while execution is non-terminal. + GoalRunSpecSnapshot: + type: object + description: Exact immutable RunSpec identity and version pinned by a Goal Run. + additionalProperties: false + required: + - id + - version + properties: + id: + type: string + minLength: 1 + description: Exact immutable RunSpec id pinned when the Goal Run was accepted. + version: + type: integer + minimum: 1 + description: Published RunSpec version pinned for this Goal Run. + 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. + enum: + - queued + - in_progress + - completed + - failed + - canceled + GoalRunError: + type: object + description: Unified safe error returned when a Goal Run cannot produce a usable result. + additionalProperties: false + required: + - code + - message + - detail_code + properties: + code: + type: string + enum: + - call_failed + - no_answer + - declined + - timed_out + - canceled + - result_invalid + - result_unavailable + - result_failed + message: + type: string + description: Human-readable safe explanation. Do not parse this field for application logic. + minLength: 1 + detail_code: + type: + - string + - "null" + description: Optional low-cardinality diagnostic detail safe for logs or narrow application branching. + maxLength: 128 CreateCallRequest: type: object additionalProperties: false @@ -960,6 +1626,11 @@ components: - result_schema_invalid - recipient_result_schema_invalid - idempotency_conflict + - goal_not_published + - goal_not_executable + - goal_not_ready + - schema_override_not_allowed + - variables_invalid - provider_unavailable - internal_error - not_found diff --git a/scripts/verify-dist.mjs b/scripts/verify-dist.mjs index b0b84b7..0c86a6c 100644 --- a/scripts/verify-dist.mjs +++ b/scripts/verify-dist.mjs @@ -10,6 +10,7 @@ const guides = [ { slug: "quickstart", title: "Quickstart" }, { slug: "authentication", title: "Authentication" }, { slug: "calls", title: "Calls" }, + { slug: "goal-runs", title: "Goal Runs" }, { slug: "webhooks", title: "Webhooks" }, { slug: "errors", title: "Errors" }, { slug: "sdks", title: "SDKs" }, @@ -50,6 +51,14 @@ const apiCallsHtml = readRequired( "api-reference/calls.html", "Calls API Reference", ); +const apiGoalsHtml = readRequired( + "api-reference/goals.html", + "Goals API Reference", +); +const apiGoalRunsHtml = readRequired( + "api-reference/goal-runs.html", + "Goal Runs API Reference", +); readRequired("api-reference/webhooks.html", "Webhooks API Reference"); readRequired("api-reference/~schemas.html", "API schemas page"); readRequired("favicon.svg", "favicon"); @@ -142,6 +151,16 @@ if ( ) { throw new Error("Calls API Reference is missing prerendered operations."); } +if ( + !apiGoalsHtml.includes("List Goals") || + !apiGoalsHtml.includes("Get Goal") || + !apiGoalRunsHtml.includes("Create Goal Run") || + !apiGoalRunsHtml.includes("Get Goal Run") +) { + throw new Error( + "Goal API Reference is missing prerendered Goal or Goal Run operations.", + ); +} if (apiCallsHtml.includes(">Try it<") || apiCallsHtml.includes(">Send Request<")) { throw new Error("API Reference unexpectedly exposes a request playground."); } @@ -153,7 +172,12 @@ const openApi = readRequired( if (!openApi.includes("openapi: 3.1.0")) { throw new Error("OpenAPI document does not look like the public contract."); } -if (!openApi.includes("/v1/calls") || !openApi.includes("/calle/webhook")) { +if ( + !openApi.includes("/v1/calls") || + !openApi.includes("/v1/goals") || + !openApi.includes("/v1/goals/{goal_id}/runs") || + !openApi.includes("/calle/webhook") +) { throw new Error("OpenAPI document is missing public endpoint paths."); } diff --git a/tests/docs-site.spec.ts b/tests/docs-site.spec.ts index 016b8eb..fe0cdfd 100644 --- a/tests/docs-site.spec.ts +++ b/tests/docs-site.spec.ts @@ -4,6 +4,7 @@ const guides = [ { path: "/quickstart", heading: "Quickstart" }, { path: "/authentication", heading: "Authentication" }, { path: "/calls", heading: "Calls" }, + { path: "/goal-runs", heading: "Goal Runs" }, { path: "/webhooks", heading: "Webhooks" }, { path: "/errors", heading: "Errors" }, { path: "/sdks", heading: "SDKs" }, @@ -166,12 +167,20 @@ test("publishes crawler and OpenAPI artifacts", async ({ request }) => { expect(sitemapText).toContain( "https://docs.heycall-e.com/api-reference/calls", ); + expect(sitemapText).toContain( + "https://docs.heycall-e.com/api-reference/goals", + ); + expect(sitemapText).toContain( + "https://docs.heycall-e.com/api-reference/goal-runs", + ); const openApi = await request.get("/openapi/calle.openapi.yaml"); expect(openApi.status()).toBe(200); const openApiText = await openApi.text(); expect(openApiText).toContain("openapi: 3.1.0"); expect(openApiText).toContain("/v1/calls"); + expect(openApiText).toContain("/v1/goals"); + expect(openApiText).toContain("/v1/goals/{goal_id}/runs"); expect(openApiText).toContain("/calle/webhook"); const apiReference = await request.get("/api-reference"); @@ -198,6 +207,13 @@ test("bridges legacy hash routes to clean URLs", async ({ page }) => { page.getByRole("heading", { name: "Idempotency" }), ).toBeVisible(); + await page.goto("/#/goal-runs?section=create-a-run"); + await expect(page).toHaveURL(/\/goal-runs#create-a-run$/); + await expect(page.locator("#create-a-run")).toHaveCount(1); + await expect( + page.getByRole("heading", { name: "Create a Goal Run" }), + ).toBeVisible(); + await page.goto("/#/api-reference"); await expect(page).toHaveURL(/\/api-reference(?:\/calls)?$/); }); @@ -295,6 +311,36 @@ test("connects the Calls guide to HTTP and related references", async ({ ).toBeVisible(); }); +test("documents the published Goal Run flow on a clean route", async ({ + page, +}) => { + await page.goto("/goal-runs"); + + await expect( + page.getByRole("heading", { name: "Goal Runs" }), + ).toBeVisible(); + await expect( + page.getByRole("link", { name: "Goal Runs", exact: true }).first(), + ).toHaveAttribute("href", "/goal-runs"); + await expect( + page.locator("pre").filter({ + hasText: /POST[\s\S]*\/v1\/goals\/\$\{CALLE_GOAL_ID\}\/runs/, + }).first(), + ).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Poll for results" }), + ).toBeVisible(); + await expect( + page.locator('a[href="/errors"]').first(), + ).toBeVisible(); + await expect( + page.locator('a[href="/api-reference/goals"]').first(), + ).toBeVisible(); + await expect( + page.locator('a[href="/api-reference/goal-runs"]').first(), + ).toBeVisible(); +}); + test("renders a read-only OpenAPI reference", async ({ page }) => { await page.goto("/api-reference"); await expect( @@ -322,6 +368,14 @@ test("renders a read-only OpenAPI reference", async ({ page }) => { await expect( page.locator("h2#server-message"), ).toBeVisible(); + + await page.goto("/api-reference/goals"); + await expect(page.locator("h2#list-goals")).toBeVisible(); + await expect(page.locator("h2#get-goal")).toBeVisible(); + + await page.goto("/api-reference/goal-runs"); + await expect(page.locator("h2#create-goal-run")).toBeVisible(); + await expect(page.locator("h2#get-goal-run")).toBeVisible(); }); test("keeps the guide usable on a narrow screen", async ({ page }) => { diff --git a/zudoku.config.tsx b/zudoku.config.tsx index dabe3f4..58f9c77 100644 --- a/zudoku.config.tsx +++ b/zudoku.config.tsx @@ -12,6 +12,7 @@ const legacyHashRedirect = ` "/quickstart", "/authentication", "/calls", + "/goal-runs", "/webhooks", "/errors", "/sdks", @@ -144,6 +145,7 @@ const config = { { type: "doc", file: "quickstart", label: "Quickstart" }, { type: "doc", file: "authentication", label: "Authentication" }, { type: "doc", file: "calls", label: "Calls" }, + { type: "doc", file: "goal-runs", label: "Goal Runs" }, { type: "doc", file: "webhooks", label: "Webhooks" }, { type: "doc", file: "errors", label: "Errors" }, { type: "doc", file: "sdks", label: "SDKs" }, From adb5106b4fd800cbfc0b1cfb0bf34bcfba0b92fa Mon Sep 17 00:00:00 2001 From: "lin.si" Date: Thu, 30 Jul 2026 11:22:25 +0800 Subject: [PATCH 2/2] feat(docs): finalize Goal API release documentation --- CONTRIBUTING.md | 2 +- SECURITY.md | 4 +- content/guides/authentication.mdx | 13 +---- content/guides/calls.mdx | 10 ++-- content/guides/changelog.mdx | 22 +++++++- content/guides/errors.mdx | 4 +- content/guides/goal-runs.mdx | 2 +- content/guides/quickstart.mdx | 2 +- content/guides/sdks.mdx | 34 ++--------- content/guides/webhooks.mdx | 51 ++++++++++------- openapi/calle.openapi.yaml | 28 ++------- src/styles.css | 34 ++++++++--- tests/docs-site.spec.ts | 94 +++++++++++++++++++++++++------ zudoku.config.tsx | 46 +++++++-------- 14 files changed, 200 insertions(+), 146 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e7aa2a..bb64dcf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,6 @@ Keep pull requests focused and include: - Screenshots for visual changes. - Confirmation that `pnpm run validate` passes. -Do not include API keys, webhook secrets, phone numbers, customer data, private +Do not include API keys, credentials, phone numbers, customer data, private URLs, or internal operational details in issues, examples, tests, or pull requests. diff --git a/SECURITY.md b/SECURITY.md index 0d8b660..d5644e0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,10 +11,10 @@ Include: - The affected page, dependency, or API surface. - Reproduction steps or a proof of concept. - Expected impact. -- Relevant logs with API keys, webhook secrets, phone numbers, and other +- Relevant logs with API keys, credentials, phone numbers, and other private data removed. ## Secret handling -The documentation must never contain working API keys or webhook secrets. +The documentation must never contain working API keys or credentials. Examples must use environment variables and clearly non-production values. diff --git a/content/guides/authentication.mdx b/content/guides/authentication.mdx index 5b8fd1a..7839da3 100644 --- a/content/guides/authentication.mdx +++ b/content/guides/authentication.mdx @@ -75,17 +75,8 @@ Only call the Developer API from trusted server code: Do not call the Developer API directly from a browser, public frontend, or untrusted client. If a frontend user needs to start a call, send the request to your backend first, validate the user, and let your backend call CALL-E with its project API key. -## Webhook secrets - -Webhook secrets are different from API keys. - -Use API keys to authenticate your outbound requests to CALL-E. Use webhook secrets to verify inbound `POST /calle/webhook` requests from CALL-E before parsing JSON or triggering side effects. - -```bash -export CALLE_WEBHOOK_SECRET="whsec_test_key" -``` - -Verify `CALL-E-Timestamp` and `CALL-E-Signature` against the raw request body. See the [Webhooks](/webhooks) guide for runnable server examples. +See [Webhooks](/webhooks) when your backend needs terminal call task +notifications. ## Auth errors diff --git a/content/guides/calls.mdx b/content/guides/calls.mdx index 544fd10..0165bde 100644 --- a/content/guides/calls.mdx +++ b/content/guides/calls.mdx @@ -14,7 +14,7 @@ completion. `task` is the natural-language instruction for the call task. Keep it specific and outcome-oriented. -`recipients` is optional when the task text already includes the phone target. Use it for explicit batch recipients. Each recipient can include `phones` in E.164 format plus optional `region` and `locale` hints. +`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. Examples use phone placeholders such as `` and ``. Replace them with phone numbers you own or are authorized to call. @@ -82,8 +82,8 @@ const call = await client.calls.create( { task: "Call each recipient and ask whether they can attend Friday lunch in San Francisco.", recipients: [ - { phones: [""], region: "US", locale: "en-US" }, - { phones: [""], region: "US", locale: "en-US" }, + { phones: [""] }, + { phones: [""] }, ], resultSchema: { type: "object", @@ -116,8 +116,8 @@ Python: call = client.calls.create( task="Call each recipient and ask whether they can attend Friday lunch in San Francisco.", recipients=[ - {"phones": [""], "region": "US", "locale": "en-US"}, - {"phones": [""], "region": "US", "locale": "en-US"}, + {"phones": [""]}, + {"phones": [""]}, ], result_schema={ "type": "object", diff --git a/content/guides/changelog.mdx b/content/guides/changelog.mdx index 38da8c1..731e290 100644 --- a/content/guides/changelog.mdx +++ b/content/guides/changelog.mdx @@ -5,6 +5,24 @@ description: Track CALL-E Developer API and SDK product updates. Product updates for the CALL-E Developer API and server SDKs. +## July 29, 2026 + +### Terminal webhook delivery + +Call tasks that include `webhook_url` now send terminal event notifications to +that URL. Each notification includes a stable event id for idempotent +processing, and delivery is retried when the receiver does not return a `2xx` +response. CALL-E waits until the post-call outcome and requested structured +results are finalized, so the webhook contains the same complete terminal +snapshot returned by the Calls API. + +Current deliveries include `CALL-E-Event-Id` for deduplication and do not +include a webhook secret, timestamp, or signature header. Receivers must treat +the payload as untrusted input, validate the event shape, and compare the +header event id with the body `id`. The Python and TypeScript SDKs keep their +legacy signed-webhook helpers only for source compatibility and mark them +deprecated. + ## July 22, 2026 ### Phone-only Goal Run requests in API 0.6 @@ -53,7 +71,7 @@ See [Goal Runs](/goal-runs) for request, polling, version-pinning, and SDK examp pnpm add @call-e/calle ``` -Use `CalleClient` to create a call task, wait for a terminal result, and verify webhook payloads. +Use `CalleClient` to create a call task and wait for a terminal result. 3. Python server SDK: The CALL-E Python server SDK is available on PyPI. @@ -61,6 +79,6 @@ Use `CalleClient` to create a call task, wait for a terminal result, and verify pip install calle-ai ``` -Import `CalleClient` from `calle` to create call tasks, poll results, and verify webhook payloads. +Import `CalleClient` from `calle` to create call tasks and poll results. 4. Server-side integrations: API keys and server SDKs are designed for trusted backend services, workers, and automation systems. Browser SDKs, scheduled call tasks, cancel call task APIs, and project-level webhook management are not part of this release. diff --git a/content/guides/errors.mdx b/content/guides/errors.mdx index 551096c..44a7f15 100644 --- a/content/guides/errors.mdx +++ b/content/guides/errors.mdx @@ -58,11 +58,11 @@ See [Authentication](/authentication) for API key setup, server-only usage, and `insufficient_balance` means the project cannot start more calls until billing is resolved. -`unsupported_region` or `unsupported_language` means the request asks for a region or locale that CALL-E does not currently support. +`unsupported_region` or `unsupported_language` means CALL-E could not resolve a supported calling configuration for the request. `no_recipients` means CALL-E could not infer any recipients from the task and no explicit `recipients` were provided. -`invalid_recipient` means a recipient entry is malformed. Check that each explicit recipient uses supported fields such as `phones`, `region`, and `locale`. +`invalid_recipient` means a recipient entry is malformed. Check that each explicit recipient includes a non-empty `phones` array. `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. diff --git a/content/guides/goal-runs.mdx b/content/guides/goal-runs.mdx index 9b07cd6..b0cea39 100644 --- a/content/guides/goal-runs.mdx +++ b/content/guides/goal-runs.mdx @@ -247,7 +247,7 @@ curl "https://api.heycall-e.com/v1/goals/${CALLE_GOAL_ID}/runs/${GOAL_RUN_ID}" \ |---|---|---:|---| | `goal_id` | Path | Yes | The Goal identity used when creating the Run. This prevents a Run id from being read through the wrong Goal. | | `goal_run_id` | Path | Yes | `GoalRun.id` returned by `POST`, not the nested telephone `run_id`. | -| `Authorization` | Header | Yes | API key for the owning account. A missing, cross-owner, or mismatched resource returns `404`. | +| `Authorization` | Header | Yes | API key for the owning account. A missing or invalid key returns `401`; a missing, cross-owner, or mismatched resource returns `404`. | `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. diff --git a/content/guides/quickstart.mdx b/content/guides/quickstart.mdx index 6dc5fc5..00974ef 100644 --- a/content/guides/quickstart.mdx +++ b/content/guides/quickstart.mdx @@ -26,7 +26,7 @@ export CALLE_API_KEY="calle_test_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, server-only usage, and webhook secret boundaries. +See [Authentication](/authentication) for API key handling and server-only usage. ## Create a client diff --git a/content/guides/sdks.mdx b/content/guides/sdks.mdx index 0a05d0f..99c5b96 100644 --- a/content/guides/sdks.mdx +++ b/content/guides/sdks.mdx @@ -78,7 +78,6 @@ git clone https://github.com/CALLE-AI/server-sdk-typescript.git cd server-sdk-typescript pnpm install pnpm run example:create-and-wait -pnpm run example:webhook ``` Python: @@ -88,11 +87,8 @@ git clone https://github.com/CALLE-AI/server-sdk-python.git cd server-sdk-python uv sync --all-groups uv run python examples/create_and_wait.py -uv run python examples/webhook_server.py ``` -The webhook examples listen on `POST /calle/webhook` and verify `CALL-E-Timestamp` plus `CALL-E-Signature` before parsing JSON. - ## Stable 0.2 methods The SDKs expose `context` as a reserved input for future SDK-side workflow data. The current SDKs do not send `context` to the API. @@ -108,8 +104,8 @@ const created = await client.calls.create( { task: "Call each recipient and ask whether they can attend Friday lunch in San Francisco.", recipients: [ - { phones: [""], region: "US", locale: "en-US" }, - { phones: [""], region: "US", locale: "en-US" }, + { phones: [""] }, + { phones: [""] }, ], resultSchema: { type: "object", @@ -147,17 +143,6 @@ const createdAndCompleted = await client.calls.createAndWait( const events = await client.calls.listEvents(createdAndCompleted.id, { limit: 50, }); -const verified = client.webhooks.verify({ - rawBody, - timestamp: headers["CALL-E-Timestamp"], - signature: headers["CALL-E-Signature"], - secret: process.env.CALLE_WEBHOOK_SECRET!, -}); -const event = client.webhooks.unwrap({ - rawBody, - headers, - secret: process.env.CALLE_WEBHOOK_SECRET!, -}); ``` Python: @@ -166,8 +151,8 @@ Python: created = client.calls.create( task="Call each recipient and ask whether they can attend Friday lunch in San Francisco.", recipients=[ - {"phones": [""], "region": "US", "locale": "en-US"}, - {"phones": [""], "region": "US", "locale": "en-US"}, + {"phones": [""]}, + {"phones": [""]}, ], result_schema={ "type": "object", @@ -197,17 +182,6 @@ created_and_completed = client.calls.create_and_wait( interval_seconds=2, ) events = client.calls.list_events(created_and_completed["id"], limit=50) -verified = client.webhooks.verify( - raw_body=raw_body, - timestamp=headers["CALL-E-Timestamp"], - signature=headers["CALL-E-Signature"], - secret=os.environ["CALLE_WEBHOOK_SECRET"], -) -event = client.webhooks.unwrap( - raw_body=raw_body, - headers=headers, - secret=os.environ["CALLE_WEBHOOK_SECRET"], -) ``` ## Availability diff --git a/content/guides/webhooks.mdx b/content/guides/webhooks.mdx index 2072b6e..6f01537 100644 --- a/content/guides/webhooks.mdx +++ b/content/guides/webhooks.mdx @@ -1,12 +1,17 @@ --- title: Webhooks -description: Verify terminal webhooks and process events safely. +description: Receive terminal webhook events and process them safely. --- 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` is the same complete terminal snapshot returned +by `GET /v1/calls/{call_id}`. + ## Terminal events CALL-E sends terminal webhook events: @@ -17,7 +22,7 @@ CALL-E sends terminal webhook events: `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 terminal call task fields. +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. @@ -120,18 +125,23 @@ Example payload: } ``` -## Signature verification +## Receive events -Verify the signature against the raw request body before parsing JSON. The SDK helpers return the parsed event only after the timestamp and signature are valid. +Current CALL-E webhook delivery does not use a webhook secret, +`CALL-E-Timestamp`, or `CALL-E-Signature`. Treat the receiver as a public, +untrusted-input boundary: validate the JSON shape, require +`CALL-E-Event-Id`, and reject the request when that header does not match the +body event `id`. TypeScript: ```ts -const event = client.webhooks.unwrap({ - rawBody, - headers, - secret: process.env.CALLE_WEBHOOK_SECRET!, -}); +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); @@ -141,24 +151,23 @@ if (event.type === "call.completed") { Python: ```python -event = client.webhooks.unwrap( - raw_body=raw_body, - headers=headers, - secret=os.environ["CALLE_WEBHOOK_SECRET"], -) +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"]) ``` -## Example servers - -The SDK repositories include minimal webhook servers built with runtime standard libraries: - -- [TypeScript webhook server](https://github.com/CALLE-AI/server-sdk-typescript/blob/main/examples/webhook-server.ts) -- [Python webhook server](https://github.com/CALLE-AI/server-sdk-python/blob/main/examples/webhook_server.py) +Return a `2xx` response after accepting the event. CALL-E retries delivery when +the receiver returns a non-`2xx` response or the request fails. -Both examples listen on `POST /calle/webhook`, preserve the raw request body for signature verification, and return `200` only after `client.webhooks.unwrap` succeeds. +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 diff --git a/openapi/calle.openapi.yaml b/openapi/calle.openapi.yaml index 2026fef..269e273 100644 --- a/openapi/calle.openapi.yaml +++ b/openapi/calle.openapi.yaml @@ -645,7 +645,7 @@ paths: tags: - webhooks summary: Server Message - description: CALL-E sends this request to your server when a call reaches a terminal state. Configure this URL with `webhook_url` on create call or through project-level webhook settings. + 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} @@ -656,8 +656,6 @@ paths: description: Hostname for your webhook receiver. parameters: - $ref: "#/components/parameters/WebhookEventId" - - $ref: "#/components/parameters/WebhookTimestamp" - - $ref: "#/components/parameters/WebhookSignature" requestBody: required: true content: @@ -731,7 +729,7 @@ paths: value: ok: true "400": - description: Webhook rejected because the payload or signature was invalid. + description: Webhook rejected because the payload was invalid. components: securitySchemes: bearerAuth: @@ -830,22 +828,6 @@ components: type: string pattern: "^evt_[A-Za-z0-9_-]+$" example: evt_123 - WebhookTimestamp: - name: CALL-E-Timestamp - in: header - description: Unix timestamp used in the signed payload. Verify this header together with `CALL-E-Signature` before parsing JSON. - required: true - schema: - type: string - example: "1780309260" - WebhookSignature: - name: CALL-E-Signature - in: header - description: HMAC SHA-256 signature in the form `v1=`, computed over `timestamp + "." + raw_body` with your webhook secret. - required: true - schema: - type: string - example: v1=4d967f8f2b85f9c7d7f7ad1b9f8c5e3a5f1c2d3e4f5061728394050607080910 responses: ErrorResponse: description: Stable API error. @@ -1209,7 +1191,7 @@ components: description: Recipient country or region code used for routing and compliance checks, for example `US`. CallStatus: type: string - description: Current lifecycle state of a CALL-E call. + description: Current lifecycle state of a CALL-E call. `in_progress` includes post-call result finalization; terminal states are published only after the post-call outcome is available. enum: - queued - in_progress @@ -1481,7 +1463,7 @@ components: type: - string - "null" - description: ISO 8601 timestamp when the call reached a terminal state. `null` while queued or in progress. + description: ISO 8601 timestamp when the complete terminal call result was published. `null` while queued or in progress. format: date-time DeveloperEvent: type: object @@ -1580,7 +1562,7 @@ components: description: Terminal call task snapshot associated with the webhook event. $ref: "#/components/schemas/WebhookCallData" WebhookCallData: - description: Terminal call task state included in webhook events. This shape is the same stable `call_task` object returned by the calls API. + 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" WebhookAcknowledgement: diff --git a/src/styles.css b/src/styles.css index 738893a..7db01ca 100644 --- a/src/styles.css +++ b/src/styles.css @@ -4,7 +4,11 @@ --side-nav-width: 18.75rem; --padding-content-top: 2.5rem; --sidecar-grid-cols: minmax(0, 42rem) minmax(13rem, 15rem); - --call-signal: #0a9f90; + --call-shell: #f4f9ff; + --call-action: #26272b; + --call-action-hover: #111827; + --call-action-foreground: #ffffff; + --call-signal: #2563eb; --code-background: #0b0f14; --code-chrome: #11161d; --code-border: #242b35; @@ -13,7 +17,11 @@ } .dark { - --call-signal: #35c9b8; + --call-shell: #111827; + --call-action: #f7fafd; + --call-action-hover: #ffffff; + --call-action-foreground: #111827; + --call-signal: #60a5fa; } body { @@ -36,7 +44,7 @@ samp { } header[data-pagefind-ignore="all"] { - background: color-mix(in srgb, var(--background) 96%, transparent); + background: color-mix(in srgb, var(--call-shell) 96%, transparent); } header[data-pagefind-ignore="all"]::after { @@ -163,20 +171,21 @@ header[data-pagefind-ignore="all"] height: 2.25rem; padding-inline: 1rem; border-radius: 999px; - background: var(--primary); - color: var(--primary-foreground); - box-shadow: 0 1px 2px color-mix(in srgb, var(--primary) 24%, transparent); + background: var(--call-action); + color: var(--call-action-foreground); + box-shadow: 0 1px 2px color-mix(in srgb, var(--call-action) 24%, transparent); } header[data-pagefind-ignore="all"] nav[aria-label="Main"] a[href="https://dashboard.heycall-e.com/"]:hover { - background: color-mix(in srgb, var(--primary) 88%, black); - color: var(--primary-foreground); + background: var(--call-action-hover); + color: var(--call-action-foreground); } nav[class*="overflow-y-auto"][class*="shrink-0"] { padding-top: 1.5rem; + background: var(--call-shell); } nav[class*="overflow-y-auto"][class*="shrink-0"] a { @@ -203,6 +212,7 @@ nav[class*="overflow-y-auto"][class*="shrink-0"] a[aria-current="page"] { a[href="/quickstart"], a[href="/authentication"], a[href="/calls"], + a[href="/goal-runs"], a[href="/webhooks"], a[href="/errors"], a[href="/sdks"], @@ -226,6 +236,7 @@ nav[class*="overflow-y-auto"][class*="shrink-0"] a[aria-current="page"] { a[href="/quickstart"], a[href="/authentication"], a[href="/calls"], + a[href="/goal-runs"], a[href="/webhooks"], a[href="/errors"], a[href="/sdks"], @@ -242,6 +253,7 @@ nav[class*="overflow-y-auto"][class*="shrink-0"] a[aria-current="page"] { a[href="/quickstart"], a[href="/authentication"], a[href="/calls"], + a[href="/goal-runs"], a[href="/webhooks"], a[href="/errors"], a[href="/sdks"], @@ -258,6 +270,7 @@ nav[class*="overflow-y-auto"][class*="shrink-0"] a[aria-current="page"] { a[href="/quickstart"], a[href="/authentication"], a[href="/calls"], + a[href="/goal-runs"], a[href="/webhooks"], a[href="/errors"], a[href="/sdks"], @@ -284,6 +297,11 @@ nav[class*="overflow-y-auto"][class*="shrink-0"] a[aria-current="page"] { content: "Create, track, and read calls."; } + nav[class*="overflow-y-auto"][class*="shrink-0"] + a[href="/goal-runs"]::after { + content: "Run published phone workflows."; + } + nav[class*="overflow-y-auto"][class*="shrink-0"] a[href="/webhooks"]::after { content: "Receive terminal call events."; } diff --git a/tests/docs-site.spec.ts b/tests/docs-site.spec.ts index fe0cdfd..e30d2cb 100644 --- a/tests/docs-site.spec.ts +++ b/tests/docs-site.spec.ts @@ -40,21 +40,23 @@ test("serves prerendered guides on clean URLs", async ({ page, request }) => { test("uses the roomy CALL-E guide navigation on desktop", async ({ page }) => { await page.goto("/quickstart"); - const activeGuide = page.locator( - 'nav[class*="overflow-y-auto"][class*="shrink-0"] a[href="/quickstart"]', - ); - await expect(activeGuide).toBeVisible(); - - const activeGuideHeight = await activeGuide.evaluate((element) => { - return element.getBoundingClientRect().height; - }); - const descriptionContent = await activeGuide.evaluate((element) => { - return window.getComputedStyle(element, "::after").content; - }); - - expect(activeGuideHeight).toBeGreaterThanOrEqual(56); - expect(descriptionContent).not.toBe("none"); - expect(descriptionContent).not.toBe('""'); + for (const guide of guides) { + const guideLink = page.locator( + `nav[class*="overflow-y-auto"][class*="shrink-0"] a[href="${guide.path}"]`, + ); + await expect(guideLink).toBeVisible(); + + const guideHeight = await guideLink.evaluate((element) => { + return element.getBoundingClientRect().height; + }); + const descriptionContent = await guideLink.evaluate((element) => { + return window.getComputedStyle(element, "::after").content; + }); + + expect(guideHeight).toBeGreaterThanOrEqual(56); + expect(descriptionContent).not.toBe("none"); + expect(descriptionContent).not.toBe('""'); + } }); test("offers system, light, and dark appearance modes", async ({ page }) => { @@ -244,6 +246,62 @@ test("preserves CALL-E brand and favicon metadata", async ({ page }) => { ); }); +test("uses the CALL-E Web palette for docs chrome", async ({ page }) => { + await page.emulateMedia({ colorScheme: "light" }); + await page.goto("/quickstart"); + + const guideNav = page.locator( + 'nav[class*="overflow-y-auto"][class*="shrink-0"]', + ); + const activeGuide = guideNav.locator('a[href="/quickstart"]'); + const dashboard = page.getByRole("link", { + name: "Dashboard", + exact: true, + }); + + await expect(page.locator("body")).toHaveCSS("color", "rgb(46, 47, 51)"); + await expect(guideNav).toHaveCSS( + "background-color", + "rgb(244, 249, 255)", + ); + await expect(activeGuide).toHaveCSS( + "background-color", + "rgb(239, 246, 255)", + ); + await expect(activeGuide).toHaveCSS( + "box-shadow", + /rgb\(37, 99, 235\).*2px/, + ); + await expect(dashboard).toHaveCSS( + "background-color", + "rgb(38, 39, 43)", + ); + + await page.emulateMedia({ colorScheme: "dark" }); + await expect(page.locator("html")).toHaveClass("dark"); + await expect(page.locator("body")).toHaveCSS( + "background-color", + "rgb(17, 24, 39)", + ); + await expect(guideNav).toHaveCSS( + "background-color", + "rgb(17, 24, 39)", + ); + await expect(activeGuide).toHaveCSS( + "background-color", + "rgb(30, 58, 95)", + ); + await expect(activeGuide).toHaveCSS( + "box-shadow", + /rgb\(96, 165, 250\).*2px/, + ); + await expect(dashboard).toHaveCSS( + "background-color", + "rgb(247, 250, 253)", + ); + await expect(dashboard).toHaveCSS("color", "rgb(17, 24, 39)"); +}); + test("keeps quickstart requests minimal and safe to copy", async ({ page }) => { await page.goto("/quickstart"); @@ -278,6 +336,10 @@ test("preserves authentication, webhook, and SDK guidance", async ({ }).first(), ).toBeVisible(); await expect(page.getByText("transcript_turns").first()).toBeVisible(); + await expect( + page.getByText(/does not use a webhook secret/), + ).toBeVisible(); + await expect(page.getByText(/cryptographic proof of the sender/)).toBeVisible(); await page.goto("/sdks"); await expect( @@ -320,7 +382,7 @@ test("documents the published Goal Run flow on a clean route", async ({ page.getByRole("heading", { name: "Goal Runs" }), ).toBeVisible(); await expect( - page.getByRole("link", { name: "Goal Runs", exact: true }).first(), + page.locator('a[href="/goal-runs"]').first(), ).toHaveAttribute("href", "/goal-runs"); await expect( page.locator("pre").filter({ diff --git a/zudoku.config.tsx b/zudoku.config.tsx index 58f9c77..58bd9df 100644 --- a/zudoku.config.tsx +++ b/zudoku.config.tsx @@ -202,34 +202,34 @@ const config = { theme: { light: { background: "#ffffff", - foreground: "#2a2a2a", + foreground: "#2e2f33", card: "#ffffff", - cardForeground: "#2a2a2a", - primary: "#087f75", + cardForeground: "#2e2f33", + primary: "#2563eb", primaryForeground: "#ffffff", - muted: "#f5f6f7", - mutedForeground: "#666b73", - accent: "#e2f7f3", - accentForeground: "#07766d", - border: "#e6e8eb", - input: "#dfe3e6", - ring: "#0a9f90", + muted: "#f7fafd", + mutedForeground: "#5b6472", + accent: "#eff6ff", + accentForeground: "#1d4ed8", + border: "#dde5ef", + input: "#dde5ef", + ring: "#2563eb", radius: "0.5rem", }, dark: { - background: "#0f1217", - foreground: "#f3f4f6", - card: "#151920", - cardForeground: "#f3f4f6", - primary: "#35c9b8", - primaryForeground: "#08110f", - muted: "#1b2028", - mutedForeground: "#a4a9b1", - accent: "#163c38", - accentForeground: "#91e8dd", - border: "#2b3038", - input: "#343a44", - ring: "#35c9b8", + background: "#111827", + foreground: "#f7fafd", + card: "#182235", + cardForeground: "#f7fafd", + primary: "#60a5fa", + primaryForeground: "#0b1220", + muted: "#182235", + mutedForeground: "#a8b3c7", + accent: "#1e3a5f", + accentForeground: "#bfdbfe", + border: "#2d3b50", + input: "#34445b", + ring: "#60a5fa", radius: "0.5rem", }, },