Payment Docs
Recurring payments

Binding webhooks

Signed notifications about recurring binding status changes

We send a signed POST request on every binding status change. Charges are orders: their results come in the usual order webhooks with X-Type: ORDER_UPDATE.

Where notifications are sent

The address is resolved in this order:

  1. callbackUrl of the binding (passed to POST /v1/recurring/bindings);
  2. callbackUrl of the first payment order (for a binding saved with a payment);
  3. callbackUrl from your merchant settings;
  4. if none is set, no notifications are sent for the binding.

When notifications are sent

A notification is generated when the binding status changes: ACTIVE after the payer confirms it, FAILED, SUSPENDED, DELETED. The initial PENDING status is not sent: it is returned when the binding is created.

For a binding saved with the first payment, the ACTIVE notification usually arrives right before the order notification with the COMPLETED status.

Duplicate records are excluded by the combination of address, binding ID and status among unsent notifications.

Notification format

POST {callbackUrl}
Content-Type: application/json
X-Type: RECURRING_BINDING_UPDATE
X-Signature: HMAC-SHA512(request body, API token) in hex

Body

{
  "id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "status": "ACTIVE",
  "isTest": false,
  "externalId": "sub-42",
  "externalUserId": "user_987",
  "merchantId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5e",
  "purpose": "Monthly subscription",
  "initialOrderId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5c",
  "bankName": "Sberbank",
  "bankLogoUrl": null,
  "createdAt": "2026-10-21T12:34:56.000Z",
  "updatedAt": "2026-10-21T12:36:10.512Z",
  "activatedAt": "2026-10-21T12:36:10.512Z",
  "closedAt": null
}
FieldTypeDescription
idstring (UUID)Binding ID
statusstringCurrent status, see Binding statuses
isTestbooleanTest binding
externalIdstring / nullBinding ID in your system
externalUserIdstringPayer ID in your system
merchantIdstring (UUID)Your merchant ID
purposestring / nullSubscription description
initialOrderIdstring / nullOrder whose payment created the binding
bankNamestring / nullPayer's bank
bankLogoUrlstring / nullPayer's bank logo
createdAtstringCreation time
updatedAtstringLast update time
activatedAtstring / nullWhen the binding first became ACTIVE
closedAtstring / nullWhen the binding became DELETED or FAILED

Verify the signature

Every notification carries the X-Signature header: an HMAC-SHA512 hash of the raw request body in hex, computed with your API token as the secret. The same scheme is used for payment, payout and refund webhooks.

import crypto from "crypto";

// Your API token is the HMAC secret
const apiToken = process.env.API_TOKEN;

app.post("/webhooks/payments", (req, res) => {
  const signature = req.headers["x-signature"];
  const body = JSON.stringify(req.body);

  // Calculate HMAC-SHA512 of the raw request body and compare in hex
  const expected = crypto.createHmac("sha512", apiToken).update(body).digest("hex");

  if (expected !== signature) {
    return res.status(403).send("Invalid signature");
  }

  // Signature is valid — process the notification idempotently
  const data = req.body;
  if (data.status === "COMPLETED") {
    console.log(`Order ${data.externalId} has been paid`);
  }

  res.status(200).send("OK");
});

If one endpoint receives several kinds of notifications, distinguish them by the X-Type header: ORDER_UPDATE, PAYOUT_UPDATE, REFUND_UPDATE or RECURRING_BINDING_UPDATE.

Retry policy

ParameterValue
Attemptsup to 30
Interval30s × 2^n, capped at 30 minutes
Success criterionHTTP status 200–399

A circuit breaker is also applied: an address that consistently fails is temporarily excluded from delivery.

Webhook recommendations

  • Always respond with HTTP 200 to confirm receipt. If your server does not respond, we retry the notification.
  • Verify the X-Signature header before processing the body.
  • Process notifications idempotently: the same notification may be delivered more than once.
  • Match the notification to your records by externalId, not by amount.
  • Handle every possible status value, including the unsuccessful ones.

See also

On this page