Webhooks
Signed events for launches, payouts, low credit and keys at their limit, as they happen.
Add an endpoint (a public https:// URL) on the dashboard's Webhooks page or with POST /webhooks, for some or all events. The answer carries its signing secret (whsec_...): store it, as only a retry with the same Idempotency-Key shows it again. A project has up to 5 endpoints.
Events
data holds- launch.confirmedA launch the chain confirmed: its token is on its route.
launch - launch.failedA launch the chain refused, or whose route came out other than prepared.
launch - token.importedAn import the chain confirmed: the existing token is on its route.
launch - token.graduatedA token's curve completed: it trades on PumpSwap, its fees still on its route.
tokengraduated_at - payout.creditedA payout of a token's creator fees became AI credit.
payout - account.low_balanceAn account's spendable credit fell under the threshold: once, until a credit lifts it back.
accountthreshold_usd - key.limit_reachedA router key's call was refused for its spending limit: once in each of the limit's periods.
key - route.changedA token's creator fees no longer follow the route relayfor locked (a takeover on pump.fun): its payouts stop.
tokenreasondetected_at
Each event's fields are in the reference. POST /webhooks/{id}/test sends a ping to one endpoint.
Every delivery is a POST with the same envelope. data holds what the event is about, in the API's own shapes:
{
"id": "evt_4c9LGfliJda80U3V",
"type": "payout.credited",
"created_at": "2026-10-07T13:40:01.000Z",
"project": "prj_joucOmKkV9Ikdf92",
"data": {
"payout": {
"token": "MASi45ub7Qe4ZE36UT5G6cU4ud8Fhhe4deS4F3cw9KTA",
"account": "acc_arqp1qhbpvjhzifE",
"signature": "dYmM6J4tmCUz5J2h...",
"event": "2.3",
"slot": "372918466",
"quote_mint": "So11111111111111111111111111111111111111112",
"distributed_lamports": "61500000",
"ai_bps": 5600,
"ai_lamports": "34440000",
"sol_usd": {
"price": "221.04",
"confidence": "0.064",
"published_at": "2026-10-07T13:39:58.000Z"
},
"credited_usd": "7.610413",
"created_at": "2026-10-07T13:40:01.000Z"
}
}
}Its headers name the event too: relayfor-event-id, relayfor-event-type, and the signature.
Checking a signature
Each delivery carries relayfor-signature: t=<unix seconds>,v1=<hex>: the HMAC-SHA256 of <t>.<raw body> under the endpoint's secret. Recompute it over the raw body, compare in constant time, and refuse a t more than 5 minutes from your clock.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(body: string, header: string, secret: string): boolean {
const parts = header.split(",");
const t = Number(parts.find((part) => part.startsWith("t="))?.slice(2));
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest();
return parts
.filter((part) => part.startsWith("v1="))
.some((part) => {
const given = Buffer.from(part.slice(3), "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}Read the body as raw text before parsing it: parsing and serializing again changes the bytes. After POST /webhooks/{id}/rotate, deliveries carry both secrets' signatures for a day.
Delivery
- Answer 2xx within 10 seconds. Anything else, a redirect included, is a failure.
- Failures are retried with backoff (1, 5, 15 and 30 minutes, then hourly and longer) for 24 hours.
- An event can arrive more than once: use its
idto do its work once. GET /webhooks/deliverieslists them, andPOST /webhooks/deliveries/{id}/replaysends one again.POST /webhooks/{id}/testsends apingnow.