> ## Documentation Index
> Fetch the complete documentation index at: https://cadenya.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> The basics of how Webhooks work in Cadenya's Agent Runtime

export const Asset = ({id, caption, alt}) => {
  const assets = {
    "videos/guides/getting-started/create-tool-set": {
      "url": "https://assets.cadenya.com/dotcom/videos/guides/getting-started/create-tool-set.9be9e1a903c8.mp4",
      "type": "video/mp4",
      "width": 1920,
      "height": 1030
    },
    "videos/guides/the-basics/agent-webhook": {
      "url": "https://assets.cadenya.com/dotcom/videos/guides/the-basics/agent-webhook.2a9c1538cca4.mp4",
      "type": "video/mp4",
      "width": 1920,
      "height": 1226
    },
    "videos/guides/the-basics/configure-variation-model": {
      "url": "https://assets.cadenya.com/dotcom/videos/guides/the-basics/configure-variation-model.2525e84673eb.mp4",
      "type": "video/mp4",
      "width": 1816,
      "height": 1080
    },
    "videos/guides/the-basics/configure-variation-toolset": {
      "url": "https://assets.cadenya.com/dotcom/videos/guides/the-basics/configure-variation-toolset.283859ee975e.mp4",
      "type": "video/mp4",
      "width": 1816,
      "height": 1080
    },
    "videos/guides/the-basics/memory-layer-assignment": {
      "url": "https://assets.cadenya.com/dotcom/videos/guides/the-basics/memory-layer-assignment.61fdf8b0729e.mp4",
      "type": "video/mp4",
      "width": 1920,
      "height": 1168
    },
    "videos/guides/the-basics/memory-tool-usage": {
      "url": "https://assets.cadenya.com/dotcom/videos/guides/the-basics/memory-tool-usage.09350e7a5f41.mp4",
      "type": "video/mp4",
      "width": 1920,
      "height": 1168
    }
  };
  const asset = assets[id];
  if (!asset) {
    return <div style={{
      border: "2px solid #dc2626",
      borderRadius: 8,
      padding: 12,
      color: "#dc2626",
      fontFamily: "monospace"
    }}>
        Unknown asset key: {id}. Run <code>just upload-assets</code> and check <code>assets/manifest.json</code>.
      </div>;
  }
  const isVideo = asset.type.startsWith("video/");
  const media = isVideo ? <video autoPlay muted loop playsInline controls src={asset.url} width={asset.width} height={asset.height} /> : <img src={asset.url} alt={(alt ?? caption) ?? ""} width={asset.width} height={asset.height} />;
  return caption ? <Frame caption={caption}>{media}</Frame> : <Frame>{media}</Frame>;
};

Webhooks in Cadenya are dispatched from an Agent's Objective events to your application as they happen. Set one URL on the Agent, then Cadenya sends a signed `POST` for each message, Tool Call, approval, memory read, Sub-Objective update, and error.

Use webhooks when your application needs to react to Agent work. For example, you can display a response, ask a person to approve a Tool Call, update a record, or report a failed Objective.

## How webhooks work

Each delivery contains one event from an [Objective's event log](/docs/guides/the-basics/objectives#objective-events). Cadenya signs the raw request body with your account's webhook signing key.

```mermaid theme={null}
flowchart LR
  Agent["Cadenya Agent"] --> Event["Objective Event"] --> App["Your App"]
```

Configure webhooks on each Agent. Each Agent can send events to its own URL, so you can separate responsibilities without building a router for one shared endpoint.

## Configure a webhook

<Steps>
  <Step title="Add an endpoint to your Agent">
    Open your Agent in the Cadenya dashboard and set **Webhook events URL** to the endpoint that receives Objective events.

    <Asset id="videos/guides/the-basics/agent-webhook" caption="Webhook configuration on an Agent" />
  </Step>

  <Step title="Store the signing key">
    Find the account signing key under [**Account Admin**](https://app.cadenya.com/account/webhooks) and store it as `CADENYA_WEBHOOK_SECRET` in your application.

    One signing key covers every Agent in the account. Do not expose it in browser code or commit it to your repository.
  </Step>

  <Step title="Receive and verify deliveries">
    Read the request body as raw bytes or text, then pass it and the request headers to your Cadenya SDK. The SDK checks the signature and timestamp before it parses the event.
  </Step>
</Steps>

## Receive events

Install the Cadenya SDK for your language, then add a handler for `POST /webhooks/cadenya`. For the TypeScript example, run `npm install fastify @cadenya/cadenya`. The following handlers verify each delivery and print its event type and Objective ID.

<CodeGroup>
  ```typescript Fastify theme={null}
  import Fastify from "fastify";
  import Cadenya from "@cadenya/cadenya";

  // Reads CADENYA_API_KEY and CADENYA_WEBHOOK_SECRET from the environment.
  const cadenya = new Cadenya();
  const app = Fastify({ logger: true });

  // Signature verification needs the bytes Cadenya sent, before JSON parsing.
  app.removeContentTypeParser("application/json");
  app.addContentTypeParser(
    "application/json",
    { parseAs: "string" },
    (_request, body, done) => done(null, body),
  );

  app.post<{ Body: string }>("/webhooks/cadenya", async (request, reply) => {
    try {
      const event = await cadenya.webhooks.unwrap(request.body, request.headers);
      console.log(event.type, event.data.objective.id);
    } catch {
      return reply.code(401).send("bad signature");
    }

    return reply.code(200).send();
  });

  async function start() {
    try {
      await app.listen({ port: 3000 });
    } catch (error) {
      app.log.error(error);
      process.exit(1);
    }
  }

  void start();
  ```

  ```go Go theme={null}
  package main

  import (
  	"io"
  	"log"
  	"net/http"

  	cadenya "go.cadenya.com/cadenya-go"
  )

  func main() {
  	// Reads CADENYA_API_KEY and CADENYA_WEBHOOK_SECRET from the environment.
  	client, err := cadenya.NewClient()
  	if err != nil {
  		log.Fatal(err)
  	}

  	http.HandleFunc("POST /webhooks/cadenya", func(w http.ResponseWriter, r *http.Request) {
  		rawBody, err := io.ReadAll(r.Body)
  		if err != nil {
  			http.Error(w, "read failed", http.StatusBadRequest)
  			return
  		}

  		event, err := client.Webhooks().Unwrap(rawBody, r.Header)
  		if err != nil {
  			http.Error(w, "bad signature", http.StatusUnauthorized)
  			return
  		}

  		log.Printf("%s %s", event.Type, event.Data.Objective.ID)
  		w.WriteHeader(http.StatusOK)
  	})

  	log.Fatal(http.ListenAndServe(":3000", nil))
  }
  ```

  ```ruby Ruby theme={null}
  # gem install sinatra cadenya
  require "sinatra"
  require "cadenya"

  # Reads CADENYA_API_KEY and CADENYA_WEBHOOK_SECRET from the environment.
  client = Cadenya::Client.new

  post "/webhooks/cadenya" do
    raw_body = request.body.read
    headers = {
      "webhook-id" => request.env["HTTP_WEBHOOK_ID"],
      "webhook-timestamp" => request.env["HTTP_WEBHOOK_TIMESTAMP"],
      "webhook-signature" => request.env["HTTP_WEBHOOK_SIGNATURE"]
    }

    begin
      event = client.unwrap_webhook(raw_body, headers)
    rescue Cadenya::WebhookVerificationError
      halt 401, "bad signature"
    end

    puts "#{event.type} #{event.data.objective.id}"
    status 200
  end
  ```
</CodeGroup>

<Warning>
  Pass the raw request body to `unwrap`. Parsing the JSON and serializing it again changes the signed bytes, so verification fails even when the data looks identical.
</Warning>

## Route event types

The outer `type` field names the webhook event, such as `objective_event.assistant_message`. The event-specific payload sits under `data.objectiveEvent.data`, where its `type` field acts as the payload discriminator.

Common event groups include:

| Group      | Events                                                                                                                               |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Messages   | `objective_event.user_message`, `objective_event.assistant_message`                                                                  |
| Tools      | `objective_event.tool_called`, `objective_event.tool_result`, `objective_event.tool_error`                                           |
| Approvals  | `objective_event.tool_approval_requested`, `objective_event.tool_approved`, `objective_event.tool_denied`                            |
| Agent work | `objective_event.memory_read`, `objective_event.reasoning`, `objective_event.sub_agent_spawned`, `objective_event.sub_agent_updated` |
| Completion | `objective_event.finalized`                                                                                                          |
| Errors     | `objective_event.error`                                                                                                              |

The [webhook events reference](/docs/api-reference/webhook-events) documents each payload and includes handlers for TypeScript, Go, Ruby, and cURL.

## Envelope and signatures

Every objective event arrives in the same envelope. `type` names the event, `data` carries the agent, variation, and objective it belongs to, and `data.objectiveEvent.data` holds the fields documented on each event page.

<AccordionGroup>
  <Accordion title="Headers">
    Cadenya signs every delivery per the [Standard Webhooks](https://www.standardwebhooks.com/) specification and sends it as a `POST` with a JSON body.

    | Header              | Value                                                                                                                                                                                                                   |
    | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `webhook-id`        | Unique per delivery. Key your idempotency on it.                                                                                                                                                                        |
    | `webhook-timestamp` | Unix seconds when Cadenya sent the delivery. The SDKs reject anything more than five minutes off your clock.                                                                                                            |
    | `webhook-signature` | `v1,` followed by a base64 HMAC-SHA256 of `id.timestamp.body`, keyed with the base64-decoded part of your `whsec_` signing key. The header can carry several space-separated signatures. One valid signature is enough. |
    | `content-type`      | `application/json`                                                                                                                                                                                                      |

    Find the signing key under **Account Admin** and rotate it with [rotate the webhook signing key](/docs/api-reference/accountservice/rotates-the-webhook-signing-key-for-the-account). Cadenya records every attempt, and you can inspect them with [list webhook deliveries](/docs/api-reference/agentservice/list-webhook-deliveries).
  </Accordion>

  <Accordion title="Envelope fields">
    <ResponseField name="type" type="string" required>
      The event name, for example `objective_event.tool_called`.
    </ResponseField>

    <ResponseField name="timestamp" type="string" required>
      RFC 3339 time when Cadenya emitted the delivery.
    </ResponseField>

    <ResponseField name="data" type="object" required>
      Everything you need to route the event without a lookup.

      <Expandable title="properties">
        <ResponseField name="agent" type="object" required>
          Resource metadata of the agent: `id`, `name`, `workspaceId`, `accountId`, `externalId`, `labels`, `profileId`, `createdAt`, `updatedAt`.
        </ResponseField>

        <ResponseField name="agentVariation" type="object" required>
          Resource metadata of the variation that ran. Same shape as `agent`.
        </ResponseField>

        <ResponseField name="objective" type="object" required>
          Operation metadata of the objective: `id`, `workspaceId`, `accountId`, `externalId`, `labels`, `profileId`, `createdAt`. If you set `externalId` when you created the objective, it comes back here, so you can route on your own ID.
        </ResponseField>

        <ResponseField name="objectiveEvent" type="object" required>
          The event on the objective's timeline.

          <Expandable title="properties">
            <ResponseField name="metadata" type="object" required>
              Operation metadata of the event itself. `id` is the event ID (`objevt_…`), `createdAt` is when it was persisted.
            </ResponseField>

            <ResponseField name="data" type="object" required>
              The event-specific payload. `type` is the discriminator (`toolCalled`, `userMessage`, and so on) and the matching key holds the fields. See the **Event data** section of each event page.
            </ResponseField>

            <ResponseField name="contextWindowId" type="string" required>
              The context window the event belongs to. Changes when compaction opens a new window.
            </ResponseField>

            <ResponseField name="info" type="object">
              Extra context, when present: `objective` (operation metadata) and `createdBy` (the profile that caused the event).
            </ResponseField>

            <ResponseField name="startedAt" type="string">
              When the work this event records began. Present on events that measure something (an assistant turn, a tool execution), always together with `duration`.
            </ResponseField>

            <ResponseField name="duration" type="string">
              Elapsed time as a duration string, for example `"4.1s"`. Absent when the event is instantaneous.
            </ResponseField>
          </Expandable>
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Accordion>
</AccordionGroup>

The SDK rejects a delivery when its signature does not match or its timestamp differs from your server clock by more than five minutes. Keep your server clock in sync and return `401` for an invalid signature.

## Prevent duplicate work

Treat `webhook-id` as an idempotency key. Store it with a unique constraint before you trigger side effects. If your application has handled the ID before, return `200` without repeating the work.

Objective metadata appears in every envelope. Set an [External ID](/docs/guides/the-basics/objectives#create-an-objective-with-an-external-id) or labels when you create the Objective so your webhook handler can route the event without another API request.

## Test and troubleshoot

Use [Svix Playground](https://play.svix.com/) when you want to inspect deliveries before your endpoint exists. For a local handler, expose it through a tunnel, set the tunnel URL on your Agent, then create an Objective and watch the handler logs.

Cadenya records each delivery attempt. Open the Agent's webhook deliveries in the dashboard or use [List webhook deliveries](/docs/api-reference/agentservice/list-webhook-deliveries) to inspect:

* Delivery status and attempt count
* HTTP status code and response headers
* Latency and the last attempt time
* The Objective and event that caused the delivery

The response body is not retained. Put diagnostic detail in response headers or your own application logs.

## Rotate the signing key

Rotate the account signing key if you suspect exposure. The [rotate webhook signing key](/docs/api-reference/accountservice/rotates-the-webhook-signing-key-for-the-account) endpoint returns the new key.

<Warning>
  Rotation invalidates the old key for every Agent in the account. Update `CADENYA_WEBHOOK_SECRET` in every receiver as part of the same cutover.
</Warning>

## Use case: approve a Tool Call

Objectives call tools, and some tools need approval before they run. Webhooks let your application handle that decision out of band. Cadenya pauses the Tool Call, sends an [`objective_event.tool_approval_requested`](/docs/api-reference/objective-tool-approval-requested-event) event, and waits for your application to approve or deny it through the API.

```mermaid theme={null}
flowchart LR
  Objective["Objective"] -->|"Calls a gated Tool"| Approval["Approval requested"]
  Approval -->|"Webhook"| App["Your App"]
  App -->|"Approve or deny through the API"| Objective
```

The webhook payload includes the Objective ID and `toolCallId`. Pass both values to [approve the Tool Call](/docs/api-reference/objectiveservice/approve-a-tool-call) or [deny the Tool Call](/docs/api-reference/objectiveservice/deny-a-tool-call). A denial can include a `memo` that steers the Agent toward another action.

```typescript TypeScript theme={null}
import Cadenya from "@cadenya/cadenya";

const cadenya = new Cadenya();
await cadenya.objectives.approveToolCall(
  "obj_01HXKD2E5NQM3T9AYWCFQAZGFV",
  "toolcall_01HXKD2E5NQM3T9AYWCFTANFGV",
);
```

## Use case: receive a final result

When an Agent uses structured output, the [`objective_event.finalized`](/docs/api-reference/objective-finalized-event) webhook tells your application that the result is ready. Use the Objective ID from the webhook to retrieve the Objective and read its `output` field.

```mermaid theme={null}
sequenceDiagram
  participant Agent as Cadenya Agent
  participant App as Your App
  participant API as Cadenya API
  Agent->>App: objective_event.finalized
  App->>API: GetObjective(objective ID)
  API-->>App: Objective with structured output
```

The `output` matches the schema configured on the Agent. See [structured output](/docs/guides/the-basics/agents#structured-output) to define the schema and [Get an objective](/docs/api-reference/objectiveservice/get-an-objective-by-id) to retrieve the result.
