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

> ## Agent Instructions
> Bitnob API requests authenticate with an `X-Auth-Client` API key header against https://api.bitnob.com.
> Always use the sandbox environment for testing. Never put live API keys in sample code.
> Money amounts are integers in the smallest unit of the currency (satoshis for BTC, cents for fiat). Never use floats.
> Every state-changing request takes a client-supplied `reference` and must be treated as idempotent on retry.
> Quotes expire. Re-quote rather than reusing a stale quote id.

# Test in sandbox

> Exercise deposits, quotes, expiry, webhooks, and failure paths against sandbox credentials before your integration touches real funds.

Sandbox runs the same API with simulated funds. Use it to make failure modes visible while they are still cheap.

Production is messier than any code review suggests. Webhooks arrive late or twice, users hesitate until a quote expires, payouts get rejected for a bad account number. This guide shows how to produce those conditions on purpose.

## How sandbox works

Sandbox is not a separate host. It is a separate credential.

| | Sandbox | Production |
| - | - | - |
| Base URL | `https://api.bitnob.com` | `https://api.bitnob.com` |
| `CLIENT_ID` | Same | Same |
| `CLIENT_SECRET` | Sandbox secret | Production secret |
| Funds | Simulated | Real |
| Data retention | Reset from time to time | Permanent |

The secret you sign with decides which environment you are in.

> [!WARNING]
>
> Because the host is the same, a misconfigured secret sends a test request to production. Keep the two secrets in separate configuration, and never load both into one process.

## Set up a sandbox workspace

**1. Create a sandbox key**

In the dashboard, open **Settings** > **API Keys** and create a sandbox key. See [Set up your account](https://www.bitnob.dev/docs/getting-started/account-setup.md).

**2. Keep it in its own configuration file**

```bash theme={null}
BITNOB_CLIENT_ID=yourClientId
BITNOB_CLIENT_SECRET=yourSandboxSecret
BITNOB_BASE_URL=https://api.bitnob.com
```

**3. Expose your webhook endpoint**

Run a tunnel, such as `ngrok http 3000`, and set the public URL as your webhook URL in the dashboard.

**4. Confirm your credentials**

Call `/api/whoami`. A `200` means signing works, and the response returns `"environment": "sandbox"` so you can confirm which environment the secret selects.

## Simulate an inbound deposit

Virtual accounts have a sandbox-only endpoint that fakes an inbound transfer. The request body is optional: you can send an `idempotency_key`.

```bash theme={null}
POST /api/virtual-accounts/{id}/simulate-deposit
```

The response confirms the credit and the new balance:

```json theme={null}
{
  "success": true,
  "message": "Virtual account deposit simulated",
  "data": {
    "transaction": {
      "id": "019fe85f-89cc-7b9e-a42b-07fc78089499",
      "amount": "1000000",
      "currency": "NGN",
      "type": "credit",
      "status": "completed",
      "reference": "sbx-dep-019fe85f-89cc-7b8f-bddb-c6600777eabf"
    },
    "new_balance": "1800000"
  }
}
```

This drives the same downstream path a real deposit does, so your balance update and your webhook handler both get exercised. See [Simulate deposit](https://www.bitnob.dev/api-reference/virtual-accounts/simulate-deposit-sandbox.md).

> [!NOTE]
>
> The endpoint is rejected in production. It is safe to leave in a test suite.

## Test quote expiry

Quote expiry only shows up when a user hesitates, which is exactly when nobody is watching.

**1. Create a quote**

Call [Create payout quote](https://www.bitnob.dev/api-reference/payouts/create-quote.md) and record `expires_at`.

**2. Wait past expires\_at**

Do nothing until the timestamp passes.

**3. Try to execute**

Initialize the payout against the expired `quote_id` and observe the error.

**4. Check your interface**

Confirm it blocks the attempt, explains why, and offers a fresh quote.

See [Quote lifecycle and expiry](https://www.bitnob.dev/docs/getting-started/api-best-practices/quote-expiry-and-transaction-windows.md).

## Test webhook delivery and retries

A `5xx`, a timeout, and most `4xx` responses are retried, up to the retry count set on your webhook. `400`, `404`, `410`, `422`, and `451` are final. [Retries and duplicates](https://www.bitnob.dev/api-reference/webhooks.md#retries-and-duplicates) has the full rules. Confirm your handler behaves under retries before you rely on it.

**1. Return a failure on purpose**

```javascript theme={null}
app.post('/webhooks/bitnob', (req, res) => {
  console.log('received:', req.body);
  res.status(500).send('simulated failure');
});
```

**2. Trigger an event**

Simulate a deposit and watch the retries arrive in your log.

**3. Confirm the retry count and spacing**

Compare what you see against the documented behavior in [Retries and duplicates](https://www.bitnob.dev/api-reference/webhooks.md#retries-and-duplicates). By default the delays are about 1, 5, 15, and 60 minutes, so a full retry cycle takes over an hour.

**4. Process the same event twice**

Return `200` and replay the event. Confirm the second run changes nothing.

## Test the failure paths

Most integrations test the happy path and ship. These four errors are worth producing deliberately, because each one reaches production eventually.

| Condition | How to produce it | What to confirm |
| - | - | - |
| Bad signature | Sign with the wrong secret | You return a clear error and do not retry blindly |
| Duplicate reference | Submit the same withdrawal reference twice | You treat the `409` as "already done", not a failure |
| Insufficient balance | Request more than your sandbox balance | You surface the reason to the user |
| Unreachable webhook | Stop your tunnel, then trigger an event | Your reconciliation job catches the gap |

The last row is the one people skip. Retries are finite, so an endpoint that is down long enough misses events permanently. Reconciliation is what recovers them. See [Webhooks and event handling](https://www.bitnob.dev/docs/getting-started/api-best-practices/webhook-best-practices.md).

## Edge cases worth reproducing

* A webhook arrives before your database has written the reference.
* Two webhooks for the same flow arrive in parallel.
* A payment arrives just after a quote expires.
* A user sends an asset you did not expect.
* The same reference is reused across two attempts.
* A signature header is missing or altered.

## Build a repeatable script

Ad-hoc testing stops happening once the feature ships. Put the flow in a script instead:

1. Create a quote and store it.
2. Execute against the quote.
3. Simulate the deposit.
4. Record every webhook received, with its event name and reference.
5. Assert the final state matches what you expect.

Run it in CI against sandbox credentials. A broken webhook handler then fails a build rather than a payment.

## Before you go live

**1. Swap the secret, nothing else**

The base URL and `CLIENT_ID` do not change. Only `CLIENT_SECRET` does.

**2. Point the webhook URL at production**

Update it in the dashboard and confirm the endpoint is reachable over HTTPS.

**3. Confirm compliance is approved**

Production requires completed verification. See [Set up your account](https://www.bitnob.dev/docs/getting-started/account-setup.md).

**4. Run one small real transaction**

Send the smallest amount the corridor allows and follow it end to end before you open the flow to users.

## Next

Continue to [Observability, logging, and traceability](https://www.bitnob.dev/docs/getting-started/api-best-practices/observability-logging-and-traceability.md), which covers what to record so that a production question has an answer.


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