Webhooks
Configure and enable the webhook URL and secret in the integration's Webhooks tab in the dashboard. Use an endpoint on your server that accepts POST requests with a JSON body.
Shop API sends final results only:
| Event | Status | Meaning |
|---|---|---|
order.completed | completed | Purchase completed successfully |
order.refunded | refunded | Purchase failed and the charged amount was returned |
No separate webhook is sent for processing, review, recipient checks or insufficient balance. Telegram notifications are configured separately.
Request body
{
"event": "order.completed",
"timestamp": "2026-09-26T12:00:05.000Z",
"data": {
"order_id": 125,
"partner_order_id": "order-456",
"api_user_id": "customer-789",
"module": "codes",
"status": "completed",
"amount_usd": "0.9800",
"currency": "USD",
"codes": [
{"content": "EXAMPLE-CODE"}
]
}
}
| Field | Description |
|---|---|
event | order.completed or order.refunded |
timestamp | Webhook creation time in UTC, ISO 8601; not the original order creation time |
data.order_id | AdaptGroup order ID |
data.partner_order_id | Your order ID or null |
data.api_user_id | Your customer's string ID |
data.module | stars, tg_premium, topups or codes |
data.status | Final order status |
data.amount_usd | Charged/refunded amount as a decimal string |
data.currency | Always USD |
data.codes | Issued codes only for a completed codes order; otherwise [] |
An issued code can contain content, number and/or pin. Only present values are included.
Refund example:
{
"event": "order.refunded",
"timestamp": "2026-09-26T12:00:05.000Z",
"data": {
"order_id": 125,
"partner_order_id": "order-456",
"api_user_id": "customer-789",
"module": "codes",
"status": "refunded",
"amount_usd": "0.9800",
"currency": "USD",
"codes": []
}
}
Verify the signature
The X-Webhook-Signature header contains the lowercase hexadecimal HMAC-SHA256 digest of the exact request body bytes, signed with your webhook secret.
The secret itself is not sent in the header or body. It is separate from the API key.
Verify the raw body before parsing it. Re-serializing JSON changes its bytes and can invalidate the signature.
const crypto = require('node:crypto');
function verifySignature(rawBody, secret, signature) {
if (typeof signature !== 'string' || !/^[a-f0-9]{64}$/.test(signature)) {
return false;
}
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest();
return crypto.timingSafeEqual(expected, Buffer.from(signature, 'hex'));
}
rawBody must be the original Buffer received by your HTTP server, before a JSON parser changes it.
Acknowledge and handle repeats
- Verify the signature.
- Match
data.order_idto your order. - Save and apply the final result once.
- Return HTTP 200 or 201 after you accept the event.
A response body is not required. Other statuses, including 204, do not acknowledge delivery. The sender waits up to 10 seconds for the HTTP request.
An event may be delivered repeatedly. Use the integration, data.order_id and event to recognize a result already processed. Do not issue the goods or credit a refund again. This handling of webhook repeats does not make repeated /orders/create requests the same purchase.
Delivery attempts
Once the event has been accepted by the delivery service, unsuccessful delivery is retried:
| Attempt | Delay after the preceding failed attempt |
|---|---|
| 1 | Immediately |
| 2 | 1 minute |
| 3 | 2 minutes |
| 4 | 5 minutes |
| 5 | 10 minutes |
| 6 | 15 minutes |
| 7 | 30 minutes |
| 8 | 1 hour |
| 9 | 2 hours |
| 10 | 2 hours |
A webhook is not a guarantee that every result will reach your server. If it does not arrive, retrieve the saved result with Order status. Replaying a purchase is not a delivery-recovery mechanism.