Одна интеграция.
Полный цикл заявки.
Приём ордеров Goldex и onix, статусы, чеки и обратные уведомления. Документация соответствует MVP 0.1.
Начало работы
Партнёр получает свой provider, api_key, signing_secret и отдельный callback_secret. Ключ оператора не передаётся партнёру.
BASE_URL = http://127.0.0.1:8790
PROVIDER = goldex # либо onix
Authorization: Bearer <api_key>Для внешней проверки используйте HTTPS-адрес стенда, указанный выше. Адрес localhost подходит только для локального запуска. Все ответы с данными используют оболочку {"data": ...}. Время передаётся в UTC.
| Метод | Адрес после /api/v1/integrations/{provider} |
|---|---|
| POST | /orders — новая заявка |
| GET | /orders?after=0&limit=50 — сверка списка |
| GET | /orders/{external_id} — актуальное состояние |
| POST | /events — подтверждение, отмена, результат проверки чека |
| GET | /receipts/{receipt_id}/file — исходный файл |
| GET | /availability — приём и свободная ёмкость |
Список выдаётся по возрастанию курсора создания. Передавайте meta.next_after как следующий after. Этот курсор не отслеживает изменения: для сверки перечитывайте список с нуля или конкретную заявку.
Ключ и подпись
GET-запросам нужен Bearer-ключ. Для POST дополнительно обязательны три заголовка:
Webhook-Id: <уникальный UUID события>
Webhook-Timestamp: <unix-время в секундах>
Webhook-Signature: v1,<подпись в base64>Подпись вычисляется по точным UTF-8 байтам тела запроса, до разбора JSON. Секрет используется как UTF-8 строка:
message = webhook_id + "." + timestamp + "." + raw_body
signature = base64(HMAC-SHA256(signing_secret, message))Отклонение времени — не более 300 секунд. Webhook-Id содержит 8–100 латинских букв, цифр, дефисов или подчёркиваний. Точки запрещены. Схема следует подходу Standard Webhooks.
Готовый пример
import { ProviderClient } from './provider-client.mjs';
import { randomUUID } from 'node:crypto';
const client = new ProviderClient({
baseUrl: process.env.MVP_BASE_URL,
provider: 'goldex',
apiKey: process.env.MVP_API_KEY,
signingSecret: process.env.MVP_SIGNING_SECRET,
});
const eventId = randomUUID(); // сохранить ДО отправки
const order = { /* тело заявки из следующего раздела */ };
await client.sendOrder(order, eventId);
// При сетевой ошибке повторить тот же order и eventId.
// Клиент пересчитает время и подпись.Новая заявка
POST /api/v1/integrations/goldex/orders
{
"external_id": "10da8a3c-d929-49c0-bd93-cc570e81ca69",
"amount": "12500.00",
"currency": "RUB",
"payment_details": {
"bank": "Тестовый банк",
"recipient": "Тестовый получатель",
"card": "TEST-ONLY-0000-0000",
"phone": "+7 (000) 000-00-00"
},
"expires_at": "<будущая дата UTC; new Date(Date.now()+300000).toISOString()>",
"receipt_rules": {
"content_types": ["application/pdf"],
"max_size_bytes": 10485760,
"max_files": 2
}
}external_id— полный исходный идентификатор; уникален внутри одного партнёра. В ответеid— отдельный внутренний UUID.amount— положительная десятичная строка, до 12 цифр до точки и 8 после. JSON-числа и экспоненты не принимаются.- Нужны банк, получатель и хотя бы один реквизит:
card,phoneилиaccount. expires_at— срок принятия новой заявки. После принятия этот таймер не отменяет её автоматически.- Правила чеков задаёт партнёр: PDF/JPEG/PNG, до 10 МиБ на файл и до 10 чеков. Партнёр может установить меньшие лимиты.
- Неизвестные поля отклоняются; новые поля добавляются после согласования.
Ответ содержит заявку со статусом NEW, version: 1, provider_version: 1, server_time, правилами чеков и доступными действиями.
Рабочий цикл
→ AWAITING_CONFIRMATION → COMPLETED
| Статус | Значение |
|---|---|
| NEW | Предложение получено; оператор может принять или отказаться. |
| ACCEPTED | Оператор принял заявку. |
| PROCESSING | Этап оплаты; разрешено прикрепление чеков. |
| AWAITING_CONFIRMATION | Оператор запросил завершение; финальный статус ещё не подтверждён. |
| COMPLETED | Партнёр подтвердил завершение через /events. |
| CANCELLED | Отказ оператора до принятия или отмена партнёром. |
| EXPIRED | Новое предложение не принято до expires_at. |
Финальные статусы в MVP не меняются автоматически. Исправления требуют ручной сверки. version — ревизия нашей платформы, provider_version — независимый счётчик событий партнёра. Для сопоставления с ACTIVE/WORK/PROCESSING/CLOSED реальные правила нужно согласовать.
Подтверждение и изменения
После получения order.completion_requested партнёр отправляет подписанный запрос:
POST /api/v1/integrations/goldex/events
{
"external_id": "10da8a3c-d929-49c0-bd93-cc570e81ca69",
"type": "order.completed",
"provider_version": 2
}Поддерживаются order.completed, order.cancelled, receipt.accepted, receipt.rejected. Для событий чека обязателен receipt_id; можно передать reason до 500 символов.
Счётчик provider_version должен строго возрастать для одной заявки. Уже применённая/старая версия с новым Webhook-Id отклоняется с 409 STALE_EVENT. Повтор того же события с тем же ID возвращает сохранённый ответ. Подтверждение завершения принимается только из AWAITING_CONFIRMATION.
Отклонённый чек сохраняется в истории. Если заявка ждала подтверждения, она возвращается в PROCESSING для исправления чека. Такие входящие события не отправляются обратно партнёру.
Обратные уведомления
Партнёр сообщает нам один HTTPS-адрес callback. Он настраивается отдельно, не передаётся в каждой заявке. Для проверки используется callback_secret и та же формула подписи.
{
"event_id": "<стабильный UUID>",
"type": "order.completion_requested",
"provider": "goldex",
"external_id": "10da8a3c-d929-49c0-bd93-cc570e81ca69",
"order_id": "<внутренний UUID>",
"status": "AWAITING_CONFIRMATION",
"version": 5,
"created_at": "<UTC>",
"data": { "receipts": [ /* метаданные и защищённые URL */ ] }
}События: order.accepted, order.processing, receipt.uploaded, order.completion_requested, order.refused, order.expired.
- Уведомление сохраняется в той же транзакции, что действие оператора.
- Доставка — как минимум один раз. Получатель сохраняет событие и устраняет дубли по
event_id, затем возвращает любой2xx. - Порядок доставки не гарантирован. Применяйте только более новую
versionи сверяйте состояние через GET. - 8 попыток; интервалы 1, 2, 4, 8, 16, 32, 60 секунд, таймаут ответа 5 секунд. Redirect не выполняется.
- После исчерпания попыток событие видно в журнале; ручной повтор сохраняет исходный ID. Для рабочего контура согласуем более длинное окно повторов.
- Ошибка доставки не отменяет сохранённое действие оператора.
В локальном режиме callback получает встроенный симулятор; его подпись проверяется, а повторные события сохраняются один раз.
Работа с чеками
Оператор загружает файл в наш кабинет. Сервер проверяет статус заявки, лимиты партнёра, тип содержимого, размер и повтор файла. Исходные байты сохраняются в отдельной базе MVP.
Из receipt.uploaded или order.completion_requested партнёр получает ID и URL. Скачивание требует его Bearer-ключа. Чужой партнёр и анонимный пользователь доступа не имеют.
Результаты обработки возвращаются через /events. Наличие файла не подтверждает факт оплаты. MVP не выполняет банковские платежи и не проверяет подлинность банковского документа.
Ошибки и повторы
{
"error": { "code": "VERSION_CONFLICT", "message": "Заявка изменилась." },
"request_id": "<UUID для диагностики>"
}| HTTP | Действие |
|---|---|
| 400 / 415 / 422 | Исправить JSON, Content-Type или поля запроса. |
| 401 | Проверить ключ, подпись, точные байты тела и часы. |
| 404 | Проверить provider, полный external_id или receipt_id. |
| 409 | Прочитать error.code и актуальную заявку. Для RECEPTION_UNAVAILABLE дождаться свободной ёмкости. |
| 410 | Истёк срок принятия нового предложения. |
| 413 | Уменьшить запрос / файл. |
| 5xx / timeout | Повторить с теми же Webhook-Id и телом, свежими временем и подписью; использовать задержки. |
Тот же Webhook-Id с другим телом → IDEMPOTENCY_CONFLICT. Тот же external_id с другими данными → EXTERNAL_ID_CONFLICT. Кэш повтора содержит исходный ответ, поэтому актуальный статус нужно читать отдельным GET. Авторизация и подпись проверяются до идемпотентности; ключи не пересекаются между партнёрами.
Что проверяем вместе
- Подписанная заявка появляется в кабинете, полный UID совпадает.
- Повтор и параллельные запросы не создают вторую заявку.
- Одинаковый ID Goldex и onix создаёт две независимые заявки.
- Неверный ключ, подпись и старое время отклоняются; чужие файлы недоступны.
- Приём на паузе / заполненная ёмкость дают понятный отказ.
- Срок новой заявки истекает на сервере; её уже нельзя принять.
- Принятие, оплата и чек создают отдельные callback-события.
- Неверный формат, большой файл и лишний чек отклоняются.
- Запрос завершения ждёт партнёра; order.completed действительно завершает заявку.
- Отмена и отклонение чека отражаются в кабинете и истории.
- Повторные и пришедшие не по порядку события не повреждают статус.
- Недоступный callback повторяется с тем же ID; данные переживают перезапуск.
Внешний тест
Перед внешним тестом нужны доступный партнёру HTTPS-адрес, отдельные тестовые ключи, адрес callback и согласованное сопоставление статусов. Секреты передаются отдельно от документации.
Стенд — один процесс Node.js 24+ с собственной SQLite-базой и файлами внутри неё. Это изолированный MVP, не подключённый к существующей PostgreSQL-базе Fluxora, Telegram-ботам и рабочим кабинетам. Для рабочего контура подключим общую авторизацию, основной сервер, хранилище документов, Telegram и учёт по согласованным правилам.
Существующая платформа и установленные приложения Goldex/onix остаются отдельными от этого стенда.