Запрос к платёжному API может «упасть» уже после того, как сделал свою работу: платёж создан, но ответ потерялся по таймауту, и код не знает, повторять ли запрос. Идемпотентность делает повтор безвредным. В CryptoLike вы передаёте заголовок Idempotency-Key в каждом POST, payout_id в каждой выплате и, по желанию, order_id в каждом платеже — и повтор того же запроса возвращает исходный результат, а не создаёт второй. С вебхуками та же забота нужна на вашей стороне: они могут прийти больше одного раза.
Откуда берутся дубли
Дубль платежа или выплаты почти никогда не ошибка платёжной платформы — это повтор запроса, первая попытка которого прошла успешно. Сеть теряет ответы, балансировщик обрывает соединение по таймауту, воркер перезапускается посреди запроса, покупатель дважды жмёт «Оплатить». В каждом случае у кода один и тот же вопрос: прошёл ли первый запрос?
| Ситуация | Что произошло | Без идемпотентности | С идемпотентностью CryptoLike |
|---|---|---|---|
HTTP-клиент не дождался ответа на POST /v1/invoices |
Платёж создан, ответ потерян | Повтор создаёт второй платёж к заказу | Повтор возвращает первый платёж |
| Воркер перезапустился во время отправки выплаты | Выплата то ли есть, то ли нет | Повтор может заплатить получателю дважды | payout_id возвращает существующую выплату |
| Покупатель дважды нажал «Оплатить криптовалютой» | Два запроса на один заказ | Два открытых платежа, два адреса | idempotency_by_order_id возвращает открытый платёж |
| Ваш сервер ответил на вебхук через 12 с | CryptoLike считает доставку неудачной и повторяет | Заказ обрабатывается дважды | Вы пропускаете уже применённый event_id |
Когда речь о деньгах, без повторов не обойтись — от выплаты нельзя просто отказаться, — поэтому надёжна только схема, в которой повтор безвреден.
Как работает Idempotency-Key в CryptoLike
CryptoLike принимает заголовок Idempotency-Key во всех POST публичного API: создание и отмена платежей, создание и архивирование статических адресов, создание, оценка и отмена выплат, управление эндпоинтами вебхуков и повторными доставками. Заголовок необязателен: без него каждый запрос выполняется как новый. С ним первый успешный ответ хранится 24 часа и возвращается на каждый повтор.
| Правило | Значение |
|---|---|
| Формат | 1–128 печатных символов ASCII (от ! до ~, без пробелов); более длинное значение или другие символы дают 400 validation_error с details.field: Idempotency-Key. Пустой заголовок игнорируется |
| Область действия | Отдельно для каждого API-ключа: два ключа могут использовать одно значение независимо |
| Сколько хранится ответ | 24 часа |
| Что считается тем же запросом | Тот же метод, путь с параметрами и тело |
| Повтор завершённого запроса | Сохранённые статус и тело с заголовком Idempotent-Replayed: true |
| Тот же ключ, другое тело | 422 idempotency.mismatch |
| Тот же ключ, пока первый запрос выполняется | 409 idempotency.in_progress; повторите после короткой паузы |
| Ответы с ошибкой (4xx, 5xx) | Не сохраняются: ключ освобождается, и повтор выполняется заново |
Сверено с API CryptoLike 24.09.2026; заголовок описан в справочнике API, подпись запросов — в аутентификации API.
На практике важны две детали. Сохраняются только успешные ответы, поэтому исправить запрос после 400 и отправить его с тем же ключом можно. А блокировка на время выполнения снимается через 60 секунд, так что запрос, упавший на полпути, не держит ключ целые сутки.
Как выбрать ключ
Ключ идемпотентности должен обозначать бизнес-операцию, а не HTTP-попытку. Сгенерируйте его один раз — в момент, когда решили создать платёж или выплату, — сохраните вместе с операцией и используйте при каждом повторе. Новый случайный ключ на каждую попытку лишает идемпотентность смысла: каждая попытка становится новой операцией.
Хорошие ключи получаются из ваших собственных данных:
- Платёж по заказу:
order-A-1002илиorder-A-1002-2, если покупатель сознательно начинает вторую оплату после истечения первой. - Выплата: ID строки в вашей таблице выплат, созданной до обращения к API.
- Статический адрес пользователя:
addr-user-8812-usdt-tron— хотя этот вызов и так безопасен: второй адрес для того же клиента и актива вернёт409 conflict.
Храните ключ и тело вместе. Если код при повторе собирает тело заново — скажем, с новой датой в description, — это уже другой запрос, и CryptoLike ответит 422 idempotency.mismatch вместо создания дубля.
Обёртка для повторов на TypeScript
Эта обёртка повторяет вызов CryptoLike при сетевых ошибках, 409 idempotency.in_progress, 429 и 5xx — каждый раз с тем же ключом и тем же телом, но с новой подписью для каждой попытки. Каждый повтор ждёт как минимум до следующей секунды: время в подписи — целые секунды, и повтор, подписанный в ту же секунду, отклоняется как 401 auth.replay. Официальные SDK уже делают так. Обёртка использует функцию подписи запросов из гайда для Node.js, которая подписывает каждый вызов заново; доработайте её, чтобы она возвращала HTTP-статус, код ошибки и значение Retry-After.
type ApiError = Error & { status?: number; code?: string; retryAfter?: number }; // retryAfter: seconds from the Retry-After header
const RETRYABLE = (e: ApiError) =>
e.status === undefined || // network error or timeout
e.status === 429 ||
e.status >= 500 ||
e.code === "idempotency.in_progress";
export async function withRetry<T>(call: () => Promise<T>, attempts = 5): Promise<T> {
for (let i = 0; ; i++) {
try {
return await call(); // call() signs afresh: a new X-Timestamp and signature each attempt
} catch (err) {
const e = err as ApiError;
if (i + 1 >= attempts || !RETRYABLE(e)) throw e;
// Timestamps are whole seconds: a repeat signed in the same second is refused
// as 401 auth.replay, so wait at least 1 s before the next attempt,
// and never less than the Retry-After of a 429.
const wait = Math.max(1000 * 2 ** i, (e.retryAfter ?? 0) * 1000); // 1 s, 2 s, 4 s, 8 s
await new Promise((r) => setTimeout(r, wait));
}
}
}
// The key and the body are fixed before the first attempt.
const body = { amount: "49.90", currency: "USD", order_id: "A-1002", idempotency_by_order_id: true };
const payment = await withRetry(() => cryptolike("POST", "/v1/invoices", body, "order-A-1002"));Не повторяйте 400, 401, 403, 404 и 422: они будут падать так же, пока вы не измените запрос. 401 auth.invalid_signature обычно значит, что вы захешировали одно тело, а отправили другое.
Выплаты: передавайте свой payout_id
Для выплат у CryptoLike есть вторая защита, у которой нет срока: payout_id — ваш идентификатор длиной до 128 символов, уникальный в аккаунте. POST /v1/withdrawals с уже существующим payout_id возвращает существующую выплату, а не создаёт новую — с кодом 201, если она создана меньше 5 секунд назад, и с 200 позже, — если совпадают актив, сумма и адрес. Другие параметры с тем же payout_id дают 409 conflict с details.field: payout_id.
POST /v1/withdrawals
Idempotency-Key: payout-7731
{
"coin_id": "<id USDT в сети tron из GET /v1/currencies>",
"amount": "250.00",
"to_address": "T…",
"payout_id": "payout-7731"
}Используйте оба: заголовок закрывает 24-часовое окно сетевых повторов, а payout_id — повтор через несколько дней, например перезапуск задачи после сбоя или та же строка в CSV, загруженном второй раз. В массовой выплате из CSV payout_id, повторённый внутри файла или уже использованный, — ошибка строки, и файл отклоняется до отправки чего-либо.
Платежи: один открытый платёж на заказ
Для платежей бизнес-ключ — ваш заказ. Передайте order_id вместе с idempotency_by_order_id: true, и CryptoLike вернёт открытый платёж этого заказа с кодом 200, а не создаст новый: двойной клик, перезагрузка страницы и повтор запроса приводят на одну и ту же страницу оплаты.
- Тот же заказ, другая сумма или валюта:
409 conflictсdetails.reason: order_invoice_mismatchи ID существующего платежа. Если заказ действительно изменился, а платёж ещё в статусеnewи на него ничего не отправлено, сначала отмените его; когда перевод уже в пути, отменить платёж нельзя. - По заказу уже пришли деньги:
409 conflictсdetails.reason: order_has_payment. Не открывайте второй платёж — разберитесь с первым. - Предыдущий платёж истёк: создаётся новый — с новым курсом, если цена в фиате.
Без этого флага order_id — просто ссылка, и уникальность не проверяется. Все плагины CryptoLike для CMS работают в этом режиме, поэтому покупатель, дважды нажавший «Оплатить», видит одну страницу оплаты.
Вебхуки: применяйте каждое событие один раз
Идемпотентность нужна и в обратную сторону. CryptoLike доставляет вебхуки как минимум один раз: доставка без ответа 2xx за 10 секунд повторяется через 1, 5 и 15 минут, 1, 6 и 24 часа — 7 попыток примерно за 31 час, — а сбой между вашей обработкой и ответом означает, что событие придёт снова. Порядок доставок тоже может быть любым.
Обработчик надёжен, если соблюдает три правила:
- Отбрасывайте дубли по
event_id. Он есть в теле и в заголовкеX-Event-Idи одинаков во всех доставках события. - Записывайте событие и применяйте его в одной транзакции, а потом отвечайте
200. Если транзакция не прошла, ответьте ошибкой — CryptoLike повторит доставку. - Никогда не откатывайте статус назад. Опоздавший
invoice.pendingне должен отменятьinvoice.paid: ранжируйте статусы или сравнивайте время.
-- One row per applied event; the unique key makes the second delivery a no-op.
INSERT INTO cryptolike_events (event_id, type, received_at)
VALUES ($1, $2, now())
ON CONFLICT (event_id) DO NOTHING;
-- 1 row inserted → apply the event to the order in the same transaction.
-- 0 rows → already applied: commit and answer 200.Подпись проверяйте раньше всего остального — код на четырёх языках есть в гайде проверка подписи вебхуков. Доставку, исчерпавшую попытки, можно отправить снова через POST /v1/webhooks/deliveries/{id}/retry, когда эндпоинт исправлен; список доставок — GET /v1/webhooks/deliveries.
Ошибки и что делать
| HTTP · код | Что произошло | Что делать |
|---|---|---|
409 idempotency.in_progress |
Первый запрос с этим ключом ещё выполняется | Повторить после паузы с тем же ключом и телом |
422 idempotency.mismatch |
Ключ уже использован с другим телом | Восстановить исходное тело или взять новый ключ для новой операции |
400 validation_error, поле Idempotency-Key |
Ключ длиннее 128 символов или содержит символы вне печатных ASCII (пустой заголовок игнорируется) | Использовать 1–128 печатных символов ASCII без пробелов |
409 conflict, поле payout_id |
payout_id уже есть с другими параметрами |
Проверить существующую выплату; для новой — новый ID |
409 conflict, order_invoice_mismatch |
У заказа есть открытый платёж с другой суммой | Отменить его, пока он в статусе new, или использовать |
503 unavailable |
Хранилище ключей временно недоступно | Повторить с паузой и тем же ключом |
Полный список кодов ошибок — в справочнике API.
Вопросы и ответы
Заголовок Idempotency-Key обязателен?
Нет, во всех POST API CryptoLike он необязателен. Без него каждый запрос выполняется как новый, и повтор после таймаута может создать дубль. Передавайте его в каждом вызове, который что-то создаёт, особенно в платежах и выплатах.
Сколько CryptoLike помнит ключ идемпотентности?
24 часа для успешного ответа, отдельно для каждого API-ключа. Ответы с ошибкой не сохраняются, поэтому исправленный запрос с тем же ключом выполнится. Для выплат у payout_id срока нет — это защищает от повторов через несколько дней.
Что значит Idempotent-Replayed: true?
Ответ — копия первого успешного ответа на тот же запрос с тем же ключом; ничего нового не создано. Обрабатывайте его так же, как исходный ответ. Заголовок помогает в логах отличить повтор от новой операции.
Может ли вебхук прийти дважды?
Да. CryptoLike доставляет вебхуки как минимум один раз: доставку без ответа 2xx за 10 секунд повторяет — до 7 попыток примерно за 31 час, — а сбой может повторить уже обработанную доставку. Отбрасывайте дубли по event_id и применяйте каждое событие в одной транзакции базы данных.
Можно ли брать случайный UUID в качестве ключа?
Можно, если генерировать его один раз на операцию и сохранять до первой попытки. Новый UUID на каждый повтор делает каждую попытку новой операцией и лишает идемпотентность смысла. Ключи из ID заказа или выплаты к тому же проще отследить.