camelAI Documentation

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:

bash
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/webhooksList your endpoints (at most 16)
GET, PATCH, DELETE /v1/webhooks/{webhookId}Read, change or remove one
POST /v1/webhooks/{webhookId}/secretReplace 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:

json
{"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}}}
TypeWhendata
run.startedA run (method: prompt, continue, resume, execute) beganagentId, requestId, method, actor, metadata; resumes when a turn was resumed after camelRun restarted mid-turn
run.completedIt ended without an errorAs 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.failedIt ended with an error, camelRun's or the model'sAs 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.requestedThe agent asks a person for input (the run waits)agentId, requestId, inputId, toolCallId, kind, expiresAt
input.resolvedAn input settledagentId, requestId, inputId, state
usage.recordedA 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.endedA voice session connected, ended or failed, heard a person, or spokeThe session's facts. See Voice calls
billing.balance.lowYour funded balance crosses below the alert threshold (default $2)balance, previousBalance, threshold (integer micro-USD), source
billing.balance.depletedA positive balance reaches zero or belowThe 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.lowAn end user's balance or allowance runs low or out, or you posted to their ledgerendUser 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 at outcome.result.reply), history, or inputs. metadata is what the message that started the run carried, so you can route an event without a lookup.
  • created is in Unix seconds. An event's data only 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 by created, 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:

ts
// 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.