# AETERNA API Documentation

This document mirrors the in-app API documentation page (`AeternaWebApp/Components/Pages/Docs.razor`). It must be kept in sync with that page and with the Russian version (`API_Documentation_RU.md`).

## Table of Contents

### Part 1. Merchant Integration

1. [Introduction & Authentication](#introduction--authentication)
2. [PayIn Orders](#payin-orders)
3. [PayOut Orders](#payout-orders)
4. [Cross-Border PayIn (3rd Country)](#cross-border-payin-3rd-country)
5. [Order Statuses](#order-statuses)
6. [Crypto Withdrawals](#crypto-withdrawals)
7. [Get Balance](#get-balance)
8. [Chat & Disputes](#chat--disputes)
9. [Reference: Payment Methods](#reference-payment-methods)
10. [Reference: Exchange Rates](#reference-exchange-rates)
11. [Callbacks](#callbacks)

### Part 2. Payment Orchestrator (PayIn)

1. [Payment Orchestrator — Introduction / RSA](#payment-orchestrator--introduction--rsa)
2. [Orchestrator Integration](#orchestrator-integration)
3. [CreateOrder (PayIn)](#createorder-payin)
4. [ApprovePayInOrder](#approvepayinorder)
5. [GetOrderStatus](#getorderstatus)
6. [CancelOrder (PayIn)](#cancelorder-payin)
7. [CreateDispute](#createdispute)
8. [Webhooks & Errors (PayIn)](#webhooks--errors-payin)

### Part 3. Payment Orchestrator (PayOut)

1. [PayOut Auction](#payout-auction)
2. [CreatePayoutOrder](#createpayoutorder)
3. [CancelPayoutOrder](#cancelpayoutorder)
4. [ApprovePayoutOrder](#approvepayoutorder)
5. [PayOut Callbacks](#payout-callbacks)

---

# Part 1. Merchant Integration

## Introduction & Authentication

Welcome to the API documentation for merchant integration. Our API allows you to automate the creation of deposit (PayIn) and withdrawal (PayOut) requests, retrieve statistics, and manage your balance and disputes.

### Authentication & Global Requirements

All API endpoints are accessible via the base URL of our service (e.g., `https://{host}/`). Every POST request to the API (except for public methods) must meet the following requirements:

- **Method:** `POST`
- **Content-Type:** `application/json`
- **Accept:** `application/json, text/plain`

Additionally, each request must contain the following mandatory authentication headers:

- `X-App-Token` — The merchant's public token (PublicToken).
- `X-App-Access-Ts` — Current Unix Timestamp (in seconds). The lifetime of the timestamp is limited.
- `X-App-Access-Sig` — Digital signature of the request in HMAC-SHA256 format (lowercase result).

### Creating the Signature (X-App-Access-Sig)

To create the digital signature, use your secret key (`SecretToken`) and the email associated with your merchant account. *Note: The timestamp and email strings must be concatenated directly, without any spaces or separators.*

```plaintext
signStr = "{X-App-Access-Ts}{Email}"
X-App-Access-Sig = hex(HMAC_SHA256(signStr, SecretToken))
```

**C# Example:**

```csharp
using System.Security.Cryptography;
using System.Text;

string time = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
string signStr = $"{time}{email}";

using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretToken));
byte[] hashValue = hmac.ComputeHash(Encoding.UTF8.GetBytes(signStr));
string signature = BitConverter.ToString(hashValue).Replace("-", "").ToLower();
```

**Python Example:**

```python
import hmac
import hashlib
import time

unix_time = str(int(time.time()))
sign_str = f"{unix_time}{email}"

signature = hmac.new(
    secret_token.encode('utf-8'),
    sign_str.encode('utf-8'),
    hashlib.sha256
).hexdigest()
```

**Java Example:**

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.time.Instant;

String time = String.valueOf(Instant.now().getEpochSecond());
String signStr = time + email;

Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(secretToken.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] hashValue = sha256_HMAC.doFinal(signStr.getBytes("UTF-8"));

StringBuilder signature = new StringBuilder();
for (byte b : hashValue) {
    signature.append(String.format("%02x", b));
}
String result = signature.toString();
```

### IP allowlist

If your account has the external API IP filter turned on, a request from an address that is not on the list is rejected before the operation runs. The HTTP status is `403`. The body uses the usual error shape and the text is exactly `IP Not Allowed`. An address allowed for cabinet sign-in does not allow the API, and the other way around. Public catalogs `GET /api/GetRates` and `GET /api/PaymentMethods` do not identify a merchant and are not covered by this filter. You edit the address list and turn the filter on and off yourself on the IP access page of the merchant cabinet. The filter can be turned on only when the list has at least one address. Actions you take inside the cabinet are not calls from your systems and are not limited by this list.

### Webhook Authentication

To verify the authenticity of incoming callbacks to your server, we send a signature in the `X-App-Access-Sig` header.

> **Important — different token!** Callback signatures are computed with a **separate** token — `CallBackToken` — not the `SecretToken` you use to sign your own outgoing requests to our API. The two are independent fields of your merchant profile and are typically different values. Using `SecretToken` to verify a callback will always fail.

**Verification Algorithm:**

```plaintext
signStr = raw UTF-8 bytes of the HTTP request body (compact JSON, no whitespace)
X-App-Access-Sig = hex(HMAC_SHA256(signStr, CallBackToken))   // lowercase hex
```

*Warning:* Compute HMAC over the **raw request body bytes** as received, **before** any JSON deserialization. Re-serializing the parsed object will change whitespace, key order or escape casing and the signature will diverge.

**No signature header?** If your merchant profile has an empty `CallBackToken`, the system sends the callback **without** the `X-App-Access-Sig` header at all (the header is omitted, not empty). Either accept unsigned callbacks (if agreed with us) or reject them; in the latter case request a `CallBackToken` to be set on your account.

**Comparison:** Use a constant-time comparison (e.g., `crypto.timingSafeEqual` in Node.js, `hmac.compare_digest` in Python, `CryptographicOperations.FixedTimeEquals` in .NET) and compare in lowercase.

---

## PayIn Orders

Endpoints for processing incoming payments (deposits) from users. The system validates limits and checks for available traders and details. Upon success, a trader is assigned and the order waits for the user to make a payment.

**Lifecycle Workflow:** After the user makes the payment, the trader verifies the receipt of funds and completes the order manually (or the system handles it automatically for most cases). If the order is not completed within its lifetime, it gets canceled by timeout. In case of delays, you can communicate with the trader via the Chat endpoints or open a Dispute.

### POST /api/v3/PayInOrder

Creates a new PayIn order. The system validates limits, provides suitable payment details (card, phone number, etc.), and returns a payment link or details for the client.

**Optional Header:** `X-Request-Timeout-Ms` (int) — Maximum time in milliseconds to wait for order creation (default: 5000).

#### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `amount` (Req.) | Number | Deposit amount in fiat currency. |
| `currency` (Req.) | String | Three-letter currency code (e.g., "KZT", "RUB", "AZN"). |
| `payment_method` (Req.) | String | Payment method string code (e.g., "card_number"). |
| `bank` (Opt.) | String | Optional preferred-bank filter for matching details. Must match a bank name registered for the requested currency. Unknown name → creation fails with `Bank not found`. Omit the field to let the system pick any available requisite. This is not the bank returned in `requisites` — see [PayIn requisites.bank_name](#payin-requisitesbank_name--bank-in-the-create-deposit-response). |
| `payer_id` (Opt.) | String | Unique ID of the payer in your system. |
| `payer_name` (Opt.) | String | Free-form payer name (required in some geos). |
| `payer_ip` (Opt.) | String | Payer IP address (required in some geos). |
| `incoming_id` (Opt.) | String | Your internal transaction ID to prevent duplicate charges. |
| `ttl` (Opt.) | Integer | Order lifetime in minutes (overrides merchant settings). |
| `back_link` (Opt.) | String | URL to redirect the payer to after the payment is finished (success / fail). |
| `callback_url` (Opt.) | String | URL to receive status change webhooks for this specific order. |
| `comment` (Opt.) | String | Optional free-form note stored on the order. This is **not** the issued payment details. The details the payer must use are returned in `requisites`. |
| `iterative_sum_search_enabled` (Opt.) | Boolean | Enables searching for a unique amount by slightly altering it to avoid duplicate active orders. |
| `extra` (Opt.) | Object | Additional details. Can contain `card_details` and `client_details` when you collect card data on your side, or `customer_details` if your integration already keeps the payer data there (see below). |

#### Card Data Collected on Your Side

If you collect card data yourself, send it in `extra.card_details` along with `extra.client_details` and use `payment_method = "c2becom"`. The card is charged during the request, so the payer is not sent to a payment page. Both blocks are optional: without `card_details` the order is processed as usual and you receive a `payment_link`. Collecting card data on your side requires PCI DSS compliance.

| `card_details` | Type | Description |
|----------------|------|-------------|
| `card_number` (Req.) | String | Card number, 16–19 digits. Spaces and dashes are ignored. |
| `cardholder` (Opt.) | String | Cardholder name as printed on the card. |
| `expmo` (Req.) | String | Expiry month, 2 digits (e.g., "01"). |
| `expyr` (Req.) | String | Expiry year, 4 digits (e.g., "2030"). |
| `cvv` (Req.) | String | Card security code (CVV2 / CVC2), 3–4 digits. |

Send the block in full: if the card number, expiry date or security code is missing, the order is declined — it never silently falls back to a payment page.

| `client_details` | Type | Description |
|------------------|------|-------------|
| `ip` (Req.) | String | Payer IP address (v4 or v6). Required when card data is sent. If omitted, the root-level `payer_ip` is used. |
| `ip_country` (Opt.) | String | Country of the IP address, ISO 3166-1 alpha-2. Some providers require a country when charging the card and decline the payment without one, so send this field whenever you know it. If omitted, `country` is used instead. |
| `email`, `phone` (Opt.) | String | Payer contacts. Fall back to `extra.user_email` and `extra.user_phone`. |
| `first_name`, `last_name` (Opt.) | String | Payer name. Falls back to `extra.user_name`. |
| `country` (Opt.) | String | Payer country, ISO 3166-1 alpha-2. |
| `date_of_birth` (Opt.) | String | Payer date of birth, `YYYY-MM-DD`. `DD.MM.YYYY` and `DD/MM/YYYY` are accepted too; a date we cannot read is not passed on. |
| `billing_address` (Opt.) | Object | Cardholder billing address. |
| `browser` (Req.) | Object | Payer browser parameters used by 3D Secure. Required in practice: without them the provider declines the charge (see below). |

| `client_details.billing_address` | Type | Description |
|----------------------------------|------|-------------|
| `country` | String | Billing country, ISO 3166-1 alpha-2. The key field of the block: some providers decline the charge when the request carries no billing country. If omitted, `country` and then `ip_country` of the payer is used instead. |
| `address_line1` | String | Street and building number. |
| `address_line2` | String | Apartment, office or an extra address line. |
| `city` | String | City. |
| `postal_code` | String | Postal code. |

Send the block field by field: whatever you do not collect can be omitted. Without the country the block is not forwarded to the provider at all — the remaining fields are useless without it.

| `client_details.browser` | Type | Source on your page |
|--------------------------|------|---------------------|
| `user_agent` | String | `navigator.userAgent` |
| `language` | String | `navigator.language` |
| `timezone_offset` | Integer | `new Date().getTimezoneOffset()`, minutes |
| `window_width`, `window_height` | Integer | `document.body.clientWidth` / `clientHeight` |
| `screen_width`, `screen_height` | Integer | `screen.width` / `screen.height` |
| `screen_color_depth` | Integer | `screen.colorDepth` |
| `java_enabled` | Boolean | `navigator.javaEnabled()` |

**Send the whole block.** Formally these values are optional, but a charge without them is declined: the provider has nothing to run 3-D Secure with and answers a generic error that looks like a malformed request. A partial block does not help either — we verified that a request carrying only `user_agent`, and a request without `window_width` / `window_height`, are both declined, while the same request with every value above is accepted. Only `java_enabled` may be omitted. The values must come from the payer's real browser, not be hard-coded.

Once the charge is accepted, the response contains `requisites.card_input_link` — show that link to the payer (redirect or iframe); the final order status arrives via callback.

```json
{
  "amount": 5000,
  "currency": "KZT",
  "payment_method": "c2becom",
  "payer_id": "client-42",
  "payer_ip": "185.192.23.117",
  "incoming_id": "merchant-order-123",
  "callback_url": "https://merchant.example/api/callback",
  "extra": {
    "card_details": {
      "card_number": "4111111111111111",
      "cardholder": "IVAN IVANOV",
      "expmo": "01",
      "expyr": "2030",
      "cvv": "123"
    },
    "client_details": {
      "ip": "185.192.23.117",
      "ip_country": "KZ",
      "email": "payer@example.com",
      "phone": "+77001234567",
      "first_name": "Ivan",
      "last_name": "Ivanov",
      "country": "KZ",
      "date_of_birth": "1990-02-01",
      "billing_address": {
        "country": "KZ",
        "address_line1": "Abay Avenue 10",
        "address_line2": "Apt 5",
        "city": "Almaty",
        "postal_code": "050000"
      },
      "browser": {
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
        "language": "en-GB",
        "timezone_offset": -120,
        "window_width": 1642,
        "window_height": 883,
        "screen_width": 1710,
        "screen_height": 1107,
        "screen_color_depth": 30,
        "java_enabled": false
      }
    }
  }
}
```

Card data is not stored: the card number is masked to the first six and last four digits in our request journal, and the security code is not stored at all.

If your integration already carries payer data in `extra.customer_details`, you do not have to move it: we read that block as well and use it wherever `client_details` leaves a gap. A field filled in `client_details` always wins. The same applies inside `billing_address`: if that object is present but `address_line1` or `postal_code` is empty, we fill them from `card_street` / `card_post_code`.

| `customer_details` | Fills in |
|--------------------|----------|
| `date_of_birth` | `client_details.date_of_birth` |
| `country_of_residence` | `client_details.country` |
| `card_country_code` | `billing_address.country` |
| `card_street` | `billing_address.address_line1` |
| `card_city` | `billing_address.city` |
| `card_post_code` | `billing_address.postal_code` |
| `email`, `phone`, `first_name`, `last_name` | Payer contacts and name |

This block carries no browser fingerprint, so `client_details.browser` still has to be sent — it is the one part of the payer data that has no alternative source.

#### Card Channel Specifics

The card channel works differently from the trader flow: the charge is decided by the provider, and its verdict depends on how complete the payer data is. Go through these rules before you switch the channel on in production.

- **A missing `browser` block is a decline on its own.** Of everything the payer block carries, this one is decisive: with the fingerprint the charge goes through to 3-D Secure, without it the provider answers a generic error. We established this by changing one condition at a time — amount, payer country, IP, card expiry date, cardholder name, billing address and date of birth all left the outcome unchanged, and only the fingerprint flipped it. If card charges stop working, check this block first.
- **Send the rest of the payer block in full too.** Besides `card_details` the provider checks who is paying: `date_of_birth`, the whole `billing_address`, `ip` together with `ip_country`, `email`, `phone`, `first_name`, `last_name`. When something is missing, the decline comes back as a generic code with no readable reason, and repeating the same request does not help — the data is incomplete, the format is not wrong.
- **Keep the countries consistent.** The country of the IP address, of the billing address and of the card issuer are compared against each other, and a mismatch is a decline reason on its own even when every field is filled in.
- **The payer country must belong to the channel.** Which countries a channel accepts depends on the settlement currency: a channel settling in EUR or USD is normally set up for cards issued in Europe. For a country outside the channel the provider answers that no terminal was found — that is routing on the provider side, so changing the request body will not help. Ask your account manager which countries and card brands your channel covers.
- **Test cards only work against the test environment.** On live credentials a test card is declined as a request validation error; the wording is misleading, because the request is well formed and it is the card that is not accepted.
- **Send the payer to `card_input_link` as soon as it arrives.** When the card requires 3-D Secure, the provider holds the payment until that redirect happens and cancels it by timeout if nobody follows the link. The order then ends as cancelled even though the request was accepted, so the redirect is a required step rather than a fallback.
- **The charge may be accepted asynchronously.** The provider can take the payment for processing and report the outcome later, so the absence of an immediate success is not a decline. Treat the callback as the final status.
- **Send the phone number in international format**, with the country code. We reformat it for the provider, but a number without a country code can be rejected.

#### Testing the Card Channel

Card charges are tested on the test environment only. Ask your account manager for its base URL and a separate app token: production credentials do not work there, and test card numbers sent to production are declined as a request validation error.

Build the test request exactly as the production one — the same full payer block, a payer country the channel accepts, and a small amount. Any expiry date in the future and any three-digit security code will do. The numbers below are the provider's published test cards; the outcome column is what the channel actually returns.

| Card number | What happens |
|-------------|--------------|
| `4000 0000 0000 0408` | The charge succeeds. Use this number to verify the whole flow through to the callback. |
| `4000 0000 0000 0002` | The charge is declined by the acquirer even after successful authentication. Use it to verify how your side handles a decline. |
| `4242 4242 4242 4242` | Published as failed authentication. |
| `4000 0000 0000 0416` | Published as declined authorisation. |

A successful test run looks like this:

1. Create the order with `card_details` and the full `client_details` block, `browser` included.
2. Take `requisites.card_input_link` from the response and open it the way the payer would. Expect the link on every test card, including those published as not requiring 3-D Secure.
3. A test bank authentication page opens. Choose the successful result and submit it.
4. Within a couple of minutes the callback arrives with the final status and the order is completed.

If nobody follows the link, the payment is cancelled by timeout in about twenty minutes and the order ends as cancelled — exactly as it would in production. A decline right after the order is created, without any redirect, means either that the `browser` block is missing or incomplete, or that the card number itself is not accepted.

#### Going Live with the Card Channel

1. Confirm with your account manager which currencies, payer countries and card brands your channel covers, and get production credentials for it.
2. Switch to real cards: test numbers are declined on production credentials, and the decline text mentions request validation even though the request is correct.
3. Keep sending the payer block in full, browser fingerprint included — the same fields you used on the test environment.
4. Show `card_input_link` to the payer immediately. An order left without that redirect is cancelled by timeout, not declined.
5. Treat the callback as the final status, and start with one small real charge before you route live traffic.

#### Successful Response Example

```json
{
  "result": true,
  "order_id": 123456,
  "initial_amount": 5000,
  "usdt_amount": 52.3,
  "rate": 95.6,
  "amount": 5000,
  "life_time": "2026-04-09T12:00:00Z",
  "requisites": {
    "bank_id": 12,
    "bank_name": "Sberbank",
    "name": "Ivan Ivanov",
    "payment_link": "https://payment.example.com/pay/...",
    "card_input_link": "https://payment.example.com/card-input/...",
    "card": "4276********1234",
    "holder": "IVAN IVANOV",
    "phone_number": null,
    "iban": null,
    "account_number": null,
    "transfer_info": "V2345445",
    "data_for_qr": null
  }
}
```

The card example above is for a card transfer. Do not reuse it as the shape of an `account_number` (bank-account) response — see the next subsection.

#### PayIn `requisites.bank_name` — bank in the create-deposit response

Search terms: `bank`, `requisites.bank_name`, `requisites.bank_id`, bank in the PayIn response.

There are two different `bank` values on PayIn. Do not explain one as the other.

- **Request field `bank`** — optional preferred-bank filter for *selecting* details. The name must exist in the bank registry *for that currency*. A mismatch returns HTTP 400 with `Bank not found`. If you omit the field, the system chooses an available requisite and returns *that* bank in `requisites`.
- **Response fields `requisites.bank_name` and `requisites.bank_id`** — the bank of the **issued** payment details. It is already bound to the selected requisite. It is **not** derived from the account or card number.

When `payment_method` is `account_number`, a successful `requisites` block has a filled `account_number` and `bank_name`; card fields are `null` (not the other way around):

```json
{
  "result": true,
  "order_id": 123456,
  "initial_amount": 150000,
  "usdt_amount": 110.29,
  "rate": 1360.0,
  "amount": 150000,
  "life_time": "2026-04-09T12:00:00Z",
  "requisites": {
    "bank_id": 44,
    "bank_name": "IBK",
    "name": "HONG GILDONG",
    "payment_link": null,
    "card_input_link": null,
    "card": null,
    "holder": null,
    "phone_number": null,
    "iban": null,
    "account_number": "012345678901",
    "transfer_info": null,
    "data_for_qr": null
  }
}
```

#### Common Errors

- **The order does not meet the limits** — Amount is outside allowed boundaries.
- **The payer is blocked** — `payer_id` or `payer_name` matches the payer block list: the platform-wide list or entries for your account, for example after a dispute that ended with the Fake receipt decision. To lift the block, contact your manager.
- **Bank not found** / **Currency not found** — the request `bank` is not in the registry for this currency, or the currency code is unknown.
- **The system could not find payment details, please try again later** — No free payment details available.
- **It is not possible to assign details due to the large number of duplicate amounts** — Could not find a unique amount (if iterative sum search was disabled or failed).

### POST /api/v3/CancelPayInOrder

Cancels an existing PayIn order. An order can only be canceled if it is awaiting payment (Status = 1).

#### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `order_id` (Opt.) | Number | Internal order ID in Aeterna. |
| `incoming_id` (Opt.) | String | Your external transaction ID (if order_id is not provided). |

#### Response Example

```json
{
  "result": true,
  "message": "Order canceled"
}
```

### POST /api/GetOrderStatus

Returns the current status of a PayIn order. At least one of `order_id` / `incoming_id` must be provided. If both are sent, the lookup uses `order_id`.

HTTP status is `200` for both successful and failed responses. Authentication is the same legacy HMAC scheme as the other `/api/Get*` methods (`X-App-Token`, `X-App-Access-Ts`, `X-App-Access-Sig`).

This response has no `status_id_num` — that field exists only on the callback payload. The status string is the dictionary name (e.g. `completed`), not `paid` or `success`.

#### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `order_id` (Opt.) | Number (Long) | Internal order ID. At least one of `order_id` / `incoming_id` must be provided. |
| `incoming_id` (Opt.) | String | Your external order ID (used if `order_id` is not provided). |

#### Successful Response Example

```json
{
  "result": true,
  "order_id": 33,
  "incoming_id": "PIN-1",

  "amount_in": 100,
  "currency_in": "BDT",
  "amount_out": 0.8,
  "currency_out": "USDT",
  "rate": 125.54,
  "comission": 9,
  "status_id": 6,
  "status": "completed",

  "direction": "PayIn",
  "fiat_amount": 100,
  "fiat_currency": "BDT",
  "usdt_amount": 0.8,
  "usdt_currency": "USDT",
  "commission": 9
}
```

**Compatibility:** legacy fields (`amount_in`, `currency_in`, `amount_out`, `currency_out`, `comission`) are preserved unchanged. **For new integrations**, prefer the canonical block (`fiat_amount`, `fiat_currency`, `usdt_amount`, `usdt_currency`, `direction`, `commission`).

#### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `result` | Boolean | `true` when the order is found. |
| `order_id` | Number (Long) | Internal order ID. |
| `incoming_id` | String | Your external transaction ID. |
| `amount_in` | Number (Double) | Legacy. Fiat amount of the order. |
| `currency_in` | String | Legacy. ISO code of the fiat currency the payer pays. |
| `amount_out` | Number (Double) | Legacy. Order amount in USDT. |
| `currency_out` | String | Legacy. ISO code of the outgoing currency (looked up by the order). |
| `rate` | Number (Double) | Exchange rate of the order, rounded to 6 decimal places. |
| `comission` | Number (Double) | Legacy. Merchant commission percent (historical spelling with one `m`). |
| `status_id` | Number (Integer) | Numeric status ID. See [Order Statuses](#order-statuses). |
| `status` | String | System status name for that ID (first value of the status dictionary), e.g. `"completed"`, `"trader_assingned"`. |
| `direction` | String | Canonical. Always `"PayIn"` on this endpoint. |
| `fiat_amount` | Number (Double) | Canonical. Same value as `amount_in`. |
| `fiat_currency` | String | Canonical. Same value as `currency_in`. |
| `usdt_amount` | Number (Double) | Canonical. Same value as `amount_out`. |
| `usdt_currency` | String | Canonical. Always `"USDT"`. |
| `commission` | Number (Double) | Canonical. Same value as `comission`. |

#### Error Response

HTTP `200`. The body is a JSON object with `result: false`.

```json
{
  "result": false,
  "message": "Order not found"
}
```

#### Error messages

The `message` field is one of the following strings, or the exception text for an unexpected failure:

- `Request parameters cannot be empty!` — request body is empty.
- `Merchandiser not registered!` — no merchant for `X-App-Token`.
- `Merchandiser are blocked!` — the merchant is locked.
- `The signature is not valid!` — HMAC signature does not match.
- `User not found!` — no user account for the merchant.
- `Order number can not be null` — both `order_id` and `incoming_id` are missing.
- `Order not found` — no PayIn order of this merchant matches the identifier.
- `Headers not a valid!` — request headers are missing.

### POST /api/GetPayInOrders

Retrieves a list of PayIn orders filtered by dates and status. Returns data in JSON or CSV format (tab-separated).

#### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `date_from` (Req.) | String | Start date for the query (format: `"dd.MM.yyyy"`). |
| `date_to` (Req.) | String | End date for the query (format: `"dd.MM.yyyy"`). |
| `status_id` (Opt.) | Integer/String | Status ID for filtering. Pass `0` to retrieve all orders regardless of status. |
| `format` (Req.) | String | Response format: `"json"` or `"csv"`. |

HTTP status is `200` for both successful and failed responses. A successful `format=json` response is a **JSON array** (not wrapped in `result`), `Content-Type: application/json`. A successful `format=csv` response is plain text with tab-separated columns, `Content-Type: text/plain`. An error is a JSON object `{ "result": false, "message": "..." }`.

#### Successful Response Example (format=json)

```json
[
  {
    "id": 1234567,
    "incoming_id": "merchant-order-123",
    "created_at": "01.04.2026 12:34:56",
    "status_id": 6,
    "status": "completed",
    "currency_code": "RUB",
    "amount": 1500,
    "amount_usdt": 16.45,
    "rate": 91.32,
    "comission": 1.5,
    "comission_amount": 0.25
  }
]
```

If no orders match the filter, the body is an empty array: `[]`.

#### Successful Response Example (format=csv)

Plain text, tab-separated. The first line is the header. The commission-percent column is named `comission_rate` in the header; the same value is `comission` in JSON.

```
id	incoming_id	created_at	status_id	status	currency_code	amount	amount_usdt	rate	comission_rate	comission_amount
1234567	merchant-order-123	01.04.2026 12:34:56	6	completed	RUB	1500	16.45	91.32	1.5	0.25
```

If no orders match the filter, the body is the header row only.

#### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | Number (Long) | Internal order ID. |
| `incoming_id` | String | Your external transaction ID. Empty string if it was not set. |
| `created_at` | String | Order creation time in the format `dd.MM.yyyy HH:mm:ss`. |
| `status_id` | Number (Integer) | Numeric status ID. See [Order Statuses](#order-statuses). |
| `status` | String | System status name for that ID (first value of the status dictionary), e.g. `"completed"`, `"trader_assingned"`. |
| `currency_code` | String | ISO code of the fiat currency the payer pays. |
| `amount` | Number (Double) | Order amount in that fiat currency. |
| `amount_usdt` | Number (Double) | Order amount in USDT. |
| `rate` | Number (Double) | Exchange rate of the order, rounded to 6 decimal places. |
| `comission` | Number (Double) | Merchant commission percent. In CSV the header for this column is `comission_rate`. |
| `comission_amount` | Number (Double) | Commission in USDT: `amount_usdt / 100 * comission`, rounded to 2 decimal places. |

#### Error Response

HTTP `200`. The body is a JSON object, not an array.

```json
{
  "result": false,
  "message": "The signature is not valid!"
}
```

#### Error messages

The `message` field is one of the following strings, or the exception text for an unexpected failure:

- `Merchandiser not registered!` — no merchant for `X-App-Token`.
- `Merchandiser are blocked!` — the merchant is locked.
- `The signature is not valid!` — HMAC signature does not match.
- `User not found!` — no user account for the merchant email.
- `The order number does not exist in the system, please indicate the correct status id.` — `status_id` is not `0` and is not a known status.
- ``date_from or date_to can`t be empty`` — a date is missing or cannot be parsed.
- `Format not response` — `format` is neither `json` nor `csv` (comparison is case-insensitive).
- `Headers not a valid` — request headers are missing.
- `Request parameters cannot be empty!` — request body is empty.

---

## PayOut Orders

Withdrawal orders (outgoing payments to the merchant's clients). The merchant spends USDT/crypto from their balance, and the system executes a fiat payout to the client's provided details.

**Lifecycle Workflow:** When you create a PayOut order, it awaits acceptance by a trader. Once a trader claims it, they will send the fiat funds to your specified details. After the trader marks the transfer as done, the system finalizes the order automatically. If there is an issue, you can open a Dispute.

### POST /api/v3/PayoutOrder

Creates a new payout request. The system verifies sufficient balance considering commissions and rate markups.

#### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `amount` (Req.) | Number | The amount in major units of the fiat currency (not cents / not the smallest currency unit). |
| `currency` (Req.) | String | Fiat currency code (e.g., "RUB", "KZT", "KRW"). |
| `payment_method` (Req.) | String | Code of the payment method (see list below). Which codes are enabled for a currency is a merchant setting — not the full catalog from `GET /api/PaymentMethods`. |
| `incoming_id` (Req.) | String | Your external unique transaction ID (for idempotency). If omitted or empty, the order is still created. If set, it must be unique for this merchant. |
| `comment` (Req., nullable) | String | Payment requisites in free-form text, stored as-is. A local trader sees this text as the requisites line. How an external partner reads it depends on the execution path — see [KRW payout](#krw-payout--where-to-put-recipient-details). The field is required in the request, but its value can be `null`. |
| `bank` (Opt.) | String | Optional bank name used for **rate routing**, not the recipient's bank details. An unrecognized name does **not** reject creation (unlike PayIn). The recipient bank is **not** inferred from the account number. See [KRW payout](#krw-payout--where-to-put-recipient-details). |
| `payer_id` (Opt.) | String | Stable end-user (recipient) identifier on the merchant side. Used for anti-fraud and linking repeated payouts to the same client. This is not the account / card number. |
| `payer_name` (Opt.) | String | Free-form recipient name, stored on the order. A local trader does **not** see this field on the requisites line — put the name in `comment` if the trader must read it. |
| `back_link` (Opt.) | String | Accepted on the request but not used for ordinary `account_number` / `card_number` payouts. A PayOut does not open a hosted payment page. |
| `callback_url` (Opt.) | String | Webhook URL for this specific order. |
| `ttl` (Opt.) | Integer | Order lifetime in minutes before auto-cancellation. |
| `iterative_sum_search_enabled` (Opt.) | Boolean | Enables iterative amount search to pick available requisites (used in specific scenarios). Default: `false`. |
| `extra` (Opt.) | Object | Geo-/method-specific additional fields (e.g., `user_phone`, `user_email`, `user_name`, `pok_user_cnic` for Pakistan, `bdt_user_account_no` for Bangladesh). For a KRW cascade partner `extra.user_name` is the recipient name the partner requires — a local trader does not see it on the requisites line. |

**Supported Payment Methods (`PaymentMethod`):**

- `card_number` — Transfer by bank card number (Visa, MasterCard, etc.).
- `phone_number` — Transfer by phone number (mobile transfers, SBP).
- `account_number` — Transfer to checking account.
- `iban` — IBAN transfer.
- `sepa_transfer` — SEPA transfer (Europe).
- `sbp` — Faster Payments System (SBP) for Russia.
- `fawry` — Fawry (Egypt).
- `upi` — UPI (India).
- `imps` — IMPS (India).
- `pago_movil` — PagoMovil (Venezuela).
- `qr_code` — Transfer via QR code.
- `jazz_cash` — JazzCash (Pakistan).
- `easypaisa` — Easypaisa (Pakistan).
- `quasi_ecom` — Card number payment with 3D-Secure confirmation.
- `bkash` — Bkash (Bangladesh).
- `nagad` — Nagad (Bangladesh).
- `c2becom` — Bank acquiring.
- `paypal` — PayPal e-wallet.

#### Successful Response Example

```json
{
  "result": true,
  "order_id": 987654,
  "current_rate": 95.5
}
```

The create response is only these three fields. There is no `payment_link`, `card_input_link`, or redirect for a regular PayOut.

#### KRW payout — where to put recipient details

Search terms: `KRW`, South Korea, KRW payout, `comment`, recipient bank, `account_number`.

A KRW PayOut does **not** open a hosted payment page. There is no redirect. `payment_link`, `card_input_link`, and `back_link` belong to other methods / PayIn — not to a regular KRW `account_number` payout.

`amount` is in major currency units (not cents). `ttl` is minutes, not seconds.

The API does not compute the recipient bank from the account number. An account number alone is not enough to guarantee execution.

How recipient data is read depends on who executes the order. Do not send one field layout for every path.

- **Local trader.** The trader cabinet shows the entire `comment` as the requisites line (plus the payment method and, if request `bank` matched the registry, that bank name). `payer_name` and `extra.user_name` are stored on the order but are **not** shown on that line. For a local KRW transfer the operator works from the text they see — put **bank + account number + recipient name** in `comment` on one line (free form, spaces allowed), for example `"IBK 012345678901 HONG GILDONG"`.
- **External payout partner (KRW cascade).** The partner takes the account number from the *first token* of `comment` (digits in the token before the first space), the recipient name from `extra.user_name` (required — a missing name fails at the partner), and the bank from request `bank` (must match the bank registry, otherwise the partner rejects). A string that starts with the bank code, such as `"IBK 012345678901 HONG GILDONG"`, fails here because the first token has no digits. Use `comment` = account number, `extra.user_name` = holder, `bank` = bank name.
- **External auction.** Best-effort parse of `comment`: the account / card / phone / IBAN is taken from any position; leftover tokens become bank-name candidates. A personal name in the same string may be treated as a bank candidate, not as the recipient name. Unrecognized parts do not block creation.

**API contract (all paths):** `comment` is stored as-is; request `bank` is rate routing and an unknown value does not reject creation; `payer_name` is stored on the order; `extra.user_name` is stored in the extra JSON and is required when the order is sent to the KRW cascade partner. Do not describe request `bank` as “the recipient bank” and do not put the recipient name only in `payer_name` / `extra.user_name` if a local trader must see it.

### POST /api/GetPayOutOrderStatus

Retrieves the current status of a specific payout order by its ID. At least one of `order_id` / `incoming_id` must be provided. If both are sent, the lookup uses `order_id`.

HTTP status is `200` for both successful and failed responses. This response has no `status_id_num` — that field exists only on the callback payload. For `status_id` 6 the string is `completed`, not `success` or `paid`.

#### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `order_id` (Opt.) | Number (Long) | Internal order ID in Aeterna. At least one of `order_id` / `incoming_id` must be provided. |
| `incoming_id` (Opt.) | String | Your external order ID (used if `order_id` is not provided). |

#### Response Example

```json
{
  "result": true,
  "order_id": 987654,
  "incoming_id": "ext-payout-123",

  // Legacy fields (preserved as-is for backward compatibility; for PayOut, in/out roles are mirrored)
  "amount_in": 104.71,
  "currency_in": "USDT",
  "amount_out": 10000.0,
  "currency_out": "RUB",
  "rate": 95.5,
  "comission": 1.5,
  "status_id": 6,
  "status": "completed",

  // Canonical fields (recommended for new integrations)
  "direction": "PayOut",
  "fiat_amount": 10000.0,
  "fiat_currency": "RUB",
  "usdt_amount": 104.71,
  "usdt_currency": "USDT",
  "commission": 1.5
}
```

**Compatibility:** legacy fields (`amount_in`, `currency_in`, `amount_out`, `currency_out`, `comission`) are preserved unchanged. **For new integrations**, prefer the canonical block (`fiat_amount`, `fiat_currency`, `usdt_amount`, `usdt_currency`, `direction`, `commission`) — it is symmetric across PayIn / PayOut and avoids the historical `in`/`out` swap.

### POST /api/GetPayOutOrders

Retrieves payout history. Returns a list of payouts in JSON or CSV format (tab-separated) for the specified period.

#### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `date_from` (Req.) | String | Start date for the query (format: `"dd.MM.yyyy"`). |
| `date_to` (Req.) | String | End date for the query (format: `"dd.MM.yyyy"`). |
| `status_id` (Opt.) | Integer/String | Status ID for filtering. Pass `0` to retrieve all orders regardless of status. |
| `format` (Req.) | String | Response format: `"json"` or `"csv"`. |

HTTP status is `200` for both successful and failed responses. A successful `format=json` response is a **JSON array** (not wrapped in `result`), `Content-Type: application/json`. A successful `format=csv` response is plain text with tab-separated columns, `Content-Type: text/plain`. An error is a JSON object `{ "result": false, "message": "..." }`.

#### Successful Response Example (format=json)

```json
[
  {
    "id": 987654,
    "incoming_id": "ext-payout-123",
    "created_at": "01.04.2026 12:34:56",
    "status_id": 6,
    "status": "completed",
    "currency_code": "RUB",
    "amount": 10000,
    "amount_usdt": 104.71,
    "rate": 95.5,
    "comission": 1.5,
    "comission_amount": 1.57
  }
]
```

If no orders match the filter, the body is an empty array: `[]`.

#### Successful Response Example (format=csv)

Plain text, tab-separated. The first line is the header. The commission-percent column is named `comission_rate` in the header; the same value is `comission` in JSON.

```
id	incoming_id	created_at	status_id	status	currency_code	amount	amount_usdt	rate	comission_rate	comission_amount
987654	ext-payout-123	01.04.2026 12:34:56	6	completed	RUB	10000	104.71	95.5	1.5	1.57
```

If no orders match the filter, the body is the header row only.

#### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | Number (Long) | Internal order ID. |
| `incoming_id` | String | Your external transaction ID. Empty string if it was not set. |
| `created_at` | String | Order creation time in the format `dd.MM.yyyy HH:mm:ss`. |
| `status_id` | Number (Integer) | Numeric status ID. See [Order Statuses](#order-statuses). |
| `status` | String | System status name for that ID (first value of the status dictionary), e.g. `"completed"`, `"trader_assingned"`. |
| `currency_code` | String | ISO code of the fiat currency of the payout. |
| `amount` | Number (Double) | Payout amount in that fiat currency. |
| `amount_usdt` | Number (Double) | Order amount in USDT. |
| `rate` | Number (Double) | Exchange rate of the order, rounded to 6 decimal places. |
| `comission` | Number (Double) | Merchant commission percent. In CSV the header for this column is `comission_rate`. |
| `comission_amount` | Number (Double) | Commission in USDT: `amount_usdt / 100 * comission`, rounded to 2 decimal places. |

#### Error Response

HTTP `200`. The body is a JSON object, not an array.

```json
{
  "result": false,
  "message": "The signature is not valid!"
}
```

#### Error messages

The `message` field is one of the following strings, or the exception text for an unexpected failure:

- `Merchandiser not registered!` — no merchant for `X-App-Token`.
- `Merchandiser are blocked!` — the merchant is locked.
- `The signature is not valid!` — HMAC signature does not match.
- `User not found!` — no user account for the merchant email.
- `The order number does not exist in the system, please indicate the correct status id.` — `status_id` is not `0` and is not a known status.
- ``date_from or date_to can`t be empty`` — a date is missing or cannot be parsed.
- `Format not response` — `format` is neither `json` nor `csv` (comparison is case-insensitive).
- `Headers not a valid` — request headers are missing.
- `Request parameters cannot be empty!` — request body is empty.

---

## Cross-Border PayIn (3rd Country)

A specialized PayIn flow for cross-border deposits. You define the amount in your **settlement currency** (`currency`); the payer pays in that **same currency**, and the funds are credited to the recipient in a **third-country currency** (`third_country_currency`) — for example, you create an order in KZT, the payer sends KZT via their bank's international transfer system, and the KZT is converted to UZS and credited to the recipient. The order is distributed **only to internal traders** whose third-country currency matches; **no external cascade or auction** is used.

**Lifecycle Workflow:** identical to a regular PayIn — a trader and payment details are assigned and the order waits for the payer. The expected amount credited to the recipient in the third-country currency is computed and stored on the order, and is later reconciled against the actual incoming payment by the matching system.

Authentication, trader/requisite selection and the response shape are **unified with** `/api/v3/PayInOrder`. The only additions are the `third_country_currency` field and the third-leg amount calculation.

### POST /api/NewOrderThirdCountry

Creates a cross-border PayIn order. The system validates limits, resolves exchange rates for both legs, selects a trader and payment details by the third-country currency, and returns the requisites / payment link for the payer.

**Optional Header:** `X-Request-Timeout-Ms` (int) — Maximum time in milliseconds to wait for order creation.

#### Currency Model

- `currency` — your settlement currency; `amount` is expressed in it and the payer pays in it (e.g. "KZT").
- `USDT` — what you receive after conversion (returned as `usdt_amount`).
- `third_country_currency` — the currency in which funds are credited to the recipient after the sender bank's international conversion (e.g. "UZS"); the trader is selected by a matching third-country currency.

Amounts: `usdt_amount = amount / rate` (rate = currency → USDT). The amount credited to the recipient = `amount × third_rate` (third_rate = third_country_currency → currency); both rates use a 24-hour freshness window.

#### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `currency` (Req.) | String | Your settlement currency code, in which `amount` is set and the payer pays (e.g. "KZT", "RUB"). |
| `third_country_currency` (Req.) | String | The currency credited to the recipient after the sender bank's international conversion (e.g. "UZS"). Traders and details are selected by this currency. |
| `amount` (Req.) | Number | Order amount in the settlement currency (`currency`). |
| `payment_method` (Req.) | String | Payment method string code (e.g. "card_number"). String, not a number. |
| `bank` (Opt.) | String | Preferred bank for matching details. Auto-selected if omitted. |
| `payer_id` (Opt.) | String | Unique ID of the payer in your system. |
| `payer_name` (Opt.) | String | Free-form payer name (required in some geos). |
| `incoming_id` (Opt.) | String | Your internal transaction ID to prevent duplicate orders (idempotency). |
| `ttl` (Opt.) | Integer | Order lifetime in minutes (overrides merchant settings). |
| `back_link` (Opt.) | String | URL to redirect the payer after the payment is finished. |
| `callback_url` (Opt.) | String | URL to receive status change webhooks for this order. |
| `comment` (Opt.) | String | Free-form comment for the order. |

#### Request Example

```json
{
  "currency": "KZT",
  "third_country_currency": "UZS",
  "amount": 50000,
  "payment_method": "card_number",
  "incoming_id": "merchant-order-555",
  "callback_url": "https://merchant.example/api/callback",
  "back_link": "https://merchant.example/payment/return"
}
```

#### Successful Response Example

```json
{
  "result": true,
  "order_id": 168,
  "initial_amount": 50000,
  "usdt_amount": 92.16,
  "rate": 542.56,
  "amount": 50000,
  "life_time": "2026-06-05T08:35:30Z",
  "requisites": {
    "bank_id": 171,
    "bank_name": "Humo telegram",
    "name": "Receiver Name",
    "payment_link": "https://payment.example.com/pay/...",
    "card": "7845 **** **** 1151",
    "phone_number": null,
    "account_number": null,
    "iban": null,
    "transfer_info": null
  }
}
```

**Note:** `amount` in the response is in your settlement currency (`currency`), consistent with v3 PayIn. The expected amount credited to the recipient in the third-country currency is stored on the order for downstream payment matching and is **not** returned here.

#### Common Errors

- **third_country_currency is required** — the third-country currency was not provided.
- **Currency not found** — invalid `currency` / `third_country_currency` code.
- **The order does not meet the limits** — amount is outside allowed boundaries.
- **The exchange rate is not available** — no fresh currency → USDT rate.
- **The exchange rate for the third currency is not available** — no fresh third_country_currency → currency rate.
- **There are currently no available traders…** — no trader matches the third-country currency / commission / balance.
- **…payment details cannot be provided by the trader…** — no suitable payment details available.
- **Order with this IncomingId already exists** — duplicate `incoming_id`.

---

## Order Statuses

Both PayIn and PayOut orders use the same system of internal statuses.

A successful order is `status_id = 6`, system name `completed` (not `success` or `paid`). Statuses `8`–`13` are cancellations. A cancelled or disputed order is not a credit.

A disputed order (`7`, `disputed`) — and, if an operator later confirms it, a cancelled order — can still move to `6`. That sends another callback. Treat the **latest** callback together with `POST /api/GetOrderStatus` (PayIn) or `POST /api/GetPayOutOrderStatus` (PayOut) as the source of truth.

`status_id_num` exists only on the callback payload, not on those status endpoints. Canonical money fields on the callback are `fiat_amount` / `fiat_currency` and `usdt_amount` / `usdt_currency` — do not swap mirrored legacy `amountIn` / `amountOut` between PayIn and PayOut. Signature and resend: see [Callbacks](#callbacks).

| Status ID | System Name | Description |
|-----------|-------------|-------------|
| `1` | new_order | New order created and awaiting payment or assignment. |
| `2` | trader_assingned | A trader has been assigned to the order. |
| `3` | payment_detail_assigned | Payment details have been provided for the order. |
| `4` | payment_completed_by_merch | The user has marked the payment as completed on the payment page. |
| `5` | payment_completed_by_trader | The trader has confirmed receiving the payment. |
| `6` | completed | The order is fully completed and successful. |
| `7` | disputed | A dispute has been opened for the order. Also set at the timeout of an order that is still `new_order` if the receipt you sent for it is passed to the trader for resolution (see [Chat & Disputes](#chat--disputes)). |
| `8` | canceled_by_timeout | The order was automatically canceled due to expiration (TTL). For an order executed by a payment partner, also when the partner itself canceled the payment on timeout; the partner's reason is in the callback `comment`. Also set when the trader resolves a dispute against the payment (see [Chat & Disputes](#chat--disputes)); the decision text is in the callback `comment`. Also set for a dispute that nobody handles (see [Chat & Disputes](#chat--disputes)). |
| `9` | canceled_by_merch | The order was manually canceled by the merchandiser. |
| `10` | canceled_by_trader | The order was canceled by the trader. |
| `11` | canceled_by_administrator | The order was canceled by a system administrator, or by the platform after the payment partner declined the payment; the partner's reason is in the callback `comment`. |
| `12` | canceled_by_disputed | The order was canceled as a result of a dispute resolution. |
| `13` | canceled_by_support | The order was canceled by the support team. |
| `14` | no suitable external system | No suitable external system was found for cascade/auction. |
| `15` | no payment details in external systems | No external system could provide payment details. |
| `16` | Sent to external system | The order has been forwarded to an external system for execution. |

---

## Crypto Withdrawals

Endpoints for withdrawing cryptocurrency (USDT) from the merchant's balance to external crypto wallets.

### POST /api/NewWithdrawalOrder

Creates a cryptocurrency withdrawal request. The request is forwarded to platform administrators for processing.

#### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `amount` (Req.) | Number | Amount to withdraw in USDT (must be greater than 0). |
| `wallet` (Req.) | String | External crypto wallet address (cannot be empty). |
| `net` (Req.) | String | Transfer network, e.g., "TRC20", "ERC20" (cannot be empty). |

#### Successful Response Example

```json
{
  "result": true,
  "assigned_id": 456,
  "message": "Accepted for processing"
}
```

---

## Get Balance

### POST /api/GetBalance

Requests the merchant's current available balance (usually in the base currency — USDT). The balance is cached for 1 minute.

> **Rate Limit:** You can request your balance **no more than once per minute** per token. Exceeding this limit will return an error: `You can request your balance no more than once per minute`.

If your funds are held by a payment provider in a separate provider account opened for you, the response also contains `provider_balances` — the balance available for settlement at the provider, per currency. The platform does not calculate this balance: it is refreshed from the provider every few minutes and decreases when the provider settles it to your wallet.

#### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `result` | Boolean | `true` if the balance was retrieved. |
| `balance` | String | Balance in USDT, two decimal places. |
| `provider_balances` | Array \| null | Balance held by the payment provider. An empty array means there is no such balance; `null` means it is temporarily unavailable — retry later. |
| `provider_balances[].currency` | String | Currency code, for example `EUR`. |
| `provider_balances[].amount` | String | Amount available for settlement, two decimal places. |
| `provider_balances[].updated_at` | String | Time of the last successful refresh from the provider, UTC (ISO 8601). |

#### Successful Response Example

```json
{
  "result": true,
  "balance": "15420.50",
  "provider_balances": [
    { "currency": "EUR", "amount": "1250.40", "updated_at": "2026-09-29T10:05:00Z" }
  ]
}
```

---

## Chat & Disputes

Provides API functionality for interacting with traders regarding problematic orders (e.g., amount mismatch or payment delays) via chat and dispute mechanisms.

A file sent to the chat of an order that is still `new_order` (`1`), or a dispute opened for it, does not change the order status before the order's timeout: the order keeps waiting for the payment and is completed if the payment arrives. If no payment arrives by the timeout, the order moves to `disputed` (`7`) instead of `canceled_by_timeout` (`8`) only if the receipt you sent is passed to the trader for resolution (see Dispute Resolution by the Trader below), and you receive a callback with `status` = `disputed`. Otherwise — dispute resolution by the trader is not enabled for your account, or no receipt file was sent — the order is canceled on timeout as usual, and you can open a dispute for the canceled order. An order marked as paid (`payment_completed_by_merch`, `4`) is canceled on timeout as usual. Orders executed by a payment partner get their status from the partner.

A dispute that nobody handles is canceled on timeout: the order moves from `disputed` (`7`) to `canceled_by_timeout` (`8`), and you receive the same callback as for an order canceled on timeout. This happens 15 minutes after the order's timeout if the order moved to `disputed` at its timeout but its dispute was not passed to the trader, and 5 minutes after dispute resolution by the trader is disabled for your account if the trader has not made a decision yet. Disputes reviewed by support, second reviews and disputes you opened after the order was canceled are not affected.

### POST /api/NewChatMessage

Sends messages to the order's chat. Allows attaching receipts (proof of payment). Passing a `changed_amount` together with `initial_amount` will update the order amount and automatically trigger a dispute.

If dispute resolution by the trader is enabled for your account, a message with a receipt file (`.jpg`, `.jpeg`, `.png`, `.bmp` or `.pdf`) opens a dispute with the trader — see [Dispute Resolution by the Trader](#dispute-resolution-by-the-trader). A message without a file stays a regular chat message.

#### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `order_id` (Req.) | Number | Internal order ID. |
| `message` (Opt.) | String | Message text. Can be omitted if there is an attachment or amount change. |
| `bytes` (Opt.) | String | Base64 encoded string of a file (e.g., payment receipt). Max 100 MB. |
| `extension` (Opt.) | String | File extension (e.g., ".png", ".pdf", ".jpg"). |
| `initial_amount` (Opt.) | Number | Current (original) order amount. Must be sent together with `changed_amount` when requesting an amount change. |
| `changed_amount` (Opt.) | Number | New corrected order amount. If set and different from the current amount — the order amount will be updated. |
| `actual_for_merch` (Opt.) | Boolean | Set to `false` when sending a message as a merchant to avoid self-notifications. |
| `actual_for_trader` (Opt.) | Boolean | Set to `true` to ensure the trader gets notified of new messages. |
| `actual_for_administrator` (Opt.) | Boolean | Set to `true` to notify administrators. |
| `must_show` (Opt.) | Boolean | Set to `true` (recommended) to show special forced alerts in the trader's cabinet. |

#### Response Example

```json
{
  "result": true,
  "message": "Message registered successfully"
}
```

### POST /api/OpenDispute

Forces the opening of a dispute for an order to have conflicting situations resolved by administrators/arbitrators.

The request registers the dispute in the order chat and does not change the order status by itself: `status_id` and `status` in the response show the order status at the moment of the call. If dispute resolution by the trader is enabled for your account, pass a receipt link in `path` — the receipt is then sent to the trader, see [Dispute Resolution by the Trader](#dispute-resolution-by-the-trader).

#### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `order_id` (Req.) | Number | Internal order ID. |
| `comment` (Req.) | String | Description of the reason for opening the dispute. |
| `path` (Opt.) | String | URL to a previously uploaded file (receipt/screenshot). For dispute resolution by the trader: a public `http` or `https` link that downloads without authorization within 20 seconds; a JPG, PNG, BMP or PDF file up to 20 MB (the format is detected from the file content). |

#### Response Example

```json
{
  "result": true,
  "order_id": 123456,
  "status_id": 8,
  "amount": 5000,
  "currency": "RUB",
  "status": "canceled_by_timeout"
}
```

### Dispute Resolution by the Trader

Available if dispute resolution by the trader is enabled for your account — contact your manager. The receipt is forwarded to the trader assigned to the order, and the trader resolves the dispute within the time limit configured for your account. Orders executed by a payment partner are not handled this way and follow the regular dispute process.

#### How the Dispute Starts

- A receipt is a file attached via `POST /api/NewChatMessage` or a link passed in `path` of `POST /api/OpenDispute`.
- For an order that is still `new_order` (`1`), the receipt is sent to the trader only after the order's timeout, when the order moves to `7` `disputed`; a payment that arrives before the timeout completes the order without a dispute. If dispute resolution by the trader is disabled for your account by the timeout, the order is canceled on timeout (`canceled_by_timeout`), and the receipt is not sent to the trader. If the payment is marked as completed in the meantime (`payment_completed_by_merch`, `4`), the timeout cancels the order (`canceled_by_timeout`), and then it moves to `7` `disputed`: you receive both callbacks. Both callbacks are also possible if the receipt arrives a few seconds before the timeout.
- In other statuses the order moves to `7` `disputed` right away, and you receive a callback with `status` = `disputed`.
- A receipt for an order that is already `completed` (`6`) does not change the status: support reviews it.
- Another receipt for the same order during the dispute is added to it; the time limit is not reset.

#### Outcome

| Trader's decision | Order status | Callback |
|-------------------|--------------|----------|
| Payment received | `6` `completed` | Yes |
| No payment | `8` `canceled_by_timeout` | Yes |
| Fake receipt | `8` `canceled_by_timeout` | Yes |
| Wrong payment details | `8` `canceled_by_timeout` | Yes |
| Paid under another order | Stays `7` `disputed`, support reviews the dispute | No, an explanation is added to the order chat |
| No decision within the time limit | `6`, `8` or stays `7`, according to the platform rule | Yes, if the status changes |
| No decision, and dispute resolution by the trader has been disabled for your account | `8` `canceled_by_timeout` 5 minutes after disabling | Yes, the same as for an order canceled on timeout |

In these callbacks, except the last row, `message` carries the status code (`completed` or `canceled_by_timeout`), `comment.message` carries the decision text set by the platform, and `comment.path` is an empty string. Use `status` to process the result; do not parse the decision text.

#### Second Review

After the dispute with the trader has ended or has been passed to support, support may start a second review of the order. The order moves to `7` `disputed`, and a message about the second review is added to the order chat. You receive a callback with `status` = `disputed`: `comment.message` carries the message text and `comment.path` is an empty string; `message` carries the status code `disputed` if the order was in another status, or an empty string if it was already disputed. The second review has no time limit: support decides, and the result arrives as a regular status callback. Orders in `completed` (`6`) are not sent to a second review.

#### Important Notes

- The trader's explanations and proof files are added to the order chat, which you can see in the merchant cabinet; no callback is sent for them.
- If a callback is redelivered after a failed attempt, `comment` may be `null`: the decision text stays in the order chat.
- If the receipt link cannot be downloaded or the file is not a JPG, PNG, BMP or PDF, the receipt is not sent to the trader, the order status does not change, and support is notified.
- If the file fails a mandatory receipt check (for example, it is not a bank receipt — a logo, a photo or a chat screenshot, even if an amount is visible on it — or the payment amount cannot be read from it), it is sent neither to the trader nor to support, and the order does not move to `7` `disputed` because of it. An order that has already moved to `7` on timeout is canceled as a dispute that nobody handles. The next receipt you send for the order is checked again and handled as usual.
- If support or the trader changes the order status in another way while the dispute is open, the dispute ends with that status.
- If the trader marks the receipt as fake and payer blocking after disputes is enabled by the platform, the payer's `payer_id`, `payer_name` or both are added to the block list of your account: new orders from this payer are rejected with `The payer is blocked`. The block applies to your account only.

---

## Reference: Payment Methods

### GET /api/PaymentMethods

Retrieves a dictionary of payment-method **codes** the platform knows. The result is cached for 5 minutes. The list is not filtered by currency, country, or merchant.

> **Important:** The list of payment methods is constantly expanding. To ensure your integration is robust and up-to-date, it is highly recommended to regularly fetch the available methods via this endpoint rather than hardcoding them in your application.

#### Currency vs `payment_method` (KRW, South Korea)

Search terms: `KRW`, South Korea, `payment_method`, `GET /api/PaymentMethods`.

`GET /api/PaymentMethods` is a **code catalog**, not “what is enabled for this merchant in this currency”. There is no currency→methods map in the API. Do not answer “which methods for KRW / South Korea” with the full enum (`sbp`, `upi`, `card_number`, `bkash`, …).

Which codes you may send is defined in your merchant settings. Ask operations which methods are enabled for a given currency. For KRW, operations typically enable `account_number` for both PayIn and PayOut — that is an operational setup, not a hardcoded API rule. Do not extrapolate the full catalog onto a country.

#### Known Methods Reference

At the time of this documentation, some of the supported methods include:

- `card_number` — Transfer via card number (Visa, MasterCard, etc.)
- `phone_number` — Transfer via phone number (mobile transfers, SBP)
- `account_number` — Transfer via checking account
- `iban` — IBAN transfer
- `sepa_transfer` — SEPA transfer (Europe)
- `sbp` — Faster Payments System (SBP, NSPK Russia)
- `fawry` — Fawry (Egypt)
- `upi` — UPI (India)
- `imps` — IMPS (India)
- `pago_movil` — PagoMovil (Venezuela)
- `qr_code` — Transfer via QR code
- `jazz_cash` — JazzCash (Pakistan)
- `easypaisa` — Easypaisa (Pakistan)
- `quasi_ecom` — Card number payment with 3D-Secure confirmation
- `bkash` — Bkash (Bangladesh)
- `nagad` — Nagad (Bangladesh)
- `c2becom` — Bank acquiring
- `paypal` — PayPal e-wallet

#### Response Example (Array of Objects)

```json
[
  {
    "id": 1,
    "name": "phone_number",
    "description": "Transfer via phone number"
  },
  {
    "id": 2,
    "name": "card_number",
    "description": "Transfer via card number"
  },
  {
    "id": 4,
    "name": "sbp",
    "description": "Faster Payments System (SBP)"
  }
]
```

---

## Reference: Exchange Rates

### GET /api/GetRates

Returns the list of supported USDT→fiat pairs and their current exchange rates from the in-memory cache. The base currency is always USDT — no other directions are supported.

> **Public endpoint:** no authentication is required. There is no HMAC / X-App-Token / cross-token validation. Safe for frequent polling.

An alternative versioned route `GET /api/v1/GetRates` is also available and returns the same payload.

#### Successful Response Example

```json
[
  { "currency_pair": "USDT/RUB", "exchange_rate": 92.35 },
  { "currency_pair": "USDT/INR", "exchange_rate": 85.72 },
  { "currency_pair": "USDT/AZN", "exchange_rate": 1.70 }
]
```

#### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `currency_pair` | String | Currency pair in the format `"USDT/{CurrencyCode}"` where `CurrencyCode` is the ISO code of the fiat (e.g., `"RUB"`, `"INR"`, `"AZN"`, `"KZT"`). |
| `exchange_rate` | Number (Double) | How many units of the fiat correspond to `1 USDT`. **This is a number, not a string** — keep that in mind when parsing. |

#### Empty / Unavailable Response

If the in-memory cache is empty or none of the cached currency IDs are present in the currencies dictionary, the endpoint returns an error object instead of an empty array:

```json
{ "result": false, "message": "Exchange rate not available" }
```

#### Important Notes

- Values are cached in the application memory and refreshed by a background job — there is no fixed TTL, but the staleness is measured in seconds.
- Do **not** use this rate for the actual order math: when a PayIn / PayOut order is created, the API fixes its own rate (returned in the `rate` field of the create response). Use that rate for settlement; `GetRates` is meant for pre-creation UI estimates only.

---

## Callbacks

A callback (webhook) is an HTTP POST request sent to your server when an order's status changes. The body is a compact JSON (no extra whitespace) and is authenticated via the `X-App-Access-Sig` header (HMAC-SHA256 with `CallBackToken`; see Authentication section).

An order can receive more than one callback (retries, a later confirmation after a dispute or cancel). The source of truth is the **latest** callback together with the status endpoint. `status_id_num` is present here and absent from `POST /api/GetOrderStatus` / `POST /api/GetPayOutOrderStatus`.

### PayIn Callback Payload

Sent for incoming-payment orders (`direction: "PayIn"`).

#### JSON Payload Example

```json
{
  "order_id": "123456",
  "incoming_id": "ext-123",
  "direction": "PayIn",

  // Legacy block (preserved as-is)
  "currencyOut": "RUB",
  "amountOut": "10000.0",
  "amountOut_num": 10000.0,
  "currencyIn": "USDT",
  "amountIn": "104.5",
  "amountIn_num": 104.5,
  "status_id": "6",
  "status_id_num": 6,
  "status": "completed",
  "message": "Order successfully completed",
  "comment": null,
  "rate": 95.69,
  "requisites": null,
  "requested_payment_method": "card_number",

  // Canonical block (recommended for new integrations)
  "fiat_amount": 10000.0,
  "fiat_currency": "RUB",
  "usdt_amount": 104.5,
  "usdt_currency": "USDT",
  "commission": 1.5,

  // Conditional — only present when status_id == 6 AND the order is linked
  // to a matched mobile push notification with a non-empty message.
  "trigger_push_message": "Поступление 10 000.50 RUB на карту *1111"
}
```

#### Field Reference (PayIn)

| Field | Type | Description |
|-------|------|-------------|
| `order_id` | String | Internal order ID (numeric value as a string). |
| `incoming_id` | String | Merchant's external/incoming order ID (may be empty). |
| `direction` | String | Always `"PayIn"` for this payload. |
| `currencyOut` (legacy) | String | **Payer-side** currency code (the fiat the user pays). Legacy naming — see Compatibility. |
| `amountOut` (legacy) | String | Amount in `currencyOut`, invariant culture (`.` as decimal separator). |
| `amountOut_num` (legacy) | Number | Same value as `amountOut` but as a JSON number (`double`). |
| `currencyIn` (legacy) | String | **Merchant-side** currency code (the asset the merchant receives — typically `"USDT"`). |
| `amountIn` (legacy) | String | Amount in `currencyIn`, invariant culture. |
| `amountIn_num` (legacy) | Number | Same value as `amountIn` but as a JSON number (`double`). |
| `status_id` | String | Numeric status ID as a string. See Order Statuses. |
| `status_id_num` | Number | Same value as `status_id` but as a JSON integer. |
| `status` | String | Status code in snake_case (e.g., `"completed"`, `"canceled_by_timeout"`). |
| `message` | String | Human-readable message describing the event (status name or trader/system comment). |
| `comment` | Object \| null | Optional chat/dispute snippet: `{ "message": "...", "path": "..." }` when a related chat record exists. When a payment partner canceled the order (`status_id` `8` or `11`), `message` carries the partner's reason — `"Canceled by payment partner: 3.01 Declined by Acquirer: Anti-fraud"` — and `path` is `null`. Otherwise `null`. |
| `rate` | Number | Order exchange rate (`double`), rounded to **6 decimal places**. The rate is stored unrounded, so do not compare it for exact equality and do not derive `usdt_amount` from it — use `usdt_amount` instead. |
| `requisites` | Object \| null | Parsed payment requisites JSON for the order (bank, card, holder, etc.) or `null`. |
| `requested_payment_method` | String \| null | Payment method requested when creating the order (e.g., `"card_number"`). |
| `fiat_amount` (canonical) | Number | Fiat amount (same value as `amountOut`, but as a JSON number). |
| `fiat_currency` (canonical) | String | Fiat currency code (same as `currencyOut`). |
| `usdt_amount` (canonical) | Number | USDT amount (same value as `amountIn`, but as a JSON number). |
| `usdt_currency` (canonical) | String | Always `"USDT"`. |
| `commission` (canonical) | Number | Merchant commission applied to the order. |
| `trigger_push_message` (conditional) | String | **Optional, conditional.** The text of the mobile push notification matched to the order. Present **only** when `status_id == 6` AND the order is linked to a matched notification with a non-empty message; otherwise the key is **omitted entirely** (not `null`). |

### PayOut Callback Payload

Sent for outgoing-payout orders (`direction: "PayOut"`). Compared to PayIn, the shape differs in three ways:

- **Mirrored legacy semantics**: for PayOut, `currencyOut` is the **USDT** side (what is debited from the merchant) and `currencyIn` is the **fiat** side (what the recipient receives). This is opposite to PayIn — use the canonical block (`fiat_*` / `usdt_*`) to avoid confusion.
- **No** `requisites` and **no** `requested_payment_method` fields.
- Adds `decline_reason` (populated only for cancelled/declined orders, `status_id >= 8`; `null` otherwise).

#### JSON Payload Example (declined order)

```json
{
  "order_id": "987654321",
  "incoming_id": "payout-ext-002",
  "direction": "PayOut",

  // Legacy block (preserved as-is; in/out roles are mirrored vs PayIn)
  "currencyOut": "USDT",
  "amountOut": "50.0",
  "amountOut_num": 50.0,
  "currencyIn": "RUB",
  "amountIn": "4750.0",
  "amountIn_num": 4750.0,
  "status_id": "9",
  "status_id_num": 9,
  "status": "canceled_by_merch",
  "message": "Canceled by merchant",
  "comment": null,
  "rate": 95.0,
  "decline_reason": "Merchant requested cancel",

  // Canonical block (recommended for new integrations)
  "fiat_amount": 4750.0,
  "fiat_currency": "RUB",
  "usdt_amount": 50.0,
  "usdt_currency": "USDT",
  "commission": 1.5
}
```

#### Field Reference (PayOut)

| Field | Type | Description |
|-------|------|-------------|
| `order_id` | String | Internal payout order ID (numeric value as a string). |
| `incoming_id` | String | Merchant's external/incoming order ID. |
| `direction` | String | Always `"PayOut"` for this payload. |
| `currencyOut` (legacy) | String | **USDT side** currency code for PayOut (what is debited from the merchant). Mirrored vs PayIn — see Compatibility. |
| `amountOut` (legacy) | String | USDT amount, invariant culture. |
| `amountOut_num` (legacy) | Number | Same value as `amountOut` but as a JSON number. |
| `currencyIn` (legacy) | String | **Fiat side** currency code for PayOut (what the recipient receives). |
| `amountIn` (legacy) | String | Fiat amount, invariant culture. |
| `amountIn_num` (legacy) | Number | Same value as `amountIn` but as a JSON number. |
| `status_id` | String | Numeric status ID as a string. |
| `status_id_num` | Number | Same value as `status_id` but as a JSON integer. |
| `status` | String | Status code in snake_case. |
| `message` | String | Human-readable message describing the event. |
| `comment` | Object \| null | Optional chat/dispute snippet `{ "message", "path" }` or `null`. |
| `rate` | Number | Order exchange rate, rounded to **6 decimal places**. The rate is stored unrounded, so do not compare it for exact equality and do not derive amounts from it. |
| `decline_reason` | String \| null | Populated only when `status_id >= 8` (cancelled/declined orders). Falls back to `"Unspecified decline reason"` if no reason was provided. `null` for all other statuses. |
| `fiat_amount` (canonical) | Number | Fiat amount (same value as `amountIn` for PayOut). |
| `fiat_currency` (canonical) | String | Fiat currency code (same as `currencyIn` for PayOut). |
| `usdt_amount` (canonical) | Number | USDT amount (same value as `amountOut` for PayOut). |
| `usdt_currency` (canonical) | String | Always `"USDT"`. |
| `commission` (canonical) | Number | Merchant commission applied to the payout. |

### Compatibility & Field Style

Each callback contains **two parallel field sets**:

- **Legacy block** — original camelCase fields (`currencyOut`, `currencyIn`, `amountOut`, `amountIn`) plus their string/`_num` numeric duplicates. The in/out roles are **mirrored** between PayIn and PayOut (see the per-payload tables above). Existing integrations keep working unchanged.
- **Canonical block** — `fiat_amount`, `fiat_currency`, `usdt_amount`, `usdt_currency` (always `"USDT"`), `direction` and `commission`. Recommended for **new integrations**: the semantics are unambiguous and do not depend on `direction`.

**Field style tips:**

- Key naming is **mixed by design**: snake_case (`order_id`, `incoming_id`, `status_id`, `fiat_amount`, ...) and camelCase for legacy fields (`currencyOut`, `currencyIn`, `amountOut`, `amountIn`). Treat key names as opaque strings.
- `order_id` and `status_id` are **strings**; `status_id_num`, `amountOut_num`, `amountIn_num`, `rate`, all `fiat_*`/`usdt_*`/`commission` are **numbers**.
- `amountOut` / `amountIn` use `InvariantCulture` (always `.` as decimal separator).
- The `comment` field is an object `{ "message", "path" }` when present, not a plain string. It is independent from the `message` field. For a PayIn order canceled by a payment partner, `comment.message` starts with `Canceled by payment partner: ` followed by the partner's code and text, and `comment.path` is `null`.
- `requisites` is an arbitrary object reflecting the saved payment details for the order (bank, card, holder, phone, etc.) and is **PayIn-only**.

### Retries & Missed Callbacks

If the primary send fails (non-2xx response, timeout, DNS error, etc.), the system automatically retries the callback on a fixed schedule. A separate background worker also detects orders whose status has changed but for which **no callback was ever recorded** ("missed callbacks") and sends them as well.

#### Retry Schedule

The worker ticks every minute and decides whether to attempt a pending callback based on the time elapsed since the row was first created. The minimum delay before the *next* attempt grows geometrically (base `5`):

| Attempts already made | Min. minutes since creation before the next attempt | Δ vs previous attempt |
|-----------------------|------------------------------------------------------|-----------------------|
| `0` | 1 min | — |
| `1` | 6 min | +5 |
| `2` | 31 min | +25 |
| `3` | 156 min (~2 h 36 m) | +125 |
| `4` | 781 min (~13 h) | +625 |

The attempt limit defaults to **5**; after that, the row is finalised and no further retries are performed. So a failing callback is given up on at roughly the 13-hour mark from the first failure.

#### Idempotency

Once your endpoint replies with a 2xx response (`200 OK`, `201 Created`, etc.) the callback is marked final and will **not** be retried. To avoid 2xx duplicates, return success **only after** you have persisted the new order state.

The worker also skips callbacks whose configured URL is empty — these are marked as `"Callback URL not configured"` and finalised immediately. Update your callback URL via merchant profile to (re)enable delivery.

#### Missed Callbacks

If for any reason the primary callback was never persisted (e.g., the worker process crashed mid-send), the background job will pick the order up automatically. To avoid racing the primary send, missed-callback detection uses a **5-minute freshness cutoff**: orders updated less than 5 minutes ago are skipped on the current tick.

*On retry, the JSON shape is identical to the primary send.* Only context-driven fields may differ: for PayIn, the chat snippet is not restored, so `comment` carries the payment partner's cancellation reason when there is one and is `null` otherwise, and `decline_reason` is not applicable; for PayOut, `comment` is reconstructed from the latest chat record with an attachment, and `decline_reason` falls back to the order's stored decline reason.

### POST /api/CallbackResender/resend — Resend Callbacks (manual)

A tool for forcing a bulk resend of webhooks (callbacks) to your server in case your infrastructure experienced downtime. Either pass an exact array of orders (`items`) or specify a time period (`from`, `to`) together with `resend_pay_in`/`resend_pay_out`.

#### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `items` (Opt.) | Array | Array of objects: `{ "id": "string", "type": "pay_in" \| "payout", "callback_address": "string?" }`. If provided — the period fields below are ignored. |
| `from` (Opt.) | DateTime | Start of the period (UTC). Used only when `items` is not provided. |
| `to` (Opt.) | DateTime | End of the period (UTC). Used only when `items` is not provided. |
| `resend_pay_in` (Opt.) | Boolean | Resend PayIn callbacks for the period. |
| `resend_pay_out` (Opt.) | Boolean | Resend PayOut callbacks for the period. |

**Note on `items[].type`:** the value is a string in snake_case — either `pay_in` (incoming order from a payer) or `payout` (outgoing payout to a recipient).

#### Response Example

```json
{
  "count": 150
}
```

The system queues the listed callbacks asynchronously and executes the resend.

---

# Part 2. Payment Orchestrator (PayIn)

## Payment Orchestrator — Introduction / RSA

The Payment Orchestrator is a specialized B2B API for payment systems (PS) and gateways that allows routing AETERNA orders through your platform to provide the best payment details for PayIn or to execute outgoing PayOut transfers.

### Auction Model

Routing is implemented as an **auction**: AETERNA sends a single create-order request to *all* auction participants in parallel and waits up to **5 seconds** for offers. Each participant returns their proposed exchange rate and commission (and, for PayIn, the actual payment details). The winner is selected based on three factors:

- Proposed **commission**;
- Proposed **exchange rate**;
- Participant's internal **rating** in AETERNA (success rate, dispute rate, response latency).

Because the broadcast is parallel, your payment details are reserved for **only a few seconds** while the winner is being selected — losing participants receive an immediate `CancelOrder` so they can release the reservation.

> **Tip:** if your offers are frequently rejected, consider lowering your exchange rate or commission, and improve your rating by reducing disputes and the latency of order processing on your side.

### Authentication (RSA)

Interaction uses asymmetric RSA 2048-bit signatures. You generate a key pair on your side once and share **only the public key** with us; the private key never leaves your servers.

- **Your Private Key:** PEM format, PKCS#8 — used by you to sign all outgoing requests and callbacks.
- **Your Public Key (shared with us):** PEM format, X.509 — used by AETERNA to verify your callbacks and outgoing requests to us.

> **Keep the private key secret.** Never check it into version control, never send it over chat/email, and rotate it if you suspect a leak. Compromise of the private key allows an attacker to impersonate your platform.

#### Key Generation (via OpenSSL)

```bash
# Generate private key
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048

# Extract public key to provide to AETERNA
openssl rsa -in private_key.pem -pubout -out public_key.pem
```

#### PEM Format Examples

**Private key (PKCS#8):**

```plaintext
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASC... (base64 body) ...IDAQAB
-----END PRIVATE KEY-----
```

**Public key (X.509):**

```plaintext
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A... (base64 body) ...IDAQAB
-----END PUBLIC KEY-----
```

#### Signature Generation

Every request must contain two headers:

- `X-Timestamp` — Current Unix timestamp in **UTC seconds**. Requests older than 2 minutes or with a timestamp in the future are rejected.
- `X-Signature` — RSA-SHA256 signature of the canonical string, encoded as **Base64**.

**Canonical string format:** `{timestamp}|{ExternalSystemName}|{orderId}|{MethodName}`

Example: `1712664000|MyPaySystem|ext-12345|CreateOrder`

Each integration endpoint uses its own `{MethodName}` suffix — see the corresponding section for the exact value.

#### C# Reference Implementation (BouncyCastle.NetCore)

The snippet below is the exact helper AETERNA uses internally to sign and verify canonical strings. It works for both your outgoing requests and incoming AETERNA callbacks.

```csharp
using System.Text;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;

public static class RsaAuthHelper
{
    private static readonly AsymmetricKeyParameter PrivateKey =
        LoadPrivateKey(File.ReadAllText("private_key.pem"));

    public static string SignCanonicalString(string canonical)
    {
        var signer = SignerUtilities.GetSigner("SHA256withRSA");
        signer.Init(true, PrivateKey);
        var bytes = Encoding.UTF8.GetBytes(canonical);
        signer.BlockUpdate(bytes, 0, bytes.Length);
        return Convert.ToBase64String(signer.GenerateSignature());
    }

    public static bool VerifySignature(string canonical, string base64Signature, string publicKeyPem)
    {
        var publicKey = LoadPublicKey(publicKeyPem);
        var signer = SignerUtilities.GetSigner("SHA256withRSA");
        signer.Init(false, publicKey);
        var bytes = Encoding.UTF8.GetBytes(canonical);
        signer.BlockUpdate(bytes, 0, bytes.Length);
        var signatureBytes = Convert.FromBase64String(base64Signature);
        return signer.VerifySignature(signatureBytes);
    }

    private static AsymmetricKeyParameter LoadPrivateKey(string pem)
    {
        using var reader = new StringReader(pem);
        var obj = new PemReader(reader).ReadObject();
        return obj switch
        {
            AsymmetricCipherKeyPair keyPair => keyPair.Private,
            AsymmetricKeyParameter key when key.IsPrivate => key,
            _ => throw new Exception("Cannot parse private key")
        };
    }

    private static AsymmetricKeyParameter LoadPublicKey(string pem)
    {
        using var reader = new StringReader(pem);
        var obj = new PemReader(reader).ReadObject();
        return obj switch
        {
            AsymmetricKeyParameter key when !key.IsPrivate => key,
            _ => throw new Exception("Cannot parse public key")
        };
    }
}
```

#### Self-Test Endpoint

To verify your signing implementation against AETERNA's reference, you can call the public test endpoint:

**POST /api/TestExternalAuction/TestSign**

Pass any `canonical_string`; the endpoint returns a Base64 RSA-SHA256 signature produced with the well-known *test* private key. Use the matching test public key (returned by `GET /api/TestExternalAuction/GetPublicKey`) on your side to verify. The keys are **test only** and are not used in production.

**Request body:**

```json
{ "canonical_string": "1712664000|MyPaySystem|ext-12345|CreateOrder" }
```

**Response:**

```json
{ "signature": "Base64EncodedSignatureValue==" }
```

---

## Orchestrator Integration

The Orchestrator operates as a bid/auction system where external systems act as auction participants. We expect a response with the best possible conditions (exchange rate and commission) within a strict timeframe.

### Endpoints You Must Implement

To act as an auction participant, your platform must expose the following HTTP POST endpoints under a single `{RootUrl}`. AETERNA will call them with an RSA-signed body:

| Method | Path | Direction | Description |
|--------|------|-----------|-------------|
| POST | `{RootUrl}/CreateOrder` | PayIn | Create an incoming-payment order and return your payment details. |
| POST | `{RootUrl}/ApprovePayInOrder` | PayIn | Receive the win notification for a PayIn auction — the order is locked to you. |
| POST | `{RootUrl}/GetOrderStatus` | PayIn / PayOut | Return the current status of an order. |
| POST | `{RootUrl}/CancelOrder` | PayIn | Cancel a PayIn order and release the reserved details. |
| POST | `{RootUrl}/CreateDispute` | PayIn / PayOut | Forward a chat message, change-amount request, or open a dispute. |
| POST | `{RootUrl}/CreatePayoutOrder` | PayOut | Bid for an outgoing payout (rate + commission, no requisites). |
| POST | `{RootUrl}/CancelPayoutOrder` | PayOut | Cancel a PayOut bid (e.g. when another participant won). |
| POST | `{RootUrl}/ApprovePayoutOrder` | PayOut | Receive the requisites of the winning PayOut and execute the transfer. |

### Response Time Rules

- We expect a response to `CreateOrder` / `CreatePayoutOrder` within a maximum of **5 seconds**.
- If you respond *before* `stop_auction_time_unix`, your bid is accepted into the auction.
- If you respond *after* `stop_auction_time_unix` but within 5 seconds, we will send you a `CancelOrder` / `CancelPayoutOrder` request with the reason `too_long_response`.
- If you respond *after* 5 seconds, the result is ignored entirely, and no cancel request will be sent.
- If your response fails AETERNA-side validation (missing fields, mismatched `payment_method`, mismatched amount when `iterative_sum_search_enabled = false`, etc.), we send a `CancelOrder` with reason `not_valid_response`.
- If your bid **wins** a PayIn auction, AETERNA sends you an `ApprovePayInOrder` notification confirming the order is locked to you — no separate accept is required, just continue processing. (PayOut winners receive `ApprovePayoutOrder` with the requisites instead.)

### General Requirements

- All requests and responses must be in **JSON** format with `Content-Type: application/json`.
- Fields must be named in `snake_case`.
- You must provide an RSA **Public Key** (PEM, X.509) for AETERNA to verify *your* outgoing requests and callbacks.
- You must **sign every outgoing request and callback** to AETERNA with your RSA private key (PEM, PKCS#8) — see Introduction / RSA.
- You must **validate every incoming request from AETERNA**: verify `X-Signature` against the canonical string with our public key, and reject any request whose `X-Timestamp` is older than 2 minutes or set in the future.
- Each method has its own canonical-string suffix (e.g. `...|CreateOrder`, `...|CreatePayoutOrder`) — see the corresponding section.

> **Callback notifications are mandatory.** Once an order is assigned to you, your platform **must** send status-change webhooks to the `callback_url` we passed in the create request. Without callbacks the integration will not work — AETERNA will not be able to settle the order with the merchant.

---

## CreateOrder (PayIn)

**POST {RootUrl}/CreateOrder**

AETERNA calls this endpoint on **your** platform to bid for a new PayIn order. You must respond with payment details that satisfy the `max_exchange_rate` and `max_commission` constraints, or return an error.

### Canonical String for Signature

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|CreateOrder
```

### Request Body

| Parameter | Type | Description |
|-----------|------|-------------|
| `system_order_id` (Req.) | String | Our request ID (GUID). |
| `currency` (Req.) | String | Fiat currency code (e.g., "RUB", "VES"). |
| `max_exchange_rate` (Req.) | Double | The maximum allowed exchange rate to USDT, rounded to 6 decimal places. |
| `max_commission` (Req.) | Double | The maximum allowed commission for order execution (percent). |
| `amount` (Req.) | Double | The order amount in fiat. |
| `cancel_order_time_unix` (Req.) | Long | Unix timestamp (sec) when the order becomes irrelevant/expires. |
| `stop_auction_time_unix` (Req.) | Long | Unix timestamp (sec) for the auction deadline. |
| `callback_url` (Req.) | String | URL where AETERNA expects to receive your status-change webhooks for this order. |
| `allowed_payment_method` (Req.) | String | Allowed payment method, snake_case. Supported in auction: `card_number`, `phone_number`, `account_number`, `iban`, `sbp`, `quasi_ecom`. The full registry is available at `GET https://api.skyphoenix.net/api/PaymentMethods`. |
| `iterative_sum_search_enabled` (Req.) | Boolean | If `true`, you are allowed to return details for a slightly altered amount when the exact requested amount is unavailable. If `false` and the response amount differs from the requested one — the response is rejected. |
| `allowed_bank_name` (Opt.) | String | Specific bank allowed (optional). |

### Successful Response Example

```json
{
  "is_success": true,
  "error_code": null,
  "error_message": null,
  "external_order_id": "uuid-string",
  "amount": 1000.0,
  "exchange_rate": 75.25,
  "commission": 1.5,
  "payment_details": {
     "type": "card_number",
     "name": "Ivan Ivanov",
     "bank_name": "Sberbank",
     "card": "4276........"
  }
}
```

The `payment_details` object structure depends on the `type` field — see the per-method tables below.

### payment_details by Payment Method

#### `card_number` — Bank card transfer

| Field | Type | Description |
|-------|------|-------------|
| `type` (Req.) | String | Always `"card_number"`. |
| `name` (Req.) | String | Recipient name. |
| `bank_name` (Req.) | String | Bank name. |
| `card` (Req.) | String | Card number. |

#### `phone_number` — Phone number transfer

| Field | Type | Description |
|-------|------|-------------|
| `type` (Req.) | String | Always `"phone_number"`. |
| `name` (Req.) | String | Recipient name. |
| `bank_name` (Req.) | String | Bank name. |
| `phone_number` (Req.) | String | Recipient's phone number. |

#### `account_number` — Bank account transfer

| Field | Type | Description |
|-------|------|-------------|
| `type` (Req.) | String | Always `"account_number"`. |
| `name` (Req.) | String | Recipient name. |
| `bank_name` (Req.) | String | Bank name. |
| `account_number` (Req.) | String | Recipient's bank account number. |

#### `iban` — IBAN transfer

| Field | Type | Description |
|-------|------|-------------|
| `type` (Req.) | String | Always `"iban"`. |
| `name` (Req.) | String | Recipient name. |
| `bank_name` (Req.) | String | Bank name. |
| `iban` (Req.) | String | IBAN of the recipient. |

#### `sbp` — Faster Payments System (Russia)

| Field | Type | Description |
|-------|------|-------------|
| `type` (Req.) | String | Always `"sbp"`. |
| `name` (Req.) | String | Recipient name. |
| `bank_name` (Req.) | String | Bank name. |
| `phone_number` (Req.) | String | Recipient's phone number registered in SBP. |

#### `quasi_ecom` — Card payment with 3D-Secure / hosted page

| Field | Type | Description |
|-------|------|-------------|
| `type` (Req.) | String | Always `"quasi_ecom"`. |
| `payment_link` (Req.) | String | Hosted card-input URL on your side. The merchant's payer is redirected here to enter card data and complete 3D-Secure. |

### Response Validation

Even if you return `is_success=true`, AETERNA will internally validate the response and may reject it (sending you a `CancelOrder` with reason `not_valid_response`) when:

- `iterative_sum_search_enabled = false` in the request, but the response `amount` differs from the requested amount by more than `0.1`.
- `exchange_rate` in the response is not greater than `0`.
- `payment_details` is missing or `null`.
- `payment_details.type` does not match the requested `allowed_payment_method`.
- Per-method required fields (`name`, `bank_name`, `card`/`phone_number`/`iban`/`account_number`/`payment_link`) are missing.

---

## ApprovePayInOrder

**POST {RootUrl}/ApprovePayInOrder**

After the auction is decided, AETERNA calls `ApprovePayInOrder` on the **winner's** side to explicitly confirm that the PayIn order is locked to you and must be processed further. The payment details were already returned by you in `CreateOrder` — `ApprovePayInOrder` only confirms the selection and the final agreed terms. No separate accept is required.

### Canonical String for Signature

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|ApprovePayInOrder
```

### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `system_order_id` (Req.) | String | Our request ID (GUID), same value as in `CreateOrder`. |
| `external_order_id` (Req.) | String | Your internal order ID returned in `CreateOrder`. |
| `amount` (Req.) | Double | Confirmed final amount to be paid (after optional iterative-sum search). |
| `exchange_rate` (Req.) | Double | Confirmed exchange rate to USDT. |
| `commission` (Req.) | Double | Confirmed commission (percent). |

### Response Body (JSON)

| Field | Type | Description |
|-------|------|-------------|
| `is_success` (Req.) | Boolean | Set to `true` to acknowledge that the order is accepted on your side — this stops AETERNA's retries. `false` (or an unparsable body / non-2xx status) counts as a failed attempt. |
| `error_code` (Opt.) | String | Machine-readable error code (snake_case) when `is_success=false`; `null` on success. |
| `error_message` (Opt.) | String | Human-readable error detail (optional). |
| `external_order_id` (Opt.) | String | Optional echo of your internal order ID — informational only, AETERNA does not validate it. |

> Respond with **HTTP 200** and `is_success=true` to confirm. Any non-2xx status, an unparsable body, or `is_success=false` is treated as a failed attempt and triggers the next retry.

### Successful Response Example

```json
{
  "is_success": true,
  "error_code": null,
  "error_message": null,
  "external_order_id": "uuid-string"
}
```

### Retry Mechanism (best-effort)

AETERNA retries `ApprovePayInOrder` up to **3 times** with a fixed **10-second** delay between attempts. The retry stops as soon as any attempt returns `is_success=true`.

Unlike `ApprovePayoutOrder`, if all attempts fail the order is **not** canceled: the payment details have already been handed to the payer and the payment may be in progress. `GetOrderStatus` remains the fallback AETERNA uses to reconcile the order. Every attempt — successful or not — is logged on AETERNA's side.

> **Implementation tip:** treat `ApprovePayInOrder` as an idempotent confirmation — respond with `is_success=true` even if you receive it more than once for the same order.

---

## GetOrderStatus

**POST {RootUrl}/GetOrderStatus**

AETERNA calls this endpoint on your platform to fetch the current status of an order (PayIn or PayOut).

### Canonical String for Signature

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|GetOrderStatus
```

### Request Body

| Parameter | Type | Description |
|-----------|------|-------------|
| `system_order_id` (Req.) | String | Our request ID (GUID). |
| `external_id` (Req.) | String | Your internal request ID. |

### Response Example

```json
{
  "is_success": true,
  "error_code": null,
  "error_message": null,
  "status": 6
}
```

**Statuses:**

- `1`: Created
- `2`: Trader assigned
- `3`: Payment details assigned
- `4`: Merchandiser confirmed payment
- `5`: Trader confirmed payment
- `6`: Completed (Success)
- `7`: Dispute opened
- `8`: Canceled by timeout
- `9`: Canceled by merchandiser
- `10`: Canceled by trader
- `11`: Canceled by administrator
- `12`: Canceled by supervisor
- `13`: Canceled as a result of dispute

---

## CancelOrder (PayIn)

**POST {RootUrl}/CancelOrder**

AETERNA calls this endpoint on your platform to cancel a PayIn order and ask you to release the reserved payment details. Triggered, for example, when another participant wins the auction or your response was too late or invalid.

### Canonical String for Signature

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|CancelOrder
```

### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `system_order_id` (Req.) | String | Our request ID (GUID). |
| `external_id` (Req.) | String | Your internal request ID. |
| `reason` (Req.) | String | Cancellation reason code (snake_case). |
| `reason_message` (Opt.) | String | Text explanation. |

**Cancellation Reasons (`reason` code):**

- `too_long_response`: Response took too long.
- `not_valid_response`: Incorrect response format or failed validation (e.g. missing requisites, type mismatch, mismatched amount).
- `system_selected_another_performer`: Another participant won the auction.
- `auction_timeout_after_finish`: Timeout occurred after the auction finished.
- `server_error`: Server error on our side.
- `other`: Other reason — see `reason_message` for details.

---

## CreateDispute

**POST {RootUrl}/CreateDispute**

AETERNA calls this endpoint on your platform to forward a chat message, an amount-change request, or to formally open a dispute. Applies to both PayIn and PayOut orders.

### Canonical String for Signature

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|CreateDispute
```

### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `system_order_id` (Req.) | String | Our request ID (GUID). |
| `external_order_id` (Req.) | String | Your internal request ID. |
| `comment` (Req.) | String | Message/dispute text. |
| `attachment_path` (Opt.) | String | Link to a screenshot/image (your platform may download it). |
| `type` (Req.) | String | Message type, snake_case: `message` (regular chat message), `change_amount` (request to change the order amount), or `dispute` (formally open a dispute). |
| `new_amount` (Opt.) | Double | New amount, required when `type = "change_amount"`. |

---

## Webhooks & Errors (PayIn)

### Callback Notifications

You send POST requests to the `callback_url` we passed in `CreateOrder` whenever a PayIn order's status or amount changes on your side.

#### Payload (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `system_order_id` (Req.) | String | Order ID (GUID), same value as in `CreateOrder`. |
| `status_id` (Opt.) | Integer | New status ID (1-13) if changed. |
| `amount` (Opt.) | Double | New amount if it was adjusted. |

At least one of `status_id` / `amount` must be present, otherwise AETERNA replies with `validation_error`.

#### Headers

- `Content-Type` — must be `application/json`.
- `X-Timestamp` — Unix time in UTC seconds. AETERNA accepts timestamps within the last 2 minutes only; values in the future are rejected with `timestamp_invalid_future`, values older than 2 minutes — with `timestamp_expired`.
- `X-Signature` — Your Base64 RSA-SHA256 signature of the canonical string below.

**Canonical string:**

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|AuctionCallback
```

#### AETERNA's Response — Success

On success, AETERNA replies with HTTP 200 and:

```json
{
  "is_success": true,
  "error_code": null,
  "error_message": null
}
```

#### AETERNA's Response — Failure

On any validation/auth failure, AETERNA replies with HTTP 200 and an error object — your platform should treat `is_success=false` as a delivery failure and retry the callback later:

```json
{
  "is_success": false,
  "error_code": "timestamp_expired",
  "error_message": "Timestamp is expired."
}
```

### General Error Codes (`external_auction_error_code`)

- `signature_missing`: Signature is missing.
- `signature_invalid`: Signature is invalid.
- `timestamp_invalid`: Incorrect timestamp format.
- `timestamp_invalid_future`: Timestamp is in the future relative to AETERNA's UTC clock.
- `timestamp_expired`: Timestamp is expired (older than 2 mins).
- `validation_error`: Request validation failed.
- `request_parameters_is_invalid`: Invalid request parameters.
- `order_not_found`: Order not found.
- `no_available_traders`: No available traders to execute the order.
- `unable_process_required_payment_system`: Payment system not supported.
- `unable_process_required_payment_method`: Payment method not supported.
- `all_payments_details_busy`: All payment details are currently busy.
- `too_low_commission`: Commission offered is too low.
- `too_high_exchange_rate`: Exchange rate offered is too high.
- `exists_same_amount_order`: An active order with the exact same amount already exists.
- `other`: Other generic error.

---

# Part 3. Payment Orchestrator (PayOut)

## PayOut Auction

PayOut Auction is a separate auction track for **outgoing payouts** (the merchant pays out fiat to an end user). It works similarly to PayIn auction, but with two key differences in the protocol:

- **Requisites are sent by AETERNA, not by the participant.** A PayOut bid contains only your proposed exchange rate and commission — the actual payout requisites (IBAN / card / phone / account) come *from* AETERNA *to the winner* in a separate `ApprovePayoutOrder` call.
- **Amount is fixed by AETERNA** and cannot be changed by participants — accordingly, there is no `amount` field in the bid response.

### End-to-End Flow

1. AETERNA broadcasts `CreatePayoutOrder` to all configured PayOut auction participants in parallel.
2. Each participant responds with `exchange_rate` and `commission` (no requisites) within 5 seconds.
3. AETERNA selects the best bid based on commission, rate and the participant's rating.
4. Losing participants receive `CancelPayoutOrder` with a reason describing why they were excluded.
5. The winner receives `ApprovePayoutOrder` containing the structured `requisites` object for the actual transfer.
6. `ApprovePayoutOrder` is retried up to **3 times** with a **10-second** interval if the winner returns `is_success=false`.
7. If all 3 attempts fail, AETERNA cancels the order automatically: order status becomes `8` (canceled by timeout), `CancelPayoutOrder` is sent to the winner with reason `other` and `reason_message="ApprovePayoutOrderFailed"`, and the merchant receives a cancellation callback.
8. The winner sends `PayoutAuctionCallback` notifications to AETERNA's `callback_url` as the order progresses (assigned, paid, completed, etc.).

### Endpoints to Implement on Your Side

| Method | Path | Description |
|--------|------|-------------|
| POST | `{RootUrl}/CreatePayoutOrder` | Bid with rate + commission for a PayOut. |
| POST | `{RootUrl}/CancelPayoutOrder` | Cancel a PayOut bid / approved order. |
| POST | `{RootUrl}/ApprovePayoutOrder` | Receive structured requisites for the winning PayOut and execute the transfer. |

`GetOrderStatus` and `CreateDispute` are shared with PayIn — see the corresponding sections.

---

## CreatePayoutOrder

**POST {RootUrl}/CreatePayoutOrder**

AETERNA broadcasts this request to all PayOut auction participants in parallel. You must respond with your proposed `exchange_rate` and `commission` within 5 seconds. **Do not** include payout requisites — they will arrive later in `ApprovePayoutOrder` if you win.

### Canonical String for Signature

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|CreatePayoutOrder
```

### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `system_order_id` (Req.) | String | Our request ID (GUID). |
| `currency` (Req.) | String | Fiat currency code (e.g., "RUB", "VES"). |
| `max_commission` (Req.) | Double | Maximum allowed commission (percent) at which your bid will be accepted. |
| `amount` (Req.) | Double | Fixed payout amount in fiat — cannot be altered by you. |
| `cancel_order_time_unix` (Req.) | Long | Unix timestamp (sec) when the order becomes irrelevant. |
| `stop_auction_time_unix` (Req.) | Long | Unix timestamp (sec) when the auction ends. |
| `callback_url` (Req.) | String | URL where AETERNA expects to receive your `PayoutAuctionCallback` notifications. |
| `allowed_payment_method` (Req.) | String | Allowed payment method, snake_case (see CreateOrder for the supported list). |
| `allowed_bank_name` (Opt.) | String | Specific bank allowed (optional). |

### Successful Response Example

```json
{
  "is_success": true,
  "error_code": null,
  "error_message": null,
  "external_order_id": "uuid-string",
  "exchange_rate": 75.25,
  "commission": 1.5
}
```

### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `is_success` (Req.) | Boolean | Result of processing. |
| `error_code` (Opt.) | String | Error code (see General Error Codes). |
| `error_message` (Opt.) | String | Error message text. |
| `external_order_id` (Req.) | String | Your internal order ID — referenced later in `ApprovePayoutOrder.external_order_id`. |
| `exchange_rate` (Req.) | Double | Your proposed rate. Must be greater than `0`. |
| `commission` (Req.) | Double | Your proposed commission (percent). Must be greater than or equal to `0`. |

> **Differences from PayIn `CreateOrder`:**
> - No `payment_details` in the response — requisites are delivered to the winner via `ApprovePayoutOrder`.
> - No `amount` field in the response — the participant cannot change the amount.
> - No `iterative_sum_search_enabled` in the request — irrelevant for PayOut.

### Response Validation

Even if you return `is_success=true`, AETERNA will reject the bid (sending `CancelPayoutOrder` with reason `not_valid_response`) when:

- `exchange_rate` is not greater than `0`.
- `commission` is negative.

---

## CancelPayoutOrder

**POST {RootUrl}/CancelPayoutOrder**

AETERNA calls this endpoint on your platform to cancel a PayOut bid or an approved PayOut order. The body shape and the list of `reason` codes are identical to PayIn CancelOrder.

### Canonical String for Signature

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|CancelPayoutOrder
```

### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `system_order_id` (Req.) | String | Our request ID (GUID). |
| `external_id` (Req.) | String | Your internal order ID returned in `CreatePayoutOrder`. |
| `reason` (Req.) | String | Cancellation reason code (snake_case). See PayIn CancelOrder for the full list. |
| `reason_message` (Opt.) | String | Free-form text explanation. Notably populated as `"ApprovePayoutOrderFailed"` when AETERNA cancels after 3 failed `ApprovePayoutOrder` attempts. |

### Successful Response Example

```json
{ "is_success": true }
```

---

## ApprovePayoutOrder

**POST {RootUrl}/ApprovePayoutOrder**

After the auction is decided, AETERNA calls `ApprovePayoutOrder` on the winner's side and passes the structured `requisites` object describing where to send the fiat payout. Your platform must initiate the actual transfer and respond with `is_success=true` as soon as the request has been accepted for processing on your side.

### Canonical String for Signature

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|ApprovePayoutOrder
```

### Request Body (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `system_order_id` (Req.) | String | Our request ID (GUID), same value as in `CreatePayoutOrder`. |
| `external_order_id` (Req.) | String | Your internal order ID returned in `CreatePayoutOrder`. |
| `requisites` (Req.) | Object | Structured payout requisites — see the table below. |

### Object `requisites`

| Field | Type | Description |
|-------|------|-------------|
| `payment_method` (Req.) | String | Payment method, snake_case: `iban`, `card_number`, `phone_number`, `account_number`, `sepa_transfer`, `sbp`, `quasi_ecom`, `c2becom`, `pago_movil`, `jazz_cash`, `easypaisa`, `bkash`, `nagad`, `upi`, `imps`, `fawry`. |
| `iban` (cond.) | String | Filled when `payment_method` is `iban` or `sepa_transfer`. |
| `card_number` (cond.) | String | Filled when `payment_method` is `card_number`, `quasi_ecom` or `c2becom`. |
| `phone_number` (cond.) | String | Filled when `payment_method` is `phone_number`, `sbp`, `pago_movil`, `jazz_cash`, `easypaisa`, `bkash` or `nagad`. |
| `account_number` (cond.) | String | Filled when `payment_method` is `account_number`, `upi`, `imps` or `fawry`. |
| `bank_name` (Opt.) | String | Bank name, when known. |
| `payer_name` (Opt.) | String | Recipient name (when provided by the merchant). |
| `comment` (Req.) | String | Original free-form merchant comment, preserved as-is. |

### Example `requisites` Object

```json
{
  "payment_method": "iban",
  "iban": "TR123456789012345678901234",
  "card_number": null,
  "phone_number": null,
  "account_number": null,
  "bank_name": "Ziraat Bankası",
  "payer_name": "John Doe",
  "comment": "TR123456789012345678901234 Ziraat Bankası"
}
```

### Response Body (JSON)

| Field | Type | Description |
|-------|------|-------------|
| `is_success` (Req.) | Boolean | Set to `true` to acknowledge the payout is accepted for processing on your side — this stops AETERNA's retries. `false` (or an unparsable body / non-2xx status) counts as a failed attempt. |
| `error_code` (Opt.) | String | Machine-readable error code (snake_case) when `is_success=false`; `null` on success. |
| `error_message` (Opt.) | String | Human-readable error detail (optional). |
| `external_order_id` (Opt.) | String | Optional echo of your internal order ID — informational only, AETERNA does not validate it. |

> Respond with **HTTP 200** and `is_success=true` to confirm. Any non-2xx status, an unparsable body, or `is_success=false` is treated as a failed attempt and triggers the next retry (after 3 failed attempts the order is canceled — see below).

### Successful Response Example

```json
{
  "is_success": true,
  "error_code": null,
  "error_message": null,
  "external_order_id": "uuid-string"
}
```

### Retry Mechanism

AETERNA retries `ApprovePayoutOrder` up to **3 times** with a fixed **10-second** delay between attempts. The retry stops as soon as any attempt returns `is_success=true`.

If all 3 attempts return `is_success=false` (or fail with a network error):

- The order's status is set to `8` (canceled by timeout).
- AETERNA sends you a `CancelPayoutOrder` with `reason="other"` and `reason_message="ApprovePayoutOrderFailed"`.
- The merchant receives a cancellation callback.

Every attempt — successful or not — is logged on AETERNA's side.

> **Implementation tip:** respond with `is_success=true` as soon as the payout has been queued for execution on your side, not after the actual fiat transfer is finalised. Final outcomes are delivered later via `PayoutAuctionCallback`.

---

## PayOut Callbacks

### Callback Notifications

The flow, payload shape, headers, success/failure response format and error codes for PayOut callbacks are **identical** to PayIn — see Webhooks & Errors (PayIn) for the full reference.

The only PayOut-specific difference is the **canonical string suffix**: PayOut callbacks must be signed with `PayoutAuctionCallback` instead of `AuctionCallback`.

### Canonical String for Signature (PayOut)

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|PayoutAuctionCallback
```

### Payload (JSON)

| Parameter | Type | Description |
|-----------|------|-------------|
| `system_order_id` (Req.) | String | Order ID (GUID), same value as in `CreatePayoutOrder`. |
| `status_id` (Opt.) | Integer | New status ID (1-13) if changed. |
| `amount` (Opt.) | Double | New amount, if it was adjusted. |

At least one of `status_id` / `amount` must be present, otherwise AETERNA replies with `validation_error`.

### Headers, Response Format, Error Codes

See Webhooks & Errors (PayIn) — the rules are the same. `Content-Type: application/json`, `X-Timestamp` within ±2 minutes (UTC seconds, future timestamps are rejected), `X-Signature` as Base64 RSA-SHA256 of the canonical string above.
