Test mode
Test mode is a full copy of payitgo with no coins: the same API, the same
webhooks and the same checkout. Build your whole integration with your
test key, then go live by swapping in the live key and the live webhook
secret. The dashboard shows each key with a Test or Live badge (its
livemode): the key's text does not say.
A test payment gets an address, an amount and a deadline like a live one, but nothing watches a blockchain for it. You choose what happens to it instead: simulate an outcome, and payitgo plays the deposits, status changes and webhooks a few seconds apart, exactly as live coins would. You never need test coins, a faucet or a wallet.
What is separate
| Test key | Live key | |
|---|---|---|
| Payments | Test payments only; "livemode": false |
Live payments only; "livemode": true |
| Order ids | A test order_id never collides with a live one |
|
| Coins | None: you simulate what the customer sends | Real coins on mainnet |
| Webhook | The test webhook: its own URL and its own secret | The live webhook: its own URL and secret |
| Money | Your test balance, never paid out; direct settlement to a test payout address completes on paper | Your balance, or your live payout addresses after their 24-hour wait |
| Dashboard | Add ?mode=test to dashboard routes |
The default |
A test key reading a live payment gets 404, and the other way round.
Simulate a payment
There are three ways to choose an outcome:
-
When you create the payment, add
"simulate":curl -s $PAYITGO_BASE_URL/v1/payments \ -H "Authorization: Bearer $PAYITGO_KEY" \ -H 'Content-Type: application/json' \ -d '{"order_id": "ORD-2001", "amount_usd": "20", "token": "USDT_TRC20", "simulate": "confirmed"}'With a
token, the simulation starts at once, and so doesexpired. Otherwise it starts when the customer chooses the coin on the checkout page (or you set it withPOST /v1/payments/{id}/token). -
Later, on a payment you already have:
curl -s -X POST $PAYITGO_BASE_URL/v1/payments/c41e7a9d2b05f3e8a6d17c90/simulate \ -H "Authorization: Bearer $PAYITGO_KEY" \ -H 'Content-Type: application/json' \ -d '{"outcome": "partially_paid"}'{id}is the payment id or yourorder_id. The answer is the payment, with itssimulationstarted. -
From a payment page:
POST /checkout/{checkout_token}/simulatetakes the same body, with no key, for test payments only. Use it from your own payment page (Your own payment page) to put "simulate" buttons on a test payment.
The payment shows its simulation, and so does every webhook it sends:
{
"id": "9e3a858c7701308142d545f9",
"order_id": "ORD-2001",
"status": "pending",
"token": "USDT_TRC20",
"deposit_address": "TPk2bpQqWRpXiaUHS9SXnZCDgvwrsuJ71o",
"livemode": false,
"simulation": { "outcome": "confirmed", "state": "running" },
"…": "…"
}
state is requested (asked for on create, waiting for the coin),
running or done.
Outcomes
outcome |
What happens | Webhooks, in order |
|---|---|---|
confirmed |
The exact amount arrives and confirms | payment.detecting, payment.confirmed |
overpaid |
5% more than due arrives | payment.detecting, payment.confirmed with "overpaid": true |
partially_paid |
Half of what is still due arrives. The payment stays open: simulate again to pay the rest, or let it run out | payment.partially_paid |
underpaid |
Half arrives, then the deadline passes | payment.partially_paid, payment.underpaid |
expired |
Nothing arrives, and the deadline passes now | payment.expired (payment.underpaid if it was partially paid) |
failed |
The amount arrives, then its transaction is dropped | payment.detecting, payment.failed, its deposit's fail_reason saying why |
wrong_asset |
The amount arrives in another coin on the same network (TRX for USDT on Tron, USDC for USDT on BSC…) | payment.wrong_asset. With accept_other_assets on: payment.detecting, payment.wrong_asset, payment.confirmed |
paid_late |
Half arrives, the deadline passes, then the rest arrives | payment.partially_paid, payment.underpaid, payment.detecting, payment.late_deposit, payment.confirmed with "paid_late": true |
Each step comes about 5 seconds after the one before, and its webhook is
posted within about 5 seconds of it, so a whole simulation is over in less
than half a minute. Two events of the same step (payment.detecting and
payment.late_deposit, say) can reach you in either order, as live ones can.
The rules:
- Only test payments can be simulated: a live key gets
400 only test payments can be simulated. - The payment must be
pendingorpartially_paid, and have its coin (except forexpired). - One simulation runs at a time: another request gets
409. Once it isdone, you can run another, for examplepartially_paidand thenconfirmedto pay the rest. wrong_assetneeds a coin on Tron, Ethereum or BNB Smart Chain: a Bitcoin, Litecoin, Dogecoin or XRP address only receives its own coin. Asked for on create, it needstoken.
| Status | error |
Cause |
|---|---|---|
400 |
outcome is required |
No outcome in the body |
400 |
simulate must be one of: confirmed, overpaid, … |
An unknown outcome |
400 |
only test payments can be simulated: use a test key |
A live payment, or simulate with 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 |
400 |
simulate wrong_asset needs the payment's coin: pass token |
wrong_asset on create without token |
404 |
payment not found |
Another merchant's payment, or another mode's |
409 |
a simulation is already running for this payment |
Wait for its done |
What a simulated payment looks like
A simulated deposit is like a live one, with "simulated": true. Its tx_id
looks like a transaction hash (64 hex characters) but is on no chain, so it
has no explorer_url. Tell a simulated deposit by its simulated, never by
its tx_id:
"deposits": [
{ "tx_id": "f916492ece53d1cb5bab9515cfed47d17648649cbd8e52b5a45f6e4da6e6fdf9", "token": "USDT_TRC20", "chain": "tron",
"amount": "20", "usd_value": "20", "counted": true, "status": "confirmed", "simulated": true, "…": "…" }
]
Everything after it works as live:
- The payment's confirmations,
received_amount,overpaidandpaid_lateare worked out the same way. - Each deposit is settled when it is final (the parts of a payment paid in
parts together, once it closes), into your test balance with
the same fees as live (Fees and settlement). With
direct settlement and a test payout address, the settlement completes at
once and sends
settlement.completed; nothing is sent to the address. - Coins are never converted in test mode: a deposit settles in the coin that arrived, into your test balance when you have no test payout address for it.
- Payment links, customers, analytics and notifications all count test payments in the dashboard's test mode.
Never send real coins to a test address
Nothing watches a test payment's address, so coins sent to it are never seen. Test addresses on Tron, Ethereum, BNB Smart Chain, Dogecoin and XRP look exactly like live ones, and a wallet will pay them: the coins would stay on the address until payitgo's support recovers them. Bitcoin and Litecoin test addresses use the testnets' formats, which real wallets refuse.
Scenarios to test
Create a test payment for each, and check what your server receives.
| Scenario | How | What you should see |
|---|---|---|
| Normal payment | confirmed |
payment.detecting, then payment.confirmed |
| Partial, then completed | partially_paid, then confirmed once it is done |
payment.partially_paid, then payment.detecting, payment.confirmed |
| Underpaid | underpaid |
payment.partially_paid, then payment.underpaid |
| Expired | expired, or create a payment and simulate nothing |
payment.expired (15 minutes later without a simulation, an hour if no coin was chosen) |
| Overpaid | overpaid |
payment.confirmed with "overpaid": true |
| Late completion | paid_late |
payment.underpaid, then payment.late_deposit and payment.detecting, then payment.confirmed with "paid_late": true |
| Dropped transaction | failed |
payment.detecting, then payment.failed |
| Wrong asset | wrong_asset |
payment.wrong_asset; the deposit counts only with accept_other_assets on |
| XRP | Create with "token": "XRP" and simulate |
The payment has its destination_tag; show it as prominently as the address |
| Customer chooses the coin | Create without token, with simulate; choose on the checkout page |
token_options, then an address after the choice, then the outcome's webhooks |
| Retry safety | Create the same order_id twice |
The second call returns the first payment with 200 |
| Per-payment URL | Create a payment with webhook_url and simulate |
Its notifications go to that URL, not the account's |
Test your webhook handler too:
| Scenario | How | What you should see |
|---|---|---|
| Signature check | POST /v1/webhook/test, and verify the ping |
Your handler answers 200 |
| Tampering | Change one byte of a captured body and send it to your handler | Your handler answers 400 |
| Retries | Answer 500 once |
The same notification again a minute later, with X-Webhook-Attempt: 2 |
| Duplicates | Redeliver a delivery from the dashboard | Your handler recognises the event id and does nothing twice |
| Test vs live | Look at livemode |
Your production handler ignores "livemode": false |
Going live
- The account owner turns live mode on:
POST /merchant/live {"enabled": true}(Account and team). - With the live key, read the live webhook secret (
GET /v1/webhook) and set the live URL (PUT /v1/webhook). - Put the live key and the live webhook secret on your production
server. Remove any
simulatefrom your payment requests: a live key refuses it. - Check that your handler fulfils only
"livemode": trueevents in production. - Add a live payout address for each coin you accept (payout addresses). A new live address is used after 24 hours.
- Make one small real payment end to end.