Send a message
Send, batch, schedule and cancel messages, read their status and handle sending limits.
Send a message with one request and keep the returned ID to follow its delivery. Mail validates and accepts the message before it delivers it, so a successful response confirms acceptance.
Before you start: verify a sending domain and create a product public key scoped to POST /emails. Reading status also needs GET /emails/{id}; a send-only key returns 403 on that route. Batch, reschedule and cancel operations require a platform credential, as explained below. The examples use Acme and mail.acme.com; replace the sender, recipients, product ID, key and message IDs with your values. For initial setup, follow send your first email.
Send one email
- Prepare the sender and credential. Use a
fromaddress on a verified domain of this product. Send the public key asAuthorization: Bearer.
curl -X POST "https://public.lepsto.com/$PRODUCT_ID/mail/emails" \
-H "Authorization: Bearer $LEPSTO_PUBLIC_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@mail.acme.com>",
"to": "customer@example.com",
"subject": "Your receipt",
"html": "<p>Thanks for your order.</p>"
}'- Send the request above and save the ID. After validation, Mail checks the organization’s plan before creating the email. A plan refusal returns
402and creates no message; see plan refusals. A successful send returns201. Mail wraps the result underdata:
{ "data": { "id": "9f1f0d9c-1c74-4a2e-9f9f-2b6d8a0b3c11" }, "success": true }Check the result: the response contains the new message ID. Find that message in Mail → Logs and check its status; use the status table to distinguish acceptance, delivery and suppression.
Through MCP, call mail_email_send. The management REST operation is POST /mail/emails on the Mail API.
For a manual setup check, use Mail → Emails → Send test. The form takes a mailbox name, verified sending domain, one To address, subject and HTML body. Application sending uses the API, MCP tool or SDK.
Choose where you send from
Surface | Authenticates with | Carries |
|---|---|---|
Public sending endpoint (Recommended) for application code | A product public key, as Authorization: Bearer | POST /emails and GET /emails/{id} |
Mail API, for provisioning and operations | A session token or lsk_ API key | Everything, including batch, reschedule and cancel |
Both surfaces operate on the same messages. Use the public endpoint from application code; use your product’s authenticated Mail API for batch send, reschedule and cancel.
The public base URL is https://public.lepsto.com/{product_id}/mail. Batch, reschedule and cancel are available only on the management API, https://api.lepsto.com. Authenticate those calls with a session token or lsk_ API key in Authorization: Bearer. The credential selects the product for management calls; it is not selected by a path parameter. See API and keys or the template guide’s credential comparison.
A public key authorizes the public send and read routes. Use a platform credential for the three management operations above.
Imported lmk_ Mail keys still authorize the two public routes. They now cover every verified domain in the product, rather than one domain. See authenticate the public API.
The request fields
Field | Required | Accepts |
|---|---|---|
from | Yes | you@your-domain.com or Name <you@your-domain.com> |
to | See below | One address, or an array of addresses |
cc | No | One address, or an array of addresses |
bcc | No | One address, or an array of addresses |
reply_to | No | One address, or an array of addresses |
subject | Yes, for an inline body | Any non-empty string |
html | One of html/text | The HTML body |
text | One of html/text | The plain-text body |
headers | No | An object of header name to value |
tags | No | An array of { "name": ..., "value": ... } |
template | No | { "id": ..., "variables": { ... } } |
scheduled_at | No | An ISO-8601 instant with a time zone |
Mail returns 404 when the sender’s domain is not registered in this product, and 422 invalid_from_address when from has no usable address. The domain must also be verified.
Include at least one recipient across to, cc and bcc, with no more than 50 addresses combined. Mail checks every address in those fields and reply_to before acceptance. The first malformed address returns 422:
{ "name": "invalid_recipient", "message": "invalid recipient address: not-an-address" }Mail delivers to bcc recipients without including their addresses in message headers.
Bodies: provide html, text or both. With HTML alone, Mail derives plain text by stripping tags, turning <br> and closing block tags into line breaks, and decoding common HTML entities. Text alone produces a plain-text message with no HTML part.
Custom headers: use a flat name-to-value object. Names accept printable ASCII except a colon. Values, addresses and the subject must contain no carriage returns or line feeds. Mail rejects a violation before acceptance.
Tags: group messages with your own name/value pairs. Mail includes them as X-Tag-<name> headers, using the custom-header naming rule.
Templates: use template for a published hosted template. Do not combine it with subject, html or text; that fails validation. See send from a template.
Do not send the same receipt twice
- Choose a key for the business event. Add your own
Idempotency-Keyheader to the send. For example,order-4711-receiptidentifies one order’s receipt:
curl -X POST "https://public.lepsto.com/$PRODUCT_ID/mail/emails" \
-H "Authorization: Bearer $LEPSTO_PUBLIC_KEY" \
-H "Idempotency-Key: order-4711-receipt" \
-H "Content-Type: application/json" \
-d '{ "from": "hello@mail.acme.com", "to": "customer@example.com",
"subject": "Your receipt", "html": "<p>Thanks.</p>" }'- Reuse that key and body for retries. For 24 hours, repeating the same request returns the original response without creating another message. A changed body returns
409 invalid_idempotent_request; a request still in progress returns409 concurrent_idempotent_requests. A failed request releases its key for another attempt.
Check the result: a successful repeat returns the original message ID. Derive the key from the business event so a retried job, redelivered webhook or restarted process reuses it instead of producing a second receipt.
Send a batch
- Prepare 1–100 messages. Each uses the single-send shape. Batches have no workspace screen; call
mail_email_send_batchwithemails, or send a JSON array toPOST https://api.lepsto.com/mail/emails/batchusing your platform credential:
[
{ "from": "hello@mail.acme.com", "to": "a@example.com",
"subject": "Welcome", "html": "<p>Hi A.</p>" },
{ "from": "hello@mail.acme.com", "to": "b@example.com",
"subject": "Welcome", "html": "<p>Hi B.</p>" }
]- Send the batch and check its result. The returned IDs follow the input order. A batch outside the 1–100 range returns
422 invalid_batch.
Mail validates every item, resolves each sending domain and renders each template before creating any messages. One invalid item prevents creation of the entire batch. An item-specific error includes its input position:
{ "name": "invalid_recipient", "index": 1,
"message": "invalid recipient address: nope" }Do not include scheduled_at or attachments in a batch item:
{ "name": "invalid_batch_item", "index": 0,
"message": "scheduled_at and attachments are not allowed in batch send" }Schedule each message in a separate send. After validating every batch item, Mail checks the organization’s plan once for the sum of unique recipients within each item. A 402 refusal creates and queues no item. This plan check does not establish atomicity for unrelated failures.
Send later, reschedule or cancel
-
Schedule a single message. Add
scheduled_attomail_email_sendor the single-send request. Use a full ISO-8601 instant withZor an offset such as+02:00. It must be in the future and within 30 days; loose, relative or out-of-range values return422 invalid_scheduled_at. The Send test form has no schedule field. Check the result: the message has statusscheduled.Replace this example timestamp with a future instant within 30 days of your request.
{ "scheduled_at": "2026-08-09T09:00:00Z" } -
Change its send time if needed. Call
mail_email_update, orPATCH https://api.lepsto.com/mail/emails/{id}, with anotherscheduled_atthat meets the same rules. This has no workspace screen. -
Cancel it if it should not be sent. Call
mail_email_cancel, orPOST https://api.lepsto.com/mail/emails/{id}/cancel. Alternatively, open Mail → Emails → Scheduled, which lists recipients and send times, and select Cancel. Check the result: the response includes the ID andstatus: "canceled".
Rescheduling and cancellation require status
scheduled. A message that has left that state, including one already canceled, returns409 not_scheduled.
The request fixes a scheduled message’s content at creation. Templates are rendered then, so editing a template during the wait does not change that message. The organization’s plan is also checked at acceptance: a 402 refusal creates no scheduled email. Mail’s separate reputation and quota gates run when the scheduled message comes due; a gate then marks it blocked without returning a new error to the original caller.
Read a message back
curl "https://public.lepsto.com/$PRODUCT_ID/mail/emails/9f1f0d9c-1c74-4a2e-9f9f-2b6d8a0b3c11" \
-H "Authorization: Bearer $LEPSTO_PUBLIC_KEY"{
"data": {
"id": "9f1f0d9c-1c74-4a2e-9f9f-2b6d8a0b3c11",
"from": "Acme <hello@mail.acme.com>",
"to": ["customer@example.com"],
"cc": [],
"bcc": [],
"subject": "Your receipt",
"status": "delivered",
"scheduled_at": null,
"provider_message_id": "0100019a2f...",
"created_at": "2026-08-02T10:15:04.221Z",
"last_event": { "type": "email.delivered", "created_at": "2026-08-02T10:15:09.880Z" }
},
"success": true
}last_event describes the lifecycle event behind the current status and its time. An invalid UUID returns 400; an ID belonging to another product returns 404. Through MCP, use mail_email_get.
In the workspace, enter the ID in Mail → Emails → Look up status for status and the last event. Mail → Logs filters messages by subject, recipient, domain or status. Open a row for sender, recipients, creation time, provider message ID and the ordered event history.
To receive outcomes as they happen, register delivery webhooks.
What the statuses mean
Status | Meaning |
|---|---|
scheduled | Accepted and waiting for its send time. Can still be rescheduled or canceled. |
queued | Accepted and about to be handed to the receiving mail servers. |
sent | Handed over. The receiving server has not reported an outcome yet. |
delivered | The receiving server accepted the message for its recipient. |
bounced | The receiving server rejected it. |
complained | The recipient marked it as spam. |
suppressed | Every recipient was on your suppression list, so nothing was sent. |
blocked | A sending limit or your sender reputation stopped it before it went out. |
failed | The message could not be handed over. |
canceled | You canceled it before it went out. |
Mail removes suppressed recipients at acceptance and checks again before sending. This also skips addresses suppressed while a scheduled message waits. Remaining recipients receive the message; if none remain, it becomes suppressed. See handle bounces and protect your sending.
Limits
Limit | Value |
|---|---|
Body size | A POST /emails request body may be up to 5 MiB. |
Send rate | 5 requests per second per product, over a sliding one-second window. Reading a message and canceling one do not consume this budget. |
Public endpoint rate | An additional 100 requests per minute per client IP address. |
Recipients | At most 50 across to, cc and bcc per message. |
Batch size | 1 to 100 messages. |
Schedule horizon | At most 30 days ahead. |
Read RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset on send responses. A rate-limit rejection returns 429 rate_limit_exceeded and adds Retry-After.
Additional 429 refusals use these codes in Mail’s error envelope:
Code | Meaning |
|---|---|
quota_exceeded | Mail’s sending quota is reached. The message names the cap; this is separate from the organization’s plan check. |
reputation_throttled | Your bounce or complaint rate has crossed its threshold. |
{ "data": null, "success": false,
"error": { "code": "reputation_throttled", "message": "..." } }For immediate sends and batches, the reputation and Mail quota gates apply at acceptance. For scheduled messages, they apply at delivery time: the message becomes blocked with reputation_throttled or quota_exceeded:<cap>, and the original caller receives no new error.
The platform checks keys before Mail receives a request. For 401 public_key_required, 401 public_key_invalid, 403 public_key_scope or 503 public_config_unavailable, use the key refusal guide.
When the organization’s plan does not cover the send
The organization’s plan check can return 402 Payment Required with error: "entitlement_exhausted" or "entitlement_unavailable". It happens before a message is created. A single or scheduled send counts unique recipients across to, cc and bcc; a batch sums that count for each item after validating all items.
This response uses the platform’s body rather than Mail’s data/success envelope. Product public-key callers receive only error and reason:
{ "error": "entitlement_exhausted", "reason": "trial_cap_reached" }Authenticated session and API callers receive the full body:
Field | Meaning |
|---|---|
error | entitlement_exhausted or entitlement_unavailable |
reason | trial_cap_reached, trial_class_unavailable, billing_paused or budget_paused |
meter | emails-sent |
pool | The allowance checked |
limit, used, requested | Allowed quantity, used quantity and quantity requested by this call |
plan | The organization’s plan |
upgrade_url | The upgrade destination, or null |
MCP tools use these same operations, but this table describes the HTTP response body, not an outer MCP error envelope.
Do not automatically retry a 402 as a rate-limit failure. Have the organization’s owner review the reason and address the plan or paused billing/budget condition in the app. Follow upgrade_url when supplied for an upgrade. A public-key caller has no plan details or upgrade link; tell your application’s user that sending is unavailable. entitlement_unavailable does not mean a billing connection timeout.
Next steps
- Send from a template: Reuse a published subject and body.
- Send from TypeScript: Use typed sends, errors and retries.
- Receive delivery events: Receive signed delivery notifications.
- Handle bounces and protect your sending: Investigate suppressions and reputation gates.
- Send your first email: Set up a domain and key before sending.