Mass payouts
Pay your own recipients in batches, from your own payout wallet: salaries and contractors, tournament prizes and affiliate commissions, withdrawals from a trading platform or a marketplace. You upload a list (or send it over the API), payitgo checks and prices every row, you approve the batch, and payitgo sends each payment as soon as the wallet holds enough.
| Coins | 14 coins on 7 networks: USDT on Tron, BNB Smart Chain and Ethereum; USDC on BNB Smart Chain and Ethereum; DAI; ETH on Ethereum and BNB Smart Chain; TRX, BNB, BTC, LTC, DOGE and XRP (networks and coins) |
| Where the money comes from | Your payout wallet: one address per network, yours alone, that you top up |
| What it costs | 0.5% of each payment, never less than $0.50, plus its network fee (Fees) |
| How you send payments | Each payout request as it comes in, sent together when you choose (Payout requests); or whole batches from the dashboard (rows typed or pasted, or a CSV file) or the API (The API) |
| No address? | A claim link: the recipient chooses the coin and their wallet |
| Other coins | Conversion: keep one coin in the wallet and pay in any payout coin |
| Payroll | Schedules: the same batch every week, two weeks or month |
| Safeguards | Approvals, limits per payment, per batch and per day, a wait for new recipients, sanctions screening (Approvals and limits) |
Test mode has it all, on paper: a test key, or the dashboard's test mode, tops up a test wallet with a click and sends nothing on chain (In test mode).
How it works
- Make a batch, or let your payout requests make
one. Each payment names a coin, an address, an amount and, if you like,
your own
referencefor it and the recipient's name, email and a note. - payitgo checks and prices it. Every row is checked (the coin, the
amount, the address down to its checksum, the sanctions list, your
limits) and priced. A row with an error is left out; the rest are
ready. The batch isquoted: its prices hold for an hour. - Submit it. With approvals turned on, it waits for them; otherwise it goes straight to funding.
- It is funded from your payout wallet. When the wallet holds what the batch needs, coin by coin, its money is set aside and it starts. When it does not, the batch waits and says exactly how much to send, and where. It starts by itself once the money is there.
- payitgo sends it. The batch's fees go to payitgo first, then its payments, up to 50 in each transfer (200 in one Bitcoin, Litecoin or Dogecoin transaction). Each payment gets its transaction id as it goes, and a webhook.
Payout requests
The easiest way in. You don't have to build batches: your system sends
each payout request the moment it comes in (a trader withdrawing from your
platform, a contractor's invoice approved, an employee's pay run, a prize
won) with POST /v1/payouts. payitgo checks and prices it at once, answers
with what it costs, and keeps it in your list of payout requests. When
you choose (every hour, at the end of the day, or when someone presses
Send in the dashboard), send the list: its valid requests become a
batch, priced again at today's fees and submitted, then approved and funded
like any batch. Or let the list go by itself after a few minutes
(auto-send).
A trading platform's withdrawals, start to finish:
-
A client asks to withdraw 250 USDT to their Tron wallet. Your server sends it to payitgo with your own id for the request as
reference:curl -s https://api.payitgo.co/v1/payouts \ -H "Authorization: Bearer $PAYITGO_KEY" \ -H 'Content-Type: application/json' \ -d '{"reference": "wd-88231", "asset": "USDT_TRC20", "address": "TR7mNkucnhLZoJQLLJ6DR88jpLQufWxUJi", "amount": "250", "name": "Ana Silva", "email": "[email protected]"}'201 Created: the payment,readywith itsfee. One that cannot be paid comes backinvalidwith itserrors(a wrong network, a sanctioned address, under the minimum): tell your client at once.{ "item": { "id": "5c7e9a1b3d5f7a9c1e3b5d7f", "merchant_id": "3f8a1c27b94e05d6a2c71e90", "livemode": true, "batch_id": "0d2f4a6c8e1b3d5f7a9c2e4b", "reference": "wd-88231", "row": 14, "recipient": { "name": "Ana Silva", "email": "[email protected]" }, "asset": "USDT_TRC20", "ticker": "USDT", "icon_url": "https://api.payitgo.co/coins/USDT_TRC20.svg", "chain": "tron", "address": "TR7mNkucnhLZoJQLLJ6DR88jpLQufWxUJi", "amount": "250", "send_amount": "250", "fee": "2.852417", "debit": "252.852417", "usd_rate": "1", "amount_usd": "250", "status": "ready", "errors": [], "warnings": [], "created_at": "2026-10-20T13:41:07.52Z", "updated_at": "2026-10-20T13:41:07.52Z" } } -
More requests come in the same way. At any time, read the list and what sending it takes from the wallet:
curl -s https://api.payitgo.co/v1/payouts/inbox -H "Authorization: Bearer $PAYITGO_KEY" -
Send it: all its valid requests, or only the ones you pick (
ids):curl -s -X POST https://api.payitgo.co/v1/payouts/send -H "Authorization: Bearer $PAYITGO_KEY"The answer is the new batch, already submitted: waiting for its approvals, waiting for a top-up (
waiting.shortfallsays what to send where), or being sent. -
Each request then tells you what became of it, by webhook, with your
reference:payout_item.processing: it left the list for a batch: show the withdrawal as processing;payout_item.sent, with thetx_id: mark the withdrawal paid;payout_item.failed: it was not paid, its money is still in your wallet;payout_item.canceled: it will never be paid, andcancel_reasonsays why (removed from the list, rejected by an approver of your team, its batch canceled, no longer payable): give your client their money back.
Every request ends sent, failed or canceled, and you hear it, whoever acted on it: your system, a team member in the dashboard, or payitgo. See Staying in sync. Payroll works the same way: send each employee's pay as it is approved, and the list at the end of the day.
A request's reference makes it safe to send again: the same reference
answers 200 OK with the payment that has it, wherever it is now. Once a
request is canceled, failed or expired, its reference is free again: to pay
it after all, send a new request (it may reuse the reference). A canceled
payment is never brought back.
Take a payout request
POST /v1/payouts · a key with payouts on
The body is one payment: asset, address and amount, and optionally
reference, name, email, note, tag, claim and claim_assets
(as in Create a batch), and webhook_url: where this
request's events go instead of your account's webhook URL (https; it stays
with the request when it is sent). 201 Created: {"item": payment}, in
your list (its batch_id is the list's), ready with its fee and
debit, or invalid with its errors. An invalid request stays in the
list, never sent, until you remove it. 200 OK with the payment its
reference already names. New requests follow your payout settings'
fee_paid_by and convert as they are when the request comes in; when
sent, each is priced again and follows the conversion setting as it is
then (converted again, or no longer converted, to match).
| Status | Message | Cause |
|---|---|---|
400 |
invalid request body |
Not JSON, or a field of the wrong type |
400 |
webhook_url: url must use https · webhook_url: url host does not resolve · webhook_url: url must point at a public host · webhook_url: url must point at your own server, not payitgo's |
An unusable webhook_url |
403 |
this api key cannot make payouts: turn payouts on for it in the dashboard |
(payout keys) |
409 |
your list of payout requests is full: send or cancel some first |
100,000 requests waiting |
Read the list
GET /v1/payouts/inbox?status=&limit=&offset= · any key
{
"inbox": {
"id": "0d2f4a6c8e1b3d5f7a9c2e4b",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"livemode": true,
"name": "Payout requests",
"source": "requests",
"inbox": true,
"open": true,
"created_by": "requests",
"by_api_key": false,
"fee_paid_by": "merchant",
"convert": "same_network",
"status": "quoted",
"totals": [
{ "asset": "USDT_TRC20", "ticker": "USDT", "icon_url": "https://api.payitgo.co/coins/USDT_TRC20.svg", "chain": "tron", "network": "Tron (TRC-20)", "items": 1, "amount": "250", "send_amount": "250", "fee": "2.852417", "debit": "252.852417", "debit_usd": "252.85", "saved_usd": "0" },
{ "asset": "USDT_BEP20", "ticker": "USDT", "icon_url": "https://api.payitgo.co/coins/USDT_BEP20.svg", "chain": "bsc", "network": "BNB Smart Chain (BEP-20)", "items": 1, "amount": "75", "send_amount": "75", "fee": "0.51183421", "debit": "75.51183421", "debit_usd": "75.51", "saved_usd": "0" }
],
"counts": { "total": 3, "invalid": 1, "ready": 2, "cooling": 0, "queued": 0, "sending": 0, "sent": 0, "failed": 0, "canceled": 0, "claimable": 0, "expired": 0 },
"total_usd": "328.36",
"approvals_needed": 0,
"approvals": [],
"members": {},
"created_at": "2026-10-18T08:02:11.4Z",
"updated_at": "2026-10-20T13:52:40.08Z"
},
"coverage": [
{ "asset": "USDT_TRC20", "ticker": "USDT", "network": "Tron (TRC-20)", "needed": "252.852417", "spendable": "100", "top_up": "152.852417", "address": "TMkq7dZ3vN8xR2pB6cH9sJ4fL1wY5tGaQe" },
{ "asset": "USDT_BEP20", "ticker": "USDT", "network": "BNB Smart Chain (BEP-20)", "needed": "75.51183421", "spendable": "500", "top_up": "0", "address": "0x8F3a6B2c9D1e4F7a0B5c8D2e6F9a1B4c7D0e3F6a" }
],
"items": [ { "id": "5c7e9a1b3d5f7a9c1e3b5d7f", "reference": "wd-88231", "status": "ready", "…": "…" } ],
"total": 3,
"limit": 50,
"offset": 0
}
| Field | Description |
|---|---|
inbox |
The list itself, a batch with inbox and open true: the totals and counts of the requests waiting in it (valid ones in totals). It is never submitted, and never listed among your batches; batch routes on it answer 409 this is your list of payout requests: … |
coverage |
Per coin of the valid requests: needed (what sending them takes from the wallet, their debits), spendable (what the wallet can spend now), top_up (what to send to address first; "0" when it is covered), with ticker and network |
items |
The requests waiting, in the order they came (status filters: ready, invalid, or both, comma-separated); total, limit (1 to 200, 50 by default) and offset page them |
Send the list
POST /v1/payouts/send · a key with payouts on
No body sends every valid request (up to 10,000 a call: the rest wait for
the next); {"ids": ["5c7e9a1b3d5f7a9c1e3b5d7f", …]} sends only those. They
move into a new batch (source: "requests", inbox: true, named "Payout
requests of 20 Oct 2026 14:00 UTC"), are priced again at today's fees, and
the batch is submitted as the key: then approvals, funding and the top-up
it may need, as for any batch made with an API key. Up to
100 requests are priced while the call waits; more come back validating,
and the batch is submitted by itself once priced. Invalid requests, and the
ones you did not pick, stay in the list. Each request sent raises
payout_item.processing, with its new batch_id.
A request no longer valid when priced again (prices moved under the
minimum, an address added to the sanctions list, your limit per payment
lowered) comes back to the list with its errors, and raises
payout_item.returned; the others go on. It stays in the list, never sent
and never priced again, until you remove it.
200 OK: {"batch": batch}. Should submitting it fail (none of its
requests is valid any more at today's prices, or it is over your limit per
batch), its requests go back to the list, each saying why
(payout_item.returned), and that batch is canceled (payout_batch.canceled,
canceled_by: "system"); the answer is the error. For a send priced in the
background, your team gets a "Payout requests not sent" notification
instead.
| Status | Message | Cause |
|---|---|---|
400 |
no payout request to send · no valid payout request to send |
The list is empty, or has only invalid requests |
400 |
payout request 5c7e9a1b3d5f7a9c1e3b5d7f is not a valid request waiting in your list |
An id not ready in the list |
400 |
none of the requests is valid any more: they are back in your list, each saying why |
Priced again, none can be paid |
400 |
this batch takes $328.36, over your limit of $250.00 per batch |
Submitting it was refused: the requests are back in the list |
403 |
this api key cannot make payouts: … |
Remove a request
DELETE /v1/payouts/{id} · a key with payouts on
A request still in the list is removed: 200 OK with the payment,
"status": "canceled", "cancel_reason": "removed". Its reference is
free again (it moves to released_reference), it no longer has a
batch_id, and its record stays: GET /v1/payouts/{id} keeps answering it.
payout_item.canceled is sent.
One already sent but not on its way yet is canceled like any payment
("cancel_reason": "canceled"): funded (queued, cooling, sending),
its money stays in the wallet and its fee comes back; not funded yet, its
batch is totalled again (approvals already given stand), and canceled when
nothing is left in it. A request sent at the very moment you remove it is
canceled where it went, never reported removed while it is paid.
Removing a payment canceled already answers it again, unchanged: safe to
repeat. 404 not found for another mode's payment; 409 the payment is sent (or failed, expired) once it is done.
To pay a removed request after all, send it again as a new request: it may
reuse the same reference.
Remove several requests
POST /v1/payouts/remove · a key with payouts on
{"ids": ["5c7e9a1b3d5f7a9c1e3b5d7f", "7b1d3f5a9c2e4b6d8f0a1c3e"]}, 1 to 1,000
ids (one given twice counts once). Each request still in the list is
removed as above, with its own payout_item.canceled; the list is totalled
again once. 200 OK with a result per id, in the order given:
{
"results": [
{ "id": "5c7e9a1b3d5f7a9c1e3b5d7f", "item": { "id": "5c7e9a1b3d5f7a9c1e3b5d7f", "status": "canceled", "cancel_reason": "removed", "released_reference": "wd-88231", "…": "…" } },
{ "id": "7b1d3f5a9c2e4b6d8f0a1c3e", "error": { "status": 409, "message": "not in your list of payout requests (sent already): cancel it with DELETE /v1/payouts/7b1d3f5a9c2e4b6d8f0a1c3e" } }
]
}
| Result | When |
|---|---|
item |
Removed now, or removed already (nothing changes, no second event) |
error 404 not found |
No such payment in this mode |
error 409 not in your list of payout requests (sent already): … |
Sent from the list already: this call never cancels a request that left the list. Cancel it with DELETE /v1/payouts/{id} |
error 409 the payment is sent (or failed, expired, canceled) |
It ended |
| Status | Message | Cause |
|---|---|---|
400 |
remove between 1 and 1000 payout requests at a time |
No ids, or too many |
400 |
invalid request body |
|
403 |
this api key cannot make payouts: … |
Auto-send
With auto_send_minutes in your payout settings
(1 to 1440; 0, the default, sends by hand), the list sends itself once
its oldest valid request has waited that long: a withdrawal requested at
14:00 with 30 minutes goes at 14:30, with everything valid that came in
since. The batch is made and submitted by auto-send (its created_by),
with the approval rules of a batch made with an API key: approvals unless
api_auto_submit lets it skip them (and it never skips them for a new
recipient). Auto-send waits while payitgo has paused your payouts. Turning
auto-send on, or shortening it, loosens your rules: it takes effect 24 hours
later.
The payout wallet
Your payout wallet is a set of addresses payitgo made for your account
alone: one per network (Tron, BNB Smart Chain, Ethereum, Bitcoin,
Litecoin, Dogecoin and the XRP Ledger), the same for every coin of that
network. It is made the first time it is asked for (opening the
payout wallet page, or GET /v1/payout-wallets), and it never changes. It
holds only your money, kept apart from every other account's: top it up
with what your batches need.
payitgo runs the wallet for you and signs its transfers, which only ever pay your batches' recipients, your batches' fees, and withdrawals back to your own payout addresses.
Networks and coins
| Network | chain |
Coins | Network fees paid with |
|---|---|---|---|
| Tron | tron |
USDT_TRC20, TRX |
payitgo's TRX on the wallet |
| BNB Smart Chain | bsc |
USDT_BEP20, USDC_BEP20, ETH_BEP20, BNB |
payitgo's BNB on the wallet |
| Ethereum | ethereum |
USDT_ERC20, USDC_ERC20, DAI_ERC20, ETH |
payitgo's ETH on the wallet |
| Bitcoin | bitcoin |
BTC |
The wallet's own BTC |
| Litecoin | litecoin |
LTC |
The wallet's own LTC |
| Dogecoin | dogecoin |
DOGE |
The wallet's own DOGE |
| XRP Ledger | xrp |
XRP |
The wallet's own XRP |
On Tron, BNB Smart Chain and Ethereum, payitgo pays each transfer's network
fee with its own coin, kept on the wallet for that, and charges it in each
payment's fee: your TRX, BNB or ETH are never spent on fees. On Bitcoin,
Litecoin, Dogecoin and the XRP Ledger, the wallet's own coin pays the
network: each payment's network fee stays in the wallet, set aside
(reserved) until its transaction spends it, and only the platform fee is
sent to payitgo.
- Bitcoin, Litecoin, Dogecoin: up to 200 payments go in one transaction paying them all, its change back to the wallet. A payment under the coin's smallest output is refused (0.00000546 BTC, 0.0000546 LTC, 0.01 DOGE). Should the network's fee rate rise above what the payments were quoted, their transaction waits until it falls.
- XRP Ledger: the ledger locks a reserve on every account, so the
wallet's
balanceis what it holds beyond its reserve. Its first top-up must bring the reserve too:neededand a batch's shortfall include it until the wallet is funded. Paying an address that was never funded takes at least the reserve; an address that needs a destination tag must be given one; an account that accepts only senders it authorised is refused. - ETH and BNB to a contract address are refused: a contract may not handle a plain transfer.
Send only these coins to their network's address. Another token sent there is not counted automatically: contact support and payitgo sorts it out with you.
Balances
GET /v1/payout-wallets (any key) or GET /merchant/payout-wallets?mode=test
(any role) shows each address, and what it holds of each coin:
curl -s https://api.payitgo.co/v1/payout-wallets -H "Authorization: Bearer $PAYITGO_KEY"
200 OK:
{
"wallets": [
{
"chain": "tron",
"network": "Tron",
"address": "TMkq7dZ3vN8xR2pB6cH9sJ4fL1wY5tGaQe",
"explorer_url": "https://tronscan.org/#/address/TMkq7dZ3vN8xR2pB6cH9sJ4fL1wY5tGaQe",
"livemode": true,
"assets": [
{ "asset": "USDT_TRC20", "ticker": "USDT", "icon_url": "https://api.payitgo.co/coins/USDT_TRC20.svg", "balance": "500", "reserved": "122.202417", "spendable": "377.797583", "needed": "0" },
{ "asset": "TRX", "ticker": "TRX", "icon_url": "https://api.payitgo.co/coins/TRX.svg", "balance": "0", "reserved": "0", "spendable": "0", "needed": "0" }
]
},
{
"chain": "bsc",
"network": "BNB Smart Chain",
"address": "0x8F3a6B2c9D1e4F7a0B5c8D2e6F9a1B4c7D0e3F6a",
"explorer_url": "https://bscscan.com/address/0x8F3a6B2c9D1e4F7a0B5c8D2e6F9a1B4c7D0e3F6a",
"livemode": true,
"assets": [
{ "asset": "USDT_BEP20", "ticker": "USDT", "icon_url": "https://api.payitgo.co/coins/USDT_BEP20.svg", "balance": "100", "reserved": "0", "spendable": "100", "needed": "151.26183421" },
{ "asset": "USDC_BEP20", "ticker": "USDC", "icon_url": "https://api.payitgo.co/coins/USDC_BEP20.svg", "balance": "0", "reserved": "0", "spendable": "0", "needed": "0" },
{ "asset": "ETH_BEP20", "ticker": "ETH", "icon_url": "https://api.payitgo.co/coins/ETH_BEP20.svg", "balance": "0", "reserved": "0", "spendable": "0", "needed": "0" },
{ "asset": "BNB", "ticker": "BNB", "icon_url": "https://api.payitgo.co/coins/BNB.svg", "balance": "0", "reserved": "0", "spendable": "0", "needed": "0" }
]
},
{ "chain": "ethereum", "network": "Ethereum", "…": "…" }
]
}
The other networks follow in the same shape: Ethereum, Bitcoin, Litecoin, Dogecoin, the XRP Ledger.
The wallet object
| Field | Type | Description |
|---|---|---|
chain |
string | tron, bsc, ethereum, bitcoin, litecoin, dogecoin or xrp |
network |
string | The network for people: Tron, BNB Smart Chain, Ethereum, Bitcoin, Litecoin, Dogecoin, XRP Ledger |
address |
string | Where to send that network's coins. Show it with a QR code |
explorer_url |
string, optional | The address on the network's block explorer. Live only |
livemode |
boolean | false for the test wallet |
assets |
array | What it holds of each payout coin of that network (below) |
error |
string, optional | Why its balances could not be read just now (the figures are then 0): try again in a moment |
Each asset:
| Field | Type | Description |
|---|---|---|
asset |
string | The coin: one of the payout coins of that network |
ticker, icon_url |
string | What people call it ("USDT") and its icon, an SVG image (Currencies) |
balance |
decimal string | What the address holds, read from the chain (at most 20 seconds old). For TRX, BNB and ETH, payitgo's own coin kept there for network fees is left out; for XRP, the ledger's reserve; for BTC, LTC and DOGE it counts confirmed coins and the wallet's own change. In test mode, the balance on paper |
reserved |
decimal string | Promised already: payments funded and not sent yet (with their network fee on Bitcoin, Litecoin, Dogecoin and XRP), claim links waiting (their worst-case debit), fees not sent yet, withdrawals not sent yet |
spendable |
decimal string | balance less reserved: what a new batch or a withdrawal can use |
needed |
decimal string | What your batches waiting for funds need beyond spendable: the top-up to send now. "0" when nothing waits |
withdrawal_fee |
decimal string | What withdrawing it costs right now, taken from the amount: a withdrawal must be more than it |
A batch waiting for funds also says what it lacks, coin by coin, with the
address to send it to (its waiting.shortfall).
Unused funds
Top the wallet up as your batches need it, and withdraw whatever you like,
whenever you like. Every 6 hours payitgo looks at each live payout wallet;
one holding more than $25,000 of spendable funds (payitgo's threshold today)
for a week tells your team, at most once a week, with a notification in the
payouts category, so unused funds never go unnoticed: "Payout wallet
holding unused funds: Your payout wallet has held about $31000 unused for a
week. If your next payouts won't need it all, you can withdraw the rest to
your own payout address at any time." It is in your audit log too
(payout_wallet.idle).
Withdrawals
What the wallet holds can be sent back to one of your payout addresses for that coin (one past its 24-hour wait): from the dashboard's payout wallet page, by the owner, with their password and their two-factor code. There is no API route for it: an API key can never send the wallet's money anywhere but to a batch's recipients.
POST /merchant/payout-wallets/{asset}/withdraw?mode=test · owner
| Field | Type | Required | Description |
|---|---|---|---|
amount |
decimal string | yes, unless all |
How much of the coin leaves the wallet |
all |
boolean | no | true withdraws everything spendable |
address_id |
string | yes | One of your payout addresses that receives that coin |
password |
string | yes | The owner's password |
code |
string | when they have two-factor sign-in | A code from their authenticator app (or a recovery code) |
curl -s https://api.payitgo.co/merchant/payout-wallets/USDT_TRC20/withdraw \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"all": true, "address_id": "3e7a1c9f5b2d4e8a6c0f2b1d", "password": "a-long-password", "code": "492071"}'
201 Created:
{
"withdrawal": {
"id": "1d3f5a7c9e2b4d6f8a0c2e4b",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"livemode": true,
"asset": "USDT_TRC20",
"chain": "tron",
"amount": "377.797583",
"fee": "3.71",
"send_amount": "374.087583",
"address_id": "3e7a1c9f5b2d4e8a6c0f2b1d",
"address": "TWd4Kp9mR2xH7sN3vB6cJ8fL5qY1tZgEaU",
"status": "requested",
"requested_by": "9b2e4c71d8a05f3e6c1a7b24",
"members": { "9b2e4c71d8a05f3e6c1a7b24": "[email protected]" },
"created_at": "2026-10-20T11:02:15.33Z",
"updated_at": "2026-10-20T11:02:15.33Z"
}
}
A withdrawal pays the network fee of two transfers (yours, and the one
taking its fee to payitgo), in the coin, out of amount (the coin's
withdrawal_fee in the wallet says how much right now): send_amount is
what reaches your address. Its
status goes requested → sending → sent (with tx_id, and in live
mode explorer_url, the transaction on the block explorer), or failed,
which leaves the money in the wallet. When it is sent, a
payout_wallet.withdrawn webhook and a "Payout wallet withdrawal sent"
notification follow. GET /merchant/payout-wallets/withdrawals?mode=test
lists the latest 50. While payitgo has paused payouts on your account, live
withdrawals are refused too.
| Status | Message | Cause |
|---|---|---|
400 |
payouts in SHIB_ERC20 are not available yet |
Not a payout coin |
400 |
address_id is not one of your payout addresses · that payout address receives USDT_BEP20, not USDT_TRC20 |
The wrong address |
400 |
that payout address is used from 2026-10-21T09:00:29Z |
The address is still in its 24-hour wait |
400 |
nothing to withdraw · the wallet has 12.5 USDT_TRC20 to spare |
Nothing spendable, or more than spendable |
400 |
withdrawing costs 3.71 USDT_TRC20: withdraw more than that |
The fees would take it all |
400 |
two-factor code is incorrect or missing · turn on two-factor sign-in first: this takes a code from it |
The code, or two-factor sign-in that your payout settings require |
403 |
password is incorrect |
|
403 |
insufficient permissions |
Not the owner |
409 |
payouts are paused on this account: contact support |
payitgo paused your payouts (live mode) |
A batch's life
validating ──▶ quoted ──▶ pending_approval ──▶ awaiting_funds ──▶ processing ──▶ completed
│ ▲ │ │
│ └ add, remove, new quote │
▼ ▼ ▼
canceled rejected canceled (by you, or by payitgo
when nothing can still be paid)
status |
What it means | What can happen next |
|---|---|---|
validating |
Its rows are being checked and priced. A batch of more than 100 payments is checked in the background, within a minute or two | quoted by itself; canceled |
quoted |
Checked and priced: totals, total_usd, each payment's fees. Its prices hold until quote_expires_at, an hour |
Add or remove payments, get a new quote, submit, cancel |
pending_approval |
Submitted; waiting for approvals_needed approvals |
Approved (it moves on), rejected, canceled |
awaiting_funds |
Approved; waiting for the wallet. waiting says why |
Funded (processing) by itself; canceled |
processing |
Funded: its money is set aside in the wallet, and its fees and payments are being sent | completed; canceling stops the payments not sent yet |
completed |
Funded, and every payment is sent, failed, canceled or expired | Final |
canceled |
Canceled before it was funded: nothing was sent. By you (canceled_by a member or a key), or by payitgo (canceled_by: "system"): a send of payout requests that could not be submitted, or a batch none of whose payments could still be paid when it was funded |
Final |
rejected |
An approver refused it (rejection.reason) |
Final |
Only a validating or quoted batch takes new payments. Submitting a batch
cancels the payments left out (invalid): each becomes canceled with
"cancel_reason": "invalid" (its errors say why), so a submitted batch
never holds a payment that cannot be paid. Until it is funded, a payment can
still be canceled: the batch is totalled again (approvals already given
stand), and canceled when nothing is left. To change a submitted batch
otherwise, cancel it and make another.
When a batch is rejected or canceled, each of its payments not sent is
canceled too, and each raises payout_item.canceled (cancel_reason
rejected, with the approver's reason as cancel_note, or
batch_canceled): your system hears about every payment, not only the
batch.
Why a batch waits
While awaiting_funds, waiting.reason says what it waits for, and
waiting.note says it for people:
reason |
Means | What to do |
|---|---|---|
funds |
The wallet holds less than the batch needs of at least one coin. waiting.shortfall lists what is missing: the coin, its network, the amount and the address to send it to |
Send that amount to that address. The batch starts by itself, usually within a minute of the money arriving on chain |
daily_limit |
Funding it now would pass your daily limit (live only) | Nothing: it is funded from 00:00 UTC. Or raise the limit (which takes 24 hours) |
paused |
payitgo paused payouts on your account | Contact support |
A batch whose prices expired (quote_expires_at) by the time it is funded
is priced again first, at today's network fees, so the wallet is asked for
what its payments cost now: the shortfall of a batch that waits a long time
can move a little.
A payment that can no longer be paid then (under the minimum at today's
prices, an address added to the sanctions list, your limit per payment
lowered) is canceled ("cancel_reason": "invalid", canceled_by: "system", its errors saying why), its money free in the wallet; the
others go on. A batch left with nothing to pay is canceled by payitgo, and
your team is told.
The payments
Each row is a payment with its own status:
status |
What it means |
|---|---|
validating |
Being checked |
ready |
Checked and priced: it goes with the batch |
invalid |
Left out: errors say why. It is never sent and costs nothing. Canceled (cancel_reason: invalid) when its batch is submitted; a payout request stays invalid in your list until you remove it |
cooling |
Funded; a new recipient waits until cooling_until |
queued |
Funded; waiting for its transfer |
sending |
Its transfer is on its way |
sent |
On chain: tx_id (and explorer_url, live) |
failed |
Given up (fail_reason); its money stays in your wallet |
canceled |
Canceled before it was sent: cancel_reason says why, canceled_by who |
claimable |
A claim link, funded: waiting for its recipient to claim it |
expired |
A claim link never claimed: its money is free in the wallet again |
A payment whose transfer fails on chain three times fails, and so does one
the token refuses outright (a recipient its issuer blocked); fail_reason
says which, and the rest of the batch goes on.
Checking and pricing
Every row is checked when it is added, and again whenever the batch is quoted:
- The coin: one of the payout coins.
USDTorUSDCalone is refused with a hint to add the network. - The amount: over 0, with no more decimals than the coin takes (6 for TRX, XRP, and USDT or USDC on Tron and Ethereum; 8 for the others), worth at least $1, and within your limit per payment.
- The address: valid for the coin's network down to its checksum, not
the zero address, not your own payout wallet. An address of another
network says which (
this address is for Tron, not BNB Smart Chain). - The chain: in live mode, each recipient's address is read on chain: a Tron wallet that never held USDT, or a Tron address not activated yet, costs more to pay, and its payment is priced so; ETH or BNB sent to a contract may not be handled, so that row is refused; an XRP address that was never funded, needs a tag or accepts only authorised senders is checked for it. A Bitcoin, Litecoin or Dogecoin amount under the coin's smallest output is refused.
- Claim links and conversions: a claim link has no address and a valid email (or none); a converted payment's address is checked on the network of the coin it is converted into.
- The sanctions list, your references, and duplicate rows.
Each problem is an issue: an error leaves the payment out (invalid), a
warning is worth knowing and the payment still goes.
{ "code": "wrong_network", "field": "address", "message": "this address is for Tron, not BNB Smart Chain", "hint": "pay it in a coin of that network, or check the address" }
| Field | Type | Description |
|---|---|---|
code |
string | For your code: one of the codes below |
field |
string, optional | The field at fault: asset, amount, address, tag, reference, email or claim_assets |
message |
string | For people: what is wrong |
hint |
string, optional | For people: what to do |
code |
Kind | Meaning |
|---|---|---|
unsupported_asset |
error | No coin, an unknown coin, USDT or USDC without its network, a coin payouts do not pay in, or a claim link offering such a coin (or more than 10) |
invalid_amount |
error | The amount is 0, negative or not a number |
too_precise |
error | More decimals than the coin takes |
invalid_address |
error | Not an address of the coin's network (the message says what is wrong), the zero address, a destination tag on a coin that takes none, an XRP account that needs a tag given none or accepts only authorised senders, or an address on a claim link. As a warning: an XRP account that asks not to be sent XRP |
wrong_network |
error | An address of another network |
own_wallet |
error | Your own payout wallet |
duplicate_reference |
error | Another payment already has this reference: in this batch, or in any earlier one of the same mode |
sanctioned |
error | The address is on the US Treasury's sanctions list (OFAC SDN): it cannot be paid |
contract_recipient |
error | ETH or BNB to a contract address, which may not handle a plain transfer (live only) |
below_minimum |
error | Worth less than the smallest payment, $1; under the coin's smallest output (BTC, LTC, DOGE); under the reserve a never-funded XRP address needs; or under what a conversion takes at least |
invalid_email |
error | A claim link's email is not an email address |
conversion_unavailable |
error | The payment cannot be converted into its coin (now): pay it in the wallet's coin |
over_limit |
error | Over your limit per payment |
fee_exceeds_amount |
error | The recipient pays the fees, and they would leave less than $1 |
duplicate_payment |
warning | Another row pays the same amount of the same coin to the same address |
new_holder |
warning | The first USDT sent to a Tron wallet costs more in network fees: included in its fee (live only) |
activation |
warning | A TRX payment to a Tron address not activated yet: the transfer activates it, which costs more; included in its fee (live only) |
check_failed |
warning | The address could not be read on chain just now: priced as the costlier case |
new_recipient |
warning | You never paid this address: the payment waits after the batch is funded (New recipients) |
Codes may be added: treat an unknown one by its kind (in errors or in
warnings) and show its message.
Fees
Each payment pays:
| Fee | How much |
|---|---|
| Platform fee | 0.5% of the payment's amount, never less than $0.50. A rate agreed with your account replaces it |
| Network fee | Sending it on its network, priced from the network's conditions now, at its lowest rate: its own transfer, and its share of what the batch's payments in that coin share (the transfer taking their fees to payitgo, and the network fee top-ups of each transfer of up to 50 payments) |
Both are in the payment's coin, rounded up to the coin's decimals: the
platform fee at the coin's dollar price when the batch was priced. A
payment's fee is everything it is charged, as one sum: the two together,
and for a converted payment what converting costs too
(Conversion). On Bitcoin, Litecoin, Dogecoin
and the XRP Ledger the network fee stays in your wallet for the transaction
to spend: only the platform fee is sent to payitgo. A
claim link is priced at its costliest address, and its fee
taken when claimed.
On Tron, a USDT transfer's network fee is mostly its energy, priced at the network's lowest rate. Should that rate be out of reach for a moment, the transfer waits and tries again a few minutes later rather than paying more, so the network fee you were quoted is the one you pay.
Who pays the fees is the batch's fee_paid_by:
fee_paid_by |
The recipient receives (send_amount) |
The wallet gives (debit) |
|---|---|---|
merchant (the default) |
The whole amount |
amount + fee |
recipient |
amount − fee (at least $1 must be left) |
amount |
A batch that does not say takes the default of your payout settings.
A worked example. A payment of 120 USDT. Its platform fee is 0.60 USDT: 0.5% of $120 is $0.60, over the $0.50 minimum (a payment of $60 would pay the minimum, $0.50). Its network fee depends on the network: about 1.60 USDT on Tron and about 0.012 USDT on BNB Smart Chain, at today's prices.
| 120 USDT | On Tron (USDT_TRC20) |
On BNB Smart Chain (USDT_BEP20) |
|---|---|---|
| Platform fee | 0.60 | 0.60 |
| Network fee | about 1.60 | about 0.012 |
fee |
about 2.20 | about 0.612 |
| You pay the fees: the recipient receives / the wallet gives | 120 / about 122.20 | 120 / about 120.612 |
| The recipient pays them: the recipient receives / the wallet gives | about 117.80 / 120 | about 119.388 / 120 |
The batch shows the totals per coin (totals: amounts, fees, what the
wallet gives and its dollar value) and total_usd, what the whole batch
takes from the wallet. saved_usd adds up, over a coin's payments, what
sending each with its batch saves in network fees against a transfer of its
own.
The fees are sent first. When a batch is funded, its fees in each coin are sent from the wallet to payitgo in one transfer, then its payments. A claim link's fee goes in a transfer of its own when it is claimed.
A payment never sent gets its fee back. A payment canceled, or failed,
before its batch's fees were sent is simply left out of that fee transfer.
One canceled or failed after earns a fee credit (fee_credit): its
platform fee, and its network fee too unless a transfer for it was already
broadcast. Credits are taken off your next batch's fees in the same coin.
You only ever pay for what was sent.
Withdrawing from the wallet pays the network fee of two transfers (Withdrawals), nothing else.
Claim links
Pay someone whose wallet address you don't know: a payment with
"claim": true (or with claim_assets) becomes a claim link. With an
email, payitgo emails the recipient the link; they open it, choose the coin
and enter the address of their wallet, confirm with a code sent to that
email, and the payment goes there. Without an email, share the link
yourself (the payment's claim.url, the hosted claim page) and give the
recipient its PIN (claim.pin, 6 digits) another way: a call, a text
message, in person. The recipient enters the PIN to claim, so a link that
is lost or forwarded pays no one on its own.
A claim link is only ever made when you ask for it: a payment with an email
but no address and no claim is refused for its missing address, never
turned into a link (a system that lost an address would otherwise pay
whoever holds the email). A claim link given an address too is refused
(invalid_address, "a claim link has no address: its recipient chooses
one").
curl -s https://api.payitgo.co/v1/payout-batches \
-H "Authorization: Bearer $PAYITGO_KEY" \
-H 'Content-Type: application/json' \
-d '{"reference": "prizes-2026-10", "items": [{"reference": "prize-ana", "asset": "USDT_TRC20", "amount": "250", "name": "Ana Silva", "email": "[email protected]", "claim": true, "claim_assets": ["USDT_BEP20", "USDC_BEP20"], "note": "Cup final, 1st place"}]}'
- Coins: the payment's
asset, and the other coins listed inclaim_assets(up to 10 payout coins), paid at the same dollar value. Another coin is offered only while your wallet covers it when the link is opened. - Pricing: a claim link is priced at the costliest address it could be
claimed to (a Tron wallet that never held USDT, sent on its own), and that
worst-case
debitis set aside in your wallet while it waits. When it is claimed, it is priced again for the coin and the address chosen, and its fee is taken then, in that coin: usually less than set aside. - Statuses: once its batch is funded, the payment is
claimableuntil claimed (thenqueued,sending,sentlike any payment) or until its link ends:expired. The batch staysprocessinguntil every claim link is claimed or expired; itscountshaveclaimableandexpired. - Expiry: a link works for 14 days after its batch is funded
(
claim.expires_at). An expired payment's money is free in your wallet again, no fee is taken, its reference is given back, and your team gets a "Claim link expired" notification and apayout_item.claim_expiredwebhook. - Emails (live mode only): the link when the batch is funded, a reminder 72 hours before it ends, and the code when the recipient asks for it. A claim link is never converted, and its recipient never waits as a new recipient: the code proves who they are.
- The code: 6 digits, valid 10 minutes, 5 tries; at most 10 codes per link, one a minute.
- The PIN (a link without an email): 6 digits, made with the link, never
emailed and never shown on the claim page. After 5 wrong PINs the link is
locked (
claim.pin_locked): nobody can claim it, and your team gets a "Claim link locked" notification. Make a new PIN from the dashboard (POST /merchant/payout-items/{id}/new-claim-pin): the old one stops working and the link is unlocked. Do that too when a PIN reached the wrong person.
The payment carries claim:
| Field | Type | Description |
|---|---|---|
email |
string, optional | Where the link and the codes are emailed |
assets |
array of strings | The other coins it can be received in |
url |
string, optional | The link (the hosted claim page). Share it when there is no email. Shown only to those who may make payouts (an API key with payouts on, an owner or finance member) and in webhooks. Absent in a quote, and in the dashboard's live updates (read the payment) |
pin |
string, optional | A link without an email: the PIN its recipient enters to claim it. Give it apart from the link. Shown where url is |
pin_locked |
boolean, optional | true once 5 wrong PINs were entered: make a new PIN to unlock it |
expires_at |
string, optional | When the link ends, once funded |
claimed_at |
string, optional | When it was claimed |
funded_asset, funded_amount |
string, decimal string, optional | What it was funded with, when its recipient chose another coin: the payment's asset and amount are then the coin and amount chosen |
emailed |
integer | Emails sent so far (the link, the reminder, resends) |
extended |
integer | How many times the link was extended |
From the dashboard, the team can email the link again
(POST /merchant/payout-items/{id}/resend-claim, live only, at most 5
emails in all, an hour apart), extend it
(POST /merchant/payout-items/{id}/extend-claim {"days": 7}, 1 to 30 days,
at most 3 times), give a link without an email a new PIN
(POST /merchant/payout-items/{id}/new-claim-pin), or cancel it like any payment
(Mass payouts in the dashboard).
When it is claimed, payout_item.claimed is sent with the payment as it
now is (coin, address and fee chosen).
In test mode nothing is emailed: open the link's url yourself; the
emailed code is always 000000, and a link without an email takes its
pin, as in live mode.
Conversion
Pay recipients in coins your wallet doesn't hold: with conversion on, every payment in another coin than your convert-from coin is paid from that coin, converted on its way. Keep one coin in the wallet (USDT on Tron, by default), and pay in BTC, ETH, USDC on Ethereum or any payout coin.
Turn it on in your payout settings ("convert": "convert", and
convert_from, a payout coin, USDT_TRC20 by default), or for one batch or
quote (convert and convert_from in its body). Payments in the
convert-from coin itself are paid directly; claim links are
never converted. The payout requests list follows your
settings.
A converted payment's own fields are in the wallet's coin: asset is the
convert-from coin, and amount, fee (what converting costs included) and
debit are in it. The
recipient's side is in convert, and address is the recipient's address on
convert.to's network:
| Field | Type | Description |
|---|---|---|
convert.to |
string | The coin the recipient receives (what the row asked for) |
convert.ticker, convert.network |
string | That coin for people |
convert.to_amount |
decimal string | What you asked to pay, in that coin |
convert.estimate |
decimal string | What the recipient should receive, at today's rate |
convert.amount_out |
decimal string, optional | What they received, once paid |
How it is priced. The payment sends the amount's worth, plus what
converting costs and a 0.5% buffer for the rate moving, so the recipient
receives about the amount asked, usually a little more; the rate when it is
sent decides. With the recipient paying the fees, they receive what is
left after all of them. A payment
under the conversion's smallest amount is refused (below_minimum); a pair
that cannot be converted gets conversion_unavailable. A batch with
converted payments holds its prices 15 minutes instead of an hour.
How it is sent. Converted payments go one at a time. Should a conversion
fail and its coins come back, they come back to your payout wallet and the
payment fails, its fail_reason saying how much came back.
In test mode conversions are priced at an example cost ($1 plus 1% of the amount) and paid on paper.
Schedules
Make the same batch again and again (payroll, a monthly prize, a weekly commission run) with a schedule, set up in the dashboard: its payments, and when it runs.
| Field | Type | Description |
|---|---|---|
name |
string | Your name for it; each run's batch is named "{name} · {date}" |
cadence |
string | weekly, biweekly (every other week, counted from its first run) or monthly (the default) |
weekday |
integer | Weekly and biweekly: 0 (Sunday) to 6 (Saturday) |
day |
integer | Monthly: 1 to 28, or -1 for the month's last day (default 1) |
at |
string | The time of day, HH:MM (24 hours; default 09:00) |
timezone |
string | An IANA time zone (Africa/Harare, Europe/London; default UTC): the run keeps its wall-clock time across daylight saving changes |
fee_paid_by |
string | merchant or recipient (default: your payout settings') |
auto_submit |
boolean | Submit each run's batch (approvals and limits apply as to any batch); off, the batch waits quoted for someone to submit it |
remind_days |
integer | 0 to 7: that many days before a run your wallet couldn't cover, your team is told what to top up |
items |
array | Its payments, up to 1000, as in a batch (claim links included) |
csv |
string | Instead of items: a payments file's text |
The payments are checked when the schedule is saved: a wrong row is refused
(400 row 3: this address is for Tron, not BNB Smart Chain).
Each run makes a batch (source: "schedule", reference
schedule-{id}-{YYYY-MM-DD}, each payment's reference with -{YYYY-MM-DD}
added, so a run is never made twice), checks and prices it, and submits it
when auto_submit is on. Whoever last set the schedule's payments
(edited_by) made the batch: they cannot approve it, someone else does. A
run that cannot be made (a row no longer valid, the batch over your limit)
is noted in the schedule's last_error, and your team is notified. Each run
sends a payout_schedule.run webhook: the schedule without its items
(id, livemode, name, status, cadence, weekday, day, at,
timezone, fee_paid_by, auto_submit, remind_days, totals,
next_run_at, last_run_at, last_batch_id, last_error, runs). remind_days before a
run the wallet couldn't cover, your team gets a "Top up for scheduled
payouts" notification and a payout_schedule.funding_needed webhook,
{"schedule", "shortfall"}, saying what to send where.
Pausing a schedule stops its runs; resuming it starts again from the next of its days (runs missed meanwhile are not made). Deleting it keeps the batches it made.
The schedule object: id, livemode, name, status (active or
paused), cadence, weekday, day, at, timezone, fee_paid_by,
auto_submit, remind_days, items, totals (per coin: asset,
ticker, items, amount; fees are priced at each run), next_run_at,
last_run_at, last_batch_id, last_error, runs, created_by,
edited_by, members (their emails, as on a batch),
created_at, updated_at. The routes
(/merchant/payout-schedules) are in
Mass payouts in the dashboard.
Approvals and limits
Your payout settings decide who approves a batch, how much may leave, and how new recipients and API keys are treated. The owner reads and changes them in the dashboard, with their password and two-factor code (Payout settings).
| Setting | Default | What it does |
|---|---|---|
approvals |
0 |
How many team members, other than whoever made or submitted the batch, approve it before it is funded: 0, 1 or 2 |
approver_roles |
["owner"] |
Who may approve (and reject, and release a new recipient's payment): owner, finance, or both |
two_factor |
false |
Approving, withdrawing, releasing and changing the payout settings need the person's two-factor code: someone without two-factor sign-in cannot. Turning it on needs the owner's code |
per_payout_max_usd |
"0" (none) |
The most one payment may be worth: a row over it is over_limit |
batch_max_usd |
"0" (none) |
The most a batch may take from the wallet (total_usd): submitting a larger one is refused |
daily_max_usd |
"0" (none) |
The most funded in a UTC day, live only: a batch that would pass it waits until 00:00 UTC (daily_limit) |
new_recipient_hours |
0 (none) |
How long a payment to an address you never paid waits after its batch is funded, up to 168 (New recipients) |
api_auto_submit |
false |
A batch made with an API key skips approval when it is submitted, unless it pays a new recipient (the limits still apply) |
api_require_allowlist |
false |
Only API keys limited to IP addresses may make payouts (The API) |
auto_send_minutes |
0 (send by hand) |
Your payout requests send themselves once the oldest valid one has waited this long, 1 to 1440 minutes (auto-send) |
fee_paid_by |
"merchant" |
Who pays the fees of a batch that does not say: merchant or recipient |
convert |
"same_network" |
same_network: each payment is paid in its own coin, from the wallet's balance of that coin. convert: every payment in another coin than convert_from is paid from convert_from, converted on its way (Conversion) |
convert_from |
"USDT_TRC20" |
The coin conversions are paid from |
Stricter now, looser in 24 hours. A change that tightens a rule (more
approvals, a lower limit, a longer wait, two-factor on, an approver role
removed, auto-submit off, the IP rule on, auto-send off or longer) is in
force at once. One that
loosens a rule waits 24 hours (pending_controls, in force from its
effective_at), so whoever might take over the account cannot loosen the
rules and pay out at once. Meanwhile the stricter rule stays in force. A
change made while one waits replaces it, and its 24 hours start again.
fee_paid_by, convert and convert_from are no safeguards: they change at once.
The approvals are made in the dashboard, by a person: Approve asks for their password, and their two-factor code when they have two-factor sign-in. An API key never approves. With 2 approvals, make sure two people other than the batch's maker can approve.
New recipients
With new_recipient_hours set, a payment to an address you never paid
before (and that is not one of your own payout addresses) waits that long
after its batch is funded: it is cooling, until cooling_until. It warns
of it while the batch is quoted (new_recipient). A batch made with an API
key and paying a new recipient always waits for its approvals, even with
api_auto_submit on.
An approver can let one go sooner from the dashboard (Release, with their password and code). Canceling it keeps its money in the wallet.
Sanctions screening
Every recipient's address is checked against the US Treasury's sanctions
list (OFAC's SDN list, the digital currency addresses in it), which payitgo
downloads again every day. A listed address is refused (sanctioned), its
payment is left out, and the refusal is recorded in your audit log. The
check runs whenever a batch is quoted and again when a batch waiting for
funds is priced again.
References and retries
- A batch's
referenceis unique in its mode: making a batch with a reference you used already returns that batch,200 OKinstead of201 Created, with nothing added. Send one with every batch your system makes, and retry the same request safely after a timeout. - A payment's
reference(an employee's id, a prize's, a withdrawal request's) is unique in its mode across all your batches. A row whose reference another payment carries is kept, butinvalid(duplicate_reference): itsreferencestays empty and the one it asked for is inrefused_reference. Your system never pays the same withdrawal twice. - A payment never sent gives its reference back. When a payment is
canceled or fails (a batch canceled or rejected before funding included),
its reference moves to
released_referenceand itsreferenceis emptied, so a new payment can carry it: pay the same withdrawal request again in a new batch. - A payout request's
referenceworks the same way: sendingPOST /v1/payoutsagain with it answers200 OKwith the payment that has it, in your list or sent. - Find a payment by your reference with
GET /v1/payouts?reference=.
CSV files
In the dashboard, a batch can be made from a CSV file
(POST /merchant/payout-batches/csv, the file as the body, up to 16 MiB and
10,000 rows). Start from the template, GET /merchant/payout-batches/template.csv:
reference,asset,address,amount,name,email,note
payroll-2026-10-ana,USDT_TRC20,TR7mNkucnhLZoJQLLJ6DR88jpLQufWxUJi,1500,Ana Silva,[email protected],October salary
prize-cup-final-1,USDT_BEP20,0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed,250,Player One,,Cup final 1st place
The first row names the columns, in any order and any case, with spaces or
dashes for underscores. asset, address and amount are required
(address may be left out when the file has a claim column; its rows
with claim set to yes are claim links):
| Column | Also read from | Value |
|---|---|---|
reference |
ref, id, external_id, employee_id, player_id |
Your id for the payment |
asset |
coin, token, currency |
A payout coin; or USDT, USDC or DAI with a network column |
network |
chain |
Completes an asset given without its network: TRC20, TRC-20 or Tron; BEP20, BEP-20, BSC or BNB Smart Chain; ERC20, ERC-20 or Ethereum |
address |
wallet, wallet_address, recipient_address, destination |
The recipient's address |
tag |
destination_tag, memo_tag |
XRP only: a whole number |
amount |
value, sum |
The amount in the coin (below for the decimal mark) |
name |
recipient, recipient_name, full_name |
The recipient's name |
email |
recipient_email |
Their email |
note |
description, memo, comment |
Your note |
claim |
claim_link |
yes, y, true, 1 or x: a claim link without an address |
claim_assets |
claim_coins |
A claim link's other coins, separated by spaces, | or / (USDT_BEP20 USDC_BEP20) |
- Commas, semicolons or tabs separate the columns: whichever the header row uses most. Spreadsheet exports with a byte-order mark are read as well.
- Empty lines are skipped. An amount that is not a number becomes 0, which
the check refuses (
invalid_amount), so the row shows up rather than vanishing. - The decimal mark of an amount is the last of
.and,in it:1,500.50and1.500,50are both 1500.50. A lone,is a decimal comma in a file separated by semicolons or tabs (1500,50is 1500.50), and a thousands separator in a comma-separated file ("1,500"is 1500). Spaces, non-breaking spaces and'are ignored (1 500,1'500). - A payment's
rowis its place in the batch, from 1, in the file's order: not the file's line number (the header and empty lines are not counted).
| Status | Message | Cause |
|---|---|---|
400 |
the file is empty or not CSV |
|
400 |
the header row has no amount column (it needs asset, address and amount; reference, name, email, note, claim and claim_assets are optional; address may be left out when there is a claim column) |
A required column is missing |
400 |
line 7: parse error on line 7, column 12: bare " in non-quoted-field |
A line the CSV reader cannot read (a stray quote, say) |
400 |
the file has no payment · a batch holds at most 10000 payments |
GET /merchant/payout-batches/{id}/items.csv downloads a batch's results:
row, reference, name, email, asset, address, tag, amount, fee, send_amount, debit, status, tx_id, explorer_url, fail_reason, errors, warnings, claim_url, claim_pin, claim_expires_at, convert_to, convert_amount, convert_estimate, convert_amount_out (each issue as code: message, separated by ; ; fee is the payment's whole fee). A cell that
a spreadsheet would read as a formula starts with '.
The API
Your server makes batches with an API key, like payments. The key decides the mode: a test key makes test batches, paid from the test wallet on paper.
Payouts are off for every key until the owner turns them on. Reading
(batches, payments, the wallet) works with any key, read-only ones
included; making, adding to, quoting again, submitting and canceling
batches, quotes and test top-ups need a key with payouts on. The owner turns it on in the
dashboard (Update a key), and can limit
the key to your servers' IP addresses (allowed_ips): a key limited so is
refused from any other address, on every route. With
api_require_allowlist in your payout settings, only keys limited to IP
addresses may make payouts.
Every route that takes a batch id answers 404 not found for a batch of
the other mode than the key's: a test key never reads or changes a live
batch, and the other way round.
| Method | Path | Key | Body | Response |
|---|---|---|---|---|
POST |
/v1/payouts |
payouts | a payment: reference, asset, address, tag, amount, name, email, note, claim, claim_assets, webhook_url |
201 {"item": payment}; 200 for a reference used already |
GET |
/v1/payouts/inbox?status=&limit=&offset= |
any | 200 {"inbox": batch, "coverage", "items", "total", "limit", "offset"} |
|
POST |
/v1/payouts/send |
payouts | ids (optional) |
200 {"batch": batch} |
DELETE |
/v1/payouts/{id} |
payouts | 200 {"item": payment} |
|
POST |
/v1/payouts/remove |
payouts | ids (1 to 1000) |
200 {"results": [{"id", "item"} or {"id", "error"}, …]} |
GET |
/v1/payouts?status=&updated_since=&cursor=&limit= |
any | 200 {"items": [payment, …], "has_more", "next_cursor"} |
|
GET |
/v1/events?type=&after=&created_after=&limit= |
any account key | 200 {"events": [event, …], "has_more"} |
|
POST |
/v1/payout-quotes |
payouts | fee_paid_by, convert, convert_from, items (1 to 100) |
200 {"quote": {…}, "items": [payment, …]}, nothing kept |
POST |
/v1/payout-batches |
payouts | reference, name, fee_paid_by, webhook_url, convert, convert_from, items (1 to 1000) |
201 {"batch": batch}; 200 for a reference used already |
GET |
/v1/payout-batches?status=&limit=&offset= |
any | 200 {"batches": [batch, …], "total", "limit", "offset"} |
|
GET |
/v1/payout-batches/{id} |
any | 200 {"batch": batch} |
|
GET |
/v1/payout-batches/{id}/items?status=&limit=&offset= |
any | 200 {"items": [payment, …], "total", "limit", "offset"} |
|
POST |
/v1/payout-batches/{id}/items |
payouts | items (1 to 1000) |
200 {"batch": batch} |
POST |
/v1/payout-batches/{id}/quote |
payouts | 200 {"batch": batch} |
|
POST |
/v1/payout-batches/{id}/submit |
payouts | 200 {"batch": batch} |
|
POST |
/v1/payout-batches/{id}/cancel |
payouts | 200 {"batch": batch} |
|
GET |
/v1/payouts?reference= |
any | 200 {"item": payment} |
|
GET |
/v1/payouts/{id} |
any | 200 {"item": payment} |
|
GET |
/v1/payout-wallets |
any | 200 {"wallets": [wallet, …]} |
|
POST |
/v1/payout-wallets/{asset}/simulate-top-up |
payouts, test key | amount |
200 {"wallets": [wallet, …]} |
The dashboard has the same routes under /merchant (with ?mode=test;
payout requests are under /merchant/payout-requests, payments under
/merchant/payout-items),
plus approving, rejecting, removing a payment, CSV files, canceling or
releasing one payment, withdrawals and the payout settings
(Mass payouts in the dashboard).
Quote payments
POST /v1/payout-quotes · a key with payouts on
What up to 100 payments would cost, checked and priced exactly as in a
batch, without keeping anything: use it to show a recipient their fee
before they ask for a withdrawal. The body takes fee_paid_by, convert,
convert_from and items, as a batch does.
curl -s https://api.payitgo.co/v1/payout-quotes \
-H "Authorization: Bearer $PAYITGO_KEY" \
-H 'Content-Type: application/json' \
-d '{"fee_paid_by": "recipient", "items": [{"asset": "USDT_BEP20", "address": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", "amount": "120"}]}'
200 OK: quote holds the totals a batch of them would have (livemode,
fee_paid_by, convert, convert_from (only when converting),
totals, total_usd, counts), and
items the payments as they would be, each with its
fee and issues. Nothing is kept: the payments have no id, merchant_id,
batch_id or times.
{
"quote": {
"livemode": true,
"fee_paid_by": "recipient",
"convert": "same_network",
"totals": [
{ "asset": "USDT_BEP20", "ticker": "USDT", "icon_url": "https://api.payitgo.co/coins/USDT_BEP20.svg", "chain": "bsc", "network": "BNB Smart Chain (BEP-20)", "items": 1, "amount": "120", "send_amount": "119.38816579", "fee": "0.61183421", "debit": "120", "debit_usd": "120", "saved_usd": "0" }
],
"total_usd": "120",
"counts": { "total": 1, "invalid": 0, "ready": 1, "cooling": 0, "queued": 0, "sending": 0, "sent": 0, "failed": 0, "canceled": 0, "claimable": 0, "expired": 0 }
},
"items": [
{
"livemode": true,
"row": 1,
"recipient": {},
"asset": "USDT_BEP20",
"ticker": "USDT",
"icon_url": "https://api.payitgo.co/coins/USDT_BEP20.svg",
"chain": "bsc",
"address": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"amount": "120",
"send_amount": "119.38816579",
"fee": "0.61183421",
"debit": "120",
"usd_rate": "1",
"amount_usd": "120",
"status": "ready",
"errors": [],
"warnings": []
}
]
}
| Status | Message | Cause |
|---|---|---|
400 |
quote between 1 and 100 payments |
|
400 |
fee_paid_by must be merchant or recipient |
Create a batch
POST /v1/payout-batches · a key with payouts on
| Field | Type | Required | Description |
|---|---|---|---|
reference |
string | no, but send one | Your id for the batch, unique in its mode. A reference used already returns that batch (retries) |
name |
string | no | Your name for it, for people ("October payroll") |
fee_paid_by |
string | no | merchant (fees on top) or recipient (fees out of each amount). Default: your payout settings |
webhook_url |
string | no | An https:// URL on a public host: this batch's events go there instead of your account's webhook URL (Webhooks) |
convert, convert_from |
string | no | same_network or convert, and the coin conversions are paid from (Conversion). Default: your payout settings |
items |
array | yes | 1 to 1000 payments (below). Add more with /items, up to 10,000 |
Each payment:
| Field | Type | Required | Description |
|---|---|---|---|
asset |
string | yes | A payout coin (any case). With conversion on, any payout coin: it is the coin the recipient receives |
address |
string | yes, unless a claim link | The recipient's address on the coin's network |
amount |
decimal string | yes | How much of the coin. With fee_paid_by: "recipient" the fees come out of it |
reference |
string | no | Your id for this payment, unique in its mode (retries) |
name, email, note |
string | no | Who it is for, and why: kept with it, shown in the dashboard and the results file. A claim link's link is emailed there; otherwise nothing is sent to the email |
tag |
integer | no | XRP only: the destination tag the recipient's address needs |
claim |
boolean | no | true makes it a claim link, without an address (with no email: you share its link) |
claim_assets |
array of strings | no | The other coins its recipient may choose (up to 10). Giving them makes the payment a claim link |
webhook_url |
string | no | An https:// URL on a public host: this payment's events go there instead of the batch's webhook_url or your account's URL. At most 10 different URLs in one call |
curl -s https://api.payitgo.co/v1/payout-batches \
-H "Authorization: Bearer $PAYITGO_KEY" \
-H 'Content-Type: application/json' \
-d '{
"reference": "payroll-2026-10",
"name": "October payroll",
"items": [
{"reference": "payroll-2026-10-ana", "asset": "USDT_TRC20", "address": "TR7mNkucnhLZoJQLLJ6DR88jpLQufWxUJi", "amount": "120", "name": "Ana Silva", "email": "[email protected]", "note": "October salary"},
{"reference": "payroll-2026-10-ben", "asset": "USDT_BEP20", "address": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", "amount": "250", "name": "Ben Okafor"},
{"reference": "payroll-2026-10-chen", "asset": "USDT_BEP20", "address": "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7", "amount": "75", "name": "Chen Wei"}
]
}'
201 Created. Up to 100 payments are checked while the call waits, and the
batch comes back quoted:
{
"batch": {
"id": "6a1f3c8e2d9b4a7051c3e8f2",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"livemode": true,
"reference": "payroll-2026-10",
"name": "October payroll",
"source": "api",
"inbox": false,
"open": false,
"created_by": "5e1a9c3d7b2f40e8a6c4d2b1",
"by_api_key": true,
"fee_paid_by": "merchant",
"convert": "same_network",
"status": "quoted",
"totals": [
{ "asset": "USDT_TRC20", "ticker": "USDT", "icon_url": "https://api.payitgo.co/coins/USDT_TRC20.svg", "chain": "tron", "network": "Tron (TRC-20)", "items": 1, "amount": "120", "send_amount": "120", "fee": "2.202417", "debit": "122.202417", "debit_usd": "122.2", "saved_usd": "0" },
{ "asset": "USDT_BEP20", "ticker": "USDT", "icon_url": "https://api.payitgo.co/coins/USDT_BEP20.svg", "chain": "bsc", "network": "BNB Smart Chain (BEP-20)", "items": 1, "amount": "250", "send_amount": "250", "fee": "1.26183421", "debit": "251.26183421", "debit_usd": "251.26", "saved_usd": "0" }
],
"counts": { "total": 3, "invalid": 1, "ready": 2, "cooling": 0, "queued": 0, "sending": 0, "sent": 0, "failed": 0, "canceled": 0, "claimable": 0, "expired": 0 },
"total_usd": "373.46",
"quote_expires_at": "2026-10-20T10:15:02.41Z",
"approvals_needed": 0,
"approvals": [],
"members": {},
"created_at": "2026-10-20T09:15:01.87Z",
"updated_at": "2026-10-20T09:15:02.41Z"
}
}
The third row pays a Tron address in a BNB Smart Chain coin: it is
invalid, left out of totals (list the payments
to see why). A batch of more than 100 payments comes back validating, its
counts.total and counts.ready both the number of payments until it is
checked; a payout_batch.quoted webhook says when it is ready, within a
minute or two.
| Status | Message | Cause |
|---|---|---|
400 |
a batch needs at least one payment |
No items |
400 |
send at most 1000 payments in one call: add the rest with /items, or upload a CSV file |
Over 1000 items |
400 |
fee_paid_by must be merchant or recipient |
|
400 |
webhook_url: url must use https (and the other URL errors) |
An unusable webhook_url, the batch's or a payment's |
400 |
payments may name at most 10 different webhook_urls in one call |
|
400 |
convert must be same_network or convert · convert_from: payouts are not paid from SHIB_ERC20 (pay from …) |
An unusable convert or convert_from |
400 |
invalid request body |
Not JSON, or a field of the wrong type |
403 |
this api key cannot make payouts: turn payouts on for it in the dashboard |
The key's payouts is off |
403 |
your payout rules require keys limited to IP addresses: set this key's allowed IPs |
api_require_allowlist is on and the key has no allowed_ips |
A row's own problems never fail the request: they are its errors.
Add payments
POST /v1/payout-batches/{id}/items · a key with payouts on
{"items": [payment, …]}, 1 to 1000 at a time, to a validating or
quoted batch, up to 10,000 payments in all. They are numbered after the
last row, and the whole batch is checked and priced again (in the
background past 100 payments). 200 OK: {"batch": batch}.
| Status | Message | Cause |
|---|---|---|
400 |
add between 1 and 1000 payments at a time · a batch holds at most 10000 payments |
|
404 |
not found |
No such batch |
409 |
a submitted batch cannot change: cancel it and make another |
It was submitted |
Submit a batch
POST /v1/payout-batches/{id}/submit · a key with payouts on
Sends a quoted batch on: its ready payments go, the invalid ones stay
out. With approvals in your payout settings it becomes pending_approval
(unless api_auto_submit lets it skip them); otherwise awaiting_funds,
and it is funded at once if the wallet covers it (processing). When the
wallet falls short, the answer already says by how much:
{
"batch": {
"id": "6a1f3c8e2d9b4a7051c3e8f2",
"status": "awaiting_funds",
"waiting": {
"reason": "funds",
"note": "top up the payout wallet: 122.202417 USDT_TRC20 on Tron (TRC-20), 251.26183421 USDT_BEP20 on BNB Smart Chain (BEP-20)",
"since": "2026-10-20T09:16:40.12Z",
"shortfall": [
{ "asset": "USDT_TRC20", "network": "Tron (TRC-20)", "amount": "122.202417", "address": "TMkq7dZ3vN8xR2pB6cH9sJ4fL1wY5tGaQe" },
{ "asset": "USDT_BEP20", "network": "BNB Smart Chain (BEP-20)", "amount": "251.26183421", "address": "0x8F3a6B2c9D1e4F7a0B5c8D2e6F9a1B4c7D0e3F6a" }
]
},
"approvals_needed": 0,
"submitted_by": "5e1a9c3d7b2f40e8a6c4d2b1",
"submitted_at": "2026-10-20T09:16:40.09Z",
"…": "…"
}
}
| Status | Message | Cause |
|---|---|---|
400 |
no valid payment to send |
Every payment is invalid |
400 |
this batch takes $373.46, over your limit of $250.00 per batch |
Over batch_max_usd |
404 |
not found |
|
409 |
the batch is still being checked |
validating |
409 |
its prices expired: get a new quote first |
Past quote_expires_at (an hour after it was quoted; 15 minutes with converted payments): get a new quote, then submit |
409 |
the batch was submitted already |
Get a new quote
POST /v1/payout-batches/{id}/quote · a key with payouts on
Checks and prices a batch not submitted yet (validating or quoted)
again, at today's prices: its prices then hold for another hour (15
minutes with converted payments). Use it
when submitting answers 409 its prices expired: get a new quote first.
200 OK: {"batch": batch}, quoted (or validating past 100 payments,
payout_batch.quoted following). 409 a submitted batch cannot change: cancel it and make another once submitted; 404 not found for another
mode's batch.
Cancel a batch
POST /v1/payout-batches/{id}/cancel · a key with payouts on
Before it is funded (validating, quoted, pending_approval,
awaiting_funds), the whole batch is canceled and nothing leaves the
wallet. Once it is processing, the payments not sent yet are canceled
(one whose transfer is being signed at that moment may still go): their
money stays in the wallet, their fees come back (Fees), and the
batch completes with what was sent. 200 OK: {"batch": batch}.
409 the batch is completed (or canceled, rejected) when there is
nothing left to cancel.
List batches
GET /v1/payout-batches?status=&limit=&offset= · any key
The key's mode's batches, newest first. status filters on one status;
limit is 1 to 200 (50 by default).
Get a batch
GET /v1/payout-batches/{id} · any key
200 OK: {"batch": batch}. 404 not found for another mode's batch.
List a batch's payments
GET /v1/payout-batches/{id}/items?status=&limit=&offset= · any key
In row order. status takes one status or several, comma-separated
(?status=invalid, ?status=sent,failed).
curl -s 'https://api.payitgo.co/v1/payout-batches/6a1f3c8e2d9b4a7051c3e8f2/items?status=invalid' -H "Authorization: Bearer $PAYITGO_KEY"
200 OK:
{
"items": [
{
"id": "4e6a8c0b2d4f6a1c3e5b7d9a",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"livemode": true,
"batch_id": "6a1f3c8e2d9b4a7051c3e8f2",
"reference": "payroll-2026-10-chen",
"row": 3,
"recipient": { "name": "Chen Wei" },
"asset": "USDT_BEP20",
"ticker": "USDT",
"icon_url": "https://api.payitgo.co/coins/USDT_BEP20.svg",
"chain": "bsc",
"address": "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7",
"amount": "75",
"send_amount": "0",
"fee": "0",
"debit": "0",
"usd_rate": "0",
"amount_usd": "0",
"status": "invalid",
"errors": [
{ "code": "wrong_network", "field": "address", "message": "this address is for Tron, not BNB Smart Chain", "hint": "pay it in a coin of that network, or check the address" }
],
"warnings": [],
"created_at": "2026-10-20T09:15:01.88Z",
"updated_at": "2026-10-20T09:15:02.4Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}
Get a payment
GET /v1/payouts/{id} or GET /v1/payouts?reference=payroll-2026-10-ana · any key
One payment, by its id or by your reference. By reference, the payment
that carries it now; if none does, the latest that gave it back (canceled,
failed or expired, with it as released_reference): what became of a
request sent with it. A sent payment:
{
"item": {
"id": "8b2d4f6a1c3e5a7b9d0f2c4e",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"livemode": true,
"batch_id": "6a1f3c8e2d9b4a7051c3e8f2",
"reference": "payroll-2026-10-ana",
"row": 1,
"recipient": { "name": "Ana Silva", "email": "[email protected]", "note": "October salary" },
"asset": "USDT_TRC20",
"ticker": "USDT",
"icon_url": "https://api.payitgo.co/coins/USDT_TRC20.svg",
"chain": "tron",
"address": "TR7mNkucnhLZoJQLLJ6DR88jpLQufWxUJi",
"amount": "120",
"send_amount": "120",
"fee": "2.202417",
"debit": "122.202417",
"usd_rate": "1",
"amount_usd": "120",
"status": "sent",
"errors": [],
"warnings": [],
"tx_id": "3c9f1a7e5b2d8c4f6a0e9b3d7c1f5a2e8b4d0c6f9a3e7b1d5c8f2a6e0b4d9c3f",
"explorer_url": "https://tronscan.org/#/transaction/3c9f1a7e5b2d8c4f6a0e9b3d7c1f5a2e8b4d0c6f9a3e7b1d5c8f2a6e0b4d9c3f",
"sent_at": "2026-10-20T09:31:12.6Z",
"created_at": "2026-10-20T09:15:01.88Z",
"updated_at": "2026-10-20T09:31:12.6Z"
}
}
| Status | Message | Cause |
|---|---|---|
404 |
not found |
No such payment in the key's mode |
List payments
GET /v1/payouts?status=&updated_since=&cursor=&limit= · any key
Every payment of the key's mode, whatever its batch (requests in your list, removed ones, every batch's), oldest change first: to bring your system back in sync after it was down, or to check it is.
| Query | Description |
|---|---|
status |
Comma-separated statuses (sent,failed,canceled,expired for the ended ones); all when left out |
updated_since |
RFC 3339 time: payments changed at or after it |
cursor |
The next_cursor of the page before: the payments changed after the last one it listed |
limit |
1 to 200; 50 by default |
{
"items": [ { "id": "5c7e9a1b3d5f7a9c1e3b5d7f", "reference": "wd-88231", "status": "sent", "updated_at": "2026-10-20T14:31:12.6Z", "…": "…" } ],
"has_more": true,
"next_cursor": "MjAyNi0xMC0yMFQxNDozMToxMi42WnwyYzVlN2E5YjFkM2Y1YTdjOWUxYjNkNWY"
}
Read with updated_since the first time, then with cursor while
has_more; keep the last next_cursor to start from next time. Changes of
the last few seconds are left for the next read, so a cursor never skips
one.
| Status | Message | Cause |
|---|---|---|
400 |
updated_since must be an RFC 3339 time, such as 2026-10-20T09:00:00Z |
|
400 |
limit must be 1 to 200 |
|
400 |
cursor is not one this API gave |
The batch object
| Field | Type | Description |
|---|---|---|
id |
string | The batch's id |
merchant_id |
string | Your account |
livemode |
boolean | false for a test batch |
reference |
string, optional | Your id for it, unique in its mode |
name |
string | Your name for it; "" when none |
source |
string | api, dashboard, csv, requests (made of payout requests) or schedule (a schedule's run) |
inbox |
boolean | Made of payout requests: your list of them, or a batch of sent ones |
open |
boolean | Your list of payout requests itself (one per mode): never submitted, never listed among batches |
created_by |
string | The team member who made it, the API key's id (by_api_key), auto-send for payout requests that sent themselves, or requests for the list |
by_api_key |
boolean | Whether an API key made it (true for auto-send too) |
fee_paid_by |
string | merchant or recipient |
webhook_url |
string, optional | Where its events go instead of your account's URL |
convert, convert_from |
string | Its conversion rule (same_network or convert) and, when converting, the coin converted from |
status |
string | A batch's life |
waiting |
object, optional | While awaiting_funds and waiting: reason (funds, daily_limit, paused), note for people, since, and for funds the shortfall: [{"asset", "network", "amount", "address"}], what to send where |
totals |
array | Its payments that go, per coin: asset, ticker and icon_url (what people call the coin, and its icon), chain, network, items (how many), amount, send_amount, fee (their fees, as one sum), debit (what the wallet gives), debit_usd, and saved_usd (what sending them together saves in network fees) |
counts |
object | Its payments by status: total, invalid, ready, cooling, queued, sending, sent, failed, canceled, claimable, expired |
total_usd |
decimal string | What it takes from the wallet, in dollars |
quote_expires_at |
string, optional | Its prices hold until then |
approvals_needed |
integer | Approvals it needs, set when it is submitted |
approvals |
array | [{"user_id", "at"}]: who approved it, and when |
rejection |
object, optional | {"by", "at", "reason"} once rejected |
submitted_by, submitted_at |
string, optional | Who submitted it (a member's id or a key's), and when |
funded_at, completed_at |
string, optional | When it was funded, and completed |
canceled_by, canceled_at |
string, optional | Who canceled it, and when (also on a processing batch whose remaining payments were canceled) |
members |
object | The emails of the team members it names (created_by, submitted_by, approvals[].user_id, rejection.by, canceled_by), by id, as they were when each acted: {"9b2e4c71d8a05f3e6c1a7b24": "[email protected]"}. A member who has left the team stays named. API keys, auto-send and the platform are not in it; {} when no member acted |
created_at, updated_at |
string |
The payment object
| Field | Type | Description |
|---|---|---|
id |
string | The payment's id |
merchant_id |
string | Your account |
batch_id |
string, optional | Its batch: your list's id while it is a payout request waiting there. Absent for a request removed from the list |
livemode |
boolean | |
reference |
string, optional | Your id for it, unique in its mode |
refused_reference |
string, optional | A reference it was given that another payment carries: it keeps none, and is invalid |
released_reference |
string, optional | Once it is canceled or failed (never sent): the reference it had, given back for a new payment to use; reference is then empty |
row |
integer | Its place in the batch, from 1; payments added later follow the last row |
recipient |
object | name, email, note, as you gave them (each left out when empty) |
asset, chain |
string | The coin, and its network |
ticker, icon_url |
string | What people call the coin ("USDT") and its icon, an SVG image. Absent for a coin payouts do not know |
address |
string | The recipient's address, as it is sent to (an EVM address in its checksummed form) |
tag |
integer, optional | A destination tag |
amount |
decimal string | What you asked to pay |
send_amount |
decimal string | What reaches the recipient |
fee |
decimal string | Everything it is charged, in the coin, as one sum: the platform fee and the network fee, and for a converted payment what converting costs |
debit |
decimal string | What it takes from the wallet |
usd_rate |
decimal string | The coin's dollar price when it was priced |
amount_usd |
decimal string | amount in dollars, to the cent |
status |
string | The payments |
errors, warnings |
array | Its issues; [] when none |
cooling_until |
string, optional | A new recipient's payment waits until then |
tx_id |
string, optional | Its transaction, once sent (made up in test mode) |
explorer_url |
string, optional | The transaction on the block explorer. Live only |
fail_reason |
string, optional | Why it failed |
cancel_reason |
string, optional | Why it was canceled: removed (taken out of your list of payout requests), canceled (that payment alone, by your team or a key), batch_canceled (its batch was canceled), rejected (its batch was rejected), invalid (no longer payable when its batch was submitted or funded: errors say why) |
cancel_note |
string, optional | With rejected: the approver's reason |
canceled_by, canceled_at |
string, optional | Who canceled it (a team member's id, an API key's id, or system: payitgo itself), and when |
members |
object, optional | The email of the team member in canceled_by, by id, as on batches |
webhook_url |
string, optional | Where its events go instead of its batch's webhook_url or your account's URL |
claim |
object, optional | A claim link's state: email, assets, url, pin, pin_locked, expires_at, claimed_at, funded_asset, funded_amount, emailed, extended |
convert |
object, optional | A converted payment: to, ticker, network, to_amount, estimate, amount_out |
fee_credit |
decimal string, optional | The fee given back for a payment never sent, taken off your next batch's fees in its coin |
sent_at |
string, optional | When it was sent |
created_at, updated_at |
string |
Before it is checked (validating) and when it is invalid, its fee and
dollar figures are "0".
Webhooks
Each step of a batch and of each payment raises a webhook, signed like every other with the secret of its mode. Where they go:
- a payment made with its own
webhook_url(a payout request, or a batch row): its events go there; - otherwise a batch made with
webhook_url: its batch and payment events go there; - otherwise your account's webhook URL of the mode. Withdrawals and schedules always go there.
| Event | Sent when | data |
|---|---|---|
payout_batch.quoted |
It was checked and priced: made, payments added or removed, quoted again | Batch |
payout_batch.pending_approval |
Submitted; it waits for approvals | Batch |
payout_batch.approved |
It got its last approval: it is funded next (payout_batch.processing), or waits (payout_batch.awaiting_funds) |
Batch |
payout_batch.rejected |
An approver refused it (rejection.reason) |
Batch |
payout_batch.awaiting_funds |
It cannot be funded yet: waiting says why (funds, with the shortfall to send; daily_limit; paused). Sent again whenever the reason changes. A batch the wallet covers at once goes straight to payout_batch.processing |
Batch |
payout_batch.processing |
Funded: its fees and payments are being sent | Batch |
payout_batch.completed |
Funded, and every payment is sent, failed, canceled or expired (counts) |
Batch |
payout_batch.canceled |
Canceled before it was funded: by your team or a key, or by payitgo (canceled_by: "system": a send of requests that could not be submitted, or nothing left that could be paid) |
Batch |
payout_item.processing |
A payout request left your list for a batch (sent from the dashboard, by POST /v1/payouts/send or by auto-send): batch_id is the new batch |
Payment |
payout_item.returned |
A sent payout request came back to your list: its batch could not be submitted, or it was no longer valid when priced again (status invalid, errors say why). Not final |
Payment |
payout_item.sent |
A payment is on chain (tx_id) |
Payment |
payout_item.failed |
A payment was given up (fail_reason); its money stays in the wallet |
Payment |
payout_item.canceled |
A payment was canceled before it was sent, whatever canceled it: cancel_reason (removed, canceled, batch_canceled, rejected with cancel_note, invalid) and canceled_by say why and who |
Payment |
payout_item.claimed |
A claim link was claimed: the payment with the coin, address and fee its recipient chose | Payment |
payout_item.claim_expired |
A claim link ended unclaimed; its money is free in the wallet again | Payment |
payout_schedule.run |
A schedule ran: its batch was made (last_batch_id), or last_error says why not |
The schedule, without its items |
payout_schedule.funding_needed |
remind_days before a schedule's run the wallet couldn't cover |
{"schedule", "shortfall"} (the schedule without its items) |
payout_wallet.withdrawn |
A withdrawal reached your payout address (tx_id) |
Withdrawal |
payout_wallet.withdrawal_failed |
A withdrawal could not be sent (fail_reason); its money stays in the wallet |
Withdrawal |
Taking a payout request raises no event (the answer says it all); from then on, the request's payment raises the events above.
Staying in sync
Every payment you ask for, a payout request or a batch row, ends in exactly one final state, and you hear it once, whoever acted on it: your system, a member of your team in the dashboard (removing it, rejecting or canceling its batch), or payitgo (a payment no longer payable):
Final status |
Event | What to do in your system |
|---|---|---|
sent |
payout_item.sent |
Mark it paid (tx_id) |
failed |
payout_item.failed |
Not paid: its money is in your wallet. Request it again if you still want to pay it |
canceled |
payout_item.canceled |
Not paid, never will be (cancel_reason). Give your client their money back, or request it again |
expired |
payout_item.claim_expired |
A claim link nobody claimed |
A payout request also tells you when it leaves your list
(payout_item.processing) and when it comes back (payout_item.returned).
A batch row is in its batch from the start: its batch's events say how the
batch goes.
- At least once, recorded once. A payment's event is recorded with the
change itself, so it is never lost, even if payitgo stops at that moment;
it is then sent within minutes. It keeps its
idhowever often it is sent: deduplicate on the event'sid. - In any order. Events can arrive out of order (retries, several at
once): act on the payment's
statusand its latestupdated_at, never on the order they arrive in. A final status never changes again. - Replay what you missed.
GET /v1/eventslists every event of the mode, as it was sent, from any point: after an outage, read from the last event you handled. - Check the whole picture.
GET /v1/payoutslists your payments by last change, whatever their batch. - Canceled stays canceled. A canceled payment is never brought back: to pay it after all, send a new request (its reference is free again).
Replay events
GET /v1/events?type=&after=&created_after=&limit= · any key of the
account (read-only ones too; a storefront's key is refused)
The mode's events, oldest first, each exactly as its webhook carried
it (id, type, livemode, created_at, data), payments' and payouts'
alike. Events are kept; a test ping is left out.
| Query | Description |
|---|---|
type |
Comma-separated: exact types (payout_item.sent), or a family (payout_item.*); all when left out |
after |
An event's id: the events after it. Keep the last one you handled |
created_after |
RFC 3339 time: the events after it (ignored with after) |
limit |
1 to 200; 100 by default |
curl -s "https://api.payitgo.co/v1/events?type=payout_item.*&after=4f1c2a9e7b3d5f80a2c4e6b8" \
-H "Authorization: Bearer $PAYITGO_KEY"
{
"events": [
{
"id": "9b1e3c5a7d2f4b6e8a0c1d3f",
"type": "payout_item.canceled",
"livemode": true,
"created_at": "2026-10-20T15:02:44.312Z",
"data": { "id": "5c7e9a1b3d5f7a9c1e3b5d7f", "status": "canceled", "cancel_reason": "rejected", "cancel_note": "paid by bank transfer instead", "released_reference": "wd-88231", "…": "…" }
}
],
"has_more": false
}
While has_more, read again with after the last id. Events of the last
few seconds are left for the next read, so moving forward never skips one.
A key that cannot make payouts gets claim links without their url and
pin.
| Status | Message | Cause |
|---|---|---|
400 |
type must be event types, such as payout_item.sent or payout_item.* |
|
400 |
created_after must be an RFC 3339 time, such as 2026-10-20T09:00:00Z |
|
400 |
limit must be 1 to 200 |
|
400 |
after is not an event of this mode |
An unknown id, or the other mode's |
403 |
a storefront's key cannot read the account's events: use an account key |
In test mode
A test key (or the dashboard with ?mode=test) works with your test
payout wallet: other addresses than the live ones, with balances on
paper. Nothing is ever read from or sent on a chain:
-
Top it up with a click:
POST /v1/payout-wallets/{asset}/simulate-top-upwith a test key (a key with payouts on), orPOST /merchant/payout-wallets/{asset}/simulate-top-up?mode=test(owner or finance), addsamount(over 0, up to 10,000,000) to its paper balance, and funds any test batch waiting for it. The answer is{"wallets": [wallet, …]}.curl -s https://api.payitgo.co/v1/payout-wallets/USDT_TRC20/simulate-top-up \ -H "Authorization: Bearer $PAYITGO_TEST_KEY" \ -H 'Content-Type: application/json' \ -d '{"amount": "1000"}' -
Batches are checked and priced as live ones, with the same fees, but recipients' addresses are not read on chain: no
new_holder,activationorcontract_recipientissues. -
Fees and payments are sent on paper, within seconds: each payment gets a made-up
tx_id(64 hex characters, on no chain, with noexplorer_url), the paper balance goes down, and the webhooks are sent as live. -
Withdrawals work the same way, to a test payout address.
-
Every network works on paper, Bitcoin, Litecoin, Dogecoin and XRP included.
-
Claim links email no one: open the link's
urlyourself; the code is always000000. -
Conversions are priced at an example cost ($1 plus 1%) and paid on paper.
-
Payout requests sent with a test key wait in your test list, apart from the live one, and are sent the same way.
-
The daily limit does not apply.
| Status | Message | Cause |
|---|---|---|
400 |
only a test wallet is topped up on paper: use a test key |
A live key on /v1 |
400 |
only a test wallet is topped up on paper: add ?mode=test |
The dashboard route without ?mode=test |
400 |
payouts in SHIB_ERC20 are not available yet |
Not a payout coin |
400 |
amount must be over 0 and at most 10000000 |
Limits
| What | Limit |
|---|---|
| Payments in a batch | 10,000 |
| Payout requests waiting in your list | 100,000; up to 10,000 go in each send |
| Payments in one create or add call | 1,000 (a CSV file in the dashboard takes up to 10,000) |
| Checked while the call waits | 100: a larger batch is checked in the background (validating) |
| Payments in a quote | 100 |
| A quote's prices | Hold for an hour |
| The smallest payment | $1 |
| Payments in one transfer | 50 (200 in one Bitcoin, Litecoin or Dogecoin transaction); one transfer at a time per wallet and network |
| Speed | One transfer every 15 seconds per network, from one wallet: about 240 payments an hour; on Bitcoin, Litecoin and Dogecoin one transfer pays up to 200 |
| A CSV file | 16 MiB |
allowed_ips |
20 addresses or ranges per key |
| A simulated top-up | 10,000,000 of the coin per call |
| A batch with converted payments | Its prices hold 15 minutes |
| A claim link | Works 14 days after funding; extended up to 3 times (1 to 30 days); up to 5 emails; 10 other coins |
| A schedule | 1000 payments |
/v1 requests |
20 a second per key (rate limits) |
Audit log
Every step is in your audit log, with who did it
and the figures before and after: payout_batch.created, .items_added,
.item_removed, .submitted, .approved, .rejected, .funded,
.canceled, .completed, .items_canceled (payments no longer payable
when it was funded); payout_request.created, payout_request.canceled,
payout_requests.sent, payout_requests.returned, payout_requests.removed;
payout_item.canceled, .released, .screening_hit, .claimed,
.claim_resent, .claim_extended, .claim_expired, .claim_locked,
.claim_pin_changed; payout_schedule.created, .updated,
.paused, .resumed, .deleted, .run, .funding_needed;
payout_wallet.created, .extended (a network added to it), .test_top_up,
.withdrawal_requested, .withdrawal_sent, .withdrawal_failed, .idle;
payout_controls.updated and .took_effect (a loosening whose 24 hours
ended); api_key.updated.