Webhooks

Webhooks let you skip polling and react to state changes the moment they happen. Subscribe a URL, get HTTPS POSTs whenever any resource transitions state.

Quick start

  1. List the event catalogGET /v2/webhook-event-types. Every event we can fire, with a detection mode telling you how reliably it lands today.
  2. Create a subscriptionPOST /v2/webhook-subscriptions with { url, enabled_events: ["pool.live", "airdrop.claimed"] } or ["*"] for everything. The response includes a one-time secretstore it now, you can't read it again.
  3. Verify the signature on every incoming POST (see below) and process the data.object payload.

Subscription lifecycle

POST /v2/webhook-subscriptions       → returns the subscription + full secret (once)
GET  /v2/webhook-subscriptions       → list yours (paginated)
GET  /v2/webhook-subscriptions/{id}  → single read (secret_preview only)
PATCH /v2/webhook-subscriptions/{id} → toggle active, change URL, change enabled_events
DELETE /v2/webhook-subscriptions/{id} → remove

Delivery payload

Every webhook POST has this shape:

{
  "id": "evt_a1b2c3d4e5f6",
  "type": "pool.live",
  "api_version": "2.0",
  "created": 1748800000,
  "data": {
    "object": {
      "resource_type": "pool",
      "resource_id": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "type": "dbc",
      "base_mint": "..."
    }
  },
  "external_uid": "campaign_2026_q2",
  "metadata": { "ledger_id": "tx_abc123" }
}

external_uid and metadata echo whatever you passed on the original write that produced the resource — that's how you reconcile back to your own system.

Signature verification

Every delivery includes header X-Pirate-Signature: t=<unix_ts>,v1=<hex>. Verify with:

import { createHmac, timingSafeEqual } from "crypto";

function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Reject if (a) the timestamp is older than 5 minutes (replay protection) or (b) the signature doesn't match.

Retry policy

When delivery is live, we'll retry on any non-2xx response or timeout (>10s):

AttemptDelay after previous
230 seconds
32 minutes
410 minutes
51 hour
66 hours
7 (final)24 hours

After 7 failures the subscription is auto-active: false'd. Re-enable via PATCH /v2/webhook-subscriptions/{id}.

Detection modes (read this)

Not every event is real-time. Each event in GET /v2/webhook-event-types exposes a detection field:

  • synchronous — fires immediately from the route handler. Live today.
  • tx_confirmed — fires after the user-signed transaction finalizes on-chain. Requires the transaction watcher to be running; it's part of the v2 rollout but separate from the API process.
  • polling_required — needs background polling (e.g., comparing quoteReserve to migration threshold every N seconds). Not yet shipped; events with this detection mode will be emitted once the poller comes online.

Subscribing to events whose detection mode hasn't shipped yet is harmless — you just won't receive them until that piece of infrastructure lands.

Current status

The subscription CRUD endpoints are live. The delivery worker (the process that reads events and fans out HTTPS POSTs) is the next milestone — events ARE being persisted to the events table today and will be replayed once the worker ships.