Skip to content

Mail

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

ScopeMayUse for
sending_accessSend email, and read the emails it sentA key that lives in your application(Recommended)
full_accessThe above, and manage the rest of your Mail setupProvisioning, 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…"
}
FieldRequiredWhat it is
nameYesA label for the key, 1 to 256 characters. It is only for you.
scopeYessending_access or full_access.
domainIdNoThe 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"
}

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.

AttemptResult
Send from another domain401 restricted_api_key. The from address is resolved to a domain first, and the email is not queued.
Read an email from another domain404 — 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 emails403 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; 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.

ResponseMeaning
401 restricted_api_keyNo key, a key that does not start with lmk_, a deleted key, or a locked key used for another domain.
404The 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

Was this page helpful?
Esc

Start typing to search the docs.

navigateselect