Skip to content
Sendtier Docs

Migrate from Resend

Move a Resend HTTP integration to Sendtier. Covers DNS records, the send call, webhooks, and suppressions.

Send a message with POST https://api.sendtier.com/emails and a bearer key that starts with st_test_ or st_live_. Sendtier does not support cc, bcc, reply_to, attachments, custom headers, hosted templates, or SMTP, and live delivery is in public preview.

Before you start

Live sending is limited during the public preview.

The API, workers, database, and queues run in AWS eu-central-1 (Frankfurt). Mail is sent through Amazon SES in that region, and AWS is on the subprocessor list.

TypeScript, Go, and PHP clients are in the repository and are not published yet, so use HTTP.

If your integration uses Resend SMTP, convert it to HTTP before migrating. Sendtier has no SMTP relay.

Render React content to HTML or text before the call, because Sendtier has no hosted templates. See Resend's send reference.

Step 1: Add your domain

Create the domain and copy the names from its response. See the domains guide. A live key needs a verified domain.

Publish three DKIM CNAMEs. The name is <token>._domainkey.<domain> and the target is <token>.dkim.amazonses.com.

The MAIL FROM host is send.<domain>. Point an MX record at feedback-smtp.eu-central-1.amazonses.com with priority 10, and on that same host publish TXT v=spf1 include:amazonses.com ~all.

Publish DMARC TXT at _dmarc.<domain> with value v=DMARC1; p=none; when that name is empty. If DMARC already exists, leave that record unchanged.

A pending domain is re-checked every 30 seconds until it verifies, then every 6 hours. Verification is removed only after two failed checks in a row.

Records that clash with Resend

Resend's domain guide says the records must match exactly what Resend generated. Add Sendtier's records beside them, and keep the Resend records while Resend is still sending.

DKIM names can coexist. Resend's create-domain example uses resend._domainkey, while Sendtier uses a separate name per SES token. Do not replace the Resend DKIM record.

The clash is the MAIL FROM host. Resend's return-path guide says it is set at creation and cannot be changed afterwards. The default is send.<domain>, which Sendtier also uses, with MX feedback-smtp.eu-central-1.amazonses.com. Resend's MX can point at another regional host. Do not publish both MX targets. Amazon SES requires exactly one MX record at the custom MAIL FROM hostname; multiple MX records make setup fail.

A CNAME at send.<domain> blocks Sendtier's MX and TXT. Resend's domain guide shows both a TXT/MX setup and a CNAME setup. If Resend requires conflicting records at that hostname, add a separate Sendtier sending domain, such as mail.example.com, whose MAIL FROM host is send.mail.example.com. Set from to an address on the domain you added.

One SPF TXT can serve both when the name is not a CNAME. Resend's example is v=spf1 include:amazonses.com ~all. Leave the domain's mailbox MX records unchanged.

Step 2: Send a test

Create a test key through Sendtier, then store it as SENDTIER_API_KEY on your server. Follow the quickstart.

A test key accepts mail while DNS is still pending, but those emails are never delivered and do not reach Amazon SES. The test limit is 1 000 recipients per day.

An st_live_… key needs a verified domain. The free plan allows 3 000 recipients a month and 100 a day. Limits count recipients, not messages. The daily limit resets at 00:00 UTC; the monthly limit resets then on the first day of each month. See rate limits and pricing.

Step 3: Change the send call

Resend requires Authorization: Bearer re_… and a User-Agent header on every API request, as the API introduction states. Sendtier uses Authorization: Bearer with an st_test_… or st_live_… key.

Resend's send call allows a string or an array for to, up to 50 addresses.

Resend fieldSendtier
fromfrom
toto, an array of 1 to 50 addresses. Wrap one address in an array.
ccNot supported.
bccNot supported.
reply_toNot supported.
subjectsubject
htmlhtml
texttext. Send html, text, or both.
attachmentsNot supported.
headersNot supported.
tagstags, an object of strings.
scheduled_atscheduled_at, ISO 8601 UTC, at most 72 hours ahead.
templateNot supported.

Sendtier also accepts resend_of, an existing Sendtier email ID. It preserves the original email's test or live mode; a test email cannot become live.

Resend tags are objects with a name and a value. Convert [{"name":"category","value":"receipt"}] to {"category":"receipt"}. If two tags share a name, keep one value.

Resend scheduling accepts natural language or an ISO date, up to 30 days ahead. Sendtier accepts ISO 8601 UTC only, and only up to 72 hours ahead.

Both APIs accept an optional Idempotency-Key. Resend allows up to 256 characters; Sendtier accepts 1–255 bytes. In Sendtier, retry with the same key and identical request bytes while the stored response is retained. A changed request returns 409 idempotency_conflict. See Sending for retention and replay rules.

Resend batch posts up to 100 objects as a JSON array, and attachments are not supported on that endpoint yet. POST /emails/batch takes 1 to 100 messages in an emails array, with a body of at most 10 MiB. See Sending.

Resend

const response = await fetch("https://api.resend.com/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
    "Content-Type": "application/json",
    "User-Agent": "migration-example/1.0",
    "Idempotency-Key": "receipt-123",
  },
  body: JSON.stringify({
    from: "Example <hello@example.com>",
    to: ["user@example.net"],
    subject: "Your receipt",
    text: "Thanks for your order.",
  }),
});
if (!response.ok) throw new Error(await response.text());

Sendtier

Replace the placeholder hello@mail.example.com with an address on the domain you added.

const response = await fetch("https://api.sendtier.com/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SENDTIER_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "receipt-123",
  },
  body: JSON.stringify({
    from: "Example <hello@mail.example.com>",
    to: ["user@example.net"],
    subject: "Your receipt",
    text: "Thanks for your order.",
  }),
});
if (!response.ok) throw new Error(await response.text());

Step 4: Change webhooks

Register a Sendtier webhook with a full key and adapt your handler to its payload envelope. Use data.email_id to identify the email and the event id to deduplicate deliveries. The table maps Resend event names, not payload schemas.

Resend eventSendtier
email.deliveredemail.delivered
email.delivery_delayedemail.delayed
email.bouncedemail.bounced
email.complainedemail.complained
email.failedemail.failed, a provider failure
email.sentNot delivered
email.scheduledNot delivered
email.suppressedNot delivered. The API returns 422 recipient_suppressed.
email.receivedNot delivered

Sendtier does not deliver email.queued or email.sent, and it does not deliver domain, contact, topic, suppression, or inbox events.

Resend uses svix-id, svix-timestamp, and svix-signature. Svix signs <svix-id>.<svix-timestamp>.<raw body>, as described in Svix manual verification and Resend's verification guide. Do not reuse a Svix verifier for Sendtier.

Sendtier sends Sendtier-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256> over <t>.<raw body>. Allow five minutes of clock skew. After a secret rotation, the old secret stays valid for 24 hours and the header has two v1 values. Use Verify Sendtier-Signature.

Resend retries wait 5 seconds, then 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, and 10 hours again. Each wait starts when the previous attempt fails, and persistent failure can disable the endpoint.

Sendtier retries after 30 seconds, 1 minute, 5 minutes, 15 minutes, and 30 minutes. Later attempts wait 1 hour, 2 hours, 4 hours, and 8 hours, for up to 24 hours. The webhooks guide explains how to inspect and retry deliveries.

Step 5: Copy suppressions

Export Resend suppressions before the switch with the dashboard Download CSV button on Email suppressions. Alternatively, retrieve all entries through List suppressions, including every page and origin. Refresh the export before cut-over and copy new suppressions while both providers remain active.

Sendtier has no bulk import. Use a full key to add one address per POST /suppressions call.

{
  "email": "blocked@example.com",
  "reason": "manual"
}

New entries use the manual reason; existing Sendtier suppressions remain unchanged. The import does not preserve Resend's origin, source ID, or timestamp. See Suppressions for listing and removal.

A key allows 10 requests per second, with a burst of 20. Past that, the API returns 429 rate_limited and Retry-After, so wait and then continue.

Permanent bounces and complaints automatically suppress the address; transient and undetermined bounces do not. Classification follows the SES bounce type, not the SMTP status digits. Sending to a suppressed address returns 422 recipient_suppressed.

Step 6: Cut over

Keep production traffic on Resend during the public preview. Live sending is limited, so a production cut-over waits until Sendtier announces full live sending. Until then, integrate and test with an st_test_… key while Resend keeps sending.

Domain verification is required for a live key, but it does not remove the preview sending limit. When Sendtier announces full live sending, confirm delivery to your intended recipients before you move production traffic. Also confirm webhook handling, suppression imports, and sufficient sending limits.

For rollback, switch your code path back to your Resend integration for new sends. Restore the Resend URL, key, field mapping, sender configuration, and webhook verifier. Keep both providers' DNS records and webhook handlers during the transition. Already accepted Sendtier emails may still be processed, so reconcile them, including scheduled messages, before you retry them through Resend.

Watch email.bounced and email.complained: the bounce event keeps the SES bounce type (Permanent, Transient, or Undetermined) and the subtype. For each recipient it keeps the action and the status, such as 5.1.1. When the receiving server returns a diagnostic code, the event includes that reply, for example smtp; 550 5.1.1 ... user unknown. The reporting MTA and the remote MTA IP are included when present. SES does not always include a diagnostic code.

A delivery event keeps the receiving server's SMTP response and the remote MTA.

Emails, events, and webhook deliveries stay for 30 days by default, then they are removed. See retention.

See Sendtier and Resend.

Start free with a test key.

On this page