# Create and rotate a sending key

A Mail API key is what lets your own code send email. It stands for your product and nothing else: it carries the product the key was created in, what the key is allowed to do, and — if you choose — the one domain it may send from.

Keys belong to one product. A key created in one product cannot read or send anything in another.

## Choose a scope

| Scope | May | Use for | |
|---|---|---|---|
| `sending_access` | Send email, and read the emails it sent | A key that lives in your application | **(Recommended)** |
| `full_access` | The above, and manage the rest of your Mail setup | Provisioning, and managing templates | |

## Create the key

An agent calls [`mail_apikey_create`](/reference/mcp-tools/mail_apikey_create); over REST it is `POST /mail/api-keys` on the [Mail API](/reference/openapi/mail).

```json
{
  "name": "confirmation sender",
  "scope": "sending_access",
  "domainId": "8e2c…"
}
```

| Field | Required | What it is |
|---|---|---|
| `name` | Yes | A label for the key, 1 to 256 characters. It is only for you. |
| `scope` | Yes | `sending_access` or `full_access`. |
| `domainId` | No | The id of one of your domains. Locks the key to it. |

The response carries the key:

```json
{
  "id": "b71a…",
  "name": "confirmation sender",
  "scope": "sending_access",
  "domainId": "8e2c…",
  "prefix": "lmk_Xq7f2A",
  "key": "lmk_Xq7f2A…",
  "createdAt": "2026-08-02T09:00:00.000Z"
}
```

> `key` is the secret, and this response is the only place it ever appears. Mail keeps a one-way hash of it and the `prefix`, so nothing can hand the secret back to you later — not the API, not the app, not support. Put it into your secret store as you create it. A key you did not save is not recoverable; delete it and create another.

## The `lmk_` prefix

Every Mail key starts with `lmk_`. It marks the string as a Mail sending key, so a key that turns up in a log or a configuration file is recognisable for what it is, and so a key from somewhere else is rejected before it is looked up at all — a value that does not start with `lmk_` fails as `401` with the error `restricted_api_key`.

Mail stores the key's first characters — `lmk_` plus the six that follow — and nothing else of the secret. That short prefix is what you see when you list your keys, and it is how you tell one key from another after the fact.

## Lock a key to one domain

Passing `domainId` binds the key to that single domain. The limit is enforced on every request, not just at creation.

| Attempt | Result |
|---|---|
| Send from another domain | `401 restricted_api_key`. The `from` address is resolved to a domain first, and the email is not queued. |
| Read an email from another domain | `404` — the same answer as an id that does not exist. A locked key learns nothing about the rest of the product's mail. |
| List the product's emails | `403 restricted_api_key`. A locked key cannot list emails at all. |

A key created without `domainId` has `domainId: null` and may send from any verified domain in the product.

Locking is worth doing whenever one system sends from one domain — a key leaked from your marketing site then cannot send as your billing domain.

## Rotate a key

Rotation is a create followed by a delete, in that order.

1. **Create a new key** with the same scope and, if the old key had one, the same `domainId`.
2. **Deploy the new secret** everywhere the old one is used.
3. **Confirm nothing is still sending** with the old key.
4. **Delete the old key.**

Doing it in that order means there is no moment when your application has no working key. Both keys are valid in between, which is what makes the changeover safe.

## Delete a key

An agent calls [`mail_apikey_delete`](/reference/mcp-tools/mail_apikey_delete); over REST, `DELETE /mail/api-keys/:id`. The response is `{ "id": "b71a…", "revoked": true }`.

The key stops working immediately — the next request that uses it fails as `401` with `restricted_api_key`. Deleting a key does not touch the emails it sent. An id that does not exist, belongs to another product, or is not a valid id at all answers `404`.

To read keys back: [`mail_apikey_list`](/reference/mcp-tools/mail_apikey_list), or `GET /mail/api-keys`. It returns the active keys in the product — id, name, scope, `domainId`, `prefix` and `createdAt`. Revoked keys are not listed, and the secret is never included.

## Send a request with the key

A key goes in the `X-Api-Key` header. It is never a query parameter and never a bearer token.

Mail's public sending endpoint is `https://public.lessly.dev/{product_id}/mail`, with two routes on it: `POST /emails` sends an email, and `GET /emails/{id}` returns an email and its latest event.

```bash
curl https://public.lessly.dev/$PRODUCT_ID/mail/emails \
  -H "X-Api-Key: $MAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "you@mail.example.com",
    "to": "customer@example.com",
    "subject": "Confirm your email address",
    "html": "<p>Confirm your address to finish signing up.</p>"
  }'
```

A key is only valid on its own product's path. A valid key used against another product's `{product_id}` answers `404`, which tells the caller nothing about whether the key or the product exists.

| Response | Meaning |
|---|---|
| `401 restricted_api_key` | No key, a key that does not start with `lmk_`, a deleted key, or a locked key used for another domain. |
| `404` | The key is valid but belongs to a different product, or the email id is not one this key may read. |

## Store the key

Anyone holding a Mail API key can send email as your domain, and mail sent that way is signed by your domain and looks entirely legitimate to the people who receive it. For system email that is the sharp end: a forged password-reset link arriving from the address your customers have been taught to trust.

- Keep keys on a server you control. Read them from your secret store or from the environment at run time.
- Never ship a key in a browser application, a mobile app, or a desktop app. Anything shipped to a device can be extracted from it. Have the device call your own backend and let the backend hold the key.
- Never commit a key to a repository, and never paste one into an issue, a chat message or a support ticket.
- Give each system its own key, named for that system, and lock it to the domain it sends from. Then one leak is one key to delete, not all of them.
- Delete a key the moment you suspect it is exposed. Deletion takes effect at once, and a replacement takes seconds to create.

## Next steps

- [Set up a sending domain](/ship/mail/domains): register the domain a key is locked to.
- [Send a message](/ship/mail/sending): use the key you just created.
- [Send from TypeScript](/ship/mail/sdk): let `@lessly/mail` set the header for you.
- [How system email works](/ship/mail): where the key sits in the seven-step flow.
