COSS

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 as subject are rejected.
  • tags: up to 50 name/value pairs (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.

On this page