Create and rotate a sending key
Issue a scoped Lessly Mail API key for your application, lock it to one domain, replace it without downtime, and store it where it cannot leak.
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; over REST it is POST /mail/api-keys on the Mail API.
{
"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:
{
"id": "b71a…",
"name": "confirmation sender",
"scope": "sending_access",
"domainId": "8e2c…",
"prefix": "lmk_Xq7f2A",
"key": "lmk_Xq7f2A…",
"createdAt": "2026-08-02T09:00:00.000Z"
}
keyis the secret, and this response is the only place it ever appears. Mail keeps a one-way hash of it and theprefix, 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.
- Create a new key with the same scope and, if the old key had one, the same
domainId. - Deploy the new secret everywhere the old one is used.
- Confirm nothing is still sending with the old key.
- 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; 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, 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.
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: register the domain a key is locked to.
- Send a message: use the key you just created.
- Send from TypeScript: let
@lessly/mailset the header for you. - How system email works: where the key sits in the seven-step flow.