Source: https://docs.cumuluslabs.io/sdk-http-contracts
Content version: f10231d9

# Customer HTTP callbacks

Cumulus calls your application to execute a tool, obtain initial call context, deliver a lifecycle event or tokenize private keypad input. You host these endpoints in any language or framework. The example URLs below belong to a fictional customer application; choose your own paths and configure them through [bindings and integrations](/docs/sdk-bindings).

The TypeScript [server adapters](/docs/sdk-reference/server) implement these contracts. [Customer HTTP adapters](/docs/sdk-server) shows how to mount them. The payloads below let you implement the same endpoints in Python or another framework without installing the TypeScript SDK.

## Configure and authenticate your endpoints

Expose a public HTTPS URL on port 443. Publish the tool or context action with its input/output schemas, deadline and pinned secret reference. Your application receives the matching secret bytes from your secret manager. Event destinations have their own signing secret and subscriptions.

Tool, context and vault adapters support these authentication choices. Use the same choice in the published binding and your receiver:

| Scheme | Request header and verification |
| --- | --- |
| `bearer` | `Authorization: Bearer <shared-secret>`; compare with your configured secret. |
| `header` | A configured header, such as `X-Customer-Token: <shared-secret>`. |
| `retell_signature` | `X-Retell-Signature: v=<unix-milliseconds>,d=<lowercase-hex-hmac>`. Compute HMAC-SHA256 over the exact body bytes followed by the timestamp's ASCII bytes. The default freshness tolerance is five minutes. |

The adapters require an explicit integration name and a secret of at least 16 characters. The integration name is receiver configuration, not a request field. Event signatures use the separate protocol described below and require at least 32 characters. Compare secrets and signatures in constant time. Verify signatures against the original bytes before parsing JSON.

## Tool execution

For this example, publish a POST tool whose input requires a string `topic` and whose output requires a string `guidance`. Cumulus sends:

```http
POST https://service.example.com/cumulus/tools/guide
Content-Type: application/json
Authorization: Bearer <shared-secret>
Idempotency-Key: 55555555-5555-4555-8555-555555555555
```

```json
{
  "args": { "topic": "repair" },
  "call": {
    "call_id": "33333333-3333-4333-8333-333333333333",
    "run_id": "22222222-2222-4222-8222-222222222222",
    "agent_id": "service_guide",
    "metadata": { "request_source": "service_desk" }
  }
}
```

Return HTTP `200` with the declared output object directly:

```json
{ "guidance": "Ask which equipment needs repair." }
```

`call_id` identifies the conversation; `run_id` identifies the run used by the runs API. They are different identities. `agent_id` identifies the agent, not its publication version. The SDK requires `call_id` and `agent_id`; `run_id` and `metadata` are optional in its envelope. Require a run ID in your application if an authorized run lookup is necessary. Metadata is application context, not proof of permission to access an account.

The `Idempotency-Key` identifies this tool effect. For `effect_key` tools it is required. Persist intent and outcome under your configured integration and this key. A duplicate with the same input should return the saved result; reject reuse with different input. The adapter validates the key's presence but does not deduplicate your work.

GET tools receive arguments as query parameters, without an `args`/`call` body:

```http
GET https://service.example.com/cumulus/tools/guide?topic=repair
Authorization: Bearer <shared-secret>
Idempotency-Key: 55555555-5555-4555-8555-555555555555
```

String arguments remain strings; other argument values use JSON encoding in the query. Duplicate parameter names are invalid. GET adapter context has no conversation, run or agent identity, and empty metadata. Use GET for read-only work; `none_read_only` permits omission of the effect key.

Return before the tool's configured deadline, which can be 500–120,000 milliseconds. The SDK adapter uses that deadline and defaults to a 65,536-byte request limit, configurable with `maxBodyBytes`. Validate arguments before invoking business logic and validate the response against the published output schema. An invalid output after execution is an uncertain outcome.

## Initial inbound context

Configure a dedicated phone POST context action, an `effect_key` policy and declared output variables. This endpoint runs before the agent begins speaking. Its body is the full event envelope below, without the tool's `args` wrapper:

```http
POST https://service.example.com/cumulus/context
Content-Type: application/json
Authorization: Bearer <shared-secret>
Idempotency-Key: 66666666-6666-4666-8666-666666666666
```

```json
{
  "event": "call_inbound",
  "call_inbound": {
    "agent_id": "service_guide",
    "agent_version": 1,
    "from_number": "+12025550123",
    "to_number": "+12025550124"
  }
}
```

Both phone numbers are fictional. `agent_version` is an integer identifying the selected publication. Return HTTP `200` with declared string variables:

```json
{ "call_inbound": { "dynamic_variables": { "demo_channel": "phone" } } }
```

To reject the call, return HTTP `200` with:

```json
{ "call_inbound": { "reject": true } }
```

Declare `demo_channel` in the integration's `contextOutputs` and the SDK adapter's `variables` allowlist. Return strings only, at most 4,096 characters per value. Do not replace system fields such as `user_number` or `agent_id`. An incoming number can select a candidate account; perform your business verification before exposing or changing its data.

The SDK's default deadline is three seconds, configurable with `timeoutMs`; the published context action must allow at most ten seconds. Its default request limit is 65,536 bytes. Preserve the effect identity for reconciliation: a lost response is uncertain, and Cumulus does not automatically resend an already-sent context effect. Invalid or missing required context can prevent agent startup.

## Lifecycle events

Publish a notification destination with `semantic.v1` format and subscriptions to `run.admitted`, `run.ended`, `recording.updated` or `analysis.updated`. Each POST contains one complete semantic event. For example:

```http
POST https://service.example.com/cumulus/events
Content-Type: application/json
X-Talos-Event-Id: 44444444-4444-4444-8444-444444444444
Idempotency-Key: 44444444-4444-4444-8444-444444444444
X-Talos-Signature: t=<unix-seconds>,v1=<lowercase-hex-hmac>
```

```json
{
  "owner_id": "talos",
  "environment": "test",
  "tenant_id": "11111111-1111-4111-8111-111111111111",
  "event_id": "44444444-4444-4444-8444-444444444444",
  "schema_version": 1,
  "subject": { "kind": "run", "id": "22222222-2222-4222-8222-222222222222" },
  "correlation_id": "22222222-2222-4222-8222-222222222222",
  "causation_id": null,
  "producer": "talos",
  "producer_version": "go-native-v2",
  "occurred_at": "2026-10-21T12:00:00.000Z",
  "privacy_class": "operational",
  "idempotency_key": "22222222-2222-4222-8222-222222222222:recording",
  "content": null,
  "run": {
    "run_id": "22222222-2222-4222-8222-222222222222",
    "attempt_id": null,
    "publication": {
      "agent_id": "service_guide",
      "version": 1,
      "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    }
  },
  "policy": null,
  "type": "recording.updated",
  "data": { "state": "not_required", "reason": "not_recorded" },
  "sequence": 1,
  "recorded_at": "2026-10-21T12:00:00.000Z"
}
```

Verify `X-Talos-Signature` by computing HMAC-SHA256 over the timestamp's ASCII bytes, a literal period (`.`), then the exact body bytes. The timestamp uses seconds; accept at most five minutes of clock difference by default. This differs from the millisecond tool signature above. Check the signature before trusting the payload, then validate the complete `SemanticEvent` schema.

Deduplicate the verified `event_id`. Both identity headers carry that ID; the body's `idempotency_key` is a separate event identity field and need not equal it. Durably save the event and any background-job intent before acknowledging. A successful SDK receiver returns HTTP `200`:

```json
{ "received": true }
```

Cumulus accepts a `2xx` response and allows 20 seconds for delivery. A lost response is uncertain; notifications are not a guaranteed replay feed. Fetch current resources when reconciling missed work. Run end, recording and analysis are separate events; one does not establish another's readiness.

Accept up to 262,144 body bytes to cover the sender's event limit. With `createEventHandler`, set `maxBodyBytes: 262144`; its default is 65,536. The adapter awaits your `consume` callback before responding and does not impose its own processing deadline. Keep persistence inside the sender's 20-second window.

## Private vault tokenization

Use a dedicated POST tool with `purpose: 'tokenization'`, an `effect_key` policy, the declared private-field input schema and an authenticated vault binding. Configure the same `tokenIdPath` in the capture policy and receiver. [Private workflows](/docs/sdk-private) covers consent and keypad collection.

For an eight-digit synthetic reference and `tokenIdPath: 'id'`:

```http
POST https://vault.example.com/cumulus/tokenize
Content-Type: application/json
Authorization: Bearer <shared-secret>
Idempotency-Key: 77777777-7777-4777-8777-777777777777
```

```json
{
  "args": { "reference": "87654321" },
  "call": {
    "call_id": "33333333-3333-4333-8333-333333333333",
    "run_id": "22222222-2222-4222-8222-222222222222",
    "agent_id": "service_guide",
    "metadata": {}
  }
}
```

Return HTTP `200` containing only the configured UUID token field:

```json
{ "id": "9e89b3a8-a97a-41d1-9692-55832c5194ba" }
```

The dedicated SDK adapter accepts at most 16,384 request bytes and uses the tool deadline. It projects only the configured token field, including its nesting when `tokenIdPath` is dotted. An ordinary tool adapter refuses tokenization-purpose tools.

Deduplicate vault work durably by integration and effect ID. Keep raw digits and provider error payloads out of logs, traces and stored receipts. If the vault definitely refused and created no token, return HTTP `422` with `{ "error": { "code": "tokenization_declined" } }` (`TokenizationDeclined` in the SDK). A timeout, malformed token or other error after invocation is uncertain: reconcile the original operation before creating another token.

## Errors and replay

These are the SDK adapters' JSON responses; another framework should preserve the same status and outcome semantics:

| Status | `error.code` | Meaning |
| --- | --- | --- |
| `400` | `invalid_tool_payload` | Invalid contract input/identity, missing effect key or exceeded body limit; malformed tool, context or vault JSON. |
| `401` | `unauthenticated` | Authentication or signature freshness failed. |
| `405` | `method_not_allowed` | Wrong HTTP method. |
| `422` | `tokenization_declined` | Vault definitely refused without creating a token. |
| `500` | `tool_outcome_uncertain` | Deadline, invalid post-execution output or uncertain vault outcome. |
| `500` | `tool_execution_failed` | Other handler failure; business work may already have occurred. |

For example, invalid tool input returns:

```json
{ "error": { "code": "invalid_tool_payload", "message": "Payload does not match the declared contract" } }
```

Cumulus treats ordinary tool `4xx` responses as failed, except `408` and `429`, which are uncertain. Non-`2xx` responses outside those definitive failures are also uncertain. An HTTP timeout does not undo a booking or vault write. Cumulus does not automatically resend an already-sent tool effect; retain your durable receipt so any reconciliation or duplicate request uses the original identity. See [SDK errors](/docs/sdk-errors) and [calls, results and events](/docs/sdk-runtime).
