Webhooks
Coinset webhooks let you receive Coinset events at your own HTTPS endpoint.
Quick start
- Create a webhook in the Developer UI (or via the API).
- Implement the verification handshake (Coinset tests your endpoint before it starts sending events).
- Verify signatures on every delivery using the per-webhook secret.
- 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 webhooksPOST /hooks— create (returns the secret once)GET /hooks/:id— get detailsPATCH /hooks/:id— update URL and/or filter (URL changes re-verify)DELETE /hooks/:id— deletePOST /hooks/:id/test— enqueue a test deliveryPOST /hooks/:id/rotate_secret— rotate the secret
API reference: For exact request/response schemas, examples, and Try‑It, see the Coinset API docs:
- Coinset API (webhooks) (selects the Coinset API spec)
Endpoint URL rules
Coinset validates webhook URLs to reduce SSRF risk:
- HTTPS required
http://is only allowed for local development onlocalhost/127.0.0.1
- No userinfo (
https://user:pass@hostis rejected) - No private IP literals (e.g.
127.0.0.1,10.x,192.168.x,172.16–31.x,::1, link-local) - No
.localdomains
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
challengefield 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 idX-Coinset-Delivery-Id: delivery attempt group idX-Coinset-Event-Id: SHA-256 hex of the raw JSON body (use for idempotency)X-Coinset-Timestamp-Ms: unix milliseconds timestamp used for signingX-Coinset-Signature: HMAC-SHA256 signature (hex)X-Coinset-Region: emitting region (optional; if present, matches theregionfield 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:
regioncan 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
idsto only those relevant to your filter and/or to suppress near-duplicate ids seen recently. - Offer events include
offer_id,status, and optionaltx_id/p2swhen available. - Reorg events include
old_peak_height/hash,new_peak_height/hash, andreorg_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 ofpending | confirmed | removed(only for transaction events).tx_direction: list ofincoming | 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-Idto dedupe. - Log
X-Coinset-Delivery-IdandX-Coinset-Webhook-Idfor debugging.