Чеклист интеграции платежей и восстановление
Реализуйте атомарное выполнение заказа по вебхуку, восстанавливайте пропущенные события, управляйте данными доступа и проверяйте интеграцию перед запуском.
Что разрешает выполнить заказ
Подтверждённая транзакция TRON — свидетельство блокчейна, но сама по себе не сопоставление с вашим заказом. Dedyx проверяет валюту, получателя, точную сумму и окно оплаты. Статус страницы полезен клиенту, но скриншот, перенаправление браузера и ответ по публичному токену не должны разрешать бизнес-действие.
Сервер проверяет вебхук PAID с подписью и сверяет сохранённые payment_id и точную expected_amount. Другой путь — сверка через защищённый details с проверкой PAID, payment_id, merchant_wallet и суммы. Оба пути должны использовать одну постоянную защиту от повторного выполнения заказа.
Атомарная запись события и локальное выполнение
Скачайте схему PostgreSQL и помощник транзакции asyncpg. Эти таблицы принадлежат базе мерчанта. Адаптируйте выдачу доступа к своему продукту; пример не меняет таблицы Dedyx и не подключается при импорте.
-- Example tables in YOUR merchant database, not the Dedyx service database.
CREATE TABLE merchant_orders (
order_id text PRIMARY KEY,
payment_id text UNIQUE NOT NULL,
merchant_wallet text NOT NULL,
expected_amount numeric(10,2) NOT NULL CHECK (expected_amount > 0),
fulfilment_mode text NOT NULL CHECK (fulfilment_mode IN ('local', 'external')),
accepted_at timestamptz
);
CREATE TABLE merchant_payment_events (
event_id text PRIMARY KEY,
order_id text NOT NULL REFERENCES merchant_orders(order_id),
received_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE merchant_entitlements (
order_id text PRIMARY KEY REFERENCES merchant_orders(order_id),
granted_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE merchant_delivery_jobs (
order_id text PRIMARY KEY REFERENCES merchant_orders(order_id),
idempotency_key text UNIQUE NOT NULL,
delivered_at timestamptz
);"""Merchant PostgreSQL + asyncpg example. No connection or API call at import.
Call only after raw-body signature verification OR an authenticated details GET.
The connection belongs to your merchant database; install the adjacent SQL there.
"""
from decimal import Decimal, InvalidOperation
async def accept_paid(conn, payment, *, event_id=None):
"""Commit before returning 2xx; let DB errors cause a retryable non-2xx.
event_id: verified webhook ID; None: reconciliation via authenticated API.
Validate header/payload event IDs before calling this function.
For reconciliation also require merchant_wallet from the details response.
"""
if payment.get('status') != 'PAID':
raise ValueError('Payment is not PAID')
if event_id is not None and (not isinstance(event_id, str) or not event_id):
raise ValueError('Invalid event ID')
if event_id is not None and payment.get('event_id') != event_id:
raise ValueError('Event ID mismatch')
try:
amount = Decimal(str(payment['expected_amount']))
except (KeyError, InvalidOperation):
raise ValueError('Invalid amount') from None
if not amount.is_finite() or amount <= 0:
raise ValueError('Invalid amount')
async with conn.transaction(isolation='read_committed'):
# Lock the order: distinct events and reconciliation serialize too.
order = await conn.fetchrow(
'SELECT * FROM merchant_orders WHERE payment_id=$1 FOR UPDATE',
payment.get('payment_id'),
)
if order is None or amount != order['expected_amount']:
raise ValueError('Unknown order or amount mismatch')
if event_id is None or 'merchant_wallet' in payment:
if payment.get('merchant_wallet') != order['merchant_wallet']:
raise ValueError('Wallet mismatch')
if event_id is not None:
inserted = await conn.fetchval(
'INSERT INTO merchant_payment_events(event_id,order_id) '
'VALUES($1,$2) ON CONFLICT(event_id) DO NOTHING RETURNING event_id',
event_id, order['order_id'],
)
if inserted is None:
previous = await conn.fetchval(
'SELECT order_id FROM merchant_payment_events WHERE event_id=$1',
event_id,
)
if previous != order['order_id']:
raise ValueError('Event belongs to another order')
if order['accepted_at'] is not None:
return 'already_accepted'
if order['fulfilment_mode'] == 'local':
# Local business action in the SAME transaction as event insertion.
await conn.execute(
'INSERT INTO merchant_entitlements(order_id) VALUES($1)',
order['order_id'],
)
else:
# No external network call inside this transaction.
await conn.execute(
'INSERT INTO merchant_delivery_jobs(order_id,idempotency_key) '
'VALUES($1,$2)', order['order_id'], 'fulfil:' + order['order_id'],
)
await conn.execute(
'UPDATE merchant_orders SET accepted_at=now() WHERE order_id=$1',
order['order_id'],
)
return 'accepted'Сохраните исходное тело, проверьте подпись помощником Python или Node.js, сравните ID события заголовка и тела, затем вызовите accept_paid с проверенными payload и event_id. Возвращайте 2xx только после commit функции. Проверенный повтор возвращает 2xx. Отклоняйте неверные подписи; неизвестный заказ и несовпадение суммы требуют расследования, а не успешного подтверждения. Ошибка базы должна возвращать повторяемый ответ не-2xx.
Блокировка строки сериализует доставки одного заказа. Запись события, локальная выдача доступа и accepted_at фиксируются вместе. Сбой до commit откатывает всё; сбой после commit до HTTP-ответа приводит к безвредному повтору. Другой ID события для принятого заказа не может выдать доступ второй раз. Параллельная сверка использует ту же блокировку и защиту.
Если вебхук пришёл до сохранения заказа, сохраните связь заказа или выполните сверку до подтверждения. Не отмечайте неизвестный платёж доставленным.
Для внешнего выполнения нужен идемпотентный воркер
При fulfilment_mode=external та же транзакция добавляет задачу доставки с постоянным ключом fulfil:ORDER_ID. Это запись принятия платежа, а не завершения внешней доставки. Воркер должен получать задачи с арендой или блокировкой строки, вызывать адресат с тем же ключом идемпотентности и ставить delivered_at только после подтверждённого успеха.
Если воркер упал после внешнего действия до записи успеха, он повторяет с тем же ключом. Адресат должен исключать повторы ключа или позволять сверить результат. Одна транзакция базы не гарантирует единственный внешний эффект. Если адресат не умеет ни то ни другое, направляйте неопределённые задачи на ручную сверку вместо слепого повтора. Отделяйте статус доставки клиенту от принятия платежа.
Восстановление пропущенного вебхука
curl https://api.dedyx.com/api/v1/payments/YOUR_PAYMENT_ID/details \
-H "X-API-Key: YOUR_API_KEY"Начните с сохранённого payment_id. Если details содержит PAID, сравните кошелёк и точную сумму с заказом и вызовите тот же accept_paid с event_id=None. Не выдумывайте ID вебхука. Поздний вебхук сохранит настоящий ID и не повторит бизнес-действие.
Неудачный вебхук не отменяет платёж. Обычно доставка имеет до восьми попыток. Читайте состояние через GET /api/v1/payments/{payment_id}/webhooks. После исправления приёмника POST /api/v1/merchant/webhook/events/{event_id}/retry запускает новый цикл только исчерпанного недоставленного события с прежними ID и телом. Пять запросов повтора в час на аккаунт; pending и delivered возвращают 409. Смена URL направляет ожидающие попытки на новый адрес; исчерпанным событиям нужен явный повтор. См. диагностику доставки. Исторические записи PAID без события outbox автоматически не отправляются заново.
При длительном отказе постранично читайте платежи мерчанта, сверяйте с постоянными заказами и соблюдайте общий лимит чтения. Сохраняйте прогресс сверки. EXPIRED, CANCELED и REVIEW требуют расследования; своевременный перевод после окончательного истечения автоматически не становится PAID. Не создавайте новый заказ и не просите другой перевод до сверки неопределённых средств.
Три независимых секрета
- API-ключ: X-API-Key для методов мерчанта. Ротация через POST /api/v1/merchant/rotate-key с отдельным заголовком secret-key. Старый API-ключ сразу перестаёт работать; безопасно сохраните единственный возвращённый new_api_key и вместе обновите серверные воркеры.
- Секрет ротации: право восстановления API-ключа. Храните отдельно от API-ключа. Самостоятельного метода ротации / восстановления этого секрета нет; при утрате или компрометации обратитесь в поддержку. После проверки владельца поддержка может заменить оба секрета API, сохранив merchant ID, квоту и историю. Старые ключи перестают работать. Никогда не передавайте секреты через чат поддержки.
- Ключ подписи вебхука: проверяет входящие события. Сначала настройте приёмник и сохраните полный whsec_... с key_id; секрет показывается один раз. Ротация через POST /api/v1/merchant/webhook/rotate-key с X-API-Key. Новые попытки сразу используют новый ключ; при плановой ротации сохраняйте старый пять минут для запросов в пути. При компрометации старого ключа отклоняйте его и сверяйте неопределённые события через защищённый details. Ротация API-ключа не меняет этот ключ.
До ротации подготовьте защищённое хранение и выбор ключа по ID. Если ответ с новым секретом потерялся, выполните другую авторизованную ротацию и безопасно сохраните результат. Вебхуки могут временно не проходить до установки нового ключа на приёмнике; при исчерпании попыток выполните сверку или защищённый повтор мерчанта. Не повторяйте неопределённую ротацию без разбора. Данные API и подписи не должны попадать в браузер, URL и логи.
Неактивные аккаунты не могут использовать защищённые методы API и ротацию ключей; проверки существующих платежей, публичный статус и доставка вебхуков продолжаются. Восстановление данных доступа сохраняет неактивный статус до явной активации поддержкой.
Передача ссылки на оплату
Любой получивший payment_url видит адрес, сумму, описание, статус и срок и может оплатить. Пересылка не привязывает плательщика к вашему клиентскому аккаунту и не создаёт квитанцию. Передавайте ссылку только нужным получателям; не помещайте персональные данные в description и не записывайте полные URL оплаты в логи.
Отмена ссылки не отменяет перевод и не скрывает публичные данные платежа. Не считайте отмену отзывом доступа к просмотру. Возвраты и споры о сопоставлении решает продавец. Ограничения последовательного использования адреса.
Устраните 409 до повтора
- Вебхук не настроен: настройте HTTPS-приёмник до создания платежа.
- Кошелёк и сумма заняты PENDING/CHECKING/REVIEW: завершите или сверьте заказ, используйте другую сумму или свой другой адрес.
- Кошелёк принадлежит другому мерчанту: уточните владение в поддержке.
- Ключ идемпотентности использован с изменёнными полями: восстановите сохранённый исходный запрос; новый ключ нужен только для намеренно нового заказа.
- Отмена недоступна: прочитайте защищённый details и обработайте текущее состояние; отмена не возвращает средства.
Не повторяйте 409 вслепую. При таймаутах, 429 и временных 5xx сохраняйте исходные запрос и ключ, соблюдайте Retry-After и ограничивайте повторы. Стандартное хранение идемпотентного ответа — 24 часа; после истечения сначала сверяйте неопределённое создание, затем отправляйте новый запрос. Справочник ошибок и ограничений частоты.
Чеклист запуска
- Сохраняйте каждый заказ, точную сумму, кошелёк и постоянный ключ идемпотентности; сохраните ответ API до перенаправления.
- Настройте публичный HTTPS-вебхук на порту 443, безопасно сохраните ключ подписи и захватите исходные байты до разбора JSON.
- Отклоняйте изменённое тело, устаревшее время, неверные ID событий и неизвестные ID ключей; сопоставляйте проверенное событие с сохранённым заказом.
- Проверьте повторные и параллельные события, разные события одного заказа и сбой до / после commit. Товар должен выдаваться один раз.
- Проверьте сверку с последующей поздней доставкой вебхука, неизвестные заказы и неверные сумму / кошелёк.
- Для внешней доставки проверьте постоянную идемпотентность адресата и восстановление воркера после неопределённого ответа.
- Проверьте таймаут после создания, 429/Retry-After, HTML- и JSON-ошибки шлюза, временный 503 и окончательный 409.
- Проверьте срок беты, общую / суточную квоты создания и общий лимит чтения; чтения и идемпотентные повторы не расходуют квоту создания.
- Объясните USDT TRC-20, точную сумму получения, отдельные комиссии, истечение / отмену, поздние и частичные переводы. Организуйте сверку и возвраты продавцом.
- Проверьте отказ приёмника, ротацию API-ключа и ключа подписи, восстановление после перезапуска и повтор мерчанта после исчерпания попыток вебхука.
Это проверки интеграции мерчанта. Они не заменяют проверки безопасности продакшена, резервного копирования и сквозную приёмку оператором.
Скачать контракт API
Скачайте публичный OpenAPI JSON, созданный офлайн из маршрутов и моделей приложения. Импортируйте в свои инструменты API. Swagger в продакшене отключён. Некоторые динамические ответы не имеют сгенерированной схемы; их поля и правила описаны в справочнике API.
CHECKING и подготовка кошелька
Сохраняйте expires_at и verification_until. CHECKING означает конец окна отправки и ещё 10 минут проверки своевременного перевода. Не требуйте второй платёж и не выполняйте заказ по таймеру. Дождитесь PAID или сверьте окончательный EXPIRED. Одновременные заказы могут иметь разные суммы на одном адресе; одна сумма остаётся зарезервированной во время проверки.
Стандартный QR содержит только адрес. Подготовка отдельных кошельков экспериментальная и не доказывает оплату. Всегда проверяйте получателя, USDT, TRON и точную сумму получения, особенно после комиссии вывода с биржи. Совместимость кошельков.