Webhooks

Receive a signed POST on your server whenever an order or a refund changes. That way you don't need to poll the API every few seconds.

On this page

How it works

  1. 1
    Register the webhook

    Register in your webhooks panel or with POST /webhooks, using your server URL and the events you want to receive. Your account needs an active API key. The URL must start with https:// and use a public domain that resolves in DNS. IP addresses and URLs containing a username or password are rejected with INVALID_INPUT.

  2. 2
    Store the signing secret

    It comes in the creation response, in the format whsec_…, and can be read again with GET /webhooks/{id}?include_signing_secret=true. It cannot be regenerated: if it leaks, delete the webhook and create a new one. Do not confuse it with the API key.

  3. 3
    Receive the POST

    Each event arrives as a POST, with the event JSON in the body and the svix-id, svix-timestamp and svix-signature headers.

  4. 4
    Verify, store and respond 2xx

    Check the signature, store the event and only then respond. Any response other than 2xx, or no response, triggers new attempts.

Events

EventWhen it is sent
order.createdOrder created. Query the order for its final amounts and payment.
order.completedThe order was paid and completed. Confirm with a query (status completed) before releasing the purchase.
order.expiredThe order expired without payment.
order.canceledByteMax canceled the order, for example because the payment could not be accepted and was returned to the payer.
order.updatedOrder data changed without a status change: the refunded amount, the blocked amount or the payment details. It does not mean payment: a paid order sends order.completed.
refund.createdA refund requested by your account, through the API or the dashboard, was recorded. Refunds made by support only show up in order.updated.
refund.succeededThe refund finished: the amount was returned to the payer.
refund.failedThe refund could not be completed.
webhook.testTest event, sent only to the webhook you tested.

Each event carries type, which says what happened, and related_object: the object's type (order, refund or webhook), its id and its url, the query path on https://bytemax.exchange. To act, query that url: it always returns the current state, even when the event arrives late, repeated or out of order.

Example: order.completed
{
  "id": "evt_example",
  "type": "order.completed",
  "created_at": "2026-09-10T13:05:00.000Z",
  "related_object": {
    "type": "order",
    "id": "ord_example",
    "url": "/api/v1/orders/ord_example"
  }
}

Publication states

GET /events and GET /events/{id} include publication_status, which tells whether the event left ByteMax. The result on your server is in the deliveries.

pending
Recorded and waiting to be sent, or being retried. ByteMax keeps trying to publish for about 2 hours; after that the event becomes not_published.
published
Handed over for delivery. The result of each attempt is in the deliveries.
not_published
Not sent: your account had no active API key, none of your active webhooks subscribed to this event type at the time, or publication failed. It cannot be resent; look up the order or refund for its current state.

Receive and verify

Verify with the secret and the raw request body, before parsing the JSON. Use the official Svix library for your language: it checks the signature and the time tolerance of svix-timestamp. Do not implement the verification by hand.

How to verify signatures with the official library (docs.svix.com)

What reaches your server
POST /webhooks/bytemax HTTP/1.1
Content-Type: application/json
svix-id: msg_2gkQ7Wc9cD6yYq1xkKz7GJb3wQm
svix-timestamp: 1789045500
svix-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhUjM8G3xRk2w1vNq4T0=

{ "id": "evt_example", "type": "order.completed", ... }

Process without duplicates

Events can arrive more than once and out of order. Five rules prevent duplicate effects:

  • Use the event id as a unique key: store it before responding and ignore repeats.
  • Don't rely on arrival order: on each event, query the order or refund and act on the state the query returns.
  • Store the event's effect and the processed mark in the same transaction.
  • Respond 2xx only after storing. If storing fails, respond 5xx to receive it again.
  • Slow work stays out of the response: queue it and respond quickly.

Test and resend

POST /webhooks/{id}/test-events with Content-Type: application/json and {} as the body sends a webhook.test to that webhook, which must be enabled. Follow its publication with GET /events/{id} and check on your server that the event arrived and the signature was accepted. The test proves delivery and signature; to test how you handle orders and refunds, follow a real order.

Failed deliveries get up to 7 automatic retries: after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours. A webhook that keeps failing for 5 days is disabled automatically. The history of every attempt is in the dashboard and in GET /webhooks/{id}/deliveries. To resend specific events, use POST /webhooks/{id}/redeliveries with up to 100 IDs. The call answers 200 even when some events are refused: read the status of each item.

queued
The resend was accepted.
rejected
Nothing was resent. code says why: EVENT_NOT_FOUND (the ID does not exist), EVENT_NOT_ACCEPTED (the webhook does not subscribe to this event type, or the test belongs to another webhook), EVENT_NOT_PUBLISHED (the event is still pending or ended not_published) or REDELIVERY_REJECTED (the delivery service refused it). A pending event can be resent once GET /events/{id} shows published.
retry
The resend did not happen or could not be confirmed: the call ran out of time, or the delivery service was busy or failed. Send it again; if the event arrives twice, its id prevents a duplicate effect.

Limits

  • Up to 200 webhooks per account, one per URL.
  • Events can be queried for 30 days after publication.
  • Lists use items and next_cursor: 30 per page, at most 100. A cursor only works on the same list with the same filters.

Errors

Authentication, general errors and the call limit are on the API page. These codes are specific to webhooks:

WEBHOOK_URL_EXISTSHTTP 409
You already have a webhook with this URL.
WEBHOOK_LIMIT_REACHEDHTTP 409
You already have 200 webhooks. Delete one before creating another.
WEBHOOK_NOT_ENABLED_FOR_V1HTTP 409
Tests and resends need an enabled v1 webhook. Enable it or, if it came from the previous API, migrate it to v1 with PATCH.
WEBHOOK_VERSION_REQUIREDHTTP 400
A webhook from the previous API can only be changed together with its migration to v1: send api_version: "v1" and the new events.
WEBHOOK_NOT_FOUNDHTTP 404
No webhook with this ID in your account.
EVENT_NOT_FOUNDHTTP 404
No event with this ID. Events are kept for 30 days after publication.
INVALID_CURSORHTTP 400
The cursor does not belong to this list with these filters. Start again without cursor.

A 503 SERVICE_UNAVAILABLE answer means the delivery service is temporarily unavailable. Retry after a few seconds.

Reference

Ten operations, all with Authorization: Bearer <API_KEY>. The three most used appear in full; the others follow the same format.

POST/webhooks

Create webhook

Creates the webhook and returns the signing secret.

JSON body
urlstringRequired: Yes
https:// address of your server that receives the events, on a public domain.
eventsarrayRequired: Yes
Event types the webhook receives, without repeats: one or more of the eight order and refund types in Events. webhook.test cannot be subscribed to: it only arrives through the test.
enabledbooleanRequired: No
Whether the webhook receives events. Default: true.
Request
curl --request POST "https://bytemax.exchange/api/v1/webhooks" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{"url":"https://merchant.example/webhooks/bytemax","events":["order.created","order.completed","order.expired","order.canceled","order.updated","refund.created","refund.succeeded","refund.failed"],"enabled":true}'
Response 201
{
  "id": "ep_example",
  "url": "https://merchant.example/webhooks/bytemax",
  "events": [
    "order.created",
    "order.completed",
    "order.expired",
    "order.canceled",
    "order.updated",
    "refund.created",
    "refund.succeeded",
    "refund.failed"
  ],
  "enabled": true,
  "api_version": "v1",
  "signing_secret": "<WEBHOOK_SIGNING_SECRET>"
}
POST/webhooks/{id}/test-events

Send test

Sends a webhook.test to this webhook. The body is an empty JSON object.

Parameters
ParameterWhereRequired
idpathYes
Request
curl --request POST "https://bytemax.exchange/api/v1/webhooks/ep_example/test-events" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{}'
Response 202
{
  "id": "evt_test_example",
  "type": "webhook.test",
  "created_at": "2026-09-10T13:05:00.000Z",
  "related_object": {
    "type": "webhook",
    "id": "ep_example",
    "url": "/api/v1/webhooks/ep_example"
  },
  "publication_status": "pending"
}
POST/webhooks/{id}/redeliveries

Resend events

Resends up to 100 events to this webhook. The response includes the result of each one.

Parameters
ParameterWhereRequired
idpathYes
JSON body
event_idsarrayRequired: Yes
IDs of the events to resend, 1 to 100, without repeats.
Request
curl --request POST "https://bytemax.exchange/api/v1/webhooks/ep_example/redeliveries" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{"event_ids":["evt_example"]}'
Response 200
{
  "items": [
    {
      "event_id": "evt_example",
      "status": "queued"
    }
  ]
}

Other operations

OperationNameWhat it does
GET/webhooksList webhooksAll webhooks in your account, including those created with the previous API (api_version: "legacy").
GET/webhooks/{id}Get webhookOne webhook. With include_signing_secret=true, includes the signing secret.
PATCH/webhooks/{id}Update webhookURL, events or activation. To migrate a webhook created in the previous API to v1, send api_version: "v1" together with the new events.
DELETE/webhooks/{id}Delete webhookDeletes the webhook.
GET/eventsList eventsEvents from the last 30 days and their publication state.
GET/events/{id}Get eventOne event and its publication state.
GET/webhooks/{id}/deliveriesList deliveriesDelivery attempts of a webhook: succeeded when your server answered 2xx and failed when it did not answer or answered another status. Filters by state and type.