Webhooks
Register an HTTPS endpoint and we call it when one of your orders changes: delivered, refunded, replaced, or a pre-order filled. Every call is signed, so you can prove it came from us.
Add an endpoint
Call POST /webhooks with a full key. The answer holds the signing secret. It is shown once, so store it straight away.
curl -X POST "https://www.nfa.pub/api/v1/webhooks" \ -H "X-Api-Key: rsk_..." \ -H "Content-Type: application/json" \ -d '{"url":"https://hooks.example.com/nfa","events":["order.delivered","order.refunded"]}'{ "ok": true, "webhook": { "id": "wh_Q2xZa1b2C3d4E5f6", "status": "active", ... }, "secret": "whsec_..."}- Up to 3 endpoints per account.
- The URL must be
https://on port 443, with a public hostname. The reference has the full rules. - Leave out
eventsto get every type exceptwebhook.test.
Which orders
- Only orders your account bought, by the same rule as
/order. - Only while the account has at least one active API key.
- Balance top-ups are never announced.
- Nothing from before the endpoint existed is replayed.
Events
| Event | Sent when |
|---|---|
order.delivered | Every account on the order was delivered. |
order.partial | Some accounts were delivered and the rest refunded to your balance, a part-filled pre-order included. |
order.failed | A delivery attempt failed. It may still be retried or held for review. Money going back is its own order.refunded. A retry that fails again with nothing changed is not announced twice. |
order.refunded | Money for the order went back, in full or in part, a cancelled pre-order included. One event each time the total returned changes. |
order.updated | More accounts were added to an order after it was first delivered. |
replacement.completed | A replacement was delivered or recorded, through /replace, the website, our Discord bot or by staff. |
preorder.created | A pre-order was placed and paid through the API (preorder=1 on a buy). |
preorder.delivered | A pre-order's stock arrived and it was delivered (all of it, or the next part). The same for API and website pre-orders. |
webhook.test | Sent by the test endpoint only. |
What we send
{ "id": "evt_Zk3q9V0c1mR8xT2aLp7wQe", "type": "order.partial", "created": 1793023861, "data": { "order": "339620d0e1843b5b-cc957e7efa86a0db", "product": "rust-500-999-hours", "quantity": 2, "delivered": 1, "state": "partial", "refundedCents": 145, "currency": "EUR" }}No logins are ever in an event. Fetch them with GET /order and the order id. The id is the same on every retry, so de-duplicate on it.
Verify the signature
Every call has an Nfa-Signature header: t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of t, a dot and the raw body, keyed with your secret.
- Use the raw body, byte for byte. Re-serialised JSON will not match.
- The key is the whole secret,
whsec_prefix included. - Compare in constant time, never with
==. - Reject a
tmore than 5 minutes from your clock, so a captured call cannot be replayed.
Ready-made functions for Node.js and Python, tested against real signatures, are in the reference.
Webhooks come from Cloudflare's network, not from a fixed IP address. Do not allowlist IPs for them. Check the signature instead.
Answer fast
- Answer any 2xx within 10 seconds, then do the work. Anything else, no answer or a redirect counts as a failure.
- A failed delivery is tried up to 8 times in all: again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours.
- The same event can arrive twice, and events can arrive out of order. When order matters, read the current state from
/order. - After 50 failures in a row the endpoint is disabled. Fix it, then call
POST /webhooks/{id}/enable.
Test it
POST /webhooks/{id}/test queues a webhook.test event for that endpoint. GET /webhooks/{id}/deliveries shows what was sent and what your endpoint answered.