Skip to content

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

  1. Prepare the sender and credential. Use a from address on a verified domain of this product. Send the public key as Authorization: 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>"
  }'
  1. Send the request above and save the ID. After validation, Mail checks the organization’s plan before creating the email. A plan refusal returns 402 and creates no message; see plan refusals. A successful send returns 201. Mail wraps the result under data:
{ "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

  1. Choose a key for the business event. Add your own Idempotency-Key header to the send. For example, order-4711-receipt identifies 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>" }'
  1. 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 returns 409 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

  1. Prepare 1–100 messages. Each uses the single-send shape. Batches have no workspace screen; call mail_email_send_batch with emails, or send a JSON array to POST https://api.lepsto.com/mail/emails/batch using 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>" }
]
  1. 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

  1. Schedule a single message. Add scheduled_at to mail_email_send or the single-send request. Use a full ISO-8601 instant with Z or an offset such as +02:00. It must be in the future and within 30 days; loose, relative or out-of-range values return 422 invalid_scheduled_at. The Send test form has no schedule field. Check the result: the message has status scheduled.

    Replace this example timestamp with a future instant within 30 days of your request.

    { "scheduled_at": "2026-08-09T09:00:00Z" }
  2. Change its send time if needed. Call mail_email_update, or PATCH https://api.lepsto.com/mail/emails/{id}, with another scheduled_at that meets the same rules. This has no workspace screen.

  3. Cancel it if it should not be sent. Call mail_email_cancel, or POST 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 and status: "canceled".

Rescheduling and cancellation require status scheduled. A message that has left that state, including one already canceled, returns 409 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

Was this page helpful?
Esc

Start typing to search the docs.

↑↓navigate↵select