Skip to content

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.

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

GET /api/v1/agent/webhooks # list, secretPrefix only, revoked included
POST /api/v1/agent/webhooks/:id/rotate # new signing secret, shown once
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 exhausted (Hood only today; chain is always hood)chain, 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, trigger (limit, stop or trailing_stop), and trailBps?, trailWatermark?, stopPrice? for trailing stops
transfer.executedA transfer completed on the agent transfer railchain, asset, amountRaw, from, to, destinationClass (own or approved), signature, walletId?, destinationKind?
withdrawal_address.addedThe owner approved a withdrawal addresschain, address, label?
withdrawal_address.revokedThe owner removed an approved withdrawal addresschain, address, label?
linked_wallet.trustedThe owner marked linked wallets trusted (one event per request)batchId, actor (session or device), wallets: [{ id, chain, address, label? }]
linked_wallet.untrustedThe owner cleared the trust markas linked_wallet.trusted
key.access_widenedA key moved to a wider access levelchangeId, keyPrefix, label, from: { access, scopes }, to: { access, scopes }, actor, at
key.access_narrowedA key moved to a narrower access level (by the owner or the key itself)as key.access_widened; actor may be agent
tracked_wallet.tradedA wallet on your watchlist bought or sold (never your own trade)chain, wallet, label?, side, signature, tokenMint?, tokenSymbol?, usdAmount?, at
tracked_wallet.launchedA wallet on your watchlist deployed a tokenchain, 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.

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