# Send your first email

Register and verify a sending domain, create a public key, then send a test email to your own mailbox. The seven steps below include one wait for DNS.

Set up the domain and key once. After that, your application sends each message with one request.

Steps 1–4 use **Mail → Domains** or the product-authenticated Mail API. Step 5 creates a platform credential under the product’s settings. Steps 6–7 use the public sending endpoint from your application; these are the steps that use the public key.

## Before you start

| You need | Why |
|---|---|
| A Lepsto product with Mail available | Every domain, template and message belongs to exactly one product |
| A domain your product already owns | Mail sends only from a domain the product governs |
| Access to that domain's DNS | Step 3 publishes records at your DNS provider |
| A product **Owner** or **Admin** to hand | Only they can create the public key in step 5 |
| A mailbox you can open | Step 6 sends the first message to you |

This example product sends from `mail.example.com`. Replace the example domain, product ID, key, message ID and recipient address with your own values. Use a recipient mailbox you can open.

## 1. Find the domain you may send from

First, find an apex domain your product owns:

```text
GET /mail/domains/allowed
```

Use [`mail_domain_allowed_list`](/reference/mcp-tools/mail_domain_allowed_list) through MCP. The response lists the apex domains available to this product:

```json
[
  { "domainId": "d3f1…", "domain": "example.com", "mode": "external", "extensionCount": 2 }
]
```

Register an apex from this list or one of its subdomains. A dedicated subdomain such as `mail.example.com` keeps application mail separate from your team’s own mail.

In the workspace, open **Mail → Domains → Add domain**. The **Domain** dropdown lists the same apexes and shows when none are available.

## 2. Add the domain

```text
POST /mail/domains
```

```json
{ "name": "mail.example.com" }
```

The new domain has `status: "pending"` and a `records` array. Save its `id` for the verification check in step 4.

In **Add domain**, select the apex. To use a subdomain, choose **Add a subdomain** and enter the prefix. Check the displayed sending domain, then select **Add domain** to open its detail page.

## 3. Publish the DNS records

At your DNS provider, create every record in the returned `records` array:

```json
{
  "type": "CNAME",
  "name": "abc123._domainkey.mail.example.com",
  "value": "<the signing host Mail returns>",
  "purpose": "dkim",
  "required": true
}
```

Copy each record’s `type`, `name` and `value`; include `priority` for an `MX` record. Every record with `required: true` must resolve before verification succeeds. DMARC is optional for verification, but publishing the returned record is recommended.

> Copy the names and values exactly. Some DNS providers append the zone name automatically, which can duplicate it. Keep any trailing dot in a returned value.

You can also copy the records from **DNS records to publish** on **Mail → Domains → \<domain\>**. The table identifies recommended records.

## 4. Wait for the domain to verify

Mail checks unverified domains every minute. To request a check immediately, call:

```text
POST /mail/domains/{id}/verify
```

Use [`mail_domain_verify`](/reference/mcp-tools/mail_domain_verify) through MCP. Its response adds `resolved` to each record so you can identify records Mail can read.

**Check the result:** wait for `status: "verified"`. DNS propagation usually takes minutes but can take hours; Mail continues checking for 72 hours.

Sending before verification returns `422 invalid_from_address`.

The domain page refreshes every few seconds while verification is pending. Select **Verify now** to run a check immediately.

## 5. Create a public key

Create a **product public key** for the public sending endpoint. A product **Owner** or **Admin** opens **Settings → Public Access** in the workspace.

Name the key, choose **Selected toolkits** and select Mail. That workspace scope covers both public send and read routes. For an application that only sends, the management API can create a narrower key for `POST /emails`; see [public access](/products/public-access#scope).

> Step 7 reads the message through `GET /emails/{id}`. A send-only key returns `403` on that route. For this tutorial, either use the workspace key scoped to **the whole Mail toolkit**, or create a send-only key through the management API and keep that scope and check the message in the workspace. Step 7 describes both checks. Do not widen a key only to pass a check.

> Save the key to your secret store when it appears. It is shown once and starts with `lpk_` followed by 32 hexadecimal characters. A lost key cannot be recovered; revoke it and create another.

Allow about 30 seconds for the new key to take effect. Until the platform refreshes its key snapshot, it may return `401 public_key_invalid`. That error can also mean a mistyped key or a key from another product. Check the value and product before waiting. See [key refusal responses](/ship/mail/api-keys#when-a-request-is-refused-for-its-key).

An existing imported `lmk_` Mail key works as a product public key; you do not need a replacement for this step. See [keys imported from Mail](/ship/mail/api-keys#keys-imported-from-mail).

## 6. Send the first email

Send to `https://public.lepsto.com/{product_id}/mail` with your key in `Authorization: Bearer`. Use a `from` address on the domain you just verified:

```bash
curl https://public.lepsto.com/$PRODUCT_ID/mail/emails \
  -H "Authorization: Bearer $LEPSTO_PUBLIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Example <hello@mail.example.com>",
    "to": "you@your-own-address.com",
    "subject": "Hello from Mail",
    "html": "<p>It works.</p>"
  }'
```

Or send the same message with the TypeScript SDK:

```ts

const mail = createMailer({
  productId: process.env.LEPSTO_PRODUCT_ID!,
  publicKey: process.env.LEPSTO_PUBLIC_KEY!,
  baseUrl: 'https://public.lepsto.com',
});

const { id } = await mail.emails.send({
  from: 'Example <hello@mail.example.com>',
  to: 'you@your-own-address.com',
  subject: 'Hello from Mail',
  html: '<p>It works.</p>',
});
```

Both calls return the accepted email’s ID. HTTP returns it inside Mail’s response envelope:

```json
{ "data": { "id": "1f4a…" }, "success": true }
```

The SDK unwraps `data`, so its `send` call resolves directly to `{ id }`. HTTP examples on these pages include the envelope; SDK examples use the unwrapped result.

**Check the result:** save the returned ID for step 7. This request sends real mail to your test mailbox. If it returns `402`, the organization’s plan check refused the send and no message was created; follow [plan refusals](/ship/mail/sending#when-the-organizations-plan-does-not-cover-the-send).

An inline send needs `from`, at least one recipient, a subject, and `html` or `text`. For cc, bcc, reply-to, tags, scheduling and templates, see [send a message](/ship/mail/sending).

## 7. Check what happened to it

An ID confirms acceptance. Check delivery through the workspace or the API, using the path your key’s scope permits.

**Workspace check:** open **Mail → Logs**, or enter the ID in **Mail → Emails → Look up status**. Open **Mail → Logs → \<email\>** for the ordered event history. These screens use your session and work even when the public key is send-only.

**API check:** use the following request only if the key covers `GET /emails/{id}`. A send-only key returns `403 public_key_scope`; use the workspace check instead.

```bash
curl https://public.lepsto.com/$PRODUCT_ID/mail/emails/1f4a… \
  -H "Authorization: Bearer $LEPSTO_PUBLIC_KEY"
```

```json
{
  "data": {
    "id": "1f4a…",
    "from": "Example <hello@mail.example.com>",
    "to": ["you@your-own-address.com"],
    "subject": "Hello from Mail",
    "status": "delivered",
    "created_at": "2026-08-02T09:00:00.000Z",
    "last_event": { "type": "email.delivered", "created_at": "2026-08-02T09:00:04.000Z" }
  },
  "success": true
}
```

With the SDK, read the unwrapped `Email`:

```ts
const email = await mail.emails.get(id);
console.log(email.status, email.last_event);
```

**Check the result:** confirm `status: "delivered"` and open the email in your test mailbox. `queued` means waiting to send, `sent` means handed to the receiving server, and `delivered` means that server accepted it. A hard rejection produces `bounced`; a spam report produces `complained`. See [all message statuses](/ship/mail/sending#what-the-statuses-mean).

For delivery notifications in your application, [register a webhook](/ship/mail/webhooks).

## Next steps

- [Set up a sending domain](/ship/mail/domains): Troubleshoot DNS and verification status.
- [Authenticate the public API](/ship/mail/api-keys): Manage key scopes, propagation and replacement.
- [Send a message](/ship/mail/sending): Use batches, scheduling, cancellation and limits.
- [Send from a template](/ship/mail/templates): Reuse published email content.
- [Receive delivery events](/ship/mail/webhooks): Add signed event notifications.
