# Payments

A payment is one request for money from one of your customers: an amount in
US dollars (or [in your own currency](#prices-in-other-currencies), converted
into dollars), the coin they pay with, and a deposit address only this payment
uses. Your server creates it with your secret key, sends the customer to pay,
and learns the outcome from [webhooks](/docs/webhooks).

| Method | Path | What it does |
|---|---|---|
| `POST` | [`/v1/payments`](#create-a-payment) | Create a payment (or get back the open payment of the same order) |
| `POST` | [`/v1/payments/{id}/token`](#choose-the-coin-later) | Set the coin of a payment created without one |
| `POST` | [`/v1/payments/{id}/simulate`](#simulate-a-test-payment) | Test mode: play an outcome on a test payment |
| `GET` | [`/v1/payments/{id}`](#retrieve-a-payment) | Read one payment, by payment id or by your order id |
| `GET` | [`/v1/payments`](#list-payments) | List payments, newest first |
| `GET` | [`/v1/tokens`](#list-tokens) | The coins your customers can pay with, with today's fees and minimums |

All of them take your API key (`Authorization: Bearer <key>`, see
[Authentication](/docs/authentication)). The key's mode (test or live) decides
which payments you create and see.

## Lifecycle

```
pending ─────────────────────────┐
   │                             ▼
   ├──▶ partially_paid ──▶ detecting ──▶ confirmed
   │          │                 │
   ▼          ▼                 ▼
expired    underpaid          failed
   │          │
   └────┬─────┘
        ▼   the rest arrives within 24 hours
    detecting ──▶ confirmed   ("paid_late": true)
```

| Status | Meaning | Webhook sent when the payment enters it | What you do |
|---|---|---|---|
| `pending` | Created. Waiting for the customer to choose a coin (if you did not) and to pay | none (no event is sent at creation) | Send the customer to pay |
| `partially_paid` | Less than the amount due arrived, and there is still time to send the rest | `payment.partially_paid` | Optionally tell the customer |
| `detecting` | Enough arrived. Waiting for the network's confirmations | `payment.detecting` | Show "payment received, confirming". **Do not ship yet** |
| `confirmed` | Final. The money is yours | `payment.confirmed` | **Fulfil the order** |
| `expired` | The deadline passed and nothing counted arrived | `payment.expired` | Cancel the order, or create a new payment |
| `underpaid` | The deadline passed with only part of the amount. What arrived is still settled to you | `payment.underpaid` | Refund the customer, or agree on the rest |
| `failed` | The funds were seen but never became final (dropped from the chain, or not confirmed in time) | `payment.failed` | Treat as unpaid |

Three more events report deposits rather than status changes:
`payment.wrong_asset` (a deposit in another coin than the payment asked for),
`payment.late_deposit` (a deposit after the payment closed) and, with direct
settlement, `settlement.completed` / `settlement.failed` for each deposit's
money. See [Webhooks](/docs/webhooks#events).

How the payment moves:

- **pending → partially_paid** when a deposit arrives but the total is short of
  `amount_due_usd` (beyond your [tolerance](#when-the-customer-pays-the-wrong-amount)).
- **pending or partially_paid → detecting** when the total covers
  `amount_due_usd`.
- **detecting → confirmed** when every deposit that counts has its
  confirmations ([Currencies](/docs/currencies) lists how many).
- **detecting → failed** when a deposit it relied on is not found on chain
  within 6 hours, or not confirmed within 48 hours.
- **pending → expired** and **partially_paid → underpaid** at `expires_at`.
  A payment in `detecting` never expires.
- **expired or underpaid → detecting → confirmed** when the customer pays the
  rest within 24 hours of closing ([late payments](#late-payments)); the
  payment then has `"paid_late": true`.

Only `confirmed` means you have been paid. Never fulfil an order from
`detecting`, from a customer landing on your `success_url`, or from anything
the browser says.

## The payment object

Every endpoint on this page, and every `payment.*` webhook, returns this
object. Here is a live payment that has been paid and confirmed:

```json
{
  "id": "8f3a2c1e9b7d4a6f05e1c2d3",
  "merchant_id": "3f8a1c27b94e05d6a2c71e90",
  "order_id": "ORD-1001",
  "status": "confirmed",
  "deposit_address": "TNVfZ8m3qR6xW2pL9kC4hJ7bD5sYt1uAeG",
  "token": "USDT_TRC20",
  "chain": "tron",
  "livemode": true,
  "crypto_amount": "49.9",
  "received_amount": "49.9",
  "usd_amount": "49.9",
  "amount_due_usd": "49.9",
  "fees_paid_by_customer": false,
  "network_fee": "6.019231",
  "network_fee_usd": "6.03",
  "settlement": "direct",
  "received_usd": "49.9",
  "rate_locked": "1",
  "tx_id": "c192401935e4ac25bb5f7cdba7443ada0e097cfdb537710236bf7d98c60851bd",
  "deposits": [
    {
      "tx_id": "c192401935e4ac25bb5f7cdba7443ada0e097cfdb537710236bf7d98c60851bd",
      "token": "USDT_TRC20",
      "chain": "tron",
      "amount": "49.9",
      "usd_value": "49.9",
      "counted": true,
      "status": "confirmed",
      "seen_at": "2026-10-10T09:03:02.418Z",
      "confirmed_at": "2026-10-10T09:04:03.77Z",
      "from": "TL8wHk3sD9pQ2vX6mB4nJ7cR5fY1tGzEaW",
      "block": 79412538,
      "made_at": "2026-10-10T09:02:57Z"
    }
  ],
  "expires_at": "2026-10-10T09:15:00.112Z",
  "created_at": "2026-10-10T09:00:00.112Z",
  "confirmed_at": "2026-10-10T09:04:03.77Z",
  "network_fee_note": "Final in about a minute.",
  "qr_payload": "TNVfZ8m3qR6xW2pL9kC4hJ7bD5sYt1uAeG",
  "checkout_token": "6f1e2a9c4d7b3e05a8c1f2d4",
  "checkout_url": "https://pay.example.com/pay/6f1e2a9c4d7b3e05a8c1f2d4",
  "description": "2 x Acme T-shirt",
  "metadata": { "customer_id": "cus_42" },
  "customer_email": "jane@example.com",
  "success_url": "https://acme.example/thanks",
  "cancel_url": "https://acme.example/cart",
  "webhook_url": "https://acme.example/payitgo/orders/ipn"
}
```

Amounts are **decimal strings** (`"49.9"`, `"0.00117"`), so no precision is
lost: parse them with a decimal type, not a float. Times are RFC 3339 in UTC;
the number of fractional digits varies, so use a real RFC 3339 parser.
Fields marked "optional" below are left out of the JSON when they have no
value.

| Field | Type | Description |
|---|---|---|
| `id` | string | payitgo's id for the payment: 24 hex characters. Treat it as an opaque string |
| `merchant_id` | string | Your account's id |
| `store_id` | string, optional | The [store](/docs/account#stores) whose key created the payment |
| `order_id` | string | Your order id, as you sent it (trimmed) |
| `status` | string | Where the payment is in its [lifecycle](#lifecycle) |
| `token_options` | array of [TokenInfo](#the-tokeninfo-object), optional | Only while `token` is `""`: the coins the customer may choose from, each with its minimum and fee. Gone once a coin is chosen |
| `deposit_address` | string | Where the customer sends the money. Used by this payment only, except on XRP (see `destination_tag`). `""` until a coin is chosen |
| `destination_tag` | integer, optional | XRP only: the tag the customer **must** include, which tells payments apart on the shared XRP address |
| `token` | string | The coin to pay with, such as `USDT_TRC20` ([Currencies](/docs/currencies)). `""` until chosen |
| `chain` | string | The token's network: `tron`, `ethereum`, `bsc`, `bitcoin`, `litecoin`, `dogecoin` or `xrp`. `""` until chosen |
| `livemode` | boolean | `false` for a test payment (no coins: paid by the outcome you [simulate](/docs/testing)), `true` for a real one |
| `simulation` | object, optional | A test payment's [simulation](/docs/testing#simulate-a-payment), once one is asked for: `outcome` (such as `confirmed`) and `state`: `requested` (asked for on create, waiting for the coin), `running` or `done` |
| `crypto_amount` | decimal string | **What the customer must send**, in `token`: `amount_due_usd` ÷ `rate_locked`, rounded **up** to the token's smallest unit. `"0"` until a coin is chosen |
| `received_amount` | decimal string | What has arrived so far in the payment's own `token` (late deposits included, other coins not) |
| `usd_amount` | decimal string | Your price in US dollars, as you sent it in `amount_usd`, or converted from `price_amount` |
| `price_amount` | decimal string, optional | Your price in your own currency, as you sent it ([prices in other currencies](#prices-in-other-currencies)). Absent for a payment priced in dollars |
| `price_currency` | string, optional | Its currency, the ISO 4217 code in upper case: `EUR`. Absent for a payment priced in dollars |
| `price_rate` | decimal string, optional | The dollar value of one unit of `price_currency` when the payment was created: `usd_amount` is `price_amount` × `price_rate`, rounded to cents. Absent for a payment priced in dollars |
| `amount_due_usd` | decimal string | What the customer pays, in dollars: `usd_amount`, or more when you pass the fees on (`fees_paid_by_customer`) |
| `extra_usd` | decimal string, optional | What of `amount_due_usd` the customer pays only to reach the exchange's minimum (the payment too small for its coin's conversion, the fees passed on): you receive it |
| `fees_paid_by_customer` | boolean | Whether the customer pays the service and network fees on top of your price ([Fees and settlement](/docs/settlement#passing-the-fees-on-to-the-customer)) |
| `network_fee` | decimal string | The network fee quoted for moving this payment's money, in `token`, with what converting it costs when it is converted ([fees](/docs/settlement#fees)). `"0"` until a coin is chosen, or when it could not be priced (it is then priced when the deposit settles) |
| `network_fee_usd` | decimal string | The same fee in dollars |
| `settlement` | string | Where the money goes once final: `direct` (your own payout address) or `custodial` (your payitgo balance). Your account setting when the payment was created ([Fees and settlement](/docs/settlement)) |
| `received_usd` | decimal string | The dollar value of every deposit that counts toward the payment, rounded to cents |
| `rate_locked` | decimal string | The USD price of one unit of `token`, locked when the coin was chosen. `"1"` for stablecoins. `"0"` until a coin is chosen |
| `tx_id` | string, optional | The latest deposit's transaction id |
| `overpaid` | boolean, optional | `true` when more than 1% over `amount_due_usd` arrived |
| `paid_late` | boolean, optional | `true` when the payment was completed after it had expired or fallen short |
| `deposits` | array of [Deposit](#the-deposit-object) | Every transfer to the payment, oldest first. `[]` until something arrives |
| `expires_at` | string | The customer's deadline: 15 minutes after the coin was set. Created without a coin, the customer has 1 hour to choose one, then 15 minutes to pay |
| `created_at` | string | When the payment was created |
| `confirmed_at` | string, optional | When the payment became final |
| `network_fee_note` | string | A short line for the customer about the network's fees and timing. `""` until a coin is chosen |
| `qr_payload` | string | What to encode in a QR code: the deposit address alone ([QR codes](#qr-codes)). `""` until a coin is chosen |
| `checkout_token` | string | The public key to the payer's view of this payment ([Checkout](/docs/checkout)). Not included in webhooks |
| `checkout_url` | string, optional | The hosted checkout page for this payment. Absent when the gateway has no hosted checkout configured |
| `token_name`, `network` | string, optional | The coin and its network for people: `"Tether"`, `"Tron (TRC-20)"`. Absent until a coin is chosen |
| `confirmations` | integer | The latest deposit's confirmations when last checked; `0` before any deposit |
| `confirmations_required` | integer | How many make a deposit final on the payment's network (`0` until a coin is chosen) |
| `explorer_url` | string, optional | `tx_id` on the network's block explorer. Absent for a test payment |
| `detected_at` | string, optional | When the first deposit was seen |
| `opened_at` | string, optional | When the hosted checkout was first opened |
| `channel` | string | Where the customer paid from: `mobile` or `desktop` (the hosted checkout, by browser), `api` (paid without opening it: your own page), `""` while neither happened |
| `link_id` | string, optional | The payment link the payment was made through, when it was |
| `customer_id` | string, optional | The customer of your account that `customer_email` is filed under (the same for every payment to that email, in any case) |
| `description` | string, optional | As you sent it |
| `metadata` | object, optional | As you sent it |
| `customer_email` | string, optional | As you sent it |
| `success_url` | string, optional | As you sent it |
| `cancel_url` | string, optional | As you sent it |
| `webhook_url` | string, optional | As you sent it: this payment's notifications go there instead of the account's webhook URL ([Webhooks](/docs/webhooks#per-payment-webhook-url)) |

### The deposit object

Each entry of `deposits` is one transfer to the payment's address (or its
destination tag on XRP):

| Field | Type | Description |
|---|---|---|
| `tx_id` | string | The transaction id on its network. A simulated deposit's (`simulated: true`) looks like one but is on no chain |
| `token` | string | The asset that arrived. Usually the payment's `token`; another one for a [wrong-asset](#when-the-customer-pays-with-the-wrong-asset) deposit |
| `chain` | string | The network it arrived on |
| `amount` | decimal string | How much arrived, in `token` |
| `usd_value` | decimal string | Its dollar value: at `rate_locked` for the payment's own token, at the market price when it arrived for another asset |
| `counted` | boolean | Whether it counts toward the payment. Always `true` for the payment's own token; for another asset only with `accept_other_assets` on |
| `confirmations` | integer | How many confirmations it had when last checked; a final deposit shows at least `confirmations_required` |
| `confirmations_required` | integer | How many make it final on its network |
| `explorer_url` | string, optional | `tx_id` on the network's block explorer. Absent for a test payment's deposit |
| `simulated` | boolean, optional | `true` for a test payment's deposit, played by its [simulation](/docs/testing#what-a-simulated-payment-looks-like): no coins moved |
| `late` | boolean, optional | `true` when it arrived after the payment had closed |
| `status` | string | `pending` (confirming), `confirmed` (final; settled to you) or `failed` (never became final; nothing settled) |
| `seen_at` | string | When payitgo first saw it |
| `confirmed_at` | string, optional | When it became final |
| `from` | string, optional | The sender's address, when the network reports it (for Bitcoin, Litecoin and Dogecoin, the first input's). Where a refund would go |
| `block` | integer, optional | The block (on XRP, the ledger) that holds it |
| `made_at` | string, optional | That block's time |
| `failed_at`, `fail_reason` | string, optional | When and why it failed: `transaction not found on chain` or `confirmation timeout` |

Every deposit that becomes final is settled to you (the parts of a payment
paid in parts together, once it closes: [one fee](/docs/settlement)), counted
or not, less the
fees ([Fees and settlement](/docs/settlement)).

### QR codes

`qr_payload` is the deposit address alone, on every network: the same as
`deposit_address`. Show it as a QR code next to the address and the amount.

It carries no amount, token contract or XRP destination tag, on purpose:
exchange apps such as Binance, where many customers pay from, reject payment
URIs (`ethereum:…`, `bitcoin:…`, `xrpl:…`) as a malformed address. The
customer types the amount, and for XRP the tag, so show both as text with
copy buttons.

### Amounts: what is asked, what is due, what arrived

| Field | In | Meaning |
|---|---|---|
| `usd_amount` | USD | Your price, as you sent it in `amount_usd` (or `price_amount` converted at `price_rate`) |
| `price_amount` | your currency | Your price in `price_currency`, when you gave it in another currency than the dollar. Shown, never paid in: everything else is in dollars |
| `amount_due_usd` | USD | What the customer must pay: equal to `usd_amount`, unless you pass the fees on |
| `rate_locked` | USD per coin | The exchange rate, fixed for this payment when the coin was set |
| `crypto_amount` | coin | `amount_due_usd ÷ rate_locked`, rounded up: the exact amount to show the customer |
| `received_amount` | coin | The payment's own coin received so far |
| `received_usd` | USD | Everything that counts so far, in dollars |
| `network_fee`, `network_fee_usd` | coin, USD | The fee quoted for moving the money out of the deposit address, and for converting it when it is converted. It is taken from what arrives, and is not part of `amount_due_usd` unless you pass the fees on |

For a BTC or ETH payment, read the dollars from `amount_due_usd`,
`received_usd` and each deposit's `usd_value`; `rate_locked` is the dollar
price of one coin. Settlements and payouts carry their dollar values too
([Amounts in dollars](/docs/webhooks#amounts-in-dollars)).

A payment counts as paid when `received_usd` reaches `amount_due_usd`, less
your [underpayment tolerance](#when-the-customer-pays-the-wrong-amount).

With `fees_paid_by_customer` on, `amount_due_usd` is higher than
`usd_amount`, so you receive your full price after the fees. A $100.00 order
in `USDT_TRC20` with a $1.39 network fee and the 1% service fee asks the
customer for $102.42 ([the formula](/docs/settlement#passing-the-fees-on-to-the-customer)).

## Create a payment

`POST /v1/payments` · API key

Creates a payment for one of your orders. With a `token`, the payment gets
its deposit address, locked rate and 15-minute deadline at once. Without
one, the customer chooses the coin on the checkout page first.

### Parameters

The body is JSON; send `Content-Type: application/json` (without it the body
is not read, and you get `order_id is required`).

| Field | Type | Required | Description |
|---|---|---|---|
| `order_id` | string | yes | Your id for the order, 1 to 128 bytes (spaces around it are trimmed). An order has at most one open payment per mode and per store: see [safe retries](#safe-retries) |
| `amount_usd` | number or decimal string | yes, unless `price_amount` + `price_currency` | The price in US dollars, more than 0: `49.90` or `"49.90"`. It must reach the token's [minimum](#list-tokens) |
| `price_amount` | number or decimal string | with `price_currency` | Instead of `amount_usd`: the price in another currency, more than 0: `45.50` or `"45.50"`. Converted into `amount_usd` ([prices in other currencies](#prices-in-other-currencies)) |
| `price_currency` | string | with `price_amount` | The ISO 4217 code of `price_amount`'s currency, such as `EUR`, `GBP` or `NGN` (case does not matter). `USD` works too |
| `token` | string | no | The coin to pay with, from [`GET /v1/tokens`](#list-tokens), such as `USDT_TRC20` (case does not matter). Leave it out to let the customer choose |
| `tokens` | array of strings | no | Without `token`: the coins to offer the customer, such as `["USDT_TRC20", "USDT_BEP20"]`. Default: every token you accept whose minimum the amount reaches. Ignored when `token` is set |
| `description` | string | no | What the customer is paying for, up to 500 bytes. Shown on the hosted checkout |
| `metadata` | object | no | Your own data: up to 20 keys, string values only. Keys up to 40 bytes, values up to 500 |
| `customer_email` | string | no | The customer's email address. Must be a valid address |
| `success_url` | string | no | An `http(s)` URL the hosted checkout links to once the payment is confirmed |
| `cancel_url` | string | no | An `http(s)` URL the hosted checkout links back to if the payment closes unpaid |
| `webhook_url` | string | no | An `https://` URL on a public host. This payment's notifications (its `payment.*` and `settlement.*` events) go here **instead of** your account's webhook URL, signed with the same secret. See [Webhooks](/docs/webhooks#per-payment-webhook-url) |
| `simulate` | string | no | Test keys only: the outcome the payment plays, such as `confirmed` ([outcomes](/docs/testing#outcomes)). With `token` (or for `expired`) it starts at once; without, when the coin is chosen. `wrong_asset` needs `token`. A live key gets `400` |

Give the price as `amount_usd`, or as `price_amount` and `price_currency`
together: not both. `token` and `tokens` may only name tokens you accept
([accepted tokens](/docs/account#accepted-tokens)).

### Request

```bash
curl -s https://api.payitgo.co/v1/payments \
  -H "Authorization: Bearer $PAYITGO_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "order_id": "ORD-1001",
    "amount_usd": "49.90",
    "token": "USDT_TRC20",
    "description": "2 x Acme T-shirt",
    "metadata": {"customer_id": "cus_42"},
    "customer_email": "jane@example.com",
    "success_url": "https://acme.example/thanks",
    "cancel_url": "https://acme.example/cart"
  }'
```

### Response

`201 Created` with the new [payment](#the-payment-object) (a test key, so
`livemode` is `false`):

```json
{
  "id": "8f3a2c1e9b7d4a6f05e1c2d3",
  "merchant_id": "3f8a1c27b94e05d6a2c71e90",
  "order_id": "ORD-1001",
  "status": "pending",
  "deposit_address": "TNVfZ8m3qR6xW2pL9kC4hJ7bD5sYt1uAeG",
  "token": "USDT_TRC20",
  "chain": "tron",
  "livemode": false,
  "crypto_amount": "49.9",
  "received_amount": "0",
  "usd_amount": "49.9",
  "amount_due_usd": "49.9",
  "fees_paid_by_customer": false,
  "network_fee": "3.212009",
  "network_fee_usd": "3.22",
  "settlement": "direct",
  "received_usd": "0",
  "rate_locked": "1",
  "deposits": [],
  "expires_at": "2026-10-10T09:15:00.112517888Z",
  "created_at": "2026-10-10T09:00:00.111798165Z",
  "network_fee_note": "Final in about a minute.",
  "qr_payload": "TNVfZ8m3qR6xW2pL9kC4hJ7bD5sYt1uAeG",
  "checkout_token": "6f1e2a9c4d7b3e05a8c1f2d4",
  "checkout_url": "https://pay.example.com/pay/6f1e2a9c4d7b3e05a8c1f2d4",
  "description": "2 x Acme T-shirt",
  "metadata": { "customer_id": "cus_42" },
  "customer_email": "jane@example.com",
  "success_url": "https://acme.example/thanks",
  "cancel_url": "https://acme.example/cart"
}
```

Send the customer to `checkout_url`, or show `crypto_amount`, `token`,
`deposit_address` and `expires_at` on your own page
([Checkout](/docs/checkout)).

### Let the customer choose the coin

Send only the order and the amount:

```bash
curl -s https://api.payitgo.co/v1/payments \
  -H "Authorization: Bearer $PAYITGO_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"order_id": "ORD-1002", "amount_usd": 25}'
```

`201 Created`. The payment has no coin and no address yet. `token_options`
lists what the customer may choose from (shortened here to three of the
twelve tokens), and `expires_at` is one hour away:

```json
{
  "id": "c41e7a9d2b05f3e8a6d17c90",
  "merchant_id": "3f8a1c27b94e05d6a2c71e90",
  "order_id": "ORD-1002",
  "status": "pending",
  "token_options": [
    { "token": "USDT_TRC20", "chain": "tron", "min_usd": "6.44", "network_fee_usd": "3.22", "fee_note": "Final in about a minute." },
    { "token": "USDT_BEP20", "chain": "bsc", "min_usd": "1", "network_fee_usd": "0.01", "fee_note": "Final in under a minute." },
    { "token": "BTC", "chain": "bitcoin", "min_usd": "10", "network_fee_usd": "0.22", "fee_note": "Final in about 30 minutes (3 confirmations)." }
  ],
  "deposit_address": "",
  "token": "",
  "chain": "",
  "livemode": false,
  "crypto_amount": "0",
  "received_amount": "0",
  "usd_amount": "25",
  "amount_due_usd": "25",
  "fees_paid_by_customer": false,
  "network_fee": "0",
  "network_fee_usd": "0",
  "settlement": "direct",
  "received_usd": "0",
  "rate_locked": "0",
  "deposits": [],
  "expires_at": "2026-10-10T10:00:00.832686526Z",
  "created_at": "2026-10-10T09:00:00.832686526Z",
  "network_fee_note": "",
  "qr_payload": "",
  "checkout_token": "b28d4f61e9a03c75d2e8f1a6",
  "checkout_url": "https://pay.example.com/pay/b28d4f61e9a03c75d2e8f1a6"
}
```

The customer picks a coin on the hosted checkout, or on
[your own page](/docs/checkout#your-own-payment-page): your server then
[sets it](#choose-the-coin-later) with `POST /v1/payments/{id}/token`. Only
then does the payment get its `deposit_address`, `rate_locked`,
`crypto_amount`, `qr_payload` and a new `expires_at` 15 minutes later. To
offer fewer coins, pass `"tokens": ["USDT_TRC20", "USDT_BEP20"]`.

### With its own webhook URL

```bash
curl -s https://api.payitgo.co/v1/payments \
  -H "Authorization: Bearer $PAYITGO_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"order_id": "ORD-1003", "amount_usd": 30, "token": "USDT_TRC20", "webhook_url": "https://acme.example/orders/ORD-1003/ipn"}'
```

The response is the same payment object, with
`"webhook_url": "https://acme.example/orders/ORD-1003/ipn"` at the end. Every
`payment.*` and `settlement.*` notification of this payment goes to that URL,
and none to your account's webhook URL.

### Prices in other currencies

To price a payment in your own currency, send `price_amount` and
`price_currency` (an ISO 4217 code: `EUR`, `GBP`, `NGN`…) instead of
`amount_usd`:

```bash
curl -s https://api.payitgo.co/v1/payments \
  -H "Authorization: Bearer $PAYITGO_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"order_id": "ORD-1004", "price_amount": "45.50", "price_currency": "EUR", "token": "USDT_TRC20"}'
```

payitgo converts the price into dollars when the payment is created, at the
exchange rate of the moment, and rounds it to the cent. The response is the
usual payment object, priced in dollars, with your price and the rate next to
it:

```json
{
  "usd_amount": "49.23",
  "price_amount": "45.5",
  "price_currency": "EUR",
  "price_rate": "1.082",
  "amount_due_usd": "49.23",
  "crypto_amount": "49.23",
  "token": "USDT_TRC20"
}
```

(Shortened: every other field is there as usual.)

- **Everything else is in dollars.** `usd_amount`, `amount_due_usd`,
  `crypto_amount`, the fees, the token [minimums](#list-tokens) and the
  [underpayment tolerance](#when-the-customer-pays-the-wrong-amount) all work
  from the converted `usd_amount`, exactly as for a price given in
  `amount_usd`. The rate is fixed at creation: the payment does not follow the
  currency afterwards.
- **The payment keeps your price.** `price_amount`, `price_currency` (in upper
  case) and `price_rate` (the dollar value of one unit of the currency) are on
  the payment in every response, in its webhooks and on the
  [checkout](/docs/checkout) view, so a page can show "€45.50 (≈ $49.23)".
  They are absent for a payment priced in dollars.
- **The rates** come from two market data sources (the second when the first
  does not answer), refreshed every 10 minutes. When neither answers, a rate up
  to 6 hours old is used; with none at all, the request fails with `503`
  `exchange rate unavailable for price_currency: try again shortly`: nothing
  is created, so send it again a little later.
- **A retry** for an order that has an open payment returns that payment
  unchanged, at the rate it was created with ([safe retries](#safe-retries)).

### Safe retries

While an order has an **open** payment (`pending`, `partially_paid` or
`detecting`) in the key's mode, creating it again returns that same payment
with `200 OK` instead of `201`, and makes nothing new. The retried request is
still checked for errors, but its other fields (amount, token, `webhook_url`…)
are ignored: the customer is never shown a second address or amount. So a
request that timed out can simply be sent again.

Once the payment is closed (`confirmed`, `expired`, `underpaid` or `failed`),
the same `order_id` creates a new payment. Test and live payments never share
an order.

**Order ids are unique per store.** A [store's](/docs/account#stores) key
only finds its own store's open payments: two shops on one account can both
send `"order_id": "1234"` and get two payments, as long as each uses the key
of its own store. A key with no store sees the payments made with keys that
have no store. So you never change your order numbers to keep shops apart:
give each shop its own store and key.

### Errors

Every error is `{"error": "<message>"}`.

| Status | Message | Cause |
|---|---|---|
| `400` | `invalid request body` | The body is not JSON, or a field has the wrong type (`amount_usd` or `price_amount` not a number, a `metadata` value that is not a string) |
| `400` | `order_id is required` | Missing or empty `order_id` (or no `Content-Type: application/json`) |
| `400` | `order_id is too long` | More than 128 bytes |
| `400` | `amount_usd must be > 0` | Zero, negative or missing amount (neither `amount_usd` nor `price_amount` + `price_currency`) |
| `400` | `give amount_usd, or price_amount and price_currency: not both` | `amount_usd` together with a price in another currency |
| `400` | `price_amount and price_currency go together` | Only one of the two |
| `400` | `price_currency must be the ISO 4217 code of a currency (EUR, GBP, NGN…)` | `price_currency` is not a currency in use, such as `BTC` or `EURO` |
| `400` | `price_amount must be > 0` | Zero or negative `price_amount` |
| `400` | `price_amount is worth less than a cent` | `price_amount` converts to $0.00 once rounded to the cent |
| `400` | `unsupported token in test mode` | `token` is not a token of the key's mode (`live mode` with a live key) |
| `400` | `unsupported token in test mode: SHIB` | An entry of `tokens` is not a token of the key's mode |
| `400` | `not enabled for your account: BTC (turn tokens on in your payment settings)` | A token you have not enabled ([accepted tokens](/docs/account#accepted-tokens)) |
| `400` | `minimum payment in USDT_TRC20 is $6.44 right now (network fees)` | `amount_usd` is under the token's current minimum |
| `400` | `amount_usd is below the minimum of every token on offer` | Without `token`: no coin on offer accepts so small an amount |
| `400` | `none of the tokens on offer is available in test mode` | Without `token`: none of `tokens` is offered in this mode |
| `400` | `only test payments can be simulated: use a test key` | `simulate` with a live key |
| `400` | `simulate must be one of: confirmed, overpaid, …` | An unknown `simulate` outcome |
| `400` | `simulate wrong_asset needs the payment's coin: pass token` | `"simulate": "wrong_asset"` without `token` |
| `400` | `a BTC address only receives BTC: wrong_asset needs a coin on Tron, Ethereum or BNB Smart Chain` | `wrong_asset` with a coin that has no other on its network |
| `400` | `description is too long` | More than 500 bytes |
| `400` | `metadata may hold at most 20 keys` | Too many keys |
| `400` | `metadata key or value is too long` | A key over 40 bytes or a value over 500 |
| `400` | `invalid customer_email` | Not an email address |
| `403` | `this customer is blocked from paying the merchant` | You blocked the customer with this `customer_email` in the dashboard |
| `400` | `success_url and cancel_url must be http(s) URLs` | Not an absolute `http://` or `https://` URL |
| `400` | `webhook_url: url must use https` | `webhook_url` is `http://` |
| `400` | `webhook_url: url must be an absolute http(s) URL` | Not an absolute URL |
| `400` | `webhook_url: url must point at a public host` | Its host is `localhost`, a private or link-local address |
| `400` | `webhook_url: url host does not resolve` | Its host name has no DNS record |
| `401` | `missing api key` · `invalid api key` | See [Authentication](/docs/authentication#api-key-errors) |
| `403` | `live mode is off for this account: …` | A live key before live mode is on |
| `403` | `this api key is read-only: it can only read (GET)` | A [read-only key](/docs/authentication#read-only-keys) |
| `429` | `rate limit exceeded` | Too many requests ([limits](/docs/errors#rate-limits)) |
| `500` | `payment creation failed` | Something failed on payitgo's side. Retry with the same `order_id`: it is safe |
| `503` | `exchange rate unavailable for price_currency: try again shortly` | No exchange rate for `price_currency` could be had ([prices in other currencies](#prices-in-other-currencies)). Nothing was created: retry shortly with the same `order_id` |

## Choose the coin later

`POST /v1/payments/{id}/token` · API key

Sets the coin of a payment created without one, once the customer has picked
it on your own page. The answer is the whole payment, as
[`GET /v1/payments/{id}`](#retrieve-a-payment) returns it, now with its
address, locked rate, amount, QR code payload and a deadline 15 minutes away:
everything your page shows. SDKs: `payments.selectToken(id, token)`
(`select_token` in Python, `SelectToken` in Go).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The payment's id or your `order_id` |
| `token` | body | string | yes | One of the payment's `token_options[].token`, such as `USDT_TRC20` |

```bash
curl -s https://api.payitgo.co/v1/payments/c41e7a9d2b05f3e8a6d17c90/token \
  -H "Authorization: Bearer $PAYITGO_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"token": "USDT_TRC20"}'
```

`200 OK`:

```json
{
  "id": "c41e7a9d2b05f3e8a6d17c90",
  "merchant_id": "3f8a1c27b94e05d6a2c71e90",
  "order_id": "ORD-1002",
  "status": "pending",
  "deposit_address": "TJr7Wq2nX5kP8sB3vH6mC9dF4gL1tYzRaU",
  "token": "USDT_TRC20",
  "chain": "tron",
  "livemode": false,
  "crypto_amount": "25",
  "received_amount": "0",
  "usd_amount": "25",
  "amount_due_usd": "25",
  "fees_paid_by_customer": false,
  "network_fee": "3.211899",
  "network_fee_usd": "3.22",
  "settlement": "direct",
  "received_usd": "0",
  "rate_locked": "1",
  "deposits": [],
  "expires_at": "2026-10-10T09:16:48.029135472Z",
  "created_at": "2026-10-10T09:00:00.832686526Z",
  "network_fee_note": "Final in about a minute.",
  "qr_payload": "TJr7Wq2nX5kP8sB3vH6mC9dF4gL1tYzRaU",
  "checkout_token": "b28d4f61e9a03c75d2e8f1a6",
  "checkout_url": "https://pay.example.com/pay/b28d4f61e9a03c75d2e8f1a6"
}
```

The coin can be chosen once, by your server or on the hosted checkout,
whichever comes first.

| Status | Message | Cause |
|---|---|---|
| `400` | `token is required` | No `token` in the body, or the body is not JSON |
| `400` | `token is not one of this payment's options` | A coin not in `token_options` |
| `400` | `minimum payment in … is $… right now (it is converted on its way to the merchant, and the exchange takes no less)` | The coin needs a bigger payment than this one: it is converted through the exchange on its way to the merchant, which takes no less, and the merchant pays the fees or hides such coins (`hide_below_minimum`). Offer the other coins |
| `400` | `this payment can no longer be paid` | The payment is no longer `pending`, or its hour to choose has passed |
| `404` | `payment not found` | No such payment for your key's mode (or, for a storefront's key, for its store) |
| `409` | `the coin for this payment has already been chosen` | The coin was set before |

## Simulate a test payment

`POST /v1/payments/{id}/simulate` · API key (test keys only)

A test payment moves no coins. Choose what happens to it instead: payitgo
plays the deposits, status changes and webhooks a few seconds apart, through
the same processing as live coins. Every outcome, the webhooks it sends and
the rules: [Test mode](/docs/testing#outcomes).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The payment's id or your `order_id` |
| `outcome` | body | string | yes | `confirmed`, `overpaid`, `partially_paid`, `underpaid`, `expired`, `failed`, `wrong_asset` or `paid_late` |

```bash
curl -s -X POST https://api.payitgo.co/v1/payments/c41e7a9d2b05f3e8a6d17c90/simulate \
  -H "Authorization: Bearer $PAYITGO_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"outcome": "partially_paid"}'
```

`200 OK` with the [payment](#the-payment-object), its simulation started:

```json
{
  "id": "c41e7a9d2b05f3e8a6d17c90",
  "order_id": "ORD-1002",
  "status": "pending",
  "token": "USDT_TRC20",
  "livemode": false,
  "simulation": { "outcome": "partially_paid", "state": "running" },
  "…": "…"
}
```

The payment must be `pending` or `partially_paid`, and have its coin (except
for `expired`). One simulation runs at a time; once it is `done`, you can run
another (`confirmed` after `partially_paid`, to pay the rest). To choose the
outcome when you create the payment, pass `simulate` to
[`POST /v1/payments`](#parameters).

| Status | Message | Cause |
|---|---|---|
| `400` | `outcome is required` | No `outcome` in the body, or the body is not JSON |
| `400` | `simulate must be one of: confirmed, overpaid, …` | An unknown outcome |
| `400` | `only test payments can be simulated: use a test key` | A live key |
| `400` | `choose the payment's coin first` | No coin yet, for anything but `expired` |
| `400` | `this payment is confirmed: only a pending or partially paid payment can be simulated` | The payment is closed or confirming |
| `400` | `a BTC address only receives BTC: wrong_asset needs a coin on Tron, Ethereum or BNB Smart Chain` | `wrong_asset` on a coin that has no other on its network |
| `404` | `payment not found` | No such payment for your key's mode (or, for a storefront's key, for its store) |
| `409` | `a simulation is already running for this payment` | Wait for its `done` |

## Retrieve a payment

`GET /v1/payments/{id}` · API key

Returns one payment of the key's mode.

| Parameter | In | Description |
|---|---|---|
| `id` | path | The payment's id, or your `order_id`. The payment id is tried first, then your order id: by order id you get the order's **open** payment of the key's store (or of no store, for a key with none); a closed one is found by its payment id |

```bash
curl -s https://api.payitgo.co/v1/payments/8f3a2c1e9b7d4a6f05e1c2d3 \
  -H "Authorization: Bearer $PAYITGO_KEY"
```

```bash
curl -s https://api.payitgo.co/v1/payments/ORD-1001 \
  -H "Authorization: Bearer $PAYITGO_KEY"
```

`200 OK` with the [payment object](#the-payment-object).

| Status | Message | Cause |
|---|---|---|
| `404` | `payment not found` | No such payment for your account in the key's mode, an order id with no open payment, or (with a storefront key) another store's payment |

Webhooks tell you about every change, so you rarely need to poll. When you
do (after your server was down, or to double-check a webhook), fetch the
payment from your server and trust what it says over the browser.

## List payments

`GET /v1/payments` · API key

Your payments in the key's mode, newest first. An account-wide key sees all
of the account's payments; a [storefront key](/docs/authentication#storefront-keys)
sees only its own store's.

| Query parameter | Type | Default | Description |
|---|---|---|---|
| `status` | string | all | One [status](#lifecycle), such as `confirmed`, or several separated by commas: `pending,partially_paid,detecting` |
| `token` | string | all | Only payments in this token, such as `USDT_TRC20` |
| `chain` | string | all | Only payments on this network, such as `tron` |
| `store_id` | string | all | Only payments created by this store's keys. Ignored for a storefront key, which always gets its own store's |
| `customer_id` | string | all | Only one customer's payments (from a payment's `customer_id`) |
| `link_id` | string | all | Only the payments made through one payment link |
| `q` | string | | Search: a payment id, order id, transaction hash or deposit address (exactly), or any part of the customer email or description (any case) |
| `from`, `to` | string | | Only payments created from `from` (included) to `to` (excluded): RFC 3339 times or `YYYY-MM-DD` dates (midnight UTC). Anything else is `400` |
| `limit` | integer | 50 | How many to return, 1 to 200. A value outside that range is replaced by 50 |
| `offset` | integer | 0 | How many to skip, for paging. A negative value is replaced by 0 |

```bash
curl -s 'https://api.payitgo.co/v1/payments?status=pending&limit=2&offset=0' \
  -H "Authorization: Bearer $PAYITGO_KEY"
```

`200 OK`:

```json
{
  "payments": [
    { "id": "2d9b6e14a7c3f0851e4b9a6c", "order_id": "ORD-1003", "status": "pending", "…": "…" },
    { "id": "c41e7a9d2b05f3e8a6d17c90", "order_id": "ORD-1002", "status": "pending", "…": "…" }
  ],
  "total": 3,
  "limit": 2,
  "offset": 0
}
```

| Field | Type | Description |
|---|---|---|
| `payments` | array of [payments](#the-payment-object) | This page, in full (shortened above) |
| `total` | integer | How many payments match the filters, across all pages |
| `limit`, `offset` | integer | The paging applied: what you asked for, or the defaults that replaced an out-of-range value |

To page through everything, repeat with `offset` increased by `limit` until
`offset` reaches `total`. An unknown `status`, `token` or `chain` simply
matches nothing (`"payments": []`).

## List tokens

`GET /v1/tokens` · API key · SDKs: `tokens.list()`

The coins your customers can pay with in the key's mode: the tokens you accept
([accepted tokens](/docs/account#accepted-tokens)), each with its network fee
and minimum right now. Use it to show the coins **before** you create a
payment, then create the payment with the customer's choice as its `token`.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `amount_usd` | query | decimal | no | The order's amount. Only the tokens a payment of that amount offers are listed (the ones whose `min_usd` it reaches), each with `amount_due_usd` when you pass the fees on: exactly the `token_options` a payment of that amount would have |

```bash
curl -s https://api.payitgo.co/v1/tokens -H "Authorization: Bearer $PAYITGO_KEY"
```

`200 OK` (test mode, shortened to four of the twelve tokens):

```json
{
  "tokens": [
    { "token": "USDT_TRC20", "chain": "tron", "min_usd": "6.44", "network_fee_usd": "3.22", "fee_note": "Final in about a minute." },
    { "token": "TRX", "chain": "tron", "min_usd": "1", "network_fee_usd": "0", "fee_note": "Final in about a minute." },
    { "token": "USDT_BEP20", "chain": "bsc", "min_usd": "1", "network_fee_usd": "0.01", "fee_note": "Final in under a minute." },
    { "token": "XRP", "chain": "xrp", "min_usd": "1", "network_fee_usd": "0.01", "fee_note": "Final in seconds. Include the destination tag: it is required." }
  ]
}
```

With an amount, and the fees passed on to the customer, each token says what
the customer pays in it:

```bash
curl -s 'https://api.payitgo.co/v1/tokens?amount_usd=40' -H "Authorization: Bearer $PAYITGO_KEY"
```

```json
{
  "tokens": [
    { "token": "USDT_TRC20", "chain": "tron", "min_usd": "6.44", "network_fee_usd": "3.22", "amount_due_usd": "43.66", "fee_note": "Final in about a minute." },
    { "token": "USDT_BEP20", "chain": "bsc", "min_usd": "1", "network_fee_usd": "0.01", "amount_due_usd": "40.42", "fee_note": "Final in under a minute." }
  ]
}
```

| Status | Message | Cause |
|---|---|---|
| `400` | `amount_usd must be > 0` | `amount_usd` is not a positive number |

### The TokenInfo object

| Field | Type | Description |
|---|---|---|
| `token` | string | The token symbol to use in `token` and `tokens` |
| `chain` | string | Its network |
| `name`, `network` | string | For people: `"USD Coin"`, `"Ethereum (ERC-20)"` |
| `confirmations` | integer | How many confirmations make a deposit in it final |
| `min_usd` | decimal string | The smallest `amount_usd` accepted in this token right now: the token's own minimum; twice its network fee when you pay the fees; and, for a coin converted across networks on its way to your wallet, the price that leaves the exchange its minimum, when you pay the fees or turn on `hide_below_minimum` (with the fees passed on, a smaller payment pays up to the minimum instead: `extra_usd`), whichever is highest |
| `network_fee_usd` | decimal string | The network fee for moving a payment in this token, right now: its fixed part, whatever the amount. It rises and falls with how busy the network is |
| `network_fee_bps` | integer, optional | For a coin that is converted (into your payout coin, or bought by payitgo for your balance): the share of the payment the network fee adds on top, in basis points (180: 1.8%), what the exchange keeps and the conversion fee. Absent when nothing is converted |
| `amount_due_usd` | decimal string, optional | In a payment's `token_options`, and in `GET /v1/tokens?amount_usd=…`, when you pass the fees on: what the customer pays in this coin, every fee and the conversion included, so they can pick the cheapest |
| `extra_usd` | decimal string, optional | What of `amount_due_usd` the customer pays only to reach the exchange's minimum, for a payment too small for this coin's conversion: you receive it. Absent when none |
| `fee_note` | string | A short line for the customer about fees and timing |

Fees and minimums are quoted for where a payment in the coin will really
settle: with direct settlement and a payout address for the coin (in use,
or still in its 24 hours: the payment then waits for it), two transfers (to
you, and payitgo's fee); with none for the coin, the conversion into one of
your payout coins as well
([coins you have no payout address for](/docs/settlement#coins-you-have-no-payout-address-for));
in your balance, one transfer, and for any coin but USDT on Tron the 1.5%
conversion fee.
`GET /v1/tokens`, the minimum checked by `POST /v1/payments` and a payment's
`token_options` always agree. See [Fees and settlement](/docs/settlement).

## When the customer pays the wrong amount

**Too little** (say 30 of 49.90 USDT). The payment becomes `partially_paid`
and the checkout page asks for the rest. If the rest arrives before
`expires_at`, the payment goes on to `detecting` and `confirmed`. If not, it
becomes `underpaid` at the deadline: the 30 USDT are still settled to you
(less the fees) once final, so you can refund the customer or agree on the
rest. Nothing is left on the deposit address.

**A little too little.** A payment counts as paid when the shortfall is
within your **underpayment tolerance**: 0.5% by default, so a wallet that
rounds or takes a tiny fee does not leave the customer stuck. Owners can set
it from 0 to 20% (`PATCH /merchant/settings {"underpaid_tolerance_bps": 100}`
for 1%). A payment keeps the tolerance in force when it was created.

**Too much** (say 900 of 100 USDT). The payment confirms with
`"overpaid": true` (set when more than 1% too much arrived), and all of it is
settled to you. Refund the difference if you wish: the sender's address is in
the deposit's `from`.

**Twice, or late.** See [late payments](#late-payments).

## When the customer pays with the wrong asset

A payment's address can only receive coins of its own network: a wallet will
not send Bitcoin to a Tron address. The mistakes that do happen are on the
same network, and payitgo catches them:

- another token on the same chain, such as USDC or ETH sent to a USDT (ERC-20)
  payment, or TRX to a USDT (TRC-20) one;
- the other EVM chain: a payment's address is the same on Ethereum and BNB
  Smart Chain, so USDT sent on BSC to an Ethereum payment's address is found
  too.

Each such deposit raises `payment.wrong_asset`. What happens next depends on
the **accept other assets** setting
(`PATCH /merchant/settings {"accept_other_assets": true}`, owner only):

| Setting | The deposit | The payment |
|---|---|---|
| Off (default) | `"counted": false`. Settled to you in the asset that arrived | Keeps waiting for the coin it asked for |
| On | `"counted": true`, at its dollar value (stablecoins at $1, others at the market price when it arrives), if you accept that token | Completes when the dollar total is reached |

Either way the money is yours to keep or refund. It is settled like any
payment in the coin that arrived: with direct settlement, to your payout
address for that coin, or converted into one of your payout coins when you
have none for it
([coins you have no payout address for](/docs/settlement#coins-you-have-no-payout-address-for)).

## Late payments

A payment's address is watched for **24 hours** after it expires, falls
short or confirms. A deposit in that time is marked `"late": true`, settled
to you once final, and reported with `payment.late_deposit`. If it completes an
`expired` or `underpaid` payment, the payment moves on to `detecting` and
`confirmed` with `"paid_late": true`, at the rate locked at creation. A late
deposit that is still not enough makes the payment `underpaid`. A deposit
after a payment confirmed (a customer paying twice) marks it `overpaid`.

## XRP

XRP payments share one deposit address and are told apart by
`destination_tag`. The customer **must** include the tag: a transfer without
it cannot be matched to the payment and needs payitgo's support to recover. The
hosted checkout shows the tag next to the address; `qr_payload` does not
carry it, so the customer types it in.

## Without a library

The [libraries](/docs/sdks) wrap these calls. In plain code:

**Node.js**

```js
const res = await fetch("https://api.payitgo.co/v1/payments", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.PAYITGO_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ order_id: "ORD-1001", amount_usd: "49.90", token: "USDT_TRC20" }),
});
const body = await res.json();
if (!res.ok) throw new Error(`payitgo ${res.status}: ${body.error}`);
// res.status is 201 (created) or 200 (the order's open payment)
const payment = body; // redirect the customer to payment.checkout_url
```

**Python**

```python
import os, requests

r = requests.post(
    "https://api.payitgo.co/v1/payments",
    headers={"Authorization": f"Bearer {os.environ['PAYITGO_KEY']}"},
    json={"order_id": "ORD-1001", "amount_usd": "49.90", "token": "USDT_TRC20"},
    timeout=30,
)
if r.status_code >= 400:
    raise RuntimeError(f"payitgo {r.status_code}: {r.json()['error']}")
payment = r.json()
```

**PHP**

```php
$ch = curl_init('https://api.payitgo.co/v1/payments');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('PAYITGO_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'order_id' => 'ORD-1001', 'amount_usd' => '49.90', 'token' => 'USDT_TRC20',
    ]),
]);
$body = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
    throw new RuntimeException('payitgo: ' . $body['error']);
}
$payment = $body;
```

**Go**

```go
body, _ := json.Marshal(map[string]any{"order_id": "ORD-1001", "amount_usd": "49.90", "token": "USDT_TRC20"})
req, _ := http.NewRequest(http.MethodPost, "https://api.payitgo.co/v1/payments", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("PAYITGO_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer resp.Body.Close()
var payment map[string]any
json.NewDecoder(resp.Body).Decode(&payment)
if resp.StatusCode >= 400 {
	return fmt.Errorf("payitgo %d: %v", resp.StatusCode, payment["error"])
}
```
