When it fires
- The model completes a response. A turn can carry text, tool calls, or both.
- Each entry in
toolCallsis followed by its own tool called event once Cadenya dispatches it.
{
"type": "objective_event.assistant_message",
"timestamp": "2026-08-26T14:03:11Z",
"data": {
"agent": {
"id": "agent_01HXKD2E5NQM3T9AYWCFMGWT9Y",
"accountId": "account_01HXKD2E5NQM3T9AYWCFTJHJVF",
"workspaceId": "workspace_01HXKD2E5NQM3T9AYWCF133E3Q",
"name": "Support Triage",
"externalId": "",
"labels": {},
"profileId": "profile_01HXKD2E5NQM3T9AYWCFS0AP08",
"createdAt": "2026-08-01T09:00:00Z",
"updatedAt": "2026-08-01T09:00:00Z"
},
"agentVariation": {
"id": "agentvar_01HXKD2E5NQM3T9AYWCF32BSPP",
"accountId": "account_01HXKD2E5NQM3T9AYWCFTJHJVF",
"workspaceId": "workspace_01HXKD2E5NQM3T9AYWCF133E3Q",
"name": "sonnet-strict-tools",
"externalId": "",
"labels": {},
"profileId": "profile_01HXKD2E5NQM3T9AYWCFS0AP08",
"createdAt": "2026-08-01T09:00:00Z",
"updatedAt": "2026-08-01T09:00:00Z"
},
"objective": {
"id": "obj_01HXKD2E5NQM3T9AYWCFQAZGFV",
"accountId": "account_01HXKD2E5NQM3T9AYWCFTJHJVF",
"workspaceId": "workspace_01HXKD2E5NQM3T9AYWCF133E3Q",
"externalId": "ticket-4821",
"labels": {
"source": "zendesk"
},
"profileId": "profile_01HXKD2E5NQM3T9AYWCFS0AP08",
"createdAt": "2026-08-26T14:03:11Z"
},
"objectiveEvent": {
"metadata": {
"id": "objevt_01HXKD2E5NQM3T9AYWCF8ZWBY0",
"accountId": "account_01HXKD2E5NQM3T9AYWCFTJHJVF",
"workspaceId": "workspace_01HXKD2E5NQM3T9AYWCF133E3Q",
"externalId": "",
"labels": {},
"profileId": "profile_01HXKD2E5NQM3T9AYWCFS0AP08",
"createdAt": "2026-08-26T14:03:11Z"
},
"contextWindowId": "objwin_01HXKD2E5NQM3T9AYWCFN7BSTR",
"data": {
"type": "assistantMessage",
"assistantMessage": {
"content": "I'll pull up the invoice first.",
"toolCalls": [
{
"functionName": "get_invoice",
"arguments": "{\"invoice_id\":\"inv_4821\"}",
"tool": {
"type": "tool",
"tool": {
"id": "tool_01HXKD2E5NQM3T9AYWCFWVYY9K",
"accountId": "account_01HXKD2E5NQM3T9AYWCFTJHJVF",
"workspaceId": "workspace_01HXKD2E5NQM3T9AYWCF133E3Q",
"name": "get_invoice",
"externalId": "",
"labels": {},
"profileId": "profile_01HXKD2E5NQM3T9AYWCFS0AP08",
"createdAt": "2026-08-01T09:00:00Z",
"updatedAt": "2026-08-01T09:00:00Z"
}
}
}
]
}
},
"startedAt": "2026-08-26T14:03:08Z",
"duration": "2.4s"
}
}
}
POST /webhooks/cadenya HTTP/1.1
Host: example.com
Content-Type: application/json
webhook-id: wh_01HXKD2E5NQM3T9AYWCFGVF6Y6
webhook-timestamp: 1787752991
webhook-signature: v1,K5oZfzN95Z9UVu1EsPQhBaJMSUuGgeEtBXQrZ2lZ+1s=
Event data
The event-specific fields live atdata.objectiveEvent.data. The rest of the payload is the shared envelope.
string
required
Always
assistantMessage. Switch on this, not on the outer type, when one handler serves several events.object
required
The model’s turn.
Show properties
Show properties
string
The text the model wrote. Absent when the turn is only tool calls.
object[]
required
Tool calls the model requested in this turn. Empty when the model only wrote text.
Show properties
Show properties
string
required
The function name the model invoked.
string
required
The arguments as a JSON-encoded string. Parse it before use. The tool called event carries the same arguments as an object.
object
The resolved tool.
type is tool, agent, or cadenyaProvidedTool, and the matching key holds that resource’s metadata (id, name, workspaceId, labels).Handle it
Every SDK verifies the signature and returns the typed envelope in one call. The handler reads the raw body (not a parsed and re-serialized one), unwraps it, and returns a 2xx fast.import { createServer } from "node:http";
import Cadenya from "@cadenya/cadenya";
// Reads CADENYA_API_KEY and CADENYA_WEBHOOK_SECRET from the environment.
const cadenya = new Cadenya();
createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/webhooks/cadenya") {
res.writeHead(404).end();
return;
}
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const payload = Buffer.concat(chunks).toString("utf8");
let event;
try {
// Checks the signature and timestamp, then parses the envelope.
event = await cadenya.webhooks.unwrap(payload, req.headers);
} catch {
res.writeHead(401).end("bad signature");
return;
}
if (event.type === "objective_event.assistant_message") {
const { objective, objectiveEvent } = event.data;
if (objectiveEvent.data.type === "assistantMessage") {
const { content, toolCalls } = objectiveEvent.data.assistantMessage;
console.log(objective.id, content ?? "(no text)", toolCalls.map((c) => c.functionName));
}
}
// Answer quickly. Do slow work after the 2xx.
res.writeHead(200).end();
}).listen(3000);
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) {
payload, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read failed", http.StatusBadRequest)
return
}
// Checks the signature and timestamp, then parses the envelope.
event, err := client.Webhooks().Unwrap(payload, r.Header)
if err != nil {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
if m := event.Data.ObjectiveEvent.Data.AssistantMessage; m != nil {
names := make([]string, 0, len(m.AssistantMessage.ToolCalls))
for _, c := range m.AssistantMessage.ToolCalls {
names = append(names, c.FunctionName)
}
text := ""
if m.AssistantMessage.Content != nil {
text = *m.AssistantMessage.Content
}
log.Printf("%s said %q and called %v", event.Data.Objective.ID, text, names)
}
// Answer quickly. Do slow work after the 2xx.
w.WriteHeader(http.StatusOK)
})
log.Fatal(http.ListenAndServe(":3000", nil))
}
# 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
payload = 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
# Checks the signature and timestamp, then parses the envelope.
event = client.unwrap_webhook(payload, headers)
rescue Cadenya::WebhookVerificationError
halt 401, "bad signature"
end
if event.type == "objective_event.assistant_message"
data = event.data.objective_event.data
if data.is_a?(Cadenya::Types::ObjectiveEventData_AssistantMessage)
message = data.assistant_message
puts "#{event.data.objective.id} said #{message.content.inspect} and called #{message.tool_calls.map(&:function_name)}"
end
end
# Answer quickly. Do slow work after the 2xx.
status 200
end
# Point SECRET at the same whsec_ key your handler uses, or verification fails.
SECRET="whsec_V2ViaG9va3NBcmVGdW5Ub0RvY3VtZW50ISEhISEhISE="
URL="http://localhost:3000/webhooks/cadenya"
ID="wh_01HXKD2E5NQM3T9AYWCFGVF6Y6"
TS="$(date +%s)"
BODY="$(cat <<'JSON'
{
"type": "objective_event.assistant_message",
"timestamp": "2026-08-26T14:03:11Z",
"data": {
"agent": {
"id": "agent_01HXKD2E5NQM3T9AYWCFMGWT9Y",
"accountId": "account_01HXKD2E5NQM3T9AYWCFTJHJVF",
"workspaceId": "workspace_01HXKD2E5NQM3T9AYWCF133E3Q",
"name": "Support Triage",
"externalId": "",
"labels": {},
"profileId": "profile_01HXKD2E5NQM3T9AYWCFS0AP08",
"createdAt": "2026-08-01T09:00:00Z",
"updatedAt": "2026-08-01T09:00:00Z"
},
"agentVariation": {
"id": "agentvar_01HXKD2E5NQM3T9AYWCF32BSPP",
"accountId": "account_01HXKD2E5NQM3T9AYWCFTJHJVF",
"workspaceId": "workspace_01HXKD2E5NQM3T9AYWCF133E3Q",
"name": "sonnet-strict-tools",
"externalId": "",
"labels": {},
"profileId": "profile_01HXKD2E5NQM3T9AYWCFS0AP08",
"createdAt": "2026-08-01T09:00:00Z",
"updatedAt": "2026-08-01T09:00:00Z"
},
"objective": {
"id": "obj_01HXKD2E5NQM3T9AYWCFQAZGFV",
"accountId": "account_01HXKD2E5NQM3T9AYWCFTJHJVF",
"workspaceId": "workspace_01HXKD2E5NQM3T9AYWCF133E3Q",
"externalId": "ticket-4821",
"labels": {
"source": "zendesk"
},
"profileId": "profile_01HXKD2E5NQM3T9AYWCFS0AP08",
"createdAt": "2026-08-26T14:03:11Z"
},
"objectiveEvent": {
"metadata": {
"id": "objevt_01HXKD2E5NQM3T9AYWCF8ZWBY0",
"accountId": "account_01HXKD2E5NQM3T9AYWCFTJHJVF",
"workspaceId": "workspace_01HXKD2E5NQM3T9AYWCF133E3Q",
"externalId": "",
"labels": {},
"profileId": "profile_01HXKD2E5NQM3T9AYWCFS0AP08",
"createdAt": "2026-08-26T14:03:11Z"
},
"contextWindowId": "objwin_01HXKD2E5NQM3T9AYWCFN7BSTR",
"data": {
"type": "assistantMessage",
"assistantMessage": {
"content": "I'll pull up the invoice first.",
"toolCalls": [
{
"functionName": "get_invoice",
"arguments": "{\"invoice_id\":\"inv_4821\"}",
"tool": {
"type": "tool",
"tool": {
"id": "tool_01HXKD2E5NQM3T9AYWCFWVYY9K",
"accountId": "account_01HXKD2E5NQM3T9AYWCFTJHJVF",
"workspaceId": "workspace_01HXKD2E5NQM3T9AYWCF133E3Q",
"name": "get_invoice",
"externalId": "",
"labels": {},
"profileId": "profile_01HXKD2E5NQM3T9AYWCFS0AP08",
"createdAt": "2026-08-01T09:00:00Z",
"updatedAt": "2026-08-01T09:00:00Z"
}
}
}
]
}
},
"startedAt": "2026-08-26T14:03:08Z",
"duration": "2.4s"
}
}
}
JSON
)"
# Standard Webhooks: HMAC-SHA256 over "id.timestamp.body", keyed with the base64-decoded secret.
KEY_HEX="$(printf '%s' "${SECRET#whsec_}" | base64 -d | xxd -p -c 256)"
SIG="$(printf '%s.%s.%s' "$ID" "$TS" "$BODY" \
| openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY_HEX" -binary \
| base64)"
curl -X POST "$URL" \
-H "content-type: application/json" \
-H "webhook-id: $ID" \
-H "webhook-timestamp: $TS" \
-H "webhook-signature: v1,$SIG" \
--data-binary "$BODY"
SECRET to the key that handler uses, then run the script to see the event land.
Envelope
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.
Headers
Headers
Cadenya signs every delivery per the Standard Webhooks specification and sends it as a
Find the signing key under Account Admin and rotate it with rotate the webhook signing key. Cadenya records every attempt, and you can inspect them with list webhook deliveries.
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 |
Envelope fields
Envelope fields
string
required
The event name, for example
objective_event.tool_called.string
required
RFC 3339 time when Cadenya emitted the delivery.
object
required
Everything you need to route the event without a lookup.
Show properties
Show properties
object
required
Resource metadata of the agent:
id, name, workspaceId, accountId, externalId, labels, profileId, createdAt, updatedAt.object
required
Resource metadata of the variation that ran. Same shape as
agent.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.object
required
The event on the objective’s timeline.
Show properties
Show properties
object
required
Operation metadata of the event itself.
id is the event ID (objevt_…), createdAt is when it was persisted.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.string
required
The context window the event belongs to. Changes when compaction opens a new window.
object
Extra context, when present:
objective (operation metadata) and createdBy (the profile that caused the event).string
When the work this event records began. Present on events that measure something (an assistant turn, a tool execution), always together with
duration.string
Elapsed time as a duration string, for example
"4.1s". Absent when the event is instantaneous.