Send email

Send transactional email through Amazon SES with the Resend API, single or batch, with scheduling, tags, attachments, idempotency keys and delivery statuses.

Every example here works with the official Resend SDKs pointed at https://api.stamp0.com (see the quickstart) and with @stamp0/sdk; stamp0 below is either client.

How a send works

POST /emails validates the request, writes the bodies and attachments to the S3 bucket in your account, and answers { "id": "em_…" } with the email queued (or scheduled). A worker then sends it through SES in your account, through the from-domain's own configuration set, so that domain's tracking and TLS settings apply. Delivery events come back from SES through EventBridge and update last_event, the activity log and your webhooks. Sends respect your SES account's per-second rate.

Single email

POST /emails takes from, to, subject, and html or text. to, cc, bcc and reply_to accept one address or an array of up to 50. from may include a display name: Acme <hello@acme.co>.

await stamp0.emails.send({
  from: 'Acme <billing@acme.co>',
  to: ['maya@northwind.dev'],
  subject: 'Your invoice for October',
  html: invoiceHtml,
  text: invoiceText,
  tags: [{ name: 'category', value: 'invoice' }],
});

The body is written to the S3 bucket in your account; Stamp0 stores only metadata. When you send html without text, Stamp0 generates the plain-text part from the HTML; send text: "" to leave it out.

The from domain must be a verified domain of the project, on a connected AWS account. Otherwise the send fails with 403 validation_error (unverified domain) or 409 connection_not_ready. The react field is not accepted by the API: render the component to HTML first (the Resend Node SDK does this for you when @react-email/render is installed).

Recipients on your account's suppression list are left out of the delivery; if nobody is left, the email ends as failed.

Templates

Instead of html/text, send a published template with Resend's template field. from and subject then default to the template's:

await stamp0.emails.send({
  to: 'maya@northwind.dev',
  template: { id: 'welcome', variables: { TRIAL_END: 'November 3' } },
});

Every variable the template uses needs a value, a declared fallback or an inline default; otherwise the send fails with 422.

Batch

POST /emails/batch takes an array of up to 100 emails (no attachments or scheduling). By default the whole batch is rejected if any item is invalid. Send x-batch-validation: permissive to send the valid items and get the rest back in errors, by index.

Scheduling

Set scheduled_at to an ISO 8601 time with an offset or natural language such as in 1 hour or tomorrow at 9am (read in UTC), up to 30 days ahead. Reschedule with PATCH /emails/:id or cancel with POST /emails/:id/cancel before it sends.

Attachments

Each attachment has either base64 content or an HTTPS path that Stamp0 fetches, plus an optional filename and content_type. Use content_id for inline images referenced as cid:…. Attachments may total 30 MiB (decoded), so the message stays under SES's 40 MB limit. A path must be a public HTTPS URL.

Tags and headers

Tags are name/value pairs (letters, numbers, _ and -) that come back on events and webhooks. Custom headers are allowed except the ones Stamp0 owns, such as From, To, Subject and Message-ID.

Idempotency

Send an Idempotency-Key header (up to 256 characters) on POST /emails or POST /emails/batch to make retries safe. Stamp0 remembers the key for 24 hours: the same key with the same body returns the original response (for a batch, the same data and errors); with a different body it returns 409 invalid_idempotent_request; while the first request is still running it returns 409 concurrent_idempotent_requests. A request that fails does not use up its key.

Suppressions

A hard bounce or a complaint that the provider reported blocks the address for every connection in your organization, not only the one that sent: an address that bounced through one AWS account (or through Cloudflare) is not mailed again through another. Suppressions you add (from the API or the dashboard, always with reason manual) apply to the connections the project sends through; only the provider's events record bounces and complaints.

An add is written to each of those connections on its own. If one provider refuses it (an AWS account still in the SES sandbox, say), the others keep it: POST /suppressions answers 200 with targets, one entry per connection with its provider, status (suppressed or failed) and the error for a failed one (the batch add reports targets per address). It fails, with the first refusal, only when no connection took it.

GET /suppressions lists the rows of every connection the project sends through, plus the organization's reported hard bounces and complaints, each with its connection_id. One recorded on a connection the project does not send through is read-only, with the address partly hidden (a***@example.com); an organization owner or admin sees it in full and can remove it from the dashboard. Removing a row (DELETE /suppressions/:id) takes it off its own connection's list. Each provider keeps one list per account (Amazon SES: per account and region), so projects on the same connection share it.

Statuses

An email's last_event moves through scheduled, queued, sent, delivered, delivery_delayed, opened, clicked, bounced, complained, rejected, failed, suppressed or canceled.

An email whose every recipient is on the suppression list is never sent and ends suppressed, as on Resend. A failed email carries a reason: domain_sending_disabled (sending was turned off on the domain before a scheduled email went out), connection_not_ready (the provider connection stopped working) or content_missing (the body could no longer be read from your bucket).

Reference: Send an email, Send a batch of emails, Retrieve an email.

On this page