# Test mode

> Build and test a full LayerHop integration without real funds. lh_test_ keys return sandbox quotes and placeholder transactions, run a simulated order lifecycle in about 10 seconds, send webhooks to test endpoints, and use magic amounts for refunds, failures and no-route errors.

Source: https://docs.layerhop.com/guides/test-mode

Test mode lets you build and test a complete LayerHop integration, from quote to filled order and webhook, without real funds or a real wallet. Requests made with an `lh_test_…` key are served by the **sandbox** provider: quotes are realistic (priced from live USD prices), built transactions are placeholders that cannot move funds, and reported orders run a simulated lifecycle that fills in about 10 seconds. Test data is kept apart from live data everywhere, and it never counts towards volume or partner earnings.

## Get a test key

1. Sign in to [dashboard.layerhop.com](https://dashboard.layerhop.com) and turn on **Test mode** (the switch in the header).
2. Open **API keys** and choose **Create API key**. In test mode the key starts with `lh_test_`.
3. Send it as `x-api-key`, exactly like a live key. Nothing else in your code changes.

You can also try every step below in the [API playground](/playground): paste your test key and follow the **Next step** buttons.

| | Live key `lh_live_…` | Test key `lh_test_…` |
|---|---|---|
| Quotes | Every real provider | The `sandbox` provider only, priced from live USD prices |
| `POST /v1/orders` | The real transaction for the user's wallet | A placeholder with `tx.sandbox: true` that moves no funds |
| `POST /v1/simulate` | Simulated on chain | Always `ok: true`, nothing is sent to an RPC |
| Order tracking | Provider status and chain receipts | Simulated: `pending`, then the outcome about 10 s later |
| Webhooks | Live endpoints | Test endpoints only |
| Dashboard data | Live view | Test view (toggle **Test mode**) |
| Volume and partner earnings | Counted | Never counted |
| Rate limit | 120 requests/minute by default | Same |

## Run the whole flow

```bash
export LAYERHOP_API_KEY=lh_test_…   # your test key

# 1. Quote: the sandbox answers, quoteId starts with q_test_
curl -H "x-api-key: $LAYERHOP_API_KEY" \
  "https://api.layerhop.com/v1/quote?srcChain=base&srcToken=USDC&amount=100000000&dstChain=solana&dstToken=SOL&sender=0x1111111111111111111111111111111111111111&recipient=9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"

# 2. Build: returns a placeholder transaction (tx.sandbox: true)
curl -X POST -H "x-api-key: $LAYERHOP_API_KEY" -H "content-type: application/json" \
  -d '{"quoteId":"q_test_…"}' https://api.layerhop.com/v1/orders

# 3. Report any well-formed transaction hash (0x + 64 hex characters for EVM chains)
curl -X POST -H "x-api-key: $LAYERHOP_API_KEY" -H "content-type: application/json" \
  -d '{"txHash":"0x1111111111111111111111111111111111111111111111111111111111111111"}' \
  https://api.layerhop.com/v1/orders/q_test_…/submitted

# 4. Track it: pending at first, filled about 10 seconds later
curl -H "x-api-key: $LAYERHOP_API_KEY" \
  https://api.layerhop.com/v1/orders/sandbox/0x1111111111111111111111111111111111111111111111111111111111111111
```

With the TypeScript SDK the only change is the key:

```ts
import { LayerHop } from "@layerhop/sdk";

const lh = new LayerHop({ apiKey: process.env.LAYERHOP_API_KEY }); // lh_test_…
const { best } = await lh.quote({
  srcChain: "base", srcToken: "USDC", amount: "100000000",
  dstChain: "solana", dstToken: "SOL",
  sender: "0x1111111111111111111111111111111111111111",
  recipient: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
});
const tx = await lh.buildTx(best.quoteId); // tx.sandbox === true: do not send it
await lh.reportSubmitted(best.quoteId, "0x" + "11".repeat(32));
```

## Magic amounts

The last digits of `amount` choose how a test order ends, so you can exercise every path of your integration:

| `amount` ends in | Result |
|---|---|
| `4001` | The order is **refunded** (`order.refunded` webhook) |
| `4002` | The order **fails** (`order.failed` webhook) |
| `4004` | `GET /v1/quote` returns [`NO_ROUTE`](/errors/NO_ROUTE) |
| anything else | The order is **filled** (`order.filled` webhook) with a generated destination transaction hash |

For example `amount=100004001` quotes 100.004001 USDC and ends in a refund.

## Order lifecycle and webhooks

After you report a hash, LayerHop's worker picks up the test order like any other:

1. First check, within a few seconds: `submitted` → `pending`, and an `order.submitted` webhook.
2. Second check, about 10 seconds later: `pending` → `filled`, `refunded` or `failed`, and the matching webhook.

Test-mode events are sent only to webhook endpoints created in test mode, and live events only to live endpoints, so a test order can never reach your production handler. Every event carries `livemode` (false for test events), and so does every order:

```json
{
  "id": "evt_…",
  "type": "order.filled",
  "livemode": false,
  "createdAt": "2026-10-18T10:00:12.000Z",
  "data": { "order": { "quoteId": "q_test_…", "status": "filled", "provider": "sandbox", "livemode": false } }
}
```

Create a test endpoint with **Test mode** on in the [dashboard](https://dashboard.layerhop.com) (or `POST /v1/dashboard/webhooks` with `"mode": "test"`), then verify signatures exactly as in [Webhooks](/guides/webhooks).

## Keeping test and live apart

- A quote, order or webhook endpoint of one mode is invisible to the other. Building a `q_test_` quote with a live key (or no key) returns `QUOTE_NOT_FOUND`, and the reverse too.
- `GET /v1/orders?address=` lists test orders for test keys and live orders for live keys or no key.
- The dashboard's overview, usage, logs, orders, webhooks and earnings take `?mode=test` (the **Test mode** switch sets it).
- Placeholder transactions are safe by construction: EVM placeholders send 0 to `0x000000000000000000000000000000000000dEaD` with a short memo as calldata; Solana placeholders use an all-zero blockhash that no cluster accepts. Still, never broadcast them.

## Going live

Swap `lh_test_` for an `lh_live_` key. The responses have the same shape (with `livemode: true`), so code that passes in test mode works unchanged. Remember to create live webhook endpoints too, since test endpoints never receive live events.
