# Payment API

> Create and manage cryptocurrency payment sessions with the 2328.io Payment API.

The Payment API lets you create payment sessions, redirect customers to a hosted checkout, and track payment status.

## Create payment

Creates a payment session and returns a URL for the customer to pay.

### Request parameters

| Field | Type | Required | Description | Values |
|-------|------|----------|-------------|--------|
| `amount` | decimal | yes | Payment amount in the currency, e.g. `100.00` |  |
| `currency` | string | yes | Fiat currency (USD, EUR, RUB, …) or an enabled cryptocurrency. Use `/v1/directions` as the live source of truth. `TON` is accepted as a legacy input alias for canonical `GRAM`. | `USD`, `EUR`, `RUB`, `KZT`, `UAH`, `UZS`, `USDT`, `USDC`, `BTC`, `ETH`, `GRAM`, `SOL`, `TRX`, `BNB`, `POL`, `LTC`, `DASH`, `DOGE`, `ZEC`, `XRP`, `XMR` |
| `order_id` | string | yes | Your order ID, e.g. `ORDER-12345` (up to 128 chars) |  |
| `to_currency` | string | no | Preselected enabled cryptocurrency; `TON` is normalized to `GRAM` | `USDT`, `USDC`, `BTC`, `ETH`, `GRAM`, `SOL`, `TRX`, `BNB`, `POL`, `LTC`, `DASH`, `DOGE`, `ZEC`, `XRP`, `XMR` |
| `network` | string | no\* | Canonical network code (required if `to_currency` is set or `currency` is a cryptocurrency) | `TRX-TRC20`, `ETH-ERC20`, `BASE`, `BSC-BEP20`, `AVAX-C`, `POL-MATIC`, `TON`, `SOL`, `BTC`, `LTC`, `DASH`, `DOGE`, `ZEC`, `XRP`, `XMR` |
| `url_return` | string | no | Redirect URL after payment, e.g. `https://your-site.com/return` |  |
| `url_success` | string | no | Alternative to `url_return` |  |
| `url_callback` | string | yes | Public HTTP(S) URL for webhook notifications, e.g. `https://your-site.com/webhook`. Private, loopback, and otherwise unsafe targets are rejected. |  |
| `invite_code` | string | no | Referrer code |  |
| `fee_split` | decimal | no | Share of the merchant fee passed to the payer, 0–100 (%). 0 = merchant pays fully, 100 = payer pays fully. Overrides the project-level setting. **Example: `30`** (payer covers 30% of the fee). |  |
| `price_markup` | decimal | no | Markup or discount on the invoice amount, −99 to 100 (%). Overrides the project-level setting. **Example: `5`** (+5%) or `-10` (10% discount). |  |
| `description` | string | no | Optional invoice description (max 200 chars). Shown to the payer on the payment page. **Example: `Premium plan — Order #12345`**. |  |
| `ttl_seconds` | int | no | Invoice lifetime in seconds, from `300` (5 minutes) to `86400` (24 hours). After this period the invoice expires and can no longer be paid. Default: `3600` (1 hour). **Example: `3600`**. |  |

### Response

```json
{
  "state": 0,
  "result": {
    "uuid": "abc123-def456-...",
    "order_id": "ORDER-12345",
    "amount": "100.00",
    "currency": "USD",
    "amount_usd": "100.00",
    "exchange_rate": null,
    "url": "https://2328.io/pay/abc123-def456-...",
    "tg_deeplink": "https://t.me/my2328bot?start=pay_abc123-def456-...",
    "expires_at": "2026-01-11T21:00:00Z",
    "created_at": "2026-01-11T20:00:00Z",
    "payer_currency": "USDT",
    "payer_amount": "100.50",
    "network": "TRX-TRC20",
    "address": "TXYZabc123...",
    "payment_status": "check",
    "txid": null,
    "payment_amount": null,
    "qr": "data:image/png;base64,iVBORw0..."
  }
}
```

- Redirect the customer to `result.url` to complete payment.
- `tg_deeplink` — Telegram bot deeplink for payment via Telegram MiniApp.
- `qr` — Base64-encoded QR code (data URI) of the deposit address. Present when an address is already assigned (when `network` is set together with `to_currency`, or when `currency` is a cryptocurrency); otherwise `null`.
- `txid`, `payment_amount` — `null` until the customer pays. Filled in once the transaction is detected on-chain. Listen for the `payment_status: paid` webhook to know when.
- `exchange_rate` — `null` if conversion isn't applicable yet (e.g. fiat → crypto rate hasn't been locked). Filled in once a payer currency is chosen.

> Use your project UUID and the endpoint-appropriate API key from the merchant dashboard.

#### Interactive request: `POST /v1/payment`
  - `amount` (decimal, required)
  - `currency` (enum, required): USD,EUR,RUB,KZT,UAH,UZS,USDT,USDC,BTC,ETH,GRAM,SOL,TRX,BNB,POL,LTC,DASH,DOGE,ZEC,XRP,XMR
  - `order_id` (string, required)
  - `to_currency` (enum): USDT,USDC,BTC,ETH,GRAM,SOL,TRX,BNB,POL,LTC,DASH,DOGE,ZEC,XRP,XMR
  - `network` (enum): TRX-TRC20,ETH-ERC20,BASE,BSC-BEP20,AVAX-C,POL-MATIC,TON,SOL,BTC,LTC,DASH,DOGE,ZEC,XRP,XMR
  - `url_return` (string)
  - `url_success` (string)
  - `url_callback` (string)
  - `invite_code` (string)
  - `fee_split` (decimal)
  - `price_markup` (decimal)
  - `description` (string)
  - `ttl_seconds` (integer)

## Hosted checkout, H2H, and exact crypto amounts

The same endpoint supports three distinct invoice shapes. Pick one deliberately; do not mix their amount semantics.

### Hosted checkout with payer choice

Send `amount`, `currency`, `order_id`, and `url_callback`, but omit `to_currency` and `network`. The response contains `result.url`; `address`, `qr`, and sometimes payer fields remain `null` until the payer selects a direction on the hosted page.

```json
{
  "amount": "125.00",
  "currency": "EUR",
  "order_id": "ORDER-2026-1042",
  "url_callback": "https://merchant.example/webhooks/2328",
  "url_return": "https://merchant.example/orders/ORDER-2026-1042"
}
```

### Direct-address H2H invoice

Send both `to_currency` and `network`. 2328.io creates the blockchain invoice during the API call, so a successful response can be rendered inside your checkout without redirecting the customer.

```json
{
  "amount": "100.00",
  "currency": "USD",
  "to_currency": "USDT",
  "network": "TRX-TRC20",
  "order_id": "ORDER-2026-1043",
  "url_callback": "https://merchant.example/webhooks/2328"
}
```

Render these values exactly as returned:

- `payer_amount` and `payer_currency` — the payment instruction;
- `network` and `address` — the only destination for this invoice;
- `qr` — a data URI for the same address;
- `expires_at` — the invoice deadline;
- `url` — a useful hosted fallback when the custom checkout cannot complete.

> **DANGER:** Never generate or substitute an address, reuse an address from another invoice, or calculate `payer_amount` from a public spot price. The API response is authoritative.

### Invoice for an exact crypto amount

Put the cryptocurrency in `currency` when the invoice itself is denominated in crypto:

```json
{
  "amount": "25.000000",
  "currency": "USDT",
  "network": "TRX-TRC20",
  "order_id": "ORDER-2026-1044",
  "url_callback": "https://merchant.example/webhooks/2328"
}
```

The requested crypto value is preserved in `payer_currency` / `payer_amount`. The service can also maintain a USD valuation internally for accounting and rate fields; do not replace the exact crypto instruction with that valuation. Preserve returned decimal strings, including trailing precision.

For a cryptocurrency with only one supported network, the network may be selected automatically. Supplying `network` explicitly is still recommended for a deterministic integration. For multi-network assets such as stablecoins, always send it.

## Idempotency and retries

`order_id` is scoped to the authenticated merchant project and acts as the creation idempotency key. If a payment already exists, the API returns that session with `state: 0`.

> **WARNING:** A retry with the same `order_id` does **not** mean “update this invoice.” Changed amount, currency, callback, markup, TTL, or direction fields may be ignored because the existing session is returned. Persist the first request and reject conflicting retries in your own application.

Recommended creation algorithm:

1. Insert your local payment attempt and unique `order_id` in one database transaction.
2. Send the signed API request.
3. Persist the returned `uuid` and full response.
4. If the HTTP result is lost, retry the identical request or query `/v1/payment/info` by `order_id`.
5. Never create a second local order merely because the upstream request timed out.

## Payment edge cases

| Situation | Correct handling |
|-----------|------------------|
| `address` / `qr` is `null` | The payer direction has not been initialized. Redirect to `url`, or create a new correctly specified H2H invoice with a new `order_id`. |
| HTTP `400` validation error | Read the field-level `errors`; do not retry unchanged input. |
| HTTP `429` | Retry with jittered exponential backoff and keep the same `order_id`. |
| HTTP `503` / `direction_disabled` | Refresh `/v1/directions`; hide the direction temporarily or retry later. |
| Client request timeout | Treat the result as unknown. Query by `order_id` before creating anything else. |
| `underpaid_check` | Store the partial event and await a top-up or later status. Do not credit twice when more txids arrive. |
| `underpaid` | Final underpayment state. Apply your configured fulfillment/manual-review policy to the actual credited amount. |
| `overpaid` | Successful payment with excess funds. Fulfill idempotently and retain the actual amounts for reconciliation/refund policy. |
| `aml_lock` | Do not fulfill or release funds automatically; route to compliance/support workflow. |
| `cancel` | Invoice expired or was cancelled. Do not infer that a late on-chain transfer is impossible; reconcile any later event with support. |

The browser return URL is navigation only. A customer can open it without paying, close it after paying, or replay it later. Only a verified API/webhook state may settle the merchant order.

## Payment info

Get the current payment status by `uuid` or `order_id`.

### Request parameters

| Field | Type | Required | Description | Values |
|-------|------|----------|-------------|--------|
| `uuid` | string | yes\* | Payment UUID (from `result.uuid` on creation) |  |
| `order_id` | string | yes\* | Your order ID |  |

> **INFO:** At least one of `uuid` or `order_id` is required.

#### Interactive request: `POST /v1/payment/info`
  - `uuid` (string)
  - `order_id` (string)

## Payment list

Get a list of all payments with filtering and pagination.

### Request parameters

| Field | Type | Required | Description | Values |
|-------|------|----------|-------------|--------|
| `status` | string | no | Filter by list-supported payment status (see [References](/docs/references)). `aml_lock` can appear in payment info/webhooks but is not accepted by this list filter. | `pending`, `check`, `paid`, `underpaid_check`, `underpaid`, `overpaid`, `cancel` |
| `date_from` | date | no | Start date (YYYY-MM-DD), e.g. `2026-01-01` |  |
| `date_to` | date | no | End date (YYYY-MM-DD), e.g. `2026-01-31` |  |
| `page` | int | no | Page number, default `1` |  |
| `per_page` | int | no | Items per page, default `15`, max `5000` |  |

#### Interactive request: `POST /v1/payment/list`
  - `status` (enum): pending,check,paid,underpaid_check,underpaid,overpaid,cancel
  - `date_from` (string)
  - `date_to` (string)
  - `page` (integer)
  - `per_page` (integer)