# Send an email

> Send an email from your workspace's mailbox and keep replies in one thread.

Source: https://docs.vlayers.ai/developers/send/email

Kind: How-to guide

## What you'll build

A request that emails a person from your mailbox, with a subject, and replies that stay in the same thread.

## Prerequisites

- A secret API key with the `messages:send` scope. See [Authentication and API keys](https://docs.vlayers.ai/get-started/authentication).
- A mailbox in your workspace. It is your [sender](https://docs.vlayers.ai/get-started/concepts#sender) on email. Without one, a send is refused with `409 no_sender`.
- curl, Node.js 18 or later with `@voicelayer/api`, or Python 3.9 or later with `voicelayer`.

## Step 1: Send a new email

`POST /v1/messages` with `channel: "email"`, the recipient's address in `to`, and a `subject`. The subject is only for a new email; other channels refuse it with `400 invalid_request`. With a test key, send to any address at `test.vlayers.ai`.

cURL:

```bash
curl -X POST https://api.vlayers.ai/v1/messages \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "email",
    "to": "ada@test.vlayers.ai",
    "subject": "Your booking",
    "content": {
      "type": "text",
      "text": "Your table for two is booked for 7pm. Reply to this email to change it."
    }
  }'
```

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: 'email',
    to: 'ada@test.vlayers.ai',
    subject: 'Your booking',
    content: {
      type: 'text',
      text: 'Your table for two is booked for 7pm. Reply to this email to change it.'
    }
  }
});
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": "email",
            "to": "ada@test.vlayers.ai",
            "subject": "Your booking",
            "content": {
                "type": "text",
                "text": "Your table for two is booked for 7pm. Reply to this email to change it.",
            },
        }),
    )
    print(result.message.id)
```

Response `202`:

```json
{
  "message": {
    "id": "0192f5b0-8d1f-7b62-8c3e-5f9a2b7d4e32",
    "conversationId": "0192f5b0-7c3e-7a51-9b2d-4e8f1a6c3d21",
    "channel": "sms",
    "direction": "outbound",
    "status": "queued",
    "text": "Your table for two is booked for 7pm.",
    "media": [],
    "from": "+14155550100",
    "to": "+15550123",
    "bindingId": null,
    "metadata": {},
    "error": null,
    "createdAt": "2026-10-20T15:04:05.000Z",
    "parts": [
      {
        "id": "0192f5b0-8d1f-7b62-8c3e-5f9a2b7d4e32",
        "status": "queued"
      }
    ]
  }
}
```

With several mailboxes, name one in `from`: `{ "mailboxId": "<mailbox address>" }`.

## Step 2: Reply in the thread

When the person answers, their email arrives as a `message.received` event in the same conversation. Reply with `POST /v1/conversations/{id}/messages`: the reply keeps the thread's subject, so the person's mail app shows one thread.

```bash
export CONVERSATION_ID=<the conversation's id>
```

cURL:

```bash
curl -X POST "https://api.vlayers.ai/v1/conversations/$CONVERSATION_ID/messages" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": {
      "type": "text",
      "text": "Yes, we have a table at 7pm. Shall I book it?"
    }
  }'
```

TypeScript:

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

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

const { data } = await replyToConversation({
  client,
  path: {
    id: process.env.CONVERSATION_ID!
  },
  body: {
    content: {
      type: 'text',
      text: 'Yes, we have a table at 7pm. Shall I book it?'
    }
  }
});
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.ConversationsApi(client)
    result = api.reply_to_conversation(
        id=os.environ["CONVERSATION_ID"],
        reply_to_conversation_request=voicelayer.ReplyToConversationRequest.from_dict({
            "content": {
                "type": "text",
                "text": "Yes, we have a table at 7pm. Shall I book it?",
            },
        }),
    )
    print(result.message.id)
```

## Test it

With a test key, email any address at `test.vlayers.ai`, such as `ada@test.vlayers.ai`. Nothing is sent and it costs nothing. To test a reply, simulate an incoming email first; it arrives on your mailbox, or on your free test sender if you have none:

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": "email",
    "from": "ada@test.vlayers.ai",
    "text": "Can I move my booking to Friday?"
  }'
```

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: 'email',
    from: 'ada@test.vlayers.ai',
    text: 'Can I move my booking to Friday?'
  }
});
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": "email",
            "from": "ada@test.vlayers.ai",
            "text": "Can I move my booking to Friday?",
        }),
    )
    print(result.conversation_id)
```

Save the returned `conversationId` as `CONVERSATION_ID` and reply with Step 2. See [Test mode](https://docs.vlayers.ai/developers/test-mode).

## Common errors

| Error | Status | How to fix |
|---|---|---|
| [`no_sender`](https://docs.vlayers.ai/errors/no_sender) | 409 | Connect a sender for the channel in the dashboard (for email, create a mailbox), or name one in `from`. |
| [`sender_ambiguous`](https://docs.vlayers.ai/errors/sender_ambiguous) | 409 | Name one of `details.senders` in `from`. |
| [`invalid_request`](https://docs.vlayers.ai/errors/invalid_request) | 400 | Read `message` and `details.issues` (the validation issues, by path), fix the request and send it again. |
| [`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. |
| [`rate_limited`](https://docs.vlayers.ai/errors/rate_limited) | 429 | Wait for `retry-after`, then retry (with the same Idempotency-Key for a send). |

## Next steps

- [Receive with webhooks](https://docs.vlayers.ai/developers/receive/webhooks): get replies as they arrive.
- [Send an SMS](https://docs.vlayers.ai/developers/send/sms).
- [Conversations and handoff](https://docs.vlayers.ai/developers/handoff).
