S
docs.syncra.money
Payments

Payouts to clients (PayOut)

Fund withdrawal: the merchant creates the request, and the transfer to the recipient is executed by a trader or an API provider

Payouts to clients (PayOut)

A payout (PayOut) is a P2P withdrawal operation in which the merchant creates a transfer request, and the executor — a trader or an API provider — makes the actual transfer to the recipient from their own bank account.


Payout flow (PayOut Flow)

Загрузка диаграммы...

The funds reach the recipient from the executor's account (the trader's or the API provider's), not directly from the merchant's balance. The merchant's balance compensates the executor for the completed transfer inside the Syncra platform.


PayOut creation endpoint

POST /api/v1/p2p/merchant/deals/payout

Request parameters (JSON Body)

FieldTypeRequiredDescription
amountint64YesPayout amount in minor units (kopecks). 500000 = 5,000.00 RUB.
currencystringYesISO 4217 fiat currency code of the payout ("RUB", "KZT", "TRY", ...). Echoed back in the response.
wallet_idstring(UUID)YesThe merchant's USDT wallet the funds are debited from: the amount hold is created in USDT (at the SELL rate at creation time), not on the fiat wallet. This is the wallet_id of the USDT row from GET /balance.
target_requisitestringYesRecipient payment details (card number, phone, IBAN).
target_namestringNoRecipient name for verification by the bank/trader.
target_bankstringConditional*Recipient bank: a slug ("ziraat", "garanti") or a name. Used for intra-bank cascade matching (relevant for TRY/Havale). Empty = no bank hint (cards/SBP).
requisite_detailsmap<string,string>NoNamed recipient details (see below). Valid keys: iban, account_number, document_number, bank_name, phone_number. Unknown keys → 400 with the list of valid ones.
payment_methodstringYesReference to a payout method from your GET /payment-methods catalog: a slug ("sbp_rub", "card_rub", "mobcom_rub", "express-havale", ...; identical across the stage/prod environments) or a UUID (the id field) — both values resolve to the same method. A missing value, an unknown value, or a method of a foreign currency → 400 INVALID_ARGUMENT (with the list of valid slugs).
idempotency_keystringYesUnique operation ID (idempotency).

* target_bank is mandatory for bank methods (BANK_TRANSFER / Havale) when the bank is not derived from the request itself: the platform resolves the bank from the TCMB code embedded in the IBAN (positions 5–9) against the GET /merchant/banks reference. If neither target_bank nor the IBAN-derived code is known, the request is rejected with 400 INVALID_ARGUMENT before the deal and the hold are created (see Error codes).

In PayOut, payment_method accepts a slug (sbp_rub, card_rub, mobcom_rub) or the method's UUID, and NOT the uppercase enum BANK_CARD / SBP. The RequisiteType enum values (BANK_CARD, E_WALLET, ...) describe the trader's requisite type: in the GET /payment-methods response (the requisite_types field), they are reference information about the channel; they are never passed in requests. PayIn's payment_method_id has the same unified contract (a UUID or a slug) — both legs accept both formats.

The method currency must match the deal currency. The resolver checks that the found method serves the request's currency: a card_rub payout with currency: "TRY" is rejected with 400 INVALID_ARGUMENT ("payment method currency does not match the deal currency"). Pick a method of the same currency from the GET /payment-methods catalog (the currency_code field).

Request example

{
  "amount": 500000,
  "currency": "RUB",
  "wallet_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
  "target_requisite": "2202201234567890",
  "target_name": "Ivan Ivanovich I.",
  "payment_method": "card_rub",
  "idempotency_key": "payout_order_99812"
}

Response example (200 OK)

The response is a MerchantDealInfo object in the top-level deal field (there is no data wrapper). The deal identifier is deal_id (NOT id). For PAYOUT, payment_url is empty (it exists only for PayIn); instead, target_requisite is filled. currency echoes the ISO code of the payout's fiat currency from the request.

The status in the response depends on where the cascade routed the payout (matching runs synchronously, at creation time):

  • PROCESSING — routed to an API provider: the payout was created at the provider within the same request, and the transfer is already in progress;
  • MATCHED — routed to a trader, and a free executor was found (the example below);
  • UNASSIGNED — no free executor at creation time; the retry worker keeps matching (see PayOut deal statuses).

The terminal status (COMPLETED / FAILED / EXPIRED) arrives by webhook — the PROCESSING and MATCHED holders in the creation response are equivalent in terms of the further lifecycle.

{
  "deal": {
    "deal_id": "e81d77a0-0d3a-4422-b91c-7cb052a2119c",
    "status": "MATCHED",
    "amount": "500000",
    "currency": "RUB",
    "created_at": "2026-07-12T17:21:00Z",
    "client_id": "",
    "deal_type": "PAYOUT",
    "updated_at": "2026-07-12T17:21:00Z",
    "payment_url": "",
    "target_requisite": "2202201234567890",
    "received_amount": "0",
    "amount_match_status": ""
  }
}

In V2, target_requisite in the response is an echo of the request value, without masking: these are the merchant's own payment details, not trader data. The mask like 220220******7890 remains only in the V1 legacy endpoints (GET /v1/payments/outgoing/{id} and the V1 callback envelope).

For payouts routed to an API provider, the same creation response arrives with "status": "PROCESSING" — the other fields are identical.

A created payout is a full-fledged deal: deal_type=PAYOUT records are visible in GET /deals (a combined listing with PayIn) and available via GET /deals/{deal_id} by the deal_id from the creation response (see Deals).


PayOut deal statuses

The canonical lifecycle is domain.PayOutStatus (7 values). PayOut uses a status domain separate from PayIn. The full table with the V1 codes is in the status reference.

V2 statusFinal?Description
INITIALIZEDNoPayout created, balance on hold, waiting for matching.
MATCHEDNoA trader/team has been assigned. The creation-response status when routed to a trader (a free executor exists).
UNASSIGNEDNoNo executor at creation time. The retry worker keeps matching; after the SLA expires — EXPIRED.
PROCESSINGNoTransfer in progress: for an API provider — the creation-response status (the payout was created at the provider synchronously); for a trader — after the assignment is confirmed.
COMPLETEDYes (success)The funds have been sent, the hold captured.
FAILEDYes (declined)Rejected (wrong payment details, cancellation). The hold returned to the merchant.
EXPIREDYes (timeout)Expired before the transfer completed (including by SLA from UNASSIGNED).

Special statuses

  • UNASSIGNED — at creation time, the cascade auto-matching found no free trader (all busy / limits exhausted / no requisites for that currency and amount). The deal is returned with this status in the POST /deals/payout response, after which the retry worker automatically retries matching: on success, the status moves to MATCHED → then the regular lifecycle; if the assignment SLA expires, the payout moves to the terminal EXPIRED and the hold is returned to the merchant. Not a terminal status. Manual assignment (admin override) remains available to the operator.
  • PayOut does not use the PayIn statuses (PAYMENT_NOTIFIED, PAYMENT_VERIFIED, APPEALED, SOFT_DISPUTED).

PayOut webhook events use the same names as PayIn: deal.completed (success) and deal.failed (declined, status: FAILED). NOT payout.*. For details, see Callbacks.


TRY / IBAN payouts (Havale)

For Turkey, payouts go as a bank transfer by IBAN (RequisiteType BANK_TRANSFER): the base requisite is target_requisite (the recipient's IBAN), with target_bank and requisite_details filled in addition.

New request fields

FieldTypeRequiredDescription
target_bankstringConditionalRecipient bank: a slug ("ziraat", "garanti", ...) or a name. Passed to the provider as the payout bank and used for intra-bank cascade matching. Mandatory when the bank is not derivable from the IBAN (see "Bank derivation" below); not passed for cards/SBP.
requisite_detailsmap<string,string>NoNamed details beyond target_requisite/target_name. Valid keys: iban, account_number, document_number, bank_name, phone_number. Unknown keys → 400 InvalidArgument with the list of valid ones. A missing/empty map = "no extra details".

Bank derivation

For a bank method (BANK_TRANSFER / Havale), the platform must know the recipient bank before the deal is created. The bank counts as derived when:

  1. a valid target_bank is passed (a slug from GET /merchant/banks), or
  2. the payment details contain a Turkish IBAN whose TCMB code (positions 5–9: TRxx BBBBB + account) resolves against the platform's bank reference — the bank is derived automatically, and passing target_bank is not needed.

If neither holds, the request is rejected immediately, before the deal is created and the escrow hold is taken:

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":3,\"message\":\"target_bank: bank not derivable from IBAN — pass target_bank (see GET /merchant/banks)\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

Inside the platform, the safety-net managed refusal no_bank_derived (status FAILED + webhook deal.failed + hold release) remains for the case where the bank reference changes between request validation and matching. For an integrator, the equivalent "200 → instantly FAILED" behavior is not a contract: the only expected response for an underivable bank is the 400 above.

The values of requisite_details are the merchant's own payment details (their recipient's data), so responses return them as an echo, without masking (unlike trader requisites in PayIn). In logs, the platform keeps only the set of keys, without the values.

Request example (TRY / Havale)

{
  "amount": 2500000,
  "currency": "TRY",
  "wallet_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
  "target_requisite": "TR33 0006 1005 1978 6457 8413 26",
  "target_name": "Ahmet Yilmaz",
  "target_bank": "ziraat",
  "requisite_details": {
    "iban": "TR33 0006 1005 1978 6457 8413 26",
    "document_number": "12345678901"
  },
  "payment_method": "express-havale",
  "idempotency_key": "payout_try_order_00042"
}
  • amount: 2500000 = 25,000.00 TRY (minor units are kuruş, 2 decimal places).
  • payment_method — the slug (or UUID) of the Havale method created for your tenant (for example, express-havale); the value is resolved against your tenant's active methods — an unknown reference or a method of another currency is rejected with 400 (the list of your tenant's methods and slugs — GET /api/v1/p2p/merchant/payment-methods, see Banks).
  • requisite_details.document_number — an example of an extra detail (TC kimlik / document number), not always required: pass it when the method or the recipient bank requires it.

Response example (200 OK)

{
  "deal": {
    "deal_id": "c25f9b31-7e40-4a11-8f0a-6d92b7c4e155",
    "status": "MATCHED",
    "amount": "2500000",
    "currency": "TRY",
    "created_at": "2026-08-18T11:02:00Z",
    "client_id": "",
    "deal_type": "PAYOUT",
    "updated_at": "2026-08-18T11:02:00Z",
    "payment_url": "",
    "target_requisite": "TR33 0006 1005 1978 6457 8413 26",
    "received_amount": "0",
    "amount_match_status": ""
  }
}

target_requisite — an echo from the request, without masking (see the NOTE above).


Single payout limits

Amount limits are not hardcoded — they are determined dynamically by the tenant configuration:

Limit sourceManaged byDescription
Cascade rules (cascade_rules.max_amount)Admin panelThe maximum amount for a specific routing cascade.
Trader requisites (requisites.max_amount)Trader / AdminThe per-transaction limit for a specific requisite.
Tenant config (system_config)SuperadminGlobal auto-approval limits.

Limit values can be changed by an administrator in real time without restarting the services. When you need to send an amount above the limit in a single transaction, split it into several requests with unique idempotency_key values.


Webhook events

EventPayout statusDescription
deal.createdINITIALIZED / MATCHED / UNASSIGNED / PROCESSINGPayout created (the status is the one in the creation response after synchronous auto-matching; PROCESSING — routed to an API provider).
deal.completedCOMPLETEDPayout completed
deal.failedFAILEDPayout declined
deal.expiredEXPIREDPayout expired before the transfer completed (including by SLA from UNASSIGNED); the hold returned

IBAN validation

If the payout method accepts an IBAN (requisite type BANK_TRANSFER), the recipient goes through an ISO 13616 checksum check: a mistyped requisite is rejected immediately (400, with the IBAN masked in the error message); spaces and letter case are normalized before the check. Card and phone methods require no validation.

On this page