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.

Request
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"]}'
Response
{  "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 events to get every type except webhook.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

EventSent when
order.deliveredEvery account on the order was delivered.
order.partialSome accounts were delivered and the rest refunded to your balance, a part-filled pre-order included.
order.failedA 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.refundedMoney for the order went back, in full or in part, a cancelled pre-order included. One event each time the total returned changes.
order.updatedMore accounts were added to an order after it was first delivered.
replacement.completedA replacement was delivered or recorded, through /replace, the website, our Discord bot or by staff.
preorder.createdA pre-order was placed and paid through the API (preorder=1 on a buy).
preorder.deliveredA 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.testSent by the test endpoint only.

What we send

POST to your endpoint
{  "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 t more 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.

Webhooks · API docs · nfa.pub