Payments
A payment is one request for money from one of your customers: an amount in US dollars (or in your own currency, 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.
| Method | Path | What it does |
|---|---|---|
POST |
/v1/payments |
Create a payment (or get back the open payment of the same order) |
POST |
/v1/payments/{id}/token |
Set the coin of a payment created without one |
POST |
/v1/payments/{id}/simulate |
Test mode: play an outcome on a test payment |
GET |
/v1/payments/{id} |
Read one payment, by payment id or by your order id |
GET |
/v1/payments |
List payments, newest first |
GET |
/v1/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). 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.
How the payment moves:
- pending → partially_paid when a deposit arrives but the total is short of
amount_due_usd(beyond your tolerance). - pending or partially_paid → detecting when the total covers
amount_due_usd. - detecting → confirmed when every deposit that counts has its confirmations (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 indetectingnever expires. - expired or underpaid → detecting → confirmed when the customer pays the
rest within 24 hours of closing (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:
{
"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 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 |
token_options |
array of TokenInfo, 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). "" 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), true for a real one |
simulation |
object, optional | A test payment's simulation, 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). 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) |
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). "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) |
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 | 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). "" until a coin is chosen |
checkout_token |
string | The public key to the payer's view of this payment (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) |
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 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: 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), counted or not, less the fees (Fees and 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).
A payment counts as paid when received_usd reaches amount_due_usd, less
your underpayment tolerance.
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).
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 |
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 |
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) |
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, 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 |
simulate |
string | no | Test keys only: the outcome the payment plays, such as confirmed (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).
Request
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 (a test key, so
livemode is false):
{
"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).
Let the customer choose the coin
Send only the order and the amount:
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:
{
"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: your server then
sets it 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
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:
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:
{
"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 and the underpayment tolerance all work from the convertedusd_amount, exactly as for a price given inamount_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) andprice_rate(the dollar value of one unit of the currency) are on the payment in every response, in its webhooks and on the 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
503exchange 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
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 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) |
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 |
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 |
429 |
rate limit exceeded |
Too many requests (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). 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} 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 |
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:
{
"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.
| 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 |
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, its simulation started:
{
"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.
| 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 |
curl -s https://api.payitgo.co/v1/payments/8f3a2c1e9b7d4a6f05e1c2d3 \
-H "Authorization: Bearer $PAYITGO_KEY"
curl -s https://api.payitgo.co/v1/payments/ORD-1001 \
-H "Authorization: Bearer $PAYITGO_KEY"
200 OK with 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 sees only its own store's.
| Query parameter | Type | Default | Description |
|---|---|---|---|
status |
string | all | One status, 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 |
curl -s 'https://api.payitgo.co/v1/payments?status=pending&limit=2&offset=0' \
-H "Authorization: Bearer $PAYITGO_KEY"
200 OK:
{
"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 | 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), 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 |
curl -s https://api.payitgo.co/v1/tokens -H "Authorization: Bearer $PAYITGO_KEY"
200 OK (test mode, shortened to four of the twelve tokens):
{
"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:
curl -s 'https://api.payitgo.co/v1/tokens?amount_usd=40' -H "Authorization: Bearer $PAYITGO_KEY"
{
"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);
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.
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.
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).
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 wrap these calls. In plain code:
Node.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
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
$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
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"])
}