$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогИнтеграции с платежами криптовалют: идемпотентность, подписи и проверка вебхуков

Интеграции с платежами криптовалют: идемпотентность, подписи и проверка вебхуков

$ sudo teach IT
·23 июля 2026 г.·9 мин·57
Интеграции с платежами криптовалют: идемпотентность, подписи и проверка вебхуков

На примере aiosend обсудим, как строить надёжную обработку событий оплаты: статусы, повторные уведомления и валидация входящих данных.

Содержание
Как выглядит реальная обработка оплаты криптовалютамиСобытия и статусы: почему нельзя ограничиться “paid=true”Повторяемость уведомлений: почему идемпотентность — базовое требованиеИдемпотентность: как проектировать обработчик вебхуковИдентификатор события: где взять ключ идемпотентностиДва уровня защиты: база и транзакцииПример кода: идемпотентность с “INSERT … ON CONFLICT”Подписи и подлинность: как понять, что webhook пришел от провайдераПочему проверка подписи важнее, чем “мы же только по URL принимаем”Общая схема проверки HMACВариант с подписью по timestamp: защита от replay атакПроверка данных: как валидировать входящий payload без самоуничтоженияЧто именно валидироватьСтрогая схема валидации: Pydantic/JSON SchemaПроверка согласованности: статус, сумма, подтвержденияМодель статусов и “правила переходов”: как не перепутать pending и confirmedState machine вместо “если статус == confirmed — выдать”Проверка вебхуков: как отвечать провайдеру и что логироватьHTTP-коды: “200 всегда” может быть плохой идеейЛогирование: что фиксировать для расследований

Платежи криптовалют — это не просто «запросить адрес и дождаться поступления». На практике интеграция превращается в систему обработки событий, которая должна выдерживать задержки сети, повторные уведомления, частичные отказы и, что особенно важно, валидацию источника. В итоге надежность складывается из нескольких инженерных решений: идемпотентности обработчиков, проверок подписи/подлинности, строгой проверки входящих данных, корректной модели статусов и журналирования.

Ниже разберем эти принципы на примере логики, которую часто используют при интеграциях платежных провайдеров. В качестве контекста возьмем aiosend — это удобная платформа/шаблон для отправки запросов и управления платежами, но ключевые идеи остаются общими для любых криптовалютных шлюзов. Если вы хотите вникнуть в практику работы с aiosend, можно начать с курса aiosend – для начинающих! — он помогает собрать базовую картину до того, как вы начнете проектировать обработку вебхуков «по-взрослому».


Как выглядит реальная обработка оплаты криптовалютами

События и статусы: почему нельзя ограничиться “paid=true”

В классических платежах (карты/банки) разработчики часто привыкают к простому пайплайну: «получили webhook → выставили paid=true». С криптовалютами всё сложнее:

  • транзакция может быть в процессе подтверждений (например, 0/1/N подтверждений);
  • провайдер может прислать несколько уведомлений по одной оплате;
  • возможны повторные уведомления из‑за таймаутов или ретраев;
  • может быть «отмена»/«возврат»/«неуспех» (в зависимости от провайдера и типа операции);
  • клиентская часть может отображать статус раньше, чем он станет финальным.

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

  1. created — платеж создан, ожидает поступления.
  2. pending — обнаружено поступление/инициирован процесс, но нет достаточного числа подтверждений.
  3. confirmed — достаточно подтверждений, платеж считается финальным.
  4. failed / canceled — неуспех (или отмена).
  5. refunded — если поддерживается возврат.

Важно: обработчик вебхука не должен «переприсваивать» статус слепо. Он должен применять событие к текущему состоянию по правилам переходов (state machine), иначе вы рискуете получить «откат» или неконсистентность.


Повторяемость уведомлений: почему идемпотентность — базовое требование

Практика такова: вебхук может быть доставлен повторно. Причины банальны:

  • провайдер не получил ответ 200 OK вовремя;
  • у провайдера ретраи при сетевых сбоях;
  • вы временно недоступны;
  • истек таймаут на вашей стороне, а провайдер повторил запрос.

Если ваш обработчик не идемпотентен, повтор может привести к:

  • двойной выдаче товара,
  • двойному списанию бонусов,
  • нескольким «успешным» записям в журнале,
  • росту счетчиков и расхождению сумм.

Отсюда инженерное правило: одинаковые события должны иметь один и тот же эффект.


Идемпотентность: как проектировать обработчик вебхуков

Идентификатор события: где взять ключ идемпотентности

Идемпотентность строят на «ключе события». Он должен быть стабильным и уникальным для конкретного уведомления. В криптоинтеграциях это часто:

  • webhook_id (если провайдер его присылает),
  • event_id,
  • payment_id + status + block_confirmations (комбинация),
  • хэш тела запроса (sha256(body)) — как fallback, если нет явного event_id,
  • либо invoice_id/order_id/payment_reference + status (но аккуратно: если статусов несколько, комбинация должна быть достаточно детальной).

Критично: если провайдер шлет повторно одно и то же событие, у вас ключ должен совпасть. Если провайдер шлет «эволюцию статуса» (pending → confirmed), то это разные события: их нужно обрабатывать как разные.


Два уровня защиты: база и транзакции

Надежная схема выглядит так:

  1. Получили webhook.
  2. Проверили подпись и схему данных.
  3. Достали ключ идемпотентности из payload.
  4. Попробовали «зарезервировать обработку» в БД атомарно.
  5. Если событие уже обработано — выходим без побочных эффектов.
  6. Если нет — выполняем переход статуса, начисления, выдачу товара и т.д. в транзакции.

Минимальная модель в БД:

  • таблица payments (с текущим статусом, суммой, order_id, etc.);
  • таблица webhook_events или idempotency_keys (с ключом идемпотентности, временем обработки и результатом).

Пример SQL-идей (концептуально):

  • UNIQUE(event_key) в таблице webhook_events;
  • обработка в транзакции.

Пример кода: идемпотентность с “INSERT … ON CONFLICT”

Допустим, мы пишем обработчик на Python с asyncio/fastapi и используем PostgreSQL. Логика такая: сначала создаем запись об обработке события; если она уже существует — событие повторилось.

code
import hashlib
from datetime import datetime, timezone

def event_key_from_payload(payload: dict) -> str:
    # Вариант 1: если провайдер присылает event_id / webhook_id
    if "event_id" in payload:
        return f"event:{payload['event_id']}"
    
    # Вариант 2: комбинация payment_id + status
    if "payment_id" in payload and "status" in payload:
        return f"payment:{payload['payment_id']}|status:{payload['status']}"
    
    # Вариант 3: fallback на хэш тела (осторожно: зависит от неизменности порядка полей)
    body_bytes = payload.get("_raw_body_bytes")
    if body_bytes:
        return "sha256:" + hashlib.sha256(body_bytes).hexdigest()

    raise ValueError("Cannot build idempotency key")

Далее — обработчик:

code
from fastapi import Request, HTTPException

async def handle_webhook(request: Request, db):
    raw_body = await request.body()
    payload = await request.json()

    # 1) build event_key
    payload["_raw_body_bytes"] = raw_body  # чтобы fallback на хэш работал
    event_key = event_key_from_payload(payload)

    # 2) idempotency insert
    #    Важно: это должно быть частью транзакции,
    #    чтобы не было гонок между несколькими инстансами.
    async with db.transaction():
        inserted = await db.execute_fetchval(
            """
            INSERT INTO webhook_events(event_key, received_at)
            VALUES($1, $2)
            ON CONFLICT (event_key) DO NOTHING
            RETURNING event_key
            """,
            event_key,
            datetime.now(timezone.utc),
        )

        if inserted is None:
            # Событие уже обрабатывали — не делаем побочек
            return {"ok": True, "replayed": True}

        # 3) валидируем payment_id, сумму, статус и делаем переход
        payment_id = payload["payment_id"]
        new_status = payload["status"]

        updated = await db.execute_fetchval(
            """
            UPDATE payments
            SET status = $2, updated_at = NOW()
            WHERE id = $1
              AND status_transition_allowed(status, $2)
            RETURNING id
            """,
            payment_id,
            new_status,
        )

        if updated is None:
            # Либо неверный payment_id, либо переход не разрешен.
            # На практике можно логировать и считать это "ok",
            # чтобы не заставлять провайдера ретраить бесконечно.
            return {"ok": True, "ignored": True}

        # 4) Побочные эффекты (в транзакции!)
        await db.execute(
            """
            INSERT INTO fulfillment(payment_id, created_at)
            VALUES($1, NOW())
            ON CONFLICT DO NOTHING
            """,
            payment_id,
        )

    return {"ok": True, "replayed": False}

Несколько нюансов:

  • status_transition_allowed — это идея. Реализовать можно:
    • через логику на приложении (проверить текущий статус, сравнить с допустимыми переходами);
    • или через таблицу переходов в БД.
  • fulfillment с ON CONFLICT дополнительно защищает от дублей на уровне конкретного эффекта (например, выдача товара по (payment_id, type)).

Подписи и подлинность: как понять, что webhook пришел от провайдера

Почему проверка подписи важнее, чем “мы же только по URL принимаем”

Если обработчик принимает публичный HTTP endpoint, вы не защищены от:

  • случайных запросов,
  • сканеров,
  • атакующего, который попытается отправить фейковый confirmed.

Даже если у вас есть секретный токен в query-строке, его можно угадать/подсмотреть. Надежный подход: использовать криптографическую подпись и проверять её на стороне сервера.

Обычно провайдер делает так:

  • подписывает содержимое (часто raw body или каноническое представление);
  • присылает заголовок X-Signature/X-Signature-Sha256/Authorization и т.д.;
  • вы вычисляете HMAC/подпись на своей стороне и сравниваете.

Общая схема проверки HMAC

Псевдологика:

  1. Берем raw_body (важно: не “пересериализованный” JSON из json.loads, а именно байты как пришли).
  2. Берем secret/ключ провайдера из настроек.
  3. Считаем HMAC по алгоритму (например, sha256).
  4. Сравниваем с подписью из заголовка постоянным временем (hmac.compare_digest).

Пример на Python:

code
import hmac
import hashlib
from fastapi import Request, HTTPException

def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> None:
    # Пример: подпись — это hex-код HMAC SHA256
    expected = hmac.new(
        key=secret.encode("utf-8"),
        msg=raw_body,
        digestmod=hashlib.sha256
    ).hexdigest()

    # постоянное время сравнения
    if not hmac.compare_digest(expected, signature_header):
        raise HTTPException(status_code=401, detail="Invalid signature")

async def webhook_endpoint(request: Request):
    raw_body = await request.body()
    payload = await request.json()

    signature = request.headers.get("X-Signature")
    if not signature:
        raise HTTPException(status_code=400, detail="Missing signature")

    verify_signature(raw_body, signature, secret="YOUR_WEBHOOK_SECRET")

    # далее — идемпотентность и обновление статуса
    return {"ok": True}

Подводные камни:

  • Используйте raw_body, иначе вычисление HMAC может не совпасть из‑за различий в представлении JSON (пробелы, порядок полей, отсутствие/наличие \u-экранирований).
  • Обязательно делайте сравнение compare_digest (а не ==).
  • Не допускайте “обхода” подписи в debug-моде, если endpoint доступен извне.

Вариант с подписью по timestamp: защита от replay атак

Некоторые провайдеры добавляют:

  • заголовок X-Timestamp,
  • подпись включает timestamp,
  • вы проверяете, что timestamp не “старый” (например, не старше 5 минут).

Это не заменяет идемпотентность, но добавляет дополнительный уровень защиты.

Если провайдер присылает timestamp, реализуйте:

  • проверку формата времени,
  • проверку окна (now - timestamp <= max_skew),
  • и включение timestamp в подпись так, как указано в документации.

Проверка данных: как валидировать входящий payload без самоуничтожения

Что именно валидировать

Даже при правильной подписи вам нужно защититься от некорректных данных. Минимальный набор:

  • обязательные поля присутствуют;
  • типы корректны (строка/число);
  • суммы и валюты в ожидаемом формате;
  • payment_id/order_id существуют в вашей БД;
  • статус соответствует допустимому набору;
  • поля, зависящие друг от друга (например, confirmed требует confirmations >= N).

Дополнительно стоит проверить:

  • что amount из webhook совпадает с суммой в вашей записи (или храните “расхождение”);
  • что transaction_hash имеет корректный формат (если провайдер присылает).

Строгая схема валидации: Pydantic/JSON Schema

Практический подход — объявить схему входных данных и отклонять всё, что не соответствует.

Пример схемы Pydantic:

code
from pydantic import BaseModel, Field
from typing import Literal

class CryptoPaymentWebhook(BaseModel):
    payment_id: str = Field(min_length=1)
    status: Literal["created", "pending", "confirmed", "failed", "canceled"]
    amount: str  # часто суммы приходят строкой, чтобы не терять точность
    currency: str
    tx_hash: str | None = None
    confirmations: int | None = None
    event_id: str | None = None

Дальше в обработчике:

code
async def parse_and_validate(raw_body: bytes, payload: dict):
    try:
        model = CryptoPaymentWebhook.model_validate(payload)
        return model
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Invalid payload: {e}")

Важно: если сумма приходит строкой — не парсите через float. Для криптоплатежей лучше хранить:

  • целочисленно в минимальных единицах (satoshi/wei),
  • либо BigDecimal/decimal с фиксированной точностью.

Проверка согласованности: статус, сумма, подтверждения

Типичная ошибка: принять статус confirmed без проверки, что он соответствует вашему ожиданию (например, что заказ еще не закрыт или не было earlier failed).

Правильная схема:

  • читаем текущий payments.status из БД;
  • сверяем допустимость перехода;
  • при confirmed проверяем, что сумма совпадает (или документируем поведение при расхождениях);
  • при pending допускаем обновления (pending→pending может приходить несколько раз с разными confirmations).

Модель статусов и “правила переходов”: как не перепутать pending и confirmed

State machine вместо “если статус == confirmed — выдать”

Минимально достаточная логика:

  • если текущий статус уже confirmed, игнорируем pending (иначе будет дергание);
  • если текущий failed, игнорируем confirmed;
  • confirmed имеет более высокий приоритет, чем pending;
  • у canceled и failed обычно есть приоритет выше, чем pending (зависит от провайдера).

Практический способ:

  1. Определить порядок приоритетов:
    • created (0),
    • pending (1),
    • confirmed (2),
    • failed/canceled (3) — если считаете, что это финальные “отрицательные” состояния.
  2. Для входящего статуса обновлять только если приоритет выше или переход допустим.

Пример в коде:

code
STATUS_PRIORITY = {
    "created": 0,
    "pending": 1,
    "confirmed": 2,
    "failed": 3,
    "canceled": 3,
}

def can_apply(current: str, incoming: str) -> bool:
    # финальные состояния обычно нельзя "улучшать" назад
    return STATUS_PRIORITY.get(incoming, -1) >= STATUS_PRIORITY.get(current, -1)

Это проще, чем пытаться описать все переходы вручную — но всё равно важно аккуратно настроить приоритеты под вашу предметную область.


Проверка вебхуков: как отвечать провайдеру и что логировать

HTTP-коды: “200 всегда” может быть плохой идеей

Провайдеры обычно считают:

  • 200/204 — webhook обработан успешно;
  • 4xx — ошибка данных/подписи, нужно не ретраить или ретраить ограниченно;
  • 5xx — временная ошибка, следует ретраить.

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

  • Если подпись неверна или payload невалиден — отвечайте 4xx и логируйте причину.
  • Если идемпотентность сработала (событие повторное) — можно отвечать 200 с флагом replayed.
  • Если база недоступна — отвечайте 5xx, чтобы провайдер мог повторить.

Но есть нюанс: некоторые провайдеры ретраят агрессивно. Поэтому лучше, чтобы “ошибка консистентности” (например, переход статуса неразрешен) не вызывала бесконечные ретраи. В таких случаях можно возвращать 200 и логировать, либо возвращать специфичный 4xx, если провайдер поддерживает “не ретраить”. Это зависит от их API — ориентируйтесь на документацию.


Логирование: что фиксировать для расследований

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

aiosend – для начинающих!

Курс по теме

aiosend – для начинающих!

Полный курс по библиотеке aiosend для интеграции криптовалютных платежей в Python-проекты.

Бесплатно
Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Стабильные асинхронные интеграции в Python: таймауты, отмена задач и ретраи без “скрытых” зависаний
стабильные

Стабильные асинхронные интеграции в Python: таймауты, отмена задач и ретраи без “скрытых” зависаний

Разберём типовые причины зависаний в async-коде, как правильно выставлять таймауты, как корректно отменять задачи и как строить ретраи так, чтобы при сбоях не разгонять нагрузку и не терять управление ресурсами.

14 сентября 2026 г.
640
Интеграции с ИИ для работы: от промптов к наблюдаемым результатам и тестам
интеграции

Интеграции с ИИ для работы: от промптов к наблюдаемым результатам и тестам

Построим процесс внедрения LLM в продукт: промпты как артефакты, критерии качества, набор тестовых кейсов и метрики для сравнения версий. Покажем, как снижать хаос и превращать эксперименты в воспроизводимые улучшения.

25 июля 2026 г.
630
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Как выглядит реальная обработка оплаты криптовалютамиСобытия и статусы: почему нельзя ограничиться “paid=true”Повторяемость уведомлений: почему идемпотентность — базовое требованиеИдемпотентность: как проектировать обработчик вебхуковИдентификатор события: где взять ключ идемпотентностиДва уровня защиты: база и транзакцииПример кода: идемпотентность с “INSERT … ON CONFLICT”Подписи и подлинность: как понять, что webhook пришел от провайдераПочему проверка подписи важнее, чем “мы же только по URL принимаем”Общая схема проверки HMACВариант с подписью по timestamp: защита от replay атакПроверка данных: как валидировать входящий payload без самоуничтоженияЧто именно валидироватьСтрогая схема валидации: Pydantic/JSON SchemaПроверка согласованности: статус, сумма, подтвержденияМодель статусов и “правила переходов”: как не перепутать pending и confirmedState machine вместо “если статус == confirmed — выдать”Проверка вебхуков: как отвечать провайдеру и что логироватьHTTP-коды: “200 всегда” может быть плохой идеейЛогирование: что фиксировать для расследований