# 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](#simulate-a-payment) 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"`:

  ```bash
  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 does `expired`.
  Otherwise it starts when the customer chooses the coin on the checkout page
  (or you set it with `POST /v1/payments/{id}/token`).

- **Later, on a payment you already have:**

  ```bash
  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 your `order_id`. The answer is the payment, with
  its `simulation` started.

- **From a payment page:** `POST /checkout/{checkout_token}/simulate` takes
  the same body, with no key, for test payments only. Use it from your own
  payment page ([Your own payment page](/docs/checkout#your-own-payment-page))
  to put "simulate" buttons on a test payment.

The payment shows its simulation, and so does every webhook it sends:

```json
{
  "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`](/docs/account#payment-settings) 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 `pending` or `partially_paid`, and have its coin
  (except for `expired`).
- One simulation runs at a time: another request gets `409`. Once it is
  `done`, you can run another, for example `partially_paid` and then
  `confirmed` to pay the rest.
- `wrong_asset` needs 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 needs `token`.

| 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`:

```json
"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`, `overpaid` and `paid_late`
  are 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](/docs/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

1. The account owner turns live mode on: `POST /merchant/live {"enabled": true}`
   ([Account and team](/docs/account#live-mode)).
2. With the live key, read the live webhook secret (`GET /v1/webhook`) and set
   the live URL (`PUT /v1/webhook`).
3. Put the live key and the live webhook secret on your production
   server. Remove any `simulate` from your payment requests: a live key
   refuses it.
4. Check that your handler fulfils only `"livemode": true` events in
   production.
5. Add a live payout address for each coin you accept
   ([payout addresses](/docs/account#payout-addresses)). A new live address is
   used after 24 hours.
6. Make one small real payment end to end.
