Sending email
Addresses, content, headers, tags, categories and safe retries.
const { , } = await ..(
{
: "Acme <hello@acme.com>",
: ["ada@example.com", "Grace Hopper <grace@example.com>"],
: "team@acme.com",
: "support@acme.com",
: "Your invoice",
: "<p>Your invoice is attached to your account.</p>",
: "Your invoice is attached to your account.",
: { "X-Entity-Ref-ID": "invoice-123" },
: [{ : "invoice_id", : "invoice-123" }],
: "billing",
},
{ : "invoice-123" },
);Sends return HTTP 202 with status: "queued". A background worker hands the email to the
provider. Follow delivery with coss.email.get() or the email.sent / email.failed webhooks.
Emails are kept for 45 days (see data retention).
Scheduling, rescheduling and cancellation
Pass scheduled_at as an ISO 8601 timestamp with a time zone offset, up to 30 days ahead.
The response is HTTP 202 with status: "scheduled"; the worker checks for due emails every
five seconds and rechecks the sender domain and suppression list before sending. A past timestamp
uses the normal queue. Send quotas are charged when the schedule is accepted.
const { : } = await ..({
: "hello@acme.com",
: "ada@example.com",
: "Your reminder",
: "See you tomorrow!",
: new (.() + 24 * 60 * 60 * 1000).(),
});
if () {
// Send it a day later instead…
await ..(., {
: new (.() + 2 * 24 * 60 * 60 * 1000).(),
});
// …or not at all.
await ..(.);
}Rescheduling moves a scheduled email to a new time, again at most 30 days from now, and emits
email.rescheduled with the new and previous times in data.reschedule. A time that is not in
the future sends the email within seconds; the email's current time returns it unchanged, without
an event. Only scheduled_at can change: to change the content, cancel and send again.
Rescheduling does not charge the send quota again.
Canceling changes the status to canceled and emits email.canceled; repeating the cancellation
returns the same email. Once dispatch starts, cancellation returns 422 email_not_cancelable and
rescheduling 422 email_not_reschedulable (as does rescheduling a canceled email).
Scheduling emits email.scheduled. These events describe the whole email.
Suspending an organization holds its scheduled emails until the suspension is lifted. Attachment
bytes remain stored while an email is scheduled and are deleted when it is sent, fails, or is canceled.
Addresses
from, to, cc, bcc and reply_to take "user@example.com" or
"Display Name <user@example.com>". Each recipient field takes one address or an array of up to 50.
With a live key, from must use a verified domain of your organization.
Sandbox keys accept any from.
Content
subject is required. Provide html, text, or both: sending both lets mail clients pick, and
improves deliverability.
Headers and tags
headers: up to 15 custom headers. Names are case-insensitive, and reserved names such assubjectare rejected.tags: up to 50name/valuepairs (letters, digits,_and-) for your own filtering. They are included in webhook payloads.
Categories
category says what kind of email this is, such as "billing" or "marketing" (letters, digits,
_ and -). Recipients suppressed for that category, or for every
email, are skipped. It is stored on the email and included in webhook payloads.
Sending in batches
coss.email.sendBatch() sends up to 100 emails in one request. Each email takes the same fields
as coss.email.send(), including scheduled_at, attachments and tracking, and they can have
different senders and recipients.
const { , } = await ..(
[
{
: "Acme <notifications@acme.com>",
: "ada@example.com",
: "Your weekly summary",
: "<p>Hi Ada, here is your week.</p>",
: "digest",
},
{
: "Acme <notifications@acme.com>",
: "grace@example.com",
: "Your weekly summary",
: "<p>Hi Grace, here is your week.</p>",
: "digest",
},
],
{ : "weekly-digest-2026-10-10" },
);The response is HTTP 202 with an email_batch whose data lists the emails in the order you
passed them. Each email has the batch's id as batch_id, and is delivered and reported on (with
webhooks) on its own.
A batch is all or nothing. If any email is invalid, nothing is sent and the error's issues name
it by position, such as emails.3.to. The same goes for an unverified sender domain, which is
reported as param: "emails.3.from". One idempotency key covers the whole batch. The batch's
recipients count against the send quota together: a batch with
more recipients than the quota allows at once is refused with no Retry-After, so split it into
smaller batches. The 40 MB request size limit applies to the whole batch.
Retrying safely
Every send carries an Idempotency-Key. The SDK generates one per call and reuses it across its
automatic retries; pass your own (such as invoice-123) to stay safe across process restarts too.
For 24 hours, repeating a request with the same key returns the original email instead of sending
again. See idempotency.