Webhooks
Signed push notifications for agent launches and token lifecycle events, 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 recovered later, only replaced by registering a new endpoint.
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 includedDELETE /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 | 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 |
Synchronous rejections are not events. A request refused with a 4xx (validation, daily cap, ineligibility, no delegated wallet) 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 on a widening schedule: 1 minute, 5 minutes, 15 minutes, 30 minutes, 1 hour, 2 hours, then 4 hours twice. After 8 failed attempts the delivery is dead and will not be retried, roughly 12 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 by the next 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 and createdAt as the event time rather than the delivery time.
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