Webhooks
Coinetic notifies your server of invoice changes with a POST request. A webhook is a signal to “go check the invoice,” not proof of payment.
Setup
- Dashboard → “Keys & webhooks” → “Add.” The URL must be
https://on a public address and port 443 (the sandbox also allowshttp://localhost). - The endpoint secret is shown only once. You can have up to 5 active endpoints, and every event is sent to all of them.
Request
POST /webhooks/coinetic HTTP/1.1
Content-Type: application/json
User-Agent: cryptopay-webhooks/1
X-Webhook-Id: evt_3f9c…
X-Webhook-Timestamp: 1760000000
X-Webhook-Signature: v1=5d1e…
{"id":"evt_3f9c…","type":"invoice.paid","created_at":"2026-10-12T15:04:05Z","livemode":true,"data":{"invoice_id":"inv_x7k2…","order_id":"ORDER-1001","status":"paid","paid_amount":"49990000"}}X-Webhook-Id stays the same across retries. During secret rotation, X-Webhook-Signature contains two comma-separated signatures. The technical names (cryptopay-webhooks/1) are legacy; you don't need to change anything on your side.
Verifying the signature
expected = "v1=" + hex(HMAC-SHA256(webhook_secret, X-Webhook-Timestamp + "." + raw_body))- Take the raw body: the bytes before any JSON parsing.
- Check that
X-Webhook-Timestampis within 5 minutes of the current time. - Split
X-Webhook-Signatureon commas. The signature is valid if any part matches the expected value (use a constant-time comparison). - No match? Respond with
401and do nothing.
Test vector: secret whsec-0123456789abcdef0123456789abcdef, timestamp 1760000000, body {"id":"evt_1","type":"invoice.paid"} → v1=aceeb56215ba09e7a2923d48f89afb8331f422f2d0a8cf60f76fef69fe06a6a4.
Handling
verify signature → deduplicate by X-Webhook-Id → GET /v1/invoices/{invoice_id}
→ update the order from the API status (fulfill once) → respond 2xx- Deduplication. The same event can arrive more than once. Store the
X-Webhook-Idvalues you've processed, and set the “fulfilled” flag in the same transaction as the fulfillment itself. - Respond quickly, within 10 seconds. Push long-running work to a queue.
- Response codes:
2xxmeans accepted;410 Gonemeans don't retry this delivery; any other code (including3xx, since redirects aren't followed) or a timeout triggers a retry. - Ignore unknown fields and event types: the list will grow.
Ready-made handlers in four languages are in the quickstart.
Retries
10 s → 30 s → 2 min → 10 min → 30 min → 1 h → 3 h ×7 → 1 h, with slight jitter. The final attempt comes roughly 24 hours later. You can resend a failed delivery from the dashboard: “Webhooks” → “Resend.”
Ordering
Events for the same invoice are delivered to an endpoint in order: the next one isn't sent while the previous delivery is still awaiting a retry. Once that delivery finishes, successfully or not, the following events go out. So rely on the status from GET /v1/invoices/{id}, not on the sequence of events. Events for different invoices are independent.
Event types
| Event | When |
|---|---|
invoice.detected | Payment seen on the network |
invoice.confirming | Payment is in a block, confirmations in progress |
invoice.paid | Paid and confirmed; safe to fulfill (after re-checking). data.late: true means a stablecoin arrived after expiration and was credited automatically |
invoice.overpaid | Paid with an excess |
invoice.underpaid | Confirmed amount is short |
invoice.awaiting_payment | The invoice is awaiting payment again (for example, after a reorg of its only payment) |
invoice.expired | Expired with no payment |
invoice.late_payment | Payment in a volatile asset after expiration |
invoice.manual_review | Payment under AML review |
invoice.payment_held | Payment held by AML (quarantine) |
invoice.reorged | The block containing the payment was rolled back and the status recalculated (for example, paid → confirming) |
invoice.payment_after_close | A payment arrived on an already closed invoice: it doesn't pay for the order, but it's available for refund |
invoice.cancelled | The invoice was closed unpaid by an executed refund |
refund.sent | Refund executed; data.refund contains the amount, address and tx_hash |
Billing events (billing.*) cover platform fee invoices: issued, paid, overdue, suspended and reinstated.
invoice.reorgedA reorg after paid is rare, but it happens. If you deliver goods instantly, subscribe to this event and route these cases to support.Rotating the secret
Dashboard → “Rotate secret”: Coinetic starts signing with both secrets.
- Add the new secret to your server (accept both).
- Click “Complete rotation” to delete the old one.
- Remove the old secret from your server.