Webhooks

    Coinset webhooks let you receive Coinset events at your own HTTPS endpoint.

    Quick start

    1. Create a webhook in the Developer UI (or via the API).
    2. Implement the verification handshake (Coinset tests your endpoint before it starts sending events).
    3. Verify signatures on every delivery using the per-webhook secret.
    4. Treat deliveries as at-least-once and deduplicate using X-Coinset-Event-Id.
      • Coinset may observe similar events from multiple regions; we do best-effort cross-region deduping, but you should still enforce idempotency on your side.

    If you want a working local receiver you can copy/paste, see Webhook receiver (Node.js).

    Creating & managing webhooks

    Webhook management is served under \/hooks`` and requires authentication:

    • Browser session (passkey login), or
    • API key (Authorization: Bearer <token>)

    Endpoints:

    • GET /hooks — list your webhooks
    • POST /hooks — create (returns the secret once)
    • GET /hooks/:id — get details
    • PATCH /hooks/:id — update URL and/or filter (URL changes re-verify)
    • DELETE /hooks/:id — delete
    • POST /hooks/:id/test — enqueue a test delivery
    • POST /hooks/:id/rotate_secret — rotate the secret

    API reference: For exact request/response schemas, examples, and Try‑It, see the Coinset API docs:

    Endpoint URL rules

    Coinset validates webhook URLs to reduce SSRF risk:

    • HTTPS required
      • http:// is only allowed for local development on localhost / 127.0.0.1
    • No userinfo (https://user:pass@host is rejected)
    • No private IP literals (e.g. 127.0.0.1, 10.x, 192.168.x, 172.16–31.x, ::1, link-local)
    • No .local domains

    Verification handshake (required)

    New webhooks start as pending_verification. Coinset verifies the endpoint by sending a POST request:

    • Headers
      • X-Coinset-Webhook-Id: <hook_id>
      • X-Coinset-Webhook-Challenge: <uuid>
    • Body
    {
      "type": "coinset.webhook.verify",
      "hook_id": "…",
      "challenge": "…"
    }
    

    To pass verification:

    • Return any 2xx response.
    • Optionally return JSON with a challenge field equal to the incoming challenge (Coinset checks it if present).

    Deliveries

    All webhook deliveries are sent as POST with content-type: application/json.

    Delivery headers

    • X-Coinset-Webhook-Id: webhook id
    • X-Coinset-Delivery-Id: delivery attempt group id
    • X-Coinset-Event-Id: SHA-256 hex of the raw JSON body (use for idempotency)
    • X-Coinset-Timestamp-Ms: unix milliseconds timestamp used for signing
    • X-Coinset-Signature: HMAC-SHA256 signature (hex)
    • X-Coinset-Region: emitting region (optional; if present, matches the region field in the body)

    Signature verification

    Coinset signs the exact bytes of:

    • "<timestamp_ms>." (ASCII) concatenated with
    • the raw request body bytes

    Compare the result (hex) to X-Coinset-Signature using a constant-time compare.

    Example (Node.js):

    import crypto from "crypto";
    
    function verifyCoinsetSignature({ secret, timestampMs, rawBody, signatureHex }) {
      const mac = crypto.createHmac("sha256", secret);
      mac.update(`${timestampMs}.`);
      mac.update(rawBody); // Buffer
      const expected = mac.digest("hex");
    
      // constant-time compare
      const a = Buffer.from(expected, "hex");
      const b = Buffer.from(signatureHex, "hex");
      return a.length === b.length && crypto.timingSafeEqual(a, b);
    }
    

    Event types

    Coinset can deliver:

    • Peak events (type: "peak")
    • Reorg events (type: "reorg")
    • Transaction events (type: "transaction")
    • Offer lifecycle events (type: "offer")
    • Test deliveries (type: "coinset.webhook.test")
    • Verification (type: "coinset.webhook.verify")

    Webhook deliveries use an envelope. The delivered JSON body looks like:

    {
      "region": "fmt | msp | ldn | sin",
      "instance_id": "<server-instance-id>",
      "seq": 123,
      "message": {
        "type": "transaction",
        "data": {
          "status": "pending | confirmed | removed",
          "ids": ["<txid>", "<txid>", "..."],
          "height": 123,
          "header_hash": "...",
          "p2s": ["<p2>", "..."],
          "incoming_p2s": ["<p2>", "..."],
          "outgoing_p2s": ["<p2>", "..."]
        }
      }
    }
    

    The delivered JSON body is the event payload (and is what gets hashed for X-Coinset-Event-Id).

    Notes:

    • region can be used as a read-consistency hint if you need to immediately fetch related data after receiving a delivery (e.g. call the matching region origin).
    • For transaction events, Coinset may shrink ids to only those relevant to your filter and/or to suppress near-duplicate ids seen recently.
    • Offer events include offer_id, status, and optional tx_id/p2s when available.
    • Reorg events include old_peak_height/hash, new_peak_height/hash, and reorg_depth.

    Filters

    Webhooks support a JSON filter object:

    • events: list of event types (lowercased). If omitted, Coinset defaults effectively to ["peak"].
    • tx_id: list of transaction ids (lowercased). Hard cap: 50.
    • tx_status: list of pending | confirmed | removed (only for transaction events).
    • tx_direction: list of incoming | outgoing (only for transaction events).
    • offer_id: list of offer ids (lowercased).
    • offer_status: list of offer lifecycle statuses (open | pending | confirmed | cancel_pending | cancelled | expired | parse_failed).
    • p2: list of puzzle hashes used to match transaction/offer participants.

    Example: only confirmed transaction events for a set of tx ids:

    {
      "events": ["transaction"],
      "tx_id": ["<txid1>", "<txid2>"],
      "tx_status": ["confirmed"],
      "tx_direction": ["incoming"],
      "p2": ["<p2>"]
    }
    

    Reliability & retries

    Deliveries are at-least-once:

    • Coinset retries failed deliveries with exponential backoff (up to 10 attempts).
    • Each attempt has a timeout (currently 5 seconds).
    • After 10 consecutive failures, the webhook is automatically set to disabled.

    Best practices:

    • Return 2xx quickly, queue work internally.
    • Use X-Coinset-Event-Id to dedupe.
    • Log X-Coinset-Delivery-Id and X-Coinset-Webhook-Id for debugging.