Руководство по миграции (V1 → V2)
Пошаговый план миграции с устаревшей P2P-платформы V1 на новую инфраструктуру Syncra V2
Руководство по миграции (V1 → V2)
Это руководство предназначено для технических специалистов мерчантов, уже
интегрированных с устаревшей P2P-платформой V1 (базирующейся на C#/.NET шлюзе
api-merchant.syncra.me). В нем описан детальный процесс перевода вашей системы
на новое API V2 (api.syncra.money).
Для обеспечения бесшовного переключения в Syncra V2 встроен Compatibility Adapter (Адаптер совместимости), который позволяет вашему старому коду работать без изменений в течение переходного периода (6 месяцев). Однако для использования преимуществ повышенной производительности, лимитов и безопасности мы рекомендуем выполнить полноценную миграцию.
Бесшовная миграция (2 шага)
Если вы хотите переключить трафик на новую инфраструктуру Syncra V2 мгновенно и без изменения вашего программного кода (без переписывания сигнатур, изменения формата сумм и кодов валют), воспользуйтесь встроенным Compatibility Adapter.
Миграция выполняется в два простых шага:
-
Изменить базовый URL запросов: Замените в настройках вашего API базовый адрес запросов с
https://api-merchant.syncra.me/v1/наhttps://api.syncra.money/v1/. -
Активировать режим совместимости и получить секрет: В личном кабинете администратора создайте мерчанта или откройте настройки существующего и убедитесь, что в поле API Version выбран режим
v1. Скопируйте сгенерированные API-токен и Webhook Secret. Используйте их в заголовкахMERCHANTиSIGNATUREкак обычно.
После этого ваша интеграция продолжит работу в штатном режиме, используя старые структуры данных V1, но обрабатывая платежи через высокопроизводительное ядро V2.
Справочник: V1 сигнатуры запросов (payload по эндпоинтам)
Бесшовный Compatibility Adapter принимает ровно те же V1 подписи HMAC-SHA512,
что и устаревший шлюз api-merchant.syncra.me. Категория эндпоинта определяет
формат подписной полезной нагрузки (склейка полей через ;). Эта таблица —
канонический справочник против фактических маршрутов compat-слоя; сверьте с
ней генерацию подписей в вашем текущем коде.
| Категория | Реальный маршрут V1 | Формат payload | Поля |
|---|---|---|---|
| PayIn (входящий платёж) | POST /v1/payments/incoming (зеркала: /v1/payments/incoming/idempotency, /nochannel) | {timestamp};{payInId};{salt} | timestamp — Unix сек; payInId — idempotency ID заказа; salt — случайная соль из тела запроса. |
| PayOut (выплата на карту/реквизиты) | POST /v1/payments/outgoing | {timestamp};{payOutId};{salt} | payOutId — idempotency ID выплаты; salt — соль. Структура идентична PayIn, отличается только семантика ID. |
| Withdraw (крипто-вывод TRC20) | POST /v1/merchant/withdraw | {timestamp};{requestId};{wallet};{salt} | requestId — idempotency ID вывода; wallet — TRON-адрес получателя: значение одноимённого поля тела запроса (если отправлен только алиас address — его значение); salt — соль. 4 поля, не 3 — пропуск адреса ломает подпись. |
| Dispute (апелляция по сделке) | POST /v1/disputs/create | {timestamp};{paymentId} | paymentId — ID платежа/сделки. Только 2 поля, соль не используется. |
| Banks (справочник банков) | POST /v1/banks/list | {timestamp} | Только метка времени (GET-подобные запросы без идентификатора). |
Алгоритм для всех эндпоинтов одинаковый:
HMAC-SHA512(webhook_secret, payload), результат в lowercase-hex, передаётся
в заголовке SIGNATURE. Заголовки MERCHANT (GUID мерчанта) и TIMESTAMP
(тот же timestamp, что в payload) обязательны.
Replay-защита: адаптер отклоняет запросы с timestamp старше 300 секунд
или более чем на 60 секунд в будущем (clock skew). Используйте актуальное
время сервера.
Пример: V1 Withdraw (PHP)
<?php
// V1 Withdraw — 4 поля в payload; адрес берётся из поля "wallet" тела
$merchantGuid = "7ac148fe-19a3-45bb-b992-019fac55b721";
$merchantSecret = "legacy_secret_key";
$timestamp = time();
$requestId = "wd_98765";
$wallet = "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE"; // T + 33 base58
$salt = "random_salt_wd";
$payload = "{$timestamp};{$requestId};{$wallet};{$salt}";
$signature = hash_hmac('sha512', $payload, $merchantSecret);
$headers = [
"MERCHANT: {$merchantGuid}",
"TIMESTAMP: {$timestamp}",
"SIGNATURE: {$signature}",
"Content-Type: application/json"
];
$body = json_encode([
// requestId — идемпотентность: повтор с тем же requestId вернёт
// уже существующий вывод, без второго резерва баланса
"requestId" => $requestId,
"amount" => "10.00", // строковый десятичный формат
"blockchain" => 1, // 1 = Tron (TRC20) — единственная цепочка
"wallet" => $wallet, // обязательное поле спеки; "address" — алиас
"isBalanceCommission" => true,
"salt" => $salt
]);
// POST https://api.syncra.money/v1/merchant/withdraw
?>Пример: V1 Dispute (PHP)
<?php
// V1 Dispute — только 2 поля, без соли
$merchantGuid = "7ac148fe-19a3-45bb-b992-019fac55b721";
$merchantSecret = "legacy_secret_key";
$timestamp = time();
$paymentId = "pmnt_4242";
$payload = "{$timestamp};{$paymentId}";
$signature = hash_hmac('sha512', $payload, $merchantSecret);
$headers = [
"MERCHANT: {$merchantGuid}",
"TIMESTAMP: {$timestamp}",
"SIGNATURE: {$signature}",
"Content-Type: application/json"
];
// POST https://api.syncra.money/v1/disputs/create
?>Контракты V1-адаптера: поля и формы ответов
Формы ниже — фактический контракт compat-адаптера (обновлено 2026-09-09 по
итогам аудита V1-контура). Успешный ответ завёрнут в конверт
{failures, value, isSuccess, isFailure}; списки дополнительно — в конверт
{count, data, items, next_page_token} (items — легаси-алиас data).
Крипто-вывод (POST /v1/merchant/withdraw, GET /v1/merchant/withdraw/{id}, POST /v1/merchant/withdraw/list)
Тело create: requestId, amount (строка, "10.00"), blockchain
(обрабатывается как 1 = Tron/TRC20 — единственная цепочка вывода),
wallet — обязательное поле легаси-спеки (алиас address принят для
симметрии; при заполнении обоих выигрывает wallet), comment,
isBalanceCommission, salt, callbackUrl.
- Адрес валидируется на входе: mainnet Base58Check TRON (
T+ 33 base58-символа), иначе400 INVALID_ARGUMENT. - Идемпотентность по
requestId: повторный запрос с тем жеrequestIdвозвращает уже существующий вывод — второй строки и второго резерва баланса не создаётся (гонка двух ретраев также даёт одну заявку). - Ответ create/get/list — единый профиль из 19 полей:
id,withdrawId(дублируетid),userId,status,requestId,blockchain(всегда1),currency(числовой код USDT),amount(строка),wallet,comment,isBalanceCommission,exchangeRate(1.0, USDT → USDT),sentAmount(равен сумме только в статусе3),commissionAmount,transactionHash,callbackUrl,createdAt,lastModifiedAt,sentAt.
Статистика (POST /v1/payments/incoming/statistics, POST /v1/payments/outgoing/statistics)
Тело запроса — необязательные startDate / endDate (RFC3339). Ответ —
объект {"statistics": [...]} с массивом записей по каждому методу, а не
{successCount, totalAmount}:
- incoming, 14 полей:
method,conversionPercent,paymentsCount,paymentsPaidCount,paymentsTimeoutCount,paymentsCancelledCount,paymentsReturnedCount,paymentsDisputCount,paymentsCurrency,paymentsAmount,paymentsPaidAmount,balanceCurrency,paymentsToBalanceAmount,paymentsCommissionAmount. - outgoing, 12 полей:
method,conversionPercent,paymentsCount,paymentsSentCount,paymentsCancelledCount,paymentsDisputCount,paymentsCurrency,paymentsAmount,paymentsSentAmount,balanceCurrency,paymentsFromBalanceAmount,paymentsCommissionAmount.
Диспуты (POST /v1/disputs/create, GET /v1/disputs/{id}, POST /v1/disputs/list)
- create и get — полный профиль из 17 полей:
disputId,status,paymentId,receiptFileBody,receiptFileType,receiptFileUrl,clientComment,merchantComment,description,answerFileBody,answerFileType,answerFileUrl,traderComment,adminComment,createdAt,lastModifiedAt,finalizedAt. - list — сокращённый профиль из 11 полей:
disputId,status,paymentId,clientComment,merchantComment,traderComment,adminComment,description,createdAt,lastModifiedAt,finalizedAt. - create читает из тела:
paymentId(обязателен),receiptType,receiptUrl,description,clientComment,merchantComment(легаси-алиасыcomment/proofImageBase64дополняют текст причины); переданные значения возвращаются эхом в ответе.
Банки (POST /v1/banks/list)
Элемент списка — BankProfile из 5 полей: id, country, bankName,
methodNames (массив имён методов), currencies (массив числовых кодов
валют). Полей displayName / supportedMethods в ответе нет. Тело-фильтр:
country, bankName, methodName, skip, take.
List-фильтры, изоляция, валюта
- Списковые ручки
withdraw/list,payments/incoming/list,payments/outgoing/list,disputs/listпринимают необязательный body-фильтр:status(числовой V1-код; неизвестный код отвечает400 INVALID_ARGUMENT),skip,take(1–100),startDate/endDate(RFC3339, по дате создания);incoming/listдополнительно принимаетid(UUID) для сужения по одной сделке. - GET-ручки
/payments/incoming/{id},/payments/outgoing/{id},/merchant/withdraw/{id},/disputs/{id}на сущность чужого мерчанта отвечают404 NOT_FOUND— факт существования не раскрывается (не403). - Неизвестный числовой код валюты в create PayIn/PayOut отвечает
400 INVALID_ARGUMENT(unknown currency code: N); тихая подстановка RUB отключена. - Вебхук PayOut:
PROCESSING→ код40(InProgress),FAILED/EXPIRED→ код30(Failed); ранее оба состояния приходили кодом1(WaitingProcessing), и мерчант не узнавал об ошибке выплаты. - GET
/v1/payments/incoming/{id}по сделке вREFUND_PENDINGвозвращает32(RollingBack), а вебхук на той же стадии шлёт16(Returned) — расхождение контуров осознанное (см. реестр кодов V1).
Полноценная миграция (рекомендуется)
- Получить учетные данные V2: Войдите в новый личный кабинет администратора и сгенерируйте API-токен V2 и Webhook Secret.
- Изменить базовый URL: Обновите адрес отправки запросов с
https://api-merchant.syncra.me/v1/наhttps://api.syncra.money/api/v1/p2p/merchant/. - Обновить заголовки аутентификации:
- Замените заголовок
MERCHANTнаX-Merchant-Token. - Замените заголовок
SIGNATUREнаX-Signature. - Добавьте обязательный заголовок
X-Timestamp.
- Замените заголовок
- Заменить алгоритм подписи: Смените HMAC-SHA512 на HMAC-SHA256.
- Изменить формат полезной нагрузки подписи: Вместо склеивания полей через
точку с запятой (
ts;id;salt) перейдите на стандартную формулу подписи тела запроса:timestamp + "." + raw_body. - Обновить формат суммы (Amount): Переведите суммы со строкового
представления с точкой (например,
"1500.50") на целочисленные копейки (например,150050вint64). - Обновить коды валют: Замените целочисленные коды ISO на трехбуквенные
строковые коды (например,
643→"RUB",10001→"USDT"). - Обновить обработку ответов: Перейдите со старого конверта ответа
{failures, value, isSuccess}на proto-поля верхнего уровня без обёрткиdata: единичный объект —{"deal": {...}}, списки —{"deals": [...] , "next_page_token": ""}(gRPC-gateway рендерит ответ напрямую). - Обновить проверку подписей вебхуков: Перенастройте ваш обработчик
вебхуков на декодирование нового формата подписи
X-Signatureна основе алгоритма SHA256 (вместо привязки к началу сутокStartOfDayв V1). - Провести тестирование: Проверьте весь платежный флоу на тестовом окружении (staging) и переключите боевой трафик.
Таблица маппинга параметров (PayIn Request)
При переходе с V1 на V2 параметры запроса пополнения изменяются следующим образом:
| Поле в API V1 | Поле в API V2 | Тип V1 | Тип V2 | Пример конвертации |
|---|---|---|---|---|
payInId | idempotency_key | string | string | "order_123" → "order_123" |
amount | amount | string | int64 | "1500.50" → 150050 |
currency | currency | int | string | 643 → "RUB" |
clientId | client_id | string | string | "user_99" → "user_99" |
method | payment_method_id | string | string (UUID или слаг) | "Sbp" → "550e8400-e29b-41d4-a716-446655440000" (UUID) или "sbp_rub" (слаг) |
Поле V2 называется payment_method_id и обязательно (с
2026-08-24): каскад маршрутизации методо-скопирован, пустое значение
отклоняется с 400 INVALID_ARGUMENT. Принимаются UUID (канонический
формат) и слаг метода — оба значения из
GET /api/v1/p2p/merchant/payment-methods (см.
Banks); GET /banks UUID
не возвращает. Отдельного поля payment_method для PayIn в V2 нет.
Поля successUrl / failUrl из V1 не имеют аналога в запросе
CreateMerchantPayIn V2. URL редиректов (success_redirect_url,
fail_redirect_url) теперь настраиваются на уровне мерчанта в
административной панели и применяются ко всем сделкам — их нельзя
переопределить в запросе. Если вам нужны разные страницы успеха для разных
продуктов — заведите несколько мерчантов. См. Hosted Checkout → Redirect
URLs.
Таблица маппинга параметров (PayOut Request)
При переходе с V1 на V2 параметры запроса выплаты изменяются следующим образом:
| Поле в API V1 | Поле в API V2 | Тип V1 | Тип V2 | Пример конвертации |
|---|---|---|---|---|
payOutId | idempotency_key | string | string | "payout_123" → "payout_123" |
amount | amount | string | int64 | "5000.00" → 500000 |
currency | currency | int | string | 643 → "RUB" |
cardNumber | target_requisite | string | string | "2202201234567890" → "2202201234567890" |
fullName | target_name | string | string | "Иван И." → "Иван И." |
paymentMethod | payment_method | string | string (slug или UUID) | "Card" → "card_rub" (слаг) или "550e8400-..." (UUID) |
| — | target_bank | — | string (optional) | Банк получателя (слаг/название) — для TRY/Havale-выплат (v0.2.11). |
| — | requisite_details | — | map (optional) | Именованные реквизиты (iban, account_number, document_number, bank_name, phone_number) — для TRY/Havale-выплат (v0.2.11). |
payment_method в V2 PayOut принимает слаг (card_rub, sbp_rub,
mobcom_rub, express-havale) или UUID метода, а НЕ uppercase enum
BANK_CARD/SBP. Валюта метода обязана совпадать с валютой сделки
(иначе 400). Актуальные идентификаторы — из GET /payment-methods.
Подробности и TRY-пример — в PayOut.
Таблица маппинга статусов (PayIn: V1 код → V2 enum)
| Код V1 | Статус V2 (canonical) | Комментарий |
|---|---|---|
1 | INITIALIZED | WaitingPayment — сделка создана, ожидает оплаты. |
2 | PAYMENT_NOTIFIED | ConfirmedByPayer — «Я оплатил». |
11 | COMPLETED | Paid — терминальный успех. |
12 | CANCELLED (а также SPAM_REJECTED) | Cancelled в V1. Оба V2-статуса — и явная отмена (CANCELLED, TD-157), и отклонение анти-spam (SPAM_REJECTED) — транслируются адаптером в один код 12; различить их можно только в V2-контракте. |
13 | EXPIRED | Timeout — терминальный таймаут. |
14 | COMPLETED (+ amount_match_status: overpaid) | Переплата — отдельного статуса в V2 нет. |
15 | COMPLETED (+ amount_match_status: partial) | Недоплата — отдельного статуса в V2 нет. |
16 | REFUND_PENDING | Returned — инициирован возврат. |
21 | APPEALED | Disput — открыта апелляция. |
31 | INITIALIZED | WaitingPaymentChannel — канал ещё не выбран. |
Полная canonical-таблица (включая PayOut и выводы USDT) — в
Справочнике статусов. PayOut-коды V1 отличаются от PayIn
(10 = успех, 30 = отказ) — см. таблицу PayOut там же.
Сравнение примеров кода (PHP)
Как было в API V1 (HMAC-SHA512 + ";" separator)
<?php
// API V1
$merchantGuid = "7ac148fe-19a3-45bb-b992-019fac55b721";
$merchantSecret = "legacy_secret_key";
$timestamp = time();
$payInId = "order_12345";
$salt = "random_salt_99";
$payload = "{$timestamp};{$payInId};{$salt}";
$signature = hash_hmac('sha512', $payload, $merchantSecret);
$headers = [
"MERCHANT: {$merchantGuid}",
"SIGNATURE: {$signature}",
"Content-Type: application/json"
];
$body = json_encode([
"payInId" => $payInId,
"amount" => "1500.50",
"currency" => 643,
"salt" => $salt
]);
// Отправка на https://api-merchant.syncra.me/v1/payments/incoming
?>Как стало в API V2 (HMAC-SHA256 + body signature)
<?php
// API V2
$merchantToken = "a1b2c3d4e5f607182930a4b5c6d7e8f901a2b3c4d5e6f7081920a3b4c5d6e7f8";
$webhookSecret = "whsec_920ab3e09819cd8e412cfab9e87d0c3c";
$timestamp = time();
$body = json_encode([
"amount" => 150050, // в копейках
"currency" => "RUB",
"idempotency_key" => "order_12345"
]);
$payload = $timestamp . '.' . $body;
$signature = hash_hmac('sha256', $payload, $webhookSecret);
$headers = [
"X-Merchant-Token: {$merchantToken}",
"X-Timestamp: {$timestamp}",
"X-Signature: t={$timestamp},v1={$signature}",
"Content-Type: application/json"
];
// Отправка на https://api.syncra.money/api/v1/p2p/merchant/deals/payin
?>Важные подводные камни (Gotchas & Edge Cases)
-
Конвертация сумм (Amount): При преобразовании сумм из строкового формата V1 в
int64V2 строго запрещено использовать типы с плавающей точкой (float,double). Используйте строковый парсинг: разделите строку по символу точки., проверьте, что количество знаков в дробной части не превышает двух, дополните нулями при необходимости и преобразуйте в целое число копеек. Например,"1500.5"->"1500"и"5"->"1500"и"50"->150050. -
Подписи вебхуков: Старые вебхуки V1 подписывались на основе времени начала суток в формате UTC (
StartOfDay). Новые вебхуки V2 используют текущее реальное время отправки вебхука (Timestamp). Не забудьте обновить логику валидации на вашем принимающем скрипте. -
Статусы выплаты (PayOut): Авто-матчинг каскада при создании (
POST /deals/payout) выполняется синхронно: ответ приходит сразу вMATCHED(нашёлся свободный трейдер) либо вUNASSIGNED(свободной ёмкости нет). ДляUNASSIGNEDretry-воркер продолжает мэтчинг; если SLA на назначение истекает — выплата переходит в терминальныйEXPIREDс возвратом холда мерчанту.INITIALIZED— стартовый статус стейт-машины (наблюдается только в граничных случаях: провайдер-ветка, гонки), в ответе создания практически не встречается. ДалееMATCHED→PROCESSING(провайдер выполняет перевод) →COMPLETED|FAILED(в V1EXPIRED/FAILEDмаппятся в код30; для PayOut-выплат статусаCANCELLEDв V2 не существует —CANCELLEDприменяется только к депозитным PayIn-сделкам, см. Справочник статусов). Полная FSM и таблица — в Справочнике статусов.
Payment-method catalog changed (V2-only broadcast) Webhook
Emitted when an administrator creates or updates catalog-visible fields of a payment method (`name`, `status`, `requisite_types`, `is_cross_border`). Broadcast to every ACTIVE V2 merchant of the tenant with a non-empty webhook_url (legacy V1 merchants never receive it — the V1 envelope has no catalog callbackType). No-op updates emit nothing. The envelope is a dedicated catalog shape — deal fields (`amount`, `currency`, `deal_id`) are absent. This is a NOTIFICATION, not a data source: re-read GET /api/v1/p2p/merchant/payment-methods on receipt.
История изменений (Changelog)
Next Page