# Вебхуки

Уведомление на ваш сервер, когда заказ получает окончательный статус.

Передайте `webhookUrl` в теле `POST /purchase`. Когда заказ станет `completed`, `failed` или `cancelled`, мы отправим на этот адрес POST-запрос:

```json
{
  "event": "order.status",
  "orderId": 1842,
  "status": "completed",
  "failureReason": null,
  "productId": 12,
  "price": 1490,
  "currency": "RUB",
  "idempotencyKey": "order-7731",
  "createdAt": "2026-11-13T12:36:53.000Z"
}
```

Ключа товара в вебхуке нет — получите его через [GET /orders/{orderId}](https://docs.merchant.dbdlobby.com/ru/reference/get-order).

## Заголовки

| Заголовок | Значение |
|---|---|
| `X-Merchant-Timestamp` | Время отправки, Unix-секунды. |
| `X-Merchant-Signature` | `sha256=` + hex HMAC-SHA256 от строки `<timestamp>.<тело запроса>` на вашем секрете вебхуков. Секрет выдаём вместе с ключом доступа. |
| `User-Agent` | `merchant-webhooks/1` |

## Проверка подписи

Всегда проверяйте подпись и время: иначе кто угодно сможет прислать вам «оплаченный» заказ.

```javascript
import crypto from "node:crypto";

// rawBody — тело запроса строкой, ДО JSON.parse: подпись считается по байтам.
export function verifyMerchantWebhook(rawBody, headers, secret) {
  const timestamp = headers["x-merchant-timestamp"];
  const signature = headers["x-merchant-signature"] ?? "";
  // Отбрасываем старые доставки — защита от повторной отправки перехваченного запроса.
  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express:
// app.post("/hooks/merchant", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verifyMerchantWebhook(req.body.toString("utf8"), req.headers, process.env.MERCHANT_WEBHOOK_SECRET))
//     return res.sendStatus(401);
//   const event = JSON.parse(req.body);
//   res.sendStatus(200); // ответьте быстро, обработку — в очередь
// });
```

```python
import hashlib
import hmac
import time


def verify_merchant_webhook(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers.get("X-Merchant-Timestamp", "")
    signature = headers.get("X-Merchant-Signature", "")
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False
    message = timestamp.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)
```

## Доставка и повторы

- Ответ `2xx` в течение 5 секунд — доставлено. Отвечайте сразу, а обработку ставьте в очередь.
- Ошибка сети, таймаут, `5xx`, `408`, `429` — повтор через 30 с, 2 мин, 10 мин, 30 мин, 2 ч и 6 ч (всего до 7 попыток).
- Редиректы (`3xx`) не выполняются, остальные `4xx` считаются окончательным отказом — без повторов.
- Итог виден в поле `webhookStatus` заказа: `pending`, `delivered` или `failed`.
- Одно событие может прийти больше одного раза. Обрабатывайте идемпотентно — по паре `orderId` + `status`.

## Требования к URL

Только `https://`, до 2048 символов, без логина и пароля в адресе. Нельзя `localhost`, внутренние и служебные IP-адреса — ни напрямую, ни через DNS-имя, которое на них указывает. Неверный адрес — `400 Invalid webhookUrl (причина)`.

В песочнице вебхук приходит только для успешной тестовой покупки, содержит `"sandbox": true` и повторяется до 3 раз.
