ZefirPayAPI v2
Поддержка
Разделы документации

Формат уведомлений

Когда платёж оплачен или отменён, мы отправляем POST-запрос на адрес уведомлений вашего магазина. Адрес задаётся при подключении магазина.

Когда отправляются

payment_statusСобытие
successПлатёж оплачен. Можно выдавать заказ
cancelПлатёж отменён: истёк срок оплаты или банк покупателя отклонил перевод
В редких случаях банк покупателя подтверждает перевод уже после истечения срока оплаты. Тогда после cancel придёт success по тому же платежу — деньги получены, заказ нужно выдать.

Запрос

POST на адрес уведомлений магазина, Content-Type: application/json.

Тело

{
  "id": 1482093517,
  "amount": 1500.0,
  "currency": "RUB",
  "uuid": "order-1024",
  "payment_status": "success"
}
ПолеТипОписание
idintegerID платежа
amountnumberСумма платежа в рублях
currencystringВалюта: RUB
uuidstringНомер заказа в вашей системе
payment_statusstringsuccess или cancel

Заголовки

ПолеТипОписание
X-Zefir-SignaturestringПодпись уведомления: sha256=<hex>
X-Zefir-TimestampstringВремя отправки, Unix-время в секундах. Участвует в подписи
X-Zefir-DeliverystringИдентификатор уведомления (UUID). При повторной отправке не меняется

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

Подпись — HMAC-SHA256 от строки <X-Zefir-Timestamp>.<тело запроса> с секретным ключом магазина в качестве ключа. Тело берите в исходном виде, до разбора JSON.

  1. Посчитайте ожидаемую подпись и сравните её с заголовком X-Zefir-Signature функцией сравнения за постоянное время.
  2. Отклоняйте уведомления, у которых X-Zefir-Timestamp отличается от текущего времени больше чем на 5 минут — это защищает от повторной отправки перехваченного запроса.
$secretKey = getenv('ZEFIRPAY_SECRET_KEY');
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_ZEFIR_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_ZEFIR_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secretKey);
if (!hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
if ($event['payment_status'] === 'success') {
    // Найдите заказ по $event['uuid'], сверьте сумму и выдайте заказ один раз.
}
http_response_code(200);
import hashlib
import hmac
import time


def verify(body: bytes, timestamp: str, signature: str, secret_key: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret_key.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
    ).hexdigest()
    fresh = abs(time.time() - int(timestamp or 0)) <= 300
    return fresh and hmac.compare_digest(expected, signature)
const crypto = require("crypto");
const express = require("express");

const app = express();

app.post("/zefirpay/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-Zefir-Timestamp") || "";
  const signature = req.get("X-Zefir-Signature") || "";
  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", process.env.ZEFIRPAY_SECRET_KEY)
      .update(`${timestamp}.`)
      .update(req.body)
      .digest("hex");

  const valid =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) &&
    Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
  if (!valid) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  // Найдите заказ по event.uuid, сверьте сумму и выдайте заказ один раз.
  res.sendStatus(200);
});

Ответ вашего сервера

Ответьте кодом 2xx в течение 10 секунд. Тело ответа не важно.

Если ваш сервер не ответил или ответил другим кодом, мы повторим отправку через 1 минуту, 5 минут, 15 минут, 1 час, 3 часа, 6 часов, 12 часов и 24 часа. Уведомление можно запросить заново вручную — см. Повторная отправка.

Рекомендации

  • Одно и то же уведомление может прийти несколько раз. Обрабатывайте его так, чтобы повтор не выдал заказ второй раз: например, отмечайте обработанные id платежей.
  • Сверяйте uuid и amount с заказом в своей системе.
  • Если уведомление вызывает сомнения или не пришло, запросите данные платежа: статус в ответе API — окончательный.
  • Страница, на которую покупатель возвращается после оплаты, не подтверждает оплату: покупатель может закрыть её или открыть раньше времени.