Source: https://docs.cumuluslabs.io/sdk-reference/server
Content version: 3facc87e

# Customer HTTP handlers reference

When an agent needs information from your application, Cumulus sends an HTTP request to your server. This module helps your Node application receive that request safely, run your business function and return its result. Your application owns the endpoint and its data; Cumulus runs the conversation.

Using Python? Start with the [Python SDK](/docs/sdk-python) for API calls. To receive tools, context and events in a Python server, implement the [customer HTTP callback contracts](/docs/sdk-http-contracts); this page documents the TypeScript adapters.

Import from `@cumulus/sdk/server`. This page documents SDK 0.3.4. Follow the [customer http handlers guide](/docs/sdk-server) for a worked workflow.

## Build and test a ticket lookup {#example}

This complete server reads two fictional tickets. It makes no bookings, changes no records and places no calls. Use Node 22 or newer in a project with [SDK 0.3.4 installed](/docs/sdks). Save the code as `ticket-server.mts`.

```typescript
import { createServer } from "node:http";
import { pathToFileURL } from "node:url";
import { defineTool } from "@cumulus/sdk/agent";
import { implementTool, createToolHandler, nodeHandler } from "@cumulus/sdk/server";

export const lookup = defineTool({
  key: "lookup_ticket", description: "Read the status of a service ticket.",
  input: {
    type: "object", properties: { reference: { type: "string" } },
    required: ["reference"], additionalProperties: false,
  },
  output: {
    type: "object", properties: {
      found: { type: "boolean" },
      status: { type: "string", enum: ["open", "closed", "not_found"] },
    },
    required: ["found", "status"], additionalProperties: false,
  },
  execution: {
    type: "http", binding: "tickets", path: "/tools/lookup",
    method: "POST", timeoutMs: 3000, idempotency: { mode: "effect_key" },
  },
});

export function createTicketServer(secret: string) {
  // Synthetic, read-only data. Replace this map with your authorized application lookup.
  const tickets = new Map<string, "open" | "closed">([
    ["TICKET-1001", "open"], ["TICKET-1002", "closed"],
  ]);
  const implementation = implementTool(lookup, ({ reference }, context) => {
    context.signal.throwIfAborted();
    const status = tickets.get(reference);
    return { found: status !== undefined, status: status ?? "not_found" } as const;
  });
  const handleLookup = createToolHandler(implementation, {
    auth: { scheme: "bearer", secret, integration: "tickets" },
    maxBodyBytes: 16_384,
  });
  return createServer(nodeHandler(request => {
    if (new URL(request.url).pathname === "/tools/lookup") return handleLookup(request);
    return Promise.resolve(new Response("Not found", { status: 404 }));
  }));
}

// Start when executed directly; importing the tool contract does not open a port.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  const secret = process.env.CUSTOMER_TOOL_SECRET;
  if (!secret || secret.length < 16) throw new Error("Set CUSTOMER_TOOL_SECRET to at least 16 characters");
  createTicketServer(secret).listen(8787, "127.0.0.1", () => {
    console.log("Ticket lookup listening on http://127.0.0.1:8787/tools/lookup");
  });
}
```

Compile and start it from your application directory. The shared secret below is only a local example; use your secret manager for a deployed endpoint.

```sh
npm install --save-dev typescript @types/node
npx tsc ticket-server.mts --module nodenext --target es2022 --outDir dist --skipLibCheck
export CUSTOMER_TOOL_SECRET=local-ticket-example-secret
node dist/ticket-server.mjs
```

In another terminal, set the same secret and send a synthetic tool request:

```sh
export CUSTOMER_TOOL_SECRET=local-ticket-example-secret
curl --fail-with-body http://127.0.0.1:8787/tools/lookup \
  -H "Authorization: Bearer $CUSTOMER_TOOL_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: local-ticket-lookup-0001" \
  -d '{"args":{"reference":"TICKET-1001"},"call":{"call_id":"example-conversation","run_id":"example-run","agent_id":"ticket_agent"}}'
```

The HTTP `200` response is `{"found":true,"status":"open"}`. `TICKET-1002` returns `closed`; an unknown reference returns `{"found":false,"status":"not_found"}`. The contract describes both outcomes explicitly.

Check authentication by omitting the bearer header:

```sh
curl -i -X POST http://127.0.0.1:8787/tools/lookup
```

The response is HTTP `401` with `{"error":{"code":"unauthenticated","message":"Authentication failed"}}`. The lookup does not run.

### What happens to a request

1. Your Node server accepts `/tools/lookup` and passes the request to `handleLookup`; other paths return `404`.
2. The adapter checks the method, bounds the body, authenticates it and validates the envelope and arguments.
3. Your callback reads the ticket. In a real application, this is where you check the requesting account’s permissions and query your service.
4. The adapter validates the result against the output schema and returns JSON. The tool’s three-second deadline bounds the wait; cancellation does not undo work already sent.

To connect this endpoint to an agent, reuse `lookup` in the agent definition and map its `tickets` binding to your deployed HTTPS service and matching secret reference. Follow [bindings and integrations](/docs/sdk-bindings). Cumulus cannot reach this loopback-only server until you deploy it. For Python or another framework, implement the same [customer HTTP callback contract](/docs/sdk-http-contracts).

## Serve a tool {#serve-a-tool}

### implementTool {#implementtool}

Connect a tool contract to the function that performs its work. The SDK infers the function’s arguments and result from the contract. This step opens no endpoint; pass the returned implementation to createToolHandler.

```typescript
// Inside createTicketServer above, using its ticket map.
const implementation = implementTool(lookup, ({ reference }, context) => {
  context.signal.throwIfAborted();
  const status = tickets.get(reference);
  return { found: status !== undefined, status: status ?? "not_found" } as const;
});
```

Related types and helpers: [ToolDefinition](/docs/sdk-reference/agent#tooldefinition) · [ToolImplementation](/docs/sdk-reference/server#toolimplementation).

```typescript-declaration
export declare function implementTool<T extends ToolDefinition>(tool: T, handler: ToolImplementation<T>["handler"]): ToolImplementation<T>;
```

### createToolHandler {#createtoolhandler}

Create the HTTP handler for a tool implementation. It authenticates and validates each request, calls your function with its arguments and context, then validates the result before replying.

```typescript
// Inside createTicketServer above: implementation and secret are already defined.
const handleLookup = createToolHandler(implementation, {
  auth: { scheme: "bearer", secret, integration: "tickets" },
  maxBodyBytes: 16_384,
});
```

The tool definition supplies its method, input/output schemas and `execution.timeoutMs`. `options.auth` supplies the receiver’s integration name and shared secret. `maxBodyBytes` defaults to 65,536; this example lowers it to 16,384.

POST arguments arrive as `{ args, call }`; GET arguments arrive in the query and have no run/conversation identity. In `context`, `runId` identifies the runs API resource and `conversationId` identifies the conversation. `effectId` comes from `Idempotency-Key`. The adapter requires that header for `effect_key` tools but does not store results or deduplicate work. A write operation needs a durable receipt keyed by integration and effect ID.

Bad authentication returns `401`; invalid input returns `400`. A deadline or invalid result after execution returns an uncertain `500`; reconcile any business operation before retrying. See [tool execution payloads](/docs/sdk-http-contracts#tool-execution).

Related types and helpers: [ToolDefinition](/docs/sdk-reference/agent#tooldefinition) · [ToolImplementation](/docs/sdk-reference/server#toolimplementation) · [HandlerOptions](/docs/sdk-reference/server#handleroptions) · [WebHandler](/docs/sdk-reference/server#webhandler).

```typescript-declaration
export declare function createToolHandler<T extends ToolDefinition>(implementation: ToolImplementation<T>, options: HandlerOptions): WebHandler;
```

### nodeHandler {#nodehandler}

Connect an SDK HTTP handler to Node’s createServer or Express. It translates Node requests and responses to the Web Request/Response objects used by the SDK.

```typescript
const server = createServer(nodeHandler(handleLookup));
server.listen(8787, "127.0.0.1");
```

Use this with Node’s `createServer`, as above, or mount it before Express JSON middleware. It preserves original body bytes for signature verification and aborts the request signal when the client disconnects.

Related types and helpers: [WebHandler](/docs/sdk-reference/server#webhandler).

```typescript-declaration
/** Express/Node adapter; mount before a JSON body parser so signature verification sees the raw bytes. */
export declare function nodeHandler(handler: WebHandler): (request: IncomingMessage, response: ServerResponse) => void;
```

## Resolve inbound context {#resolve-inbound-context}

### createInboundContextHandler {#createinboundcontexthandler}

Receive an incoming call’s details and return the initial variables its agent needs. Use this before the conversation to supply authorized application context or decline the call.

```typescript
// contextSecret comes from your application’s secret manager.
const handleContext = createInboundContextHandler(async (input, signal) => {
  signal.throwIfAborted();
  // Caller and destination are in input.call_inbound.
  return { dynamic_variables: { service_channel: "phone" } };
}, {
  auth: { scheme: "bearer", secret: contextSecret, integration: "tickets" },
  variables: ["service_channel"], timeoutMs: 3000,
});
```

Your resolver receives `(input, signal)`, with the full `{ event: "call_inbound", call_inbound: { agent_id, agent_version, from_number, to_number } }` envelope. Return `{ dynamic_variables }` using only strings named in `variables`, or `{ reject: true }`. The adapter wraps this as `{ call_inbound: result }`.

`variables` must be nonempty and unique. `timeoutMs` defaults to 3,000; the published action permits at most 10,000. `maxBodyBytes` defaults to 65,536. Configure the same output names in the integration’s `contextOutputs`. Invalid variables return `400`. See [initial inbound context](/docs/sdk-http-contracts#initial-inbound-context) for complete request and response JSON.

Related types and helpers: [InboundContextInput](/docs/sdk-reference/server#inboundcontextinput) · [InboundContextOutput](/docs/sdk-reference/server#inboundcontextoutput) · [HandlerOptions](/docs/sdk-reference/server#handleroptions) · [WebHandler](/docs/sdk-reference/server#webhandler).

```typescript-declaration
export declare function createInboundContextHandler(resolve: (input: InboundContextInput, signal: AbortSignal) => Promise<InboundContextOutput> | InboundContextOutput, options: HandlerOptions & {
    variables: string[];
    timeoutMs?: number;
}): WebHandler;
```

## Receive events and signed requests {#receive-events-and-signed-requests}

### createEventHandler {#createeventhandler}

Receive signed notifications such as run.ended or recording.updated. Your consume callback saves the event; the handler replies only after that callback resolves.

```typescript
// eventStore is your application’s durable storage interface.
const handleEvents = createEventHandler({
  secret: eventSecret,
  maxBodyBytes: 262_144,
  consume: async event => {
    await eventStore.persistOnce(event.event_id, event);
  },
});
```

Provide a signing `secret` of at least 32 characters and an async `consume` callback. The handler verifies the original body’s `X-Talos-Signature`, validates the event and awaits `consume` before returning `200` with `{ received: true }`. It does not deduplicate events for you. In the example, `eventStore.persistOnce` represents your durable event store: it must save or recognize `event.event_id` before resolving.

Set `maxBodyBytes: 262144` to accept the sender’s full 256 KiB event range; the adapter defaults to 65,536. Signature tolerance is five minutes. The sender allows 20 seconds, so persist the event and any job intent promptly. The adapter imposes no separate processing deadline. See [lifecycle event payloads and signing](/docs/sdk-http-contracts#lifecycle-events).

Related types and helpers: [components](/docs/sdk-reference/client#components) · [SemanticEvent](/docs/sdk-reference/client#semanticevent) · [WebHandler](/docs/sdk-reference/server#webhandler).

```typescript-declaration
export declare function createEventHandler(options: {
    secret: string;
    consume: (event: components["schemas"]["SemanticEvent"]) => Promise<void>;
    maxBodyBytes?: number;
}): WebHandler;
```

### verifyWebhook {#verifywebhook}

Verify a lifecycle notification’s signature and return its validated event. Use this directly when your framework handles the HTTP response itself.

```typescript
const event = verifyWebhook(rawBody, signatureHeader, eventSecret);
```

Use this when your framework handles routing and raw-body reads itself. It returns the validated `SemanticEvent` or throws. Verify before parsing or modifying the bytes. The signature uses Unix seconds and HMAC-SHA256 over `timestamp + "." + rawBody`; tolerance defaults to 300 seconds. `options.now` is in seconds and `toleranceSeconds` changes the window.

Related types and helpers: [components](/docs/sdk-reference/client#components) · [SemanticEvent](/docs/sdk-reference/client#semanticevent).

```typescript-declaration
export declare function verifyWebhook(raw: Uint8Array, signature: string, secret: string, options?: {
    now?: number;
    toleranceSeconds?: number;
}): components["schemas"]["SemanticEvent"];
```

### verifyRetellSignature {#verifyretellsignature}

Verify a Retell-compatible tool signature against the original body bytes. Use this for that tool authentication scheme; lifecycle events use verifyWebhook.

```typescript
verifyRetellSignature(rawBody, signatureHeader, toolSecret);
```

This verifies the separate tool-signature protocol and returns nothing on success; it does not validate the JSON or execute a tool. The signature uses Unix milliseconds and HMAC-SHA256 over `rawBody + timestamp`. `options.now` and `toleranceMs` are in milliseconds; tolerance defaults to 300,000. Invalid or stale signatures throw `AuthenticationError`.

```typescript-declaration
export declare function verifyRetellSignature(raw: Uint8Array, signature: string, secret: string, options?: {
    now?: number;
    toleranceMs?: number;
}): void;
```

### readBounded {#readbounded}

Read a request body into bytes while enforcing a size limit. Use the bytes for signature verification before parsing JSON.

```typescript
const rawBody = await readBounded(request, 262_144);
```

Read original bytes before signature verification. `limit` defaults to 65,536 and applies to both declared content length and streamed bytes. Exceeding it throws `DefinitionError`. This function does not authenticate or parse the body.

```typescript-declaration
export declare function readBounded(request: Request, limit?: number): Promise<Uint8Array>;
```

## Serve private tokenization {#serve-private-tokenization}

### createTokenizationHandler {#createtokenizationhandler}

Receive private keypad input at your vault endpoint and return a UUID token. Use this dedicated adapter so the response exposes only the configured token field.

```typescript
const handleCapture = createTokenizationHandler(vaultImplementation, {
  auth: vaultAuth, tokenIdPath: "id",
});
```

Use a dedicated tool with `purpose: "tokenization"`, POST execution and `effect_key` idempotency. `tokenIdPath` must match the capture policy, for example `id` or `vault.id`. The response contains only the UUID token at that path. The request cap is 16,384 bytes; a lower `maxBodyBytes` is allowed. The tool supplies the deadline.

Persist vault intent and outcome by integration and effect ID without retaining private digits. Throw `TokenizationDeclined` only when the vault definitely created no token (`422`). Other failures after invocation are uncertain (`500`). See [private workflow setup](/docs/sdk-private) and [vault HTTP payloads](/docs/sdk-http-contracts#private-vault-tokenization).

Related types and helpers: [ToolDefinition](/docs/sdk-reference/agent#tooldefinition) · [ToolImplementation](/docs/sdk-reference/server#toolimplementation) · [HandlerOptions](/docs/sdk-reference/server#handleroptions) · [WebHandler](/docs/sdk-reference/server#webhandler).

```typescript-declaration
/** Dedicated private endpoint. Never log/retain input; deduplicate durably by context.effectId.
 * Only the configured UUID token is returned. JS strings cannot be reliably zeroed. */
export declare function createTokenizationHandler<T extends ToolDefinition>(implementation: ToolImplementation<T>, options: HandlerOptions & {
    tokenIdPath: string;
}): WebHandler;
```

## Handle endpoint failures {#handle-endpoint-failures}

### AuthenticationError {#authenticationerror}

Endpoint authentication failed before your callback was invoked. The HTTP adapter returns unauthenticated with status 401.

```typescript-declaration
export declare class AuthenticationError extends Error {
    constructor();
}
```

### ToolOutcomeUncertain {#tooloutcomeuncertain}

The customer callback may have executed but its result was not confirmed. The adapter returns tool_outcome_uncertain with status 500; reconcile using the effect identity.

```typescript-declaration
export declare class ToolOutcomeUncertain extends Error {
    constructor();
}
```

### TokenizationDeclined {#tokenizationdeclined}

A private capture provider definitively declined without creating a token. The private adapter returns tokenization_declined with status 422.

```typescript-declaration
/** Throw only when the provider definitively rejected capture without creating a token. */
export declare class TokenizationDeclined extends Error {
    constructor();
}
```

## Types and declarations {#types}

### EndpointAuth {#endpointauth}

Choose bearer, named header or supported Retell signature authentication. Each configuration identifies the integration; signatures also support a time tolerance.

All three variants require `secret` and `integration`. Bearer uses `Authorization: Bearer …`; header auth also requires `headerName`; Retell-compatible signing accepts optional `toleranceMs` (default 300,000). These are tool/context/vault credentials. Lifecycle events use the separate signing-secret option.

```typescript-declaration
export type EndpointAuth = {
    scheme: "bearer";
    secret: string;
    integration: string;
} | {
    scheme: "header";
    headerName: string;
    secret: string;
    integration: string;
} | {
    scheme: "retell_signature";
    secret: string;
    integration: string;
    toleranceMs?: number;
};
```

### HandlerOptions {#handleroptions}

Required endpoint auth plus optional maxBodyBytes. Ordinary adapters default to the raw reader’s 65,536-byte limit; tokenization applies its tighter cap.

`auth` is required. Choose bearer, a named header or Retell-compatible tool signing, with a server-side secret of at least 16 characters and a receiver integration name. `maxBodyBytes` defaults to 65,536 for tools and context; tokenization additionally caps it at 16,384. Event handlers have their own options described above.

Related types and helpers: [EndpointAuth](/docs/sdk-reference/server#endpointauth).

```typescript-declaration
export interface HandlerOptions {
    auth: EndpointAuth;
    maxBodyBytes?: number;
}
```

### InboundContextInput {#inboundcontextinput}

The native inbound call envelope includes agent identity/version and from/to phone numbers.

The callback receives this full envelope; the phone details are nested under `call_inbound`. `agent_version` identifies the selected publication. See [complete inbound JSON](/docs/sdk-http-contracts#initial-inbound-context).

```typescript-declaration
export interface InboundContextInput {
    event: "call_inbound";
    call_inbound: {
        agent_id: string;
        agent_version: number;
        from_number: string;
        to_number: string;
    };
}
```

### InboundContextOutput {#inboundcontextoutput}

Your inbound resolver may reject the call or supply declared dynamic variables as strings.

Return declared string `dynamic_variables`, or `reject: true`. The handler adds the outer `call_inbound` wrapper. See [complete responses](/docs/sdk-http-contracts#initial-inbound-context).

```typescript-declaration
export interface InboundContextOutput {
    reject?: boolean;
    dynamic_variables?: Record<string, string>;
}
```

### ToolContext {#toolcontext}

Callback context: authenticated integration alias, optional run/conversation/agent identities, metadata, effectId and cancellation signal. runId identifies the run API resource; conversationId is the native call identity. An integration credential does not establish the caller’s business identity.

`integration` comes from your receiver configuration. `runId`, `conversationId` and `agentId` come from the request’s optional/required call fields; GET tools have none of these. `metadata` defaults to an empty object. `effectId` is the request’s idempotency header. Pass `signal` to cancellable application work. The shared credential authenticates the integration; your application still authorizes the account or operation.

```typescript-declaration
export interface ToolContext {
    integration: string;
    runId?: string;
    conversationId?: string;
    agentId?: string;
    metadata: Record<string, unknown>;
    effectId?: string;
    signal: AbortSignal;
}
```

### ToolImplementation {#toolimplementation}

A tool definition and its input/output-typed callback. The callback may return synchronously or asynchronously.

Related types and helpers: [ToolDefinition](/docs/sdk-reference/agent#tooldefinition) · [ToolInput](/docs/sdk-reference/agent#toolinput) · [ToolContext](/docs/sdk-reference/server#toolcontext) · [ToolOutput](/docs/sdk-reference/agent#tooloutput).

```typescript-declaration
export interface ToolImplementation<T extends ToolDefinition = ToolDefinition> {
    tool: T;
    handler: (input: ToolInput<T>, context: ToolContext) => Promise<ToolOutput<T>> | ToolOutput<T>;
}
```

### WebHandler {#webhandler}

A portable asynchronous Web Request to Web Response function. Pass it directly to a compatible server or wrap it with nodeHandler.

```typescript-declaration
export type WebHandler = (request: Request) => Promise<Response>;
```

## Export index {#exports}

- **Serve a tool:** [implementTool](#implementtool) · [createToolHandler](#createtoolhandler) · [nodeHandler](#nodehandler)
- **Resolve inbound context:** [createInboundContextHandler](#createinboundcontexthandler)
- **Receive events and signed requests:** [createEventHandler](#createeventhandler) · [verifyWebhook](#verifywebhook) · [verifyRetellSignature](#verifyretellsignature) · [readBounded](#readbounded)
- **Serve private tokenization:** [createTokenizationHandler](#createtokenizationhandler)
- **Handle endpoint failures:** [AuthenticationError](#authenticationerror) · [ToolOutcomeUncertain](#tooloutcomeuncertain) · [TokenizationDeclined](#tokenizationdeclined)
- **Types:** [EndpointAuth](#endpointauth) · [HandlerOptions](#handleroptions) · [InboundContextInput](#inboundcontextinput) · [InboundContextOutput](#inboundcontextoutput) · [ToolContext](#toolcontext) · [ToolImplementation](#toolimplementation) · [WebHandler](#webhandler)
