# Test mode

> Build against the real API with a test key — every check runs, nothing reaches a carrier, and it costs nothing.

Source: https://docs.vlayers.ai/developers/test-mode

Kind: How-to guide

## What you'll build

A test setup where simulated customers write to you and your code (or your agent) answers, through the real API, at no cost.

## How test mode works

A **test key** (`vl_test_…`) runs the whole sending pipeline — validation, scopes, content checks, rate limits — except the two things that cost: the carrier and the charge. Messages leave through a simulated carrier instead of a real one.

## Prerequisites

- A test key: in the dashboard, **Developers → API keys → Create key**, "Your servers or the SDK", environment **Testing**. See [Authentication and API keys](https://docs.vlayers.ai/get-started/authentication).
- Nothing else. You don't need a phone number or a connected account: without one, test mode uses your [free test sender](#your-free-test-sender).
- curl, Node.js 18 or later with `@voicelayer/api`, or Python 3.9 or later with `voicelayer`.

## Reserved test addresses

Test keys talk only to these addresses, and only test keys may use them:

| Channel | Addresses |
|---|---|
| SMS, MMS, WhatsApp | `+15550100` to `+15550199` |
| Telegram, Discord | `test-1`, `test-2`, … (`test-` and a number) |
| Email | any address at `test.vlayers.ai` |

A test key that names any other address is refused with `422 test_address_required`. A live key that names a test address is refused with `422 test_address_not_allowed`.

## Your free test sender

Every workspace gets a free **test sender** the first time it needs one. When you have no number, connected account or mailbox on a channel, `POST /v1/test/inbound` without `to` and a test key's `POST /v1/messages` without `from` use it. It never reaches a carrier and costs nothing.

- It has no agent, so its conversations start with the controller `api`: your code answers them (see [Conversations and handoff](https://docs.vlayers.ai/developers/handoff)).
- It works only in test mode: a live key never sends through it.
- It has no reply window or registration of its own. To test your agent's replies, or a channel's real rules (WhatsApp's 24-hour window, for example), simulate the message on your real number or account by naming it in `to`.

## Step 1: Simulate someone writing to you

`POST /v1/test/inbound` plays a person writing from a test address. It works with test keys only; a live key gets `test_mode_only`.

cURL:

```bash
curl -X POST https://api.vlayers.ai/v1/test/inbound \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "from": "+15550123",
    "text": "Do you have a table for two tonight?"
  }'
```

TypeScript:

```ts
import { createVoiceLayerClient, createTestInbound } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await createTestInbound({
  client,
  body: {
    channel: 'sms',
    from: '+15550123',
    text: 'Do you have a table for two tonight?'
  }
});
console.log(data.conversationId);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.TestApi(client)
    result = api.create_test_inbound(
        create_test_inbound_request=voicelayer.CreateTestInboundRequest.from_dict({
            "channel": "sms",
            "from": "+15550123",
            "text": "Do you have a table for two tonight?",
        }),
    )
    print(result.conversation_id)
```

Response `202`:

```json
{
  "conversationId": "0192f5b0-7c3e-7a51-9b2d-4e8f1a6c3d21",
  "messageId": "0192f5b0-8d1f-7b62-8c3e-5f9a2b7d4e32",
  "test": true
}
```

`to` is optional: left out, the message goes to your one sender on that channel, or to your free test sender if you have none. Name a sender in `to` when you have several. Email works too: send `"channel": "email"` with a `from` at `test.vlayers.ai`, and the message arrives on your mailbox (or the test sender).

## Step 2: Reply or send

Reply as usual with `POST /v1/conversations/{id}/messages`, or send to a test address with `POST /v1/messages`:

cURL:

```bash
curl -X POST https://api.vlayers.ai/v1/messages \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "to": "+15550123",
    "content": {
      "type": "text",
      "text": "Your table for two is booked for 7pm."
    }
  }'
```

TypeScript:

```ts
import { createVoiceLayerClient, sendMessage } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await sendMessage({
  client,
  body: {
    channel: 'sms',
    to: '+15550123',
    content: {
      type: 'text',
      text: 'Your table for two is booked for 7pm.'
    }
  }
});
console.log(data.message.id);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.MessagesApi(client)
    result = api.send_message(
        send_message_request=voicelayer.SendMessageRequest.from_dict({
            "channel": "sms",
            "to": "+15550123",
            "content": {
                "type": "text",
                "text": "Your table for two is booked for 7pm.",
            },
        }),
    )
    print(result.message.id)
```

## What a test conversation does

The conversation a simulated message opens is marked `test: true`. That mark decides the rest:

- every send in it — your replies, and your agent's — goes to the simulated carrier, never a real one;
- it costs $0;
- its events (`message.received`, `message.sent`, …) reach your webhooks and the stream like any other, with `"livemode": false`;
- the dashboard hides test conversations by default: turn on **Show test conversations** on the **Conversations** page to see them.

## Daily caps

Test mode is free and capped per workspace per UTC day: by default 1,000 test messages (simulated inbound messages and sends together) and 100 agent replies. Past a cap, requests are refused with `429 test_limit_reached` until midnight UTC. Ask support if you need more.

## Test it

Run Step 1, save the `conversationId` as `CONVERSATION_ID`, reply, then read the conversation with `GET /v1/conversations/{id}/messages`. The [Quickstart](https://docs.vlayers.ai/get-started/quickstart) walks through it.

## Going live

Switch the test key for a live one and the test addresses for real ones. Nothing else in your code changes: the routes, bodies, events and error codes are the same.

## Common errors

| Error | Status | How to fix |
|---|---|---|
| [`test_mode_only`](https://docs.vlayers.ai/errors/test_mode_only) | 403 | Use a test key (`vl_test_…`) from the dashboard. |
| [`test_address_required`](https://docs.vlayers.ai/errors/test_address_required) | 422 | Use a reserved test address, or a live key to reach a real person. |
| [`test_address_not_allowed`](https://docs.vlayers.ai/errors/test_address_not_allowed) | 422 | Use a test key (`vl_test_…`) for test addresses. |
| [`test_limit_reached`](https://docs.vlayers.ai/errors/test_limit_reached) | 429 | Try again after midnight UTC, or ask for a higher test limit. |
| [`sender_ambiguous`](https://docs.vlayers.ai/errors/sender_ambiguous) | 409 | Name one of `details.senders` in `from`. |

## Next steps

- [Quickstart](https://docs.vlayers.ai/get-started/quickstart).
- [Receive with webhooks](https://docs.vlayers.ai/developers/receive/webhooks).
- [Errors, retries and idempotency](https://docs.vlayers.ai/developers/errors-and-retries).
