CryptoLike подписывает каждый вебхук HMAC-SHA256: заголовок X-Signature — это "sha256=" плюс hex-значение HMAC от строки X-Timestamp + "." + сырое тело на секрете вашего эндпоинта. Чтобы проверить доставку, посчитайте то же значение по байтам тела, сравните за постоянное время, отклоните метку времени, которая расходится с вашими часами больше чем на 300 секунд в ту или другую сторону, затем отсейте повтор по event_id и ответьте 2xx в течение 10 секунд.
Вебхук сообщает вашему серверу, что у платежа, депозита или выплаты сменился статус; подпись вебхука доказывает, что сообщение отправил CryptoLike и его не изменили по дороге. Полный контракт — в документации по вебхукам. Здесь он превращён в рабочий код на TypeScript, Python, Go и PHP; каждый пример проверен на эталонном тестовом векторе и на негативных векторах, которые публикуются вместе с официальными SDK.
Что нужно заранее
Чтобы проверить первую доставку вебхука CryptoLike, нужны аккаунт CryptoLike, эндпоинт вебхука на публичном https://-адресе, секрет этого эндпоинта на вашем сервере и сервер, который умеет читать сырое тело запроса. Тестовые ключи (ck_test_) и тестовые сети позволяют отладить всё до запуска.
- Эндпоинт: Разработчикам → Вебхуки → Добавить эндпоинт или
POST /v1/webhooks/endpoints; события вродеinvoice.paidили*— все. - Секрет: сгенерированный (64 hex-символа) или свой (16–256 печатных ASCII-символов без пробелов, коды 0x21–0x7e), показывается один раз.
- Часы: синхронизированы по NTP — проверка метки времени допускает ±5 минут.
1. Сохраните секрет эндпоинта на сервере
CryptoLike создаёт отдельный секрет для каждого эндпоинта вебхуков и показывает его один раз. Сохраните его в переменных окружения сервера или в менеджере секретов, например как CRYPTOLIKE_WEBHOOK_SECRET, — но не в браузерном коде, мобильном приложении или репозитории. Если секрет мог утечь, выпустите новый в кабинете и сразу разверните его.
У каждого эндпоинта свой секрет, поэтому тестовый и боевой контуры никогда его не делят. Пустой секрет — например, забытая переменная окружения — должен приводить к отказу на любой запрос; все четыре примера из шага 4 так и делают.
2. Прочитайте сырое тело запроса и два заголовка
CryptoLike считает подпись по телу запроса ровно в том виде, в каком оно отправлено, поэтому обработчик должен получить сырые байты до того, как их тронет JSON-парсер. Тело — канонический JSON в том виде, в каком он хранится на платформе: порядок ключей и пробелы могут отличаться от примеров, а повторная сериализация даёт другие байты и неверную подпись.
Нужны два заголовка: X-Timestamp (Unix-секунды) и X-Signature (sha256= и 64 hex-символа в нижнем регистре). Сырое тело по фреймворкам: Express — express.raw({ type: "application/json" }); Flask — request.get_data(); Go — io.ReadAll(r.Body); PHP — file_get_contents('php://input'), а не $_POST.
3. Отклоняйте устаревшие метки времени
CryptoLike передаёт время отправки в X-Timestamp и подписывает его вместе с телом, поэтому перехваченный запрос нельзя повторить позже с новой меткой. Отклоняйте доставку, если ваши часы и X-Timestamp расходятся больше чем на 300 секунд, — это окно по умолчанию во всех четырёх официальных SDK. Отклонённая доставка не теряется: повтор придёт с новой меткой и новой подписью.
4. Посчитайте HMAC и сравните за постоянное время
CryptoLike формирует подпись как "sha256=" + hex(HMAC-SHA256(secret, X-Timestamp + "." + raw_body)). Посчитайте ту же строку у себя и сравните её с X-Signature функцией постоянного времени — timingSafeEqual, hmac.compare_digest, hmac.Equal или hash_equals, — чтобы время ответа не выдавало, сколько символов совпало. Сравнивайте строку целиком, вместе с префиксом sha256=.
TypeScript (Node.js, Express)
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const TOLERANCE_S = 300; // reject deliveries signed more than 5 minutes away from our clock
export function verifyCryptoLikeWebhook(
rawBody: Buffer,
timestamp: string | undefined,
signature: string | undefined,
secret: string,
nowS = Math.floor(Date.now() / 1000),
): boolean {
if (!secret || !timestamp || !signature || !/^[0-9]+$/.test(timestamp)) return false;
if (Math.abs(nowS - Number(timestamp)) > TOLERANCE_S) return false;
const expected = Buffer.from(
"sha256=" + createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex"),
);
const received = Buffer.from(signature);
return expected.length === received.length && timingSafeEqual(expected, received);
}
const app = express();
app.post("/hooks/cryptolike", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyCryptoLikeWebhook(req.body, req.header("X-Timestamp"), req.header("X-Signature"),
process.env.CRYPTOLIKE_WEBHOOK_SECRET ?? "");
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
if (!alreadySeen(event.event_id)) saveForProcessing(event); // your storage
res.sendStatus(200);
});Python (Flask)
import hashlib, hmac, json, os, time
from typing import Optional
from flask import Flask, abort, request
TOLERANCE_S = 300 # reject deliveries signed more than 5 minutes away from our clock
def verify_cryptolike_webhook(raw_body: bytes, timestamp: Optional[str], signature: Optional[str],
secret: str, now: Optional[float] = None) -> bool:
if not secret or not timestamp or not signature:
return False
if not (timestamp.isascii() and timestamp.isdecimal()):
return False
if abs((time.time() if now is None else now) - int(timestamp)) > TOLERANCE_S:
return False
mac = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256)
return hmac.compare_digest(("sha256=" + mac.hexdigest()).encode(), signature.encode())
app = Flask(__name__)
@app.post("/hooks/cryptolike")
def cryptolike_webhook():
raw = request.get_data() # raw bytes, before any JSON parsing
if not verify_cryptolike_webhook(raw, request.headers.get("X-Timestamp"),
request.headers.get("X-Signature"),
os.environ.get("CRYPTOLIKE_WEBHOOK_SECRET", "")):
abort(401)
event = json.loads(raw)
if not already_seen(event["event_id"]):
save_for_processing(event) # your storage
return "", 200Go (net/http)
const tolerance = 300 * time.Second // reject deliveries signed more than 5 minutes away from our clock
func VerifyCryptoLikeWebhook(rawBody []byte, timestamp, signature, secret string, now time.Time) bool {
sec, err := strconv.ParseUint(timestamp, 10, 63)
if err != nil || secret == "" || signature == "" {
return false
}
if d := now.Sub(time.Unix(int64(sec), 0)); d > tolerance || d < -tolerance {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "."))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
func cryptoLikeWebhook(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if !VerifyCryptoLikeWebhook(body, r.Header.Get("X-Timestamp"), r.Header.Get("X-Signature"),
os.Getenv("CRYPTOLIKE_WEBHOOK_SECRET"), time.Now()) {
w.WriteHeader(http.StatusUnauthorized)
return
}
var event struct {
EventID string `json:"event_id"`
Type string `json:"type"`
}
if err := json.Unmarshal(body, &event); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if !alreadySeen(event.EventID) {
saveForProcessing(event.EventID, body) // your storage
}
w.WriteHeader(http.StatusOK)
}Импорты: crypto/hmac, crypto/sha256, encoding/hex, encoding/json, io, net/http, os, strconv, time.
PHP (7.4 и новее)
<?php
const CRYPTOLIKE_TOLERANCE_S = 300; // reject deliveries signed more than 5 minutes away from our clock
function verify_cryptolike_webhook(string $rawBody, ?string $timestamp, ?string $signature, string $secret, ?int $now = null): bool
{
if ($secret === '' || $timestamp === null || $signature === null || !ctype_digit($timestamp)) {
return false;
}
if (abs(($now ?? time()) - (int) $timestamp) > CRYPTOLIKE_TOLERANCE_S) {
return false;
}
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signature);
}
$raw = file_get_contents('php://input'); // raw body, not $_POST
$ok = verify_cryptolike_webhook($raw, $_SERVER['HTTP_X_TIMESTAMP'] ?? null,
$_SERVER['HTTP_X_SIGNATURE'] ?? null, getenv('CRYPTOLIKE_WEBHOOK_SECRET') ?: '');
if (!$ok) { http_response_code(401); exit; }
$event = json_decode($raw, true);
if (!already_seen($event['event_id'])) { save_for_processing($event); } // your storage
http_response_code(200);5. Отсейте повторы по event_id и ответьте 2xx за 10 секунд
CryptoLike доставляет вебхуки по принципу «как минимум один раз», поэтому одно событие может прийти несколько раз — с тем же event_id, тем же телом и новой подписью. Сначала ищите event_id у себя: уже есть — ответьте 2xx и остановитесь; нет — сохраните, примените событие, ответьте 2xx. Обработчик должен быть быстрым: сохранить и подтвердить за 10 секунд, обработать потом.
Доставка без ответа 2xx повторяется через 1 мин, 5 мин, 15 мин, 1 ч, 6 ч и 24 ч — 7 попыток примерно за 31 час, — после чего помечается как исчерпанная. Отправить её снова можно из кабинета: Разработчикам → Вебхуки → Доставки → Повторить. Доставки независимы, и повторный invoice.pending может прийти после invoice.paid: упорядочивайте по created_at или смотрите status в data — это полное состояние объекта на момент created_at.
event_id для входящих событий играет ту же роль, что ключ идемпотентности для ваших API-запросов: одна логическая операция применяется один раз. Обе стороны разобраны в статье ключи идемпотентности в платёжном API.
6. Проверьте код на тестовом векторе и тестовом событии
CryptoLike публикует тестовый вектор, который обязан принять любой верификатор, — код можно проверить до первой реальной доставки. Вызовите свою функцию со значениями из таблицы, отключив проверку времени или закрепив часы на 1700000000: результат должен быть true. Измените один байт тела — результат должен стать false.
| Параметр | Значение |
|---|---|
| secret | whsec_9f2c1e4b7a6d5c3b2a1f0e9d8c7b6a5f |
| X-Timestamp | 1700000000 |
| body | {"event_id":"6f1c2c1e-3b2a-4d5e-8f90-1234567890ab","type":"invoice.paid","created_at":"2023-11-14T22:13:20Z","data":{"invoice":{"id":"0e8c9a4b-1111-4222-8333-444455556666","status":"paid"}}} |
| X-Signature | sha256=788e60a2141669b4c7616c1c616fefb8ec38df0948fba19c6f4e48c616e46c21 |
Затем проверьте вживую: Разработчикам → Вебхуки → эндпоинт → «Отправить тестовое событие» доставляет подписанный ping только на этот эндпоинт. Ответьте 2xx и не обрабатывайте его в бизнес-логике.
Готовая проверка в SDK
Официальные SDK CryptoLike для TypeScript, Python, Go и PHP содержат готовую функцию проверки, которая делает ровно те же шаги: префикс sha256=, сравнение за постоянное время, окно 300 секунд по умолчанию и разобранное событие на выходе. Неудачная проверка выбрасывает ошибку верификации — отвечайте 401. SDK пока в пре-релизе и ставятся из репозитория — см. документацию по SDK.
| Язык | Вызов | Страница |
|---|---|---|
| TypeScript | verifyWebhook(req.headers, req.body, secret) |
SDK для TypeScript |
| Python | verify_webhook(request.headers, request.get_data(), secret) |
SDK для Python |
| Go | cryptolike.VerifyWebhook(r.Header, body, secret, nil) |
SDK для Go |
| PHP | Webhook::verify(getallheaders(), file_get_contents('php://input'), $secret) |
SDK для PHP |
Ошибки и решения
Когда проверка вебхука CryptoLike не проходит, причина обычно одна из четырёх ошибок интеграции: разобранное тело вместо сырого, не тот секрет, расхождение часов или медленный обработчик. Таблица связывает то, что вы видите в логах или в журнале «Доставки» в кабинете, с причиной и способом исправить.
| Что видно | Причина | Что делать |
|---|---|---|
| Подпись не совпадает, тестовый вектор проходит | JSON-middleware пересобрал тело | Читайте сырые байты до разбора (шаг 2). |
| Подпись не совпадает, тестовый вектор не проходит | Потерян префикс sha256=, hex в верхнем регистре или нет точки после метки времени |
Соберите строку точно как в шаге 4. |
| На тесте работает, в бою нет | Секрет другого эндпоинта | У каждого эндпоинта свой секрет; возьмите боевой. |
| Все доставки отклоняются как устаревшие | Часы сервера ушли | Синхронизируйте по NTP; окно ±300 с. |
| Доставки повторяются, хотя код отработал | Ответ дольше 10 с или не 2xx | Сохраните, ответьте 2xx, обработайте потом. |
| Заказ выполнен дважды | Нет проверки event_id |
Храните event_id и пропускайте повторы (шаг 5). |
Вопросы и ответы
Здесь собраны вопросы, которые разработчики чаще всего задают, когда подключают и проверяют подпись вебхуков CryptoLike: какой секрет подписывает какую доставку, почему тело нельзя разбирать до проверки, что происходит после отказа и как событие invoice.paid связано со статусом платежа в API.
Каким секретом подписаны доставки на webhook_url конкретного платежа?
События invoice.* платежа, созданного с webhook_url, подписываются секретом вашего самого старого включённого эндпоинта; отдельного секрета для платежа нет. Поэтому webhook_url требует включённого эндпоинта: без него POST /v1/invoices отвечает 400 validation_error с полем webhook_url. Сначала добавьте эндпоинт в разделе Разработчикам → Вебхуки.
Почему нельзя проверять уже разобранный JSON?
Подпись считается по байтам, а не по структуре данных. Разбор и повторная сериализация меняют порядок ключей, пробелы и экранирование — HMAC перестаёт совпадать. Сначала проверьте сырое тело, потом разбирайте. При каждой доставке события байты одинаковые.
Что будет после ответа 401?
Попытка считается неудачной и повторяется по расписанию 1 мин, 5 мин, 15 мин, 1 ч, 6 ч и 24 ч. После последней попытки доставка исчерпана: устраните причину и отправьте её снова через Разработчикам → Вебхуки → Доставки → Повторить.
Достаточно ли invoice.paid, чтобы отгрузить заказ?
Да: invoice.paid означает, что платёж подтверждён и зачислен на ваш баланс. Обрабатывайте и invoice.reverted — реорганизация сети откатила подтверждение: остановите выполнение заказа до нового invoice.paid. Модель статусов описана в документации API.