Webhooks
Signed push notifications for your account’s launches, token lifecycle, trades, transfers, key and wallet changes, and watchlist alerts, so an integration stops polling for outcomes it can be told about. Registration uses your normal authenticated Candle session; deliveries are signed with a per-endpoint secret.
Registering an endpoint
Section titled “Registering an endpoint”Requires agent features to be enabled on the account (see Agent access & API keys).
POST /api/v1/agent/webhookscontent-type: application/json
{ "url": "https://example.com/hooks/candle", "events": ["launch.confirmed", "migration.completed"] }{ "success": true, "webhook": { "id": "...", "url": "https://example.com/hooks/candle", "events": ["launch.confirmed", "migration.completed"], "secret": "whsec_...", "secretPrefix": "a1b2c3d4", "createdAt": 1754400000000 }}The signing secret is returned only in this response. Store it immediately; it cannot be read back later. To replace it, call POST /api/v1/agent/webhooks/:id/rotate, which returns a new secret once and keeps the endpoint’s id, events and delivery history. Queued deliveries are signed at send time, so update your verifier immediately. A revoked endpoint answers 409; an unknown id answers 404.
The URL must be public HTTPS. Private, loopback, and link-local targets are rejected at registration and re-checked before every delivery, so an endpoint whose DNS is later repointed inward stops receiving rather than reaching internal services.
Managing endpoints
Section titled “Managing endpoints”GET /api/v1/agent/webhooks # list, secretPrefix only, revoked includedPOST /api/v1/agent/webhooks/:id/rotate # new signing secret, shown onceDELETE /api/v1/agent/webhooks/:id # revokeGET /api/v1/agent/webhooks/:id/deliveries # last 20 attempts, for debuggingThe deliveries read returns eventId, event, status, attempts, lastStatusCode, lastError, and timestamps, which is usually enough to diagnose a failing consumer without contacting support.
An account may hold at most 3 active endpoints. Registering a fourth returns WEBHOOK_LIMIT_REACHED (400); revoke one first.
Events
Section titled “Events”| Event | When it fires | Payload core |
|---|---|---|
launch.confirmed | A headless launch confirmed on-chain | clientLaunchId, chain, mint, pool, signature, devBuy? |
launch.failed | A launch failed definitively after acceptance | clientLaunchId, chain, errorCode |
curve.graduated | Graduation observed on the curve | chain, mint, curve |
migration.completed | The graduated venue is seeded and trading | chain, mint, pool, migratedAt |
migration.delayed | Automated migration retries were exhausted (Hood only today; chain is always hood) | chain, mint, curve, attempts, lastError? |
trade.executed | An agent trade recorded: a main payer’s inline execution, or a linked payer’s verified confirm | clientTradeId, chain, side, mint, quoteAsset, amountRaw, expectedOutRaw, signature, fee: { bps, feeRaw } |
order.triggered | A limit order’s price condition held and its status flipped to triggered | clientOrderId, chain, mint, side, amountRaw, targetPrice, price, expiresAt, trigger (limit, stop or trailing_stop), and trailBps?, trailWatermark?, stopPrice? for trailing stops |
transfer.executed | A transfer completed on the agent transfer rail | chain, asset, amountRaw, from, to, destinationClass (own or approved), signature, walletId?, destinationKind? |
withdrawal_address.added | The owner approved a withdrawal address | chain, address, label? |
withdrawal_address.revoked | The owner removed an approved withdrawal address | chain, address, label? |
linked_wallet.trusted | The owner marked linked wallets trusted (one event per request) | batchId, actor (session or device), wallets: [{ id, chain, address, label? }] |
linked_wallet.untrusted | The owner cleared the trust mark | as linked_wallet.trusted |
key.access_widened | A key moved to a wider access level | changeId, keyPrefix, label, from: { access, scopes }, to: { access, scopes }, actor, at |
key.access_narrowed | A key moved to a narrower access level (by the owner or the key itself) | as key.access_widened; actor may be agent |
tracked_wallet.traded | A wallet on your watchlist bought or sold (never your own trade) | chain, wallet, label?, side, signature, tokenMint?, tokenSymbol?, usdAmount?, at |
tracked_wallet.launched | A wallet on your watchlist deployed a token | chain, wallet, label?, mint, symbol?, at |
Registering an event name not in this table returns 400. A new event reaches an endpoint only once it is added to that endpoint’s list.
Synchronous rejections are not events. A request refused with a 4xx (validation, daily cap, ineligibility, no embedded wallet with agent access approved) already told the caller in the response body; only failures that happen after a launch was accepted produce launch.failed.
migration.delayed is the machine-readable form of an internal alert: it means the automated seeding gave up and a human is looking. The token is not lost, but it will not become tradable on the graduated venue until the issue clears.
trade.executed fires once per trade that actually records, from the Agent trading API. A failed on-chain verification or an idempotent replay of an already-confirmed trade never fires it again.
order.triggered moves no money: the keeper’s flip is a signal, and your agent still completes the order through the normal build-and-confirm rail (see limit orders). The payload carries everything the completing /build call needs plus the observed price that satisfied the condition, so a stateless consumer can act without a lookup. Expiry fires nothing, and the completing trade still fires trade.executed.
Delivery format
Section titled “Delivery format”Each delivery is a POST with this body:
{ "id": "evt_9f2c...", "event": "launch.confirmed", "createdAt": 1754400000000, "data": { "clientLaunchId": "my-bot-run-42", "chain": "solana", "mint": "...", "pool": "...", "signature": "..." }}and these headers:
| Header | Meaning |
|---|---|
x-candle-event | The event name, so a consumer can route before parsing |
x-candle-delivery | The unique event id. Dedupe on this |
x-candle-signature | t=<unix seconds>,v1=<hex hmac-sha256(secret, "<t>.<raw body>")> |
Respond with any 2xx as soon as you have durably accepted the event. Anything else counts as a failed attempt and is retried.
Verifying a delivery
Section titled “Verifying a delivery”Recompute the HMAC over the string "<t>.<raw request body>" using your endpoint’s secret, compare it to v1 in constant time, and reject timestamps outside a tolerance window (300 seconds is a sane default). Verify against the raw body bytes, not a re-serialized object: re-encoding can change key order or whitespace and will break the signature.
The TypeScript SDK ships this as verifyWebhookSignature(secret, header, body, nowSec), implementing the same scheme.
Retries and ordering
Section titled “Retries and ordering”A failed delivery is retried after 1 minute, 5 minutes, 15 minutes, 30 minutes, 1 hour, 2 hours and 4 hours. The 8th failed attempt is final, roughly 8 hours after the event. That window is wide enough to ride out a consumer deploy without dropping events, and short enough that a permanently dead endpoint stops costing anything the same day.
First delivery normally lands within about 30 seconds of the event. Delivery order is not guaranteed and duplicates are possible after a timeout, so treat id as an idempotency key. The body’s createdAt is the time that attempt was sent, not the event time, and it changes across retries.
Next steps
Section titled “Next steps”- Launch and let the events tell you the outcome: Headless launch API
- Trade and get
trade.executed: Agent trading API - Verify signatures without hand-rolling HMAC: TypeScript SDK
- Poll instead, or in addition: Market state API