> ## Documentation Index
> Fetch the complete documentation index at: https://docs.infinityapp.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox

> Build and test against a simulated payout network. Fund wallets, activate payees and force any payout outcome on demand.

The sandbox is a separate deployment of the Infinity API that runs the same code as live, connected to a simulated payout network instead of real banks. Requests, responses, validation, idempotency, statuses and webhooks behave as they do in production; only the movement of funds is simulated. Use it to build your integration and to test the outcomes that are rare in production, such as failures and bank returns.

Sandbox keys are issued by our team. [Contact us](mailto:support@infinityapp.in?subject=Sandbox%20API%20key) with your company name and we'll set up your sandbox account and send you an `sk_test_` key.

## How the sandbox differs from live

| | Sandbox behaviour |
| - | - |
| Host and keys | `https://sandbox-api.infinityapp.in/v1`, `sk_test_` keys only |
| Funding | Test deposits credit a wallet instantly; there are no funding transfers |
| Payout lifecycle | `pending` → `processing` after about 5 seconds → `paid` after about 30 seconds |
| Exchange rates | Real indicative market rates, so quotes reflect production pricing |
| Emails | None are sent. Payees are never contacted. |
| Hosted onboarding | `onboarding_url` is returned but doesn't lead to a working page. Submit bank details through the API, or activate payees with the helper below. |
| Currencies | USD, EUR and GBP payees. INR isn't available in the sandbox yet. |
| Webhooks | Delivered exactly as in live, with `"environment": "sandbox"` on every event |
| Daily release of payouts on hold | Runs at 09:00 IST, as in live. Trigger it on demand with `POST /test/holds/release`. |
| Rate limits | Identical to live |
| Data retention | Data may be reset. Infinity announces resets in advance. |

## Test helpers

Test helpers are endpoints under `/test` that stand in for things a real bank, payee or scheduler would do. They exist only in the sandbox; on the live host they return `404 path_not_found`. Each helper runs the same internal path the real event would, so it fires the same webhooks.

### Fund a wallet

```bash theme={null}
curl "$BASE/test/deposits" \
  -H "Authorization: Bearer $INFINITY_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "wallet_id": "wallet_7t4m9k2qx8d3hn5r", "amount": "10000.00", "description": "Test top-up" }'
```

| Field | Required | Rules |
| - | - | - |
| `wallet_id` | Yes | One of your wallets. Sandbox accounts are created with a USD wallet. |
| `amount` | Yes | Greater than 0, at most `1000000.00` per call |
| `description` | No | Up to 500 characters |

The `Idempotency-Key` header is required, as on every request that moves money. The response is a [deposit](https://docs.infinityapp.in/api-reference/deposits/get-a-deposit.md) with `rail: null`, which marks a manual credit, and Infinity emits `deposit.created`.

A deposit doesn't release payouts that are waiting for funds, in the sandbox or in live. They're released by the daily release, which you trigger here with [release payouts on hold](#release-payouts-on-hold).

### Activate a payee

Skips hosted onboarding: Infinity adds a test bank account to the payee through the normal address path, and the payee becomes `active`.

```bash theme={null}
curl "$BASE/test/payees/payee_x3k7r9mwq2dn8hat/activate" \
  -H "Authorization: Bearer $INFINITY_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "currency": "usd", "rail": "ach" }'
```

| Field | Required | Rules |
| - | - | - |
| `currency` | Unless the payee already has one | `usd`, `eur` or `gbp`. Must match the payee's `currency` when it's set. |
| `rail` | No | `usd` only: `ach` or `wire`. Defaults to `wire`. |

The test account uses the payee's name as the account holder and `Sandbox Bank` as the bank. Only payees in `invited` or `details_pending` can be activated; an `active` payee returns `409 payee_already_active`, and a `verifying` or `inactive` payee returns `409 test_transition_not_allowed`. The response is the payee with `status: active`, and Infinity emits `payee.active`.

### Force a payout outcome

```bash theme={null}
curl "$BASE/test/payouts/payout_4kz9xq7m2w8dn3ra/advance" \
  -H "Authorization: Bearer $INFINITY_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "to": "failed", "failure_code": "undeliverable" }'
```

| `to` | Allowed from | Response | How it's applied |
| - | - | - | - |
| `processing` | `pending` | `202` | Through the simulated network's status webhook |
| `paid` | `pending`, `processing` | `202` | Same, passing through `processing` |
| `failed` | `pending`, `processing` | `202` | Same, with the network status that maps to `failure_code` |
| `returned` | `paid` | `202` | Same; `failure_code` is always `returned_by_bank` |
| `expired` | `awaiting_payee`, `awaiting_funds` | `200` | Immediately, through the hold-expiry path |

* `failure_code` is accepted only with `to: failed`. Allowed values are `undeliverable` (the default), `refunded`, `refund_failed`, `canceled` and `processing_error`.
* A `202` response returns the payout as it is at that moment. The new status lands asynchronously, usually within 2 seconds, through the same webhook path a real network status takes. Read it from the event or from `GET /payouts/{id}`, exactly as you would in production.
* Any other transition returns `409 test_transition_not_allowed`. To cancel, use the regular [cancel endpoint](https://docs.infinityapp.in/api-reference/payouts/cancel-a-payout-on-hold.md).

### Release payouts on hold

In live, payouts on hold are released once a day at 09:00 IST. This helper runs that release immediately, for your account only:

```bash theme={null}
curl -X POST "$BASE/test/holds/release" -H "Authorization: Bearer $INFINITY_KEY"
```

```json Response theme={null}
{ "released": 2, "awaiting_funds": 1, "still_held": 0 }
```

| Field | Counts payouts that |
| - | - |
| `released` | moved to `pending` and were sent |
| `awaiting_funds` | have a ready payee, but the wallet's available balance was too low |
| `still_held` | are still waiting for their payee |

Each payout that changes status emits its own events, as the daily release does.

## Simulating a rejected bank account

Any account number ending in `0000` passes format validation and is then rejected by the simulated network with `400 bank_details_invalid` and `param: null`, which is how a real bank-side rejection appears.

| Rail | Rejected example |
| - | - |
| `sepa` | IBAN `NL82ABNA0417160000` (a valid IBAN, so it passes format checks) |
| `faster_payments` | Sort code `200000`, account number `31920000` |
| `ach`, `wire` | Routing number `021000021`, account number `123450000` |

## Common test scenarios

> [accordion] A payout created before the payee is ready
>
> 1. Create a payee without a bank account.
> 2. Create a payout to them. It's created as `awaiting_payee` and the amount is reserved in the wallet.
> 3. Activate the payee with the helper.
> 4. Release payouts on hold. The payout moves to `pending`, then to `paid`.

> [accordion] A bank return after payment
>
> 1. Create a payout and wait for `payout.paid`.
> 2. Advance it with `{ "to": "returned" }`.
> 3. Handle `payout.returned`, and the batch event that follows when the batch's status changes.

> [accordion] A rejected bank account
>
> 1. Create a payee.
> 2. Add an address with an account number ending in `0000`.
> 3. Handle `400 bank_details_invalid` with `param: null`; the payee's status doesn't change.

## Limitations

The helpers cover outcomes that occur after a payout is sent. Failures that occur while sending (`below_minimum`, `bank_details_invalid`, `rate_changed`, `bank_account_missing`) can't be simulated yet. `insufficient_funds` occurs naturally when a wallet's available balance is too low.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.