How it works
- 1Register 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 withhttps://and use a public domain that resolves in DNS. IP addresses and URLs containing a username or password are rejected withINVALID_INPUT. - 2Store the signing secret
It comes in the creation response, in the format
whsec_…, and can be read again withGET /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. - 3Receive the POST
Each event arrives as a POST, with the event JSON in the body and the
svix-id,svix-timestampandsvix-signatureheaders. - 4Verify, 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
| Event | When it is sent |
|---|---|
order.created | Order created. Query the order for its final amounts and payment. |
order.completed | The order was paid and completed. Confirm with a query (status completed) before releasing the purchase. |
order.expired | The order expired without payment. |
order.canceled | ByteMax canceled the order, for example because the payment could not be accepted and was returned to the payer. |
order.updated | Order 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.created | A refund requested by your account, through the API or the dashboard, was recorded. Refunds made by support only show up in order.updated. |
refund.succeeded | The refund finished: the amount was returned to the payer. |
refund.failed | The refund could not be completed. |
webhook.test | Test 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.
{
"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)
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
idas 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.
codesays 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 stillpendingor endednot_published) orREDELIVERY_REJECTED(the delivery service refused it). Apendingevent can be resent onceGET /events/{id}showspublished. - 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
idprevents a duplicate effect.
Limits
- Up to 200 webhooks per account, one per URL.
- Events can be queried for 30 days after publication.
- Lists use
itemsandnext_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 newevents. 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.
/webhooksCreate webhook
Creates the webhook and returns the signing secret.
urlstringRequired: Yeshttps://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.testcannot be subscribed to: it only arrives through the test. enabledbooleanRequired: No- Whether the webhook receives events. Default:
true.
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | https:// address of your server that receives the events, on a public domain. |
events | array | 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. |
enabled | boolean | No | Whether the webhook receives events. Default: true. |
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}'{
"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>"
}/webhooks/{id}/test-eventsSend test
Sends a webhook.test to this webhook. The body is an empty JSON object.
| Parameter | Where | Required |
|---|---|---|
id | path | Yes |
curl --request POST "https://bytemax.exchange/api/v1/webhooks/ep_example/test-events" \
--header "Authorization: Bearer <API_KEY>" \
--header "Content-Type: application/json" \
--data '{}'{
"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"
}/webhooks/{id}/redeliveriesResend events
Resends up to 100 events to this webhook. The response includes the result of each one.
| Parameter | Where | Required |
|---|---|---|
id | path | Yes |
event_idsarrayRequired: Yes- IDs of the events to resend, 1 to 100, without repeats.
| Field | Type | Required | Description |
|---|---|---|---|
event_ids | array | Yes | IDs of the events to resend, 1 to 100, without repeats. |
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"]}'{
"items": [
{
"event_id": "evt_example",
"status": "queued"
}
]
}Other operations
| Operation | Name | What it does |
|---|---|---|
GET/webhooks | List webhooks | All webhooks in your account, including those created with the previous API (api_version: "legacy"). |
GET/webhooks/{id} | Get webhook | One webhook. With include_signing_secret=true, includes the signing secret. |
PATCH/webhooks/{id} | Update webhook | URL, 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 webhook | Deletes the webhook. |
GET/events | List events | Events from the last 30 days and their publication state. |
GET/events/{id} | Get event | One event and its publication state. |
GET/webhooks/{id}/deliveries | List deliveries | Delivery 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. |
