Channels and voice
camelRun webhooks: run, input and usage events
Get signed webhooks from camelRun when runs start and end, when an agent asks for input, and what each model call cost.
View as Markdown · Docs index for coding agents
camelRun can tell your server when runs start and end, when an agent asks a person for input, and what each model response cost, so you can react without holding a connection open. Register an endpoint with the event types it wants:
curl https://run.camelai.com/v1/webhooks \
-H "Authorization: Bearer $CAMELAI_API_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/agents", "events": ["run.completed", "run.failed", "input.requested"], "description": "prod"}'The answer carries the endpoint's id (we_...) and its signing secret
(whsec_...), shown only this once.
| Endpoint | |
|---|---|
GET /v1/webhooks | List your endpoints (at most 16) |
GET, PATCH, DELETE /v1/webhooks/{webhookId} | Read, change or remove one |
POST /v1/webhooks/{webhookId}/secret | Replace its secret. The old one also signs for 24 hours |
Each endpoint's setBy says who last set it: token:<API token id>,
console, or oauth:<grant id> for one an OAuth app set before they no longer
could. An endpoint keeps sending when the token that set it is revoked:
revoking a token answers with left, the endpoints (and usage webhook and trace
export) it set, so you can review them. OAuth apps can't create or change
webhooks.
Events
Every event is an envelope:
{"id": "evt_3f9c0a1b2c3d4e5f60718293", "type": "run.completed", "created": 1790000000,
"data": {"agentId": "client_...", "requestId": "...", "method": "prompt", "metadata": {"source": "web"},
"replyIndex": 3, "messageCount": 4,
"usage": {"responses": 2, "input": 2300, "output": 140, "cacheRead": 0, "cacheWrite": 0, "costUsd": 0.0081}}}| Type | When | data |
|---|---|---|
run.started | A run (method: prompt, continue, resume, execute) began | agentId, requestId, method, actor, metadata; resumes when a turn was resumed after camelRun restarted mid-turn |
run.completed | It ended without an error | As above, plus usage; stopped (input_required with inputIds, spend_limit or turn_limit) if it stopped early; replyIndex and messageCount; steeredInto for a message a running turn took |
run.failed | It ended with an error, camelRun's or the model's | As above, plus usage, error, code where the error has a name (cancelled: a stop cancelled it before it began; aborted; model_stream_stalled; turn_limit…), and uncertain when a restart cut it short |
input.requested | The agent asks a person for input (the run waits) | agentId, requestId, inputId, toolCallId, kind, expiresAt |
input.resolved | An input settled | agentId, requestId, inputId, state |
usage.recorded | A model response's usage was counted, a transcription's, or images' | agentId, requestId, subject, actor, context, keyScope, provider, model, kind (response, compaction, transcription, image or speech), tokens, audioSeconds (a transcription's), images (how many were made), cost: {usd, source}, at. A transcription or images made without an agent have agentId: null. Treat a kind you don't know as other usage: kinds may be added |
voice.session.started, voice.session.ended, voice.session.error, voice.utterance, voice.speech.started, voice.speech.ended | A voice session connected, ended or failed, heard a person, or spoke | The session's facts. See Voice calls |
billing.balance.low | Your funded balance crosses below the alert threshold (default $2) | balance, previousBalance, threshold (integer micro-USD), source |
billing.balance.depleted | A positive balance reaches zero or below | The same balance fields |
end_user.balance.low, end_user.credit.exhausted, end_user.credit.granted, end_user.credit.refunded, end_user.credit.adjusted, end_user.allowance.low | An end user's balance or allowance runs low or out, or you posted to their ledger | endUser and the balance facts. See End-user credits |
Billing events are recorded in the transaction that changes the balance, so
usage, storage, refunds and adjustments can all cross a threshold. Staying
below a threshold doesn't repeat the event; recovering above it lets a later
crossing notify again. One charge can emit both types. A new account with no
credit emits neither. A threshold change can emit billing.balance.low with
source: "threshold_changed" and no previousBalance; ordinary crossings
have source: "balance".
- Payloads are small: ids and the key facts. Read the rest with your API key:
the run (
GET /v1/agents/{id}/requests/{requestId}, with its reply atoutcome.result.reply), history, or inputs.metadatais what the message that started the run carried, so you can route an event without a lookup. createdis in Unix seconds. An event'sdataonly gains fields; a change you couldn't ignore comes as a new type.- An endpoint receives the runs that begin after it is made.
Verify and handle
Requests are signed per Standard Webhooks:
webhook-id (the event's id), webhook-timestamp (Unix seconds) and
webhook-signature, v1,<base64 HMAC-SHA256 of "<id>.<timestamp>.<body>">
keyed with the secret's base64 part (space-separated when two secrets sign
during a rotation). Any Standard Webhooks library verifies them. In Python,
verify_webhook checks the signature in constant time and that the request was
sent within 5 minutes, and returns the event; pass the raw body, before parsing
it.
import { Webhook } from "standardwebhooks";
import type { WebhookEvent } from "@camelai/run";
const event = new Webhook(process.env.WEBHOOK_SECRET!.replace(/^whsec_/, "")).verify(body, headers) as WebhookEvent;During a rotation, pass both secrets as a list. The Python SDK also manages
endpoints: create_webhook(url, events), webhooks(), update_webhook,
delete_webhook and rotate_webhook_secret on AgentRuntime.
- Delivery is at least once. Answer 2xx within 10 seconds; anything else is retried with exponential backoff, from 5 seconds to an hour apart, for 3 days.
- Dedupe by
id(a retried event keeps it), and order bycreated, because retries can reorder deliveries. - Do the work after answering: queue it, then return 200.
Continue from a webhook
A handler can act on the agent itself. Send your own ids as metadata with
each message, and they come back on the run's events:
// When the message was sent: agent.run(text, { metadata: { thread: thread.id } })
if (event.type === "run.failed" && event.data.metadata?.thread) {
const agent = await agents.upsert(`thread-${event.data.metadata.thread}`, config);
await agent.run("The last step failed; tell the user what happened and what to try.");
}A serverless handler that upserts without local tools never contends with the process serving the agent's tools. See Processes and connections.
The usage webhook (deprecated)
Before endpoints, PUT /v1/usage-webhook {"url"} set one URL that receives
each model response's usage in its own body (not an envelope). It still works,
but new integrations should register an endpoint for usage.recorded instead.