ПерфектПак · API v1.1
openapi.yaml ↗

Вебхуки

Подписка на изменения вместо периодического опроса. Событие отправляется тогда, когда состояние изменилось: склад сообщает об этом сам — подтвердили заявку, закончили приёмку, передали поставку. Обычная задержка от действия на складе до запроса на ваш адрес — несколько секунд.

Раз в несколько минут мы дополнительно сверяем состояние целиком, чтобы изменение, о котором сообщить не удалось (сбой сети, наша перезагрузка), всё равно доехало — с задержкой, но не пропало. Поэтому проектируйте обработчик на «событие придёт, порядок и точный момент не гарантированы», а не на «событие придёт ровно через N секунд».

Регистрация и проверка URL

POST https://api.perfektpak.ru/v1/webhooks
Authorization: Bearer $PP_KEY
Content-Type: application/json
{ "url": "https://your-system.example/webhooks/perfektpak", "events": ["receipt.completed", "order.status_changed"], "description": "Синхронизация статусов" }

При создании мы сразу отправляем на этот URL событие webhook.ping. Если он не ответил 2xx в течение 10 секунд — подписка не создаётся, а ответ на POST400 webhook_url_unverified: так подписку нельзя завести на URL, который вам не принадлежит или временно недоступен. Иначе — 201:

{
  "webhook": {
    "id": "whk_5f3a9c1e2b7d", "url": "https://your-system.example/webhooks/perfektpak",
    "events": ["receipt.completed", "order.status_changed"], "description": "Синхронизация статусов",
    "active": true, "created_at": "2026-09-02T10:00:00+03:00"
  },
  "secret": "whsec_9f3a1c2b7e4d5f60718293a4b5c6d7e8"
}
Секрет показывается один раз

secret нужен только для проверки подписи доставок (см. ниже) и не возвращается повторно ни в GET /v1/webhooks/{id}, ни где-либо ещё. Потеряли — удалите подписку (DELETE /v1/webhooks/{id}) и заведите новую.

Лимит — 10 подписок на клиента; сверх лимита POST отвечает 400 webhook_limit. Управление подпиской:

РучкаЧто делает
GET /v1/webhooksСписок ваших подписок.
GET /v1/webhooks/{id}Одна подписка (без секрета).
DELETE /v1/webhooks/{id}Удалить подписку.
POST /v1/webhooks/{id}/pingПереслать тестовое событие webhook.ping вручную — проверить обработчик, не дожидаясь реального события.

Формат доставки

Каждое событие — отдельный POST на ваш URL:

POST /webhooks/perfektpak HTTP/1.1
Content-Type: application/json
X-PP-Event: receipt.completed
X-PP-Delivery: dlv_7a1b2c3d
X-PP-Timestamp: 1757000000
X-PP-Signature: sha256=5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d
{
  "id": "evt_7c1b2a",
  "type": "receipt.completed",
  "occurred_at": "2026-08-12T16:45:00+03:00",
  "data": { "id": "r_2026_0812_04", "status": "RECEIVED_WITH_DISCREPANCY", "discrepancy_qty": 120 }
}

data для событий *.status_changed — конверт {"object": {…}, "previous_status": "…"}; для остальных — сам объект (Receipt / Shipment / Order), в точности как он выглядит в соответствующей ручке GET. Ответ 2xx в течение 10 секунд считается доставкой; дольше или с любым другим статусом — неудачей, включается расписание повторов ниже. Отвечайте быстро и обрабатывайте асинхронно, если сама обработка может занять больше времени, чем таймаут.

Идемпотентность

Повтор доставки после локального сбоя на нашей стороне может прислать одно и то же событие дважды с одинаковым id (но, возможно, другим X-PP-Delivery — это разные попытки одного события). Сохраняйте обработанные id событий и пропускайте повтор — пример в гайде «Статусы заказов FBS в CRM».

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

X-PP-Signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + body)>, где timestamp — значение заголовка X-PP-Timestamp (unix-время), а bodyсырое, необработанное тело запроса, ровно те байты, что пришли по сети. Разбор в объект и повторная сериализация в JSON перед проверкой — частая причина, по которой подпись «не сходится»: форматирование чисел, порядок полей или пробелы после такого прохода могут отличаться от исходных байтов.

import hashlib
import hmac

def verify_signature(secret: str, timestamp: str, raw_body: bytes, signature_header: str) -> bool:
    payload = timestamp.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)

# в обработчике запроса (Flask):
# ok = verify_signature(secret, request.headers["X-PP-Timestamp"], request.get_data(), request.headers["X-PP-Signature"])
<?php
function verifySignature(string $secret, string $timestamp, string $rawBody, string $signatureHeader): bool
{
    $expected = "sha256=" . hash_hmac("sha256", $timestamp . "." . $rawBody, $secret);
    return hash_equals($expected, $signatureHeader);
}

// в обработчике запроса:
// $ok = verifySignature($secret, $_SERVER["HTTP_X_PP_TIMESTAMP"], file_get_contents("php://input"), $_SERVER["HTTP_X_PP_SIGNATURE"]);
const crypto = require("crypto");

function verifySignature(secret, timestamp, rawBody, signatureHeader) {
  const payload = Buffer.concat([Buffer.from(timestamp), Buffer.from("."), Buffer.from(rawBody)]);
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(payload).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// в обработчике запроса (Express, с express.raw() для этого пути, не express.json()):
// const ok = verifySignature(secret, req.headers["x-pp-timestamp"], req.body, req.headers["x-pp-signature"]);

Повторы и отключение

Неудачная доставка (не 2xx, таймаут 10 секунд, недоступный хост) повторяется по расписанию: через 1 минуту, 5 минут, 30 минут, 2 часа, 12 часов. После восьмой подряд неудачи подписка автоматически отключается (active: false, disabled_reason) — новые события по ней не отправляются. Включить обратно можно только пересозданием подписки: DELETE старую и POST новую (с новым webhook.ping и новым секретом).

События

СобытиеКогда
receipt.createdЗаявка на приёмку создана (в том числе через POST /v1/receipts).
receipt.confirmedАдминистратор подтвердил заявку — PENDING_CONFIRMATION → EXPECTED.
receipt.status_changedЛюбой другой переход статуса приёмки.
receipt.completedПриёмка завершена — RECEIVED или RECEIVED_WITH_DISCREPANCY.
shipment.status_changedСмена статуса отгрузки, включая переход в ACCEPTED.
order.status_changedСмена нормализованного статуса заказа FBS.
webhook.pingПроверка URL при регистрации и по ручному вызову POST /v1/webhooks/{id}/ping.

Журнал доставок

Каждая попытка доставки видна вам самим — полезно, когда обработчик молчит и непонятно, дошло ли событие вообще:

curl "https://api.perfektpak.ru/v1/webhooks/whk_5f3a9c1e2b7d/deliveries?status=failed&limit=50" \
  -H "Authorization: Bearer $PP_KEY"

GET /v1/webhooks/{id}/deliveries/{delivery_id} отдаёт ту же запись с телом события — удобно свериться, что именно мы отправили, байт в байт, при разборе проблем с подписью.

Что дальше

Готовый пример подписчика для одного конкретного события — в гайдах «Статусы заказов FBS в CRM» и «Сверка недостач по приёмке».