Documentation sections

Documentation/Webhooks

Webhooks

Payment result notifications, retries, idempotency

How it works

When a payment reaches a final status (SUCCESS, FAILED, EXPIRED, REFUNDED, CANCELED), TabPay sends a POST request to your shop's Webhook URL. Configure the address in the dashboard on the Webhook tab of the shop card - the signing secret lives there as well.

Request headers
POST /your-webhook-path HTTP/1.1
Content-Type: application/json
X-Signature: 2f8e1c4b9a...e7d3      # HMAC-SHA256 of the raw body
X-Timestamp: 1785400000             # unix send time (seconds)
X-Signature-V2: 8a1d5f30bc...92c1   # HMAC-SHA256 of the string "timestamp.body"

Notification body

id

string (UUID)

Payment identifier in TabPay.

orderId

string

Your order number.

status

string

Final status: SUCCESS, FAILED, EXPIRED, REFUNDED, or CANCELED. Acknowledge statuses your code does not recognize with a 200 and log them - the set may grow.

amountKopecks

integer

Payment amount in kopecks.

telegramId

string | null

The buyer's Telegram ID if it was passed when creating the payment - your bot does not need its own mapping table.

metadata

object | null

Your arbitrary data from the create-payment request (echoed back), or null.

test

boolean

true - the notification is a test one: sent by the "Send test webhook" button in shop settings or by a sandbox-shop payment; acknowledge it with a 200 but do not deliver the product. Live payments have false. Check the field's value, not its presence.

Webhook body
{
  "id": "6b9d2c88-4b1a-4f0e-9c37-1f2ab34cd561",
  "orderId": "order-1001",
  "status": "SUCCESS",
  "amountKopecks": 19900,
  "telegramId": "987654321",
  "metadata": { "productId": 42, "tariff": "month" },
  "test": false
}

Every webhook is signed with the X-Signature and X-Signature-V2 headers - always verify the signature before trusting the body. The v2 scheme additionally includes a timestamp and protects against replaying intercepted notifications: Signature verification.

Receiver requirements

  • a public address (not on a private network); we strongly recommend HTTPS - notifications contain your payment data;
  • a 2xx response within 5 seconds - that is enough, the response body is not read;
  • redirects are not followed: a 3xx response counts as a failed delivery;
  • respond with 200 first, then do the slow work (delivering the product) - otherwise you risk missing the timeout.

Retries

If your server did not respond with 2xx, the notification is retried: after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, and 24 hours - up to 7 attempts in total. The body does not change between attempts. Once the receiver is fixed, undelivered notifications arrive on their own. The delivery log (result, attempt count, error) with a manual resend button is on the Webhook tab of the shop card; a resend is available even after all attempts are exhausted.

Idempotency

Process notifications idempotently: because of retries, the same webhook can arrive twice. A repeated notification with the same id and status must not, for example, deliver the product a second time. A simple approach is to store processed (id, status) pairs and skip duplicates. Different statuses for the same id are not duplicates: a payment can receive SUCCESS after EXPIRED or FAILED (a late SBP QR payment) and REFUNDED after SUCCESS - process each such transition.

Webhooks are the primary channel for payment results. If a webhook never arrived (your server was unreachable for the entire retry window), verify the status with an API request.

Test webhook

You can verify your integration without a real payment: in shop settings, on the Webhook tab, click "Send test webhook". Your URL receives a notification built exactly like a live one - with valid X-Signature and X-Signature-V2 signatures - but with the "test": true field. The id of a test notification is prefixed with test- and is not a valid UUID - do not store it in a uuid-typed column. Your handler must respond with 200; there is no need to deliver a product for a test notification. The result (HTTP code and your server's response time) is shown in the dashboard.