Skip to content

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.

Requires agent features to be enabled on the account (see Agent access & API keys).

POST /api/v1/agent/webhooks
content-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.

GET /api/v1/agent/webhooks # list, secretPrefix only, revoked included
DELETE /api/v1/agent/webhooks/:id # revoke
GET /api/v1/agent/webhooks/:id/deliveries # last 20 attempts, for debugging

The 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.

EventWhen it firesPayload core
launch.confirmedA headless launch confirmed on-chainclientLaunchId, chain, mint, pool, signature, devBuy?
launch.failedA launch failed definitively after acceptanceclientLaunchId, chain, errorCode
curve.graduatedGraduation observed on the curvechain, mint, curve
migration.completedThe graduated venue is seeded and tradingchain, mint, pool, migratedAt
migration.delayedAutomated migration retries were exhaustedchain, mint, curve, attempts, lastError?
trade.executedAn agent trade recorded: a main payer’s inline execution, or a linked payer’s verified confirmclientTradeId, chain, side, mint, quoteAsset, amountRaw, expectedOutRaw, signature, fee: { bps, feeRaw }
order.triggeredA limit order’s price condition held and its status flipped to triggeredclientOrderId, 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.

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:

HeaderMeaning
x-candle-eventThe event name, so a consumer can route before parsing
x-candle-deliveryThe unique event id. Dedupe on this
x-candle-signaturet=<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.

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.

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.