# Convert API

> Convert between cryptocurrencies directly from your merchant balance — get a live quote and execute at market price.

The Convert API lets you swap between the currencies held in your merchant balance at the current market price — the same engine that powers the **Swap** tab in the merchant dashboard, now callable from your backend.

> **WARNING:** Convert endpoints are signed with your **regular API key** — the same one used for [Payment API](/docs/payments) requests, **not** the Payout API key. Executing a convert immediately debits and credits your merchant balance, so treat this key with the same care as any money-moving credential.

## Get conversion price

Returns an indicative quote for a conversion at the current market price — the effective rate and the resulting amounts. Nothing is debited or reserved; call it as often as you need before executing.

`POST /v1/convert/price`

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

#### Interactive request: `POST /v1/convert/price`
  - `from_currency` (enum, required): BTC,ETH,USDT,USDC,TRX,BNB,GRAM,SOL,POL,LTC,DASH,DOGE,ZEC,XRP,XMR
  - `to_currency` (enum, required): USDT,USDC,BTC,ETH,TRX,BNB,GRAM,SOL,POL,LTC,DASH,DOGE,ZEC,XRP,XMR
  - `amount` (decimal, required)
  - `amount_type` (enum, required): from,to

### Request parameters

| Field | Type | Required | Description | Values |
|-------|------|----------|-------------|--------|
| `from_currency` | string | yes | Uppercase source asset code. A usable route and balance must exist; the displayed catalog is not a guarantee of an active market. | `BTC`, `ETH`, `USDT`, `USDC`, `TRX`, `BNB`, `GRAM`, `SOL`, `POL`, `LTC`, `DASH`, `DOGE`, `ZEC`, `XRP`, `XMR` |
| `to_currency` | string | yes | Uppercase target asset code; must differ from `from_currency` and have a tradable direct or bridge route | `USDT`, `USDC`, `BTC`, `ETH`, `TRX`, `BNB`, `GRAM`, `SOL`, `POL`, `LTC`, `DASH`, `DOGE`, `ZEC`, `XRP`, `XMR` |
| `amount` | decimal | yes | Amount to convert, greater than `0` |  |
| `amount_type` | string | yes | Which side `amount` refers to | `from`, `to` |

> **INFO:** `amount_type=from` spends exactly `amount` of `from_currency`. `amount_type=to` receives exactly `amount` of `to_currency`.

**🟢 200 OK** · `application/json`

```json
{
  "state": 0,
  "result": {
    "success": true,
    "from_currency": "BTC",
    "to_currency": "USDT",
    "amount_type": "from",
    "from_amount": "0.01000000",
    "to_amount": "947.86690000",
    "effective_rate": "94786.69000000",
    "from_amount_usd": "947.87",
    "to_amount_usd": "947.87"
  }
}
```

#### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Whether the quote was computed successfully |
| `from_currency` | string | Source currency |
| `to_currency` | string | Target currency |
| `amount_type` | string | Echoes the request's `amount_type` |
| `from_amount` | string | Amount that would be debited in `from_currency` |
| `to_amount` | string | Amount that would be credited in `to_currency` |
| `effective_rate` | string | Rate applied to this quote — 1 unit of `from_currency` in `to_currency` (already includes the platform's pricing) |
| `from_amount_usd` | string \| null | USD equivalent of `from_amount` |
| `to_amount_usd` | string \| null | USD equivalent of `to_amount` |

- The quote is **indicative only** — the market price can move between the quote and the execute call.
- No balance is debited or reserved by this call.

## Execute convert

Executes a conversion at the current market price and updates your merchant balance. There is no separate "commit a quote" step — call this directly with the amount you want to convert.

`POST /v1/convert`

#### Interactive request: `POST /v1/convert`
  - `from_currency` (enum, required): BTC,ETH,USDT,USDC,TRX,BNB,GRAM,SOL,POL,LTC,DASH,DOGE,ZEC,XRP,XMR
  - `to_currency` (enum, required): USDT,USDC,BTC,ETH,TRX,BNB,GRAM,SOL,POL,LTC,DASH,DOGE,ZEC,XRP,XMR
  - `amount` (decimal, required)
  - `amount_type` (enum, required): from,to

> **INFO:** **Idempotency.** Repeating the exact same request (same `from_currency`, `to_currency`, `amount`, `amount_type`) within about a minute of the first call returns the existing conversion instead of creating a second one. Once that window passes, an identical request is treated as a new conversion — don't blindly retry on a timeout without checking the previous result first.

> **WARNING:** This endpoint is limited to **10 requests per minute** per caller — tighter than the general API rate limit — because every call moves real balance.

### Request parameters

| Field | Type | Required | Description | Values |
|-------|------|----------|-------------|--------|
| `from_currency` | string | yes | Uppercase source asset code. A usable route and balance must exist. | `BTC`, `ETH`, `USDT`, `USDC`, `TRX`, `BNB`, `GRAM`, `SOL`, `POL`, `LTC`, `DASH`, `DOGE`, `ZEC`, `XRP`, `XMR` |
| `to_currency` | string | yes | Uppercase target asset code; must differ from `from_currency` and have a tradable direct or bridge route | `USDT`, `USDC`, `BTC`, `ETH`, `TRX`, `BNB`, `GRAM`, `SOL`, `POL`, `LTC`, `DASH`, `DOGE`, `ZEC`, `XRP`, `XMR` |
| `amount` | decimal | yes | Amount to convert, greater than `0` |  |
| `amount_type` | string | yes | Which side `amount` refers to | `from`, `to` |

**🟢 200 OK** · `application/json`

```json
{
  "state": 0,
  "result": {
    "id": 12345,
    "type": "manual",
    "status": "completed",
    "from_currency": "BTC",
    "to_currency": "USDT",
    "from_amount": "0.01000000",
    "requested_from_amount": "0.01000000",
    "refund_amount": null,
    "to_amount": "947.86690000",
    "exchange_rate": "94786.69000000",
    "fee_amount": "0.00000000",
    "from_amount_usd": "947.87",
    "to_amount_usd": "947.87",
    "processed_at": "2026-01-20T15:30:24Z",
    "created_at": "2026-01-20T15:30:22Z"
  }
}
```

#### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Convert order ID assigned by the system |
| `type` | string | Always `manual` for this API |
| `status` | string | Current status (see Convert statuses below) |
| `from_currency` | string | Source currency |
| `to_currency` | string | Target currency |
| `from_amount` | string | Amount debited in `from_currency` |
| `requested_from_amount` | string \| null | Your originally requested source amount when `amount_type = from`. `null` when `amount_type = to` |
| `refund_amount` | string \| null | Portion of the pre-debited amount refunded back to you after a partial fill. `null` when the order filled completely |
| `to_amount` | string | Amount credited in `to_currency` |
| `exchange_rate` | string | Rate actually applied to this conversion, 1 unit of `from_currency` in `to_currency` (already includes the platform's pricing) |
| `fee_amount` | string | Platform fee charged on this conversion, denominated in `from_currency` or `to_currency` depending on trade direction. Already reflected in `exchange_rate` — shown for transparency |
| `from_amount_usd` | string \| null | USD equivalent of `from_amount` |
| `to_amount_usd` | string \| null | USD equivalent of `to_amount` |
| `processed_at` | string (ISO 8601) \| null | When the conversion finished executing. `null` while still processing |
| `created_at` | string (ISO 8601) | When the conversion order was created |

#### Convert statuses

| Status | Description |
|--------|-------------|
| `pending` | Created, not yet sent to the market |
| `processing` | Balance locked and the order placed on the market |
| `completed` | Fully executed — `to_amount` has been credited to your balance |
| `failed` | Could not execute — any pre-debited amount was automatically refunded |
| `partially_completed` | Only for currency pairs with no direct market (routed through an intermediate currency): the first leg completed but the second failed. You're credited the intermediate currency instead of `to_currency` — convert again from there to reach your original target |

## Errors

On failure the response has `state: 1` and an `error_code` — shared by both `/v1/convert/price` and `/v1/convert`:

**🔴 422 / 400** · `application/json`

```json
{
  "state": 1,
  "error_code": "amount_too_small",
  "errors": {
    "amount": "Amount is too small for this conversion. Please increase the amount and try again."
  }
}
```

| `error_code` | HTTP status | Description |
|--------------|-------------|-------------|
| `validation_failed` | 422 | Invalid or missing parameters, or a business-rule rejection (e.g. insufficient balance) — see the `errors` field for details |
| `amount_too_small` | 422 | `amount` is below the minimum tradable size for this currency pair |
| `convert_unavailable` | 400 | The conversion couldn't be executed right now (market data unavailable or no route between the two currencies) — retry shortly |
| `internal_error` | 400 | Unexpected server error while processing the request |

## Automatic conversion of incoming payments

Auto-convert is a project setting for incoming invoice and static-wallet credits. It is configured in the merchant dashboard, not by adding fields to `/v1/payment`. Each rule selects one or more source currencies and a target currency.

When conversion completes, payment info and merchant webhooks can include:

```json
{
  "payment_amount": "0.14800000",
  "merchant_amount": "0.146520000000000000",
  "payer_currency": "XMR",
  "convert": {
    "to_currency": "USDT",
    "commission": "0.09000000",
    "rate": "323.21000000",
    "amount": "47.262015740000000000"
  }
}
```

The amount domains are intentionally separate:

- `payment_amount` — what was detected on-chain in the source payment currency;
- `merchant_amount` — the net source amount attributable to the merchant before conversion;
- `convert.amount` — the amount credited in `convert.to_currency`;
- `convert.rate` and `convert.commission` — the executed conversion result, not a price you should recompute locally.

> **WARNING:** The absence of `convert` is meaningful: conversion may not have completed, may not be configured for that source, or may have fallen back to source-currency credit. Never invent a target amount from `/exchange-rates` or a public market price.

### Auto-convert failure and fallback

Conversion is downstream of receiving the blockchain payment. Market availability, minimum order sizes, precision limits, exchange timeouts, and insufficient executable liquidity can delay or prevent conversion.

- Deposits below the global/project minimum bypass the conversion pipeline and credit the source currency.
- Transient failures can be retried asynchronously.
- Large or untradeable deposits can fall back to a source-currency credit after the retry policy is exhausted.
- A payment can therefore be valid even when the desired target-currency conversion did not occur.

Your integration should persist the verified payment first, then reconcile the actual credited currency from payment info, the optional `convert` block, and merchant balances. Do not block acknowledgement of the payment webhook while waiting on your own analytics or notification systems.

### Auto-convert acceptance tests

Test at least: successful direct conversion, bridge/multi-hop conversion, dust below minimum, transient retry, fallback to source currency, underpayment, overpayment, duplicate webhook, missing `convert`, and reconciliation after an ambiguous timeout.

## Manual conversion edge cases

- `/v1/convert/price` is an indicative preview; market movement can change the execution result.
- `amount_type: from` fixes the source-side request, while `amount_type: to` requests a target-side amount. Do not swap the meaning when presenting confirmation UI.
- A pair without a direct market can be routed through an intermediate currency. If only one leg completes, `partially_completed` reports the intermediate credit.
- If an execute call times out, reconcile before retrying. A market order can execute even when its HTTP response is lost.
- Treat `failed` as a state to reconcile, not as permission to apply a local compensating balance entry; the platform owns debit/refund accounting.