ПРОЕКТ КОНТРАКТА · SANDBOX

Одна интеграция.
Полный цикл заявки.

Приём ордеров Goldex и onix, статусы, чеки и обратные уведомления. Документация соответствует MVP 0.1.

Это предлагаемый контракт для согласования. Локальный стенд использует симуляторы. Подключение и соответствие статусам реального Goldex/onix ещё предстоит согласовать с партнёрами.
01 / ПОДКЛЮЧЕНИЕ

Начало работы

Партнёр получает свой 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. Этот курсор не отслеживает изменения: для сверки перечитывайте список с нуля или конкретную заявку.

02 / БЕЗОПАСНОСТЬ ОБМЕНА

Ключ и подпись

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.
// Клиент пересчитает время и подпись.
03 / ПРИЁМ

Новая заявка

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
  }
}
201 Created означает, что предложение сохранено в базе. Оно ещё не принято оператором, не оплачено и не завершено.

Ответ содержит заявку со статусом NEW, version: 1, provider_version: 1, server_time, правилами чеков и доступными действиями.

04 / СОСТОЯНИЯ

Рабочий цикл

NEW → ACCEPTED → PROCESSING
→ AWAITING_CONFIRMATION → COMPLETED
СтатусЗначение
NEWПредложение получено; оператор может принять или отказаться.
ACCEPTEDОператор принял заявку.
PROCESSINGЭтап оплаты; разрешено прикрепление чеков.
AWAITING_CONFIRMATIONОператор запросил завершение; финальный статус ещё не подтверждён.
COMPLETEDПартнёр подтвердил завершение через /events.
CANCELLEDОтказ оператора до принятия или отмена партнёром.
EXPIREDНовое предложение не принято до expires_at.

Финальные статусы в MVP не меняются автоматически. Исправления требуют ручной сверки. version — ревизия нашей платформы, provider_version — независимый счётчик событий партнёра. Для сопоставления с ACTIVE/WORK/PROCESSING/CLOSED реальные правила нужно согласовать.

05 / ПАРТНЁР → ПЛАТФОРМА

Подтверждение и изменения

После получения 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 для исправления чека. Такие входящие события не отправляются обратно партнёру.

06 / ПЛАТФОРМА → ПАРТНЁР

Обратные уведомления

Партнёр сообщает нам один 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.

В локальном режиме callback получает встроенный симулятор; его подпись проверяется, а повторные события сохраняются один раз.

07 / ДОКУМЕНТЫ

Работа с чеками

Оператор загружает файл в наш кабинет. Сервер проверяет статус заявки, лимиты партнёра, тип содержимого, размер и повтор файла. Исходные байты сохраняются в отдельной базе MVP.

Из receipt.uploaded или order.completion_requested партнёр получает ID и URL. Скачивание требует его Bearer-ключа. Чужой партнёр и анонимный пользователь доступа не имеют.

Результаты обработки возвращаются через /events. Наличие файла не подтверждает факт оплаты. MVP не выполняет банковские платежи и не проверяет подлинность банковского документа.

↓ Вымышленный тестовый PDF

08 / НАДЁЖНОСТЬ

Ошибки и повторы

{
  "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. Авторизация и подпись проверяются до идемпотентности; ключи не пересекаются между партнёрами.

09 / ПРИЁМКА

Что проверяем вместе

  1. Подписанная заявка появляется в кабинете, полный UID совпадает.
  2. Повтор и параллельные запросы не создают вторую заявку.
  3. Одинаковый ID Goldex и onix создаёт две независимые заявки.
  4. Неверный ключ, подпись и старое время отклоняются; чужие файлы недоступны.
  5. Приём на паузе / заполненная ёмкость дают понятный отказ.
  6. Срок новой заявки истекает на сервере; её уже нельзя принять.
  7. Принятие, оплата и чек создают отдельные callback-события.
  8. Неверный формат, большой файл и лишний чек отклоняются.
  9. Запрос завершения ждёт партнёра; order.completed действительно завершает заявку.
  10. Отмена и отклонение чека отражаются в кабинете и истории.
  11. Повторные и пришедшие не по порядку события не повреждают статус.
  12. Недоступный callback повторяется с тем же ID; данные переживают перезапуск.
10 / СЛЕДУЮЩИЙ ЭТАП

Внешний тест

Перед внешним тестом нужны доступный партнёру HTTPS-адрес, отдельные тестовые ключи, адрес callback и согласованное сопоставление статусов. Секреты передаются отдельно от документации.

Стенд — один процесс Node.js 24+ с собственной SQLite-базой и файлами внутри неё. Это изолированный MVP, не подключённый к существующей PostgreSQL-базе Fluxora, Telegram-ботам и рабочим кабинетам. Для рабочего контура подключим общую авторизацию, основной сервер, хранилище документов, Telegram и учёт по согласованным правилам.

Существующая платформа и установленные приложения Goldex/onix остаются отдельными от этого стенда.