Migrate from Resend
Migrating from Resend to Sendpository
Everything that changes when you move your email from Resend, in the order you'll do it. Usually under an hour for the code, plus DNS propagation.
In short
Moving from Resend to Sendpository is mostly a key and base-URL change: both take a JSON body with from, to, subject and html over HTTPS with a Bearer key, both honour an Idempotency-Key header, and event names are almost the same. The three real changes are tags (an array of name/value pairs becomes a plain object), webhook verification (Svix headers become one Sendpository-Signature header) and re-verifying your domain.Or let your coding agent do the whole migration
Paste it in. It finds every Resend call, rewrites it, updates your webhook handler and switches over behind a flag you can flip back.
Show the promptHide the prompt
Switch this project's email from Resend to Sendpository. First run `npx sendpository@latest init` - it opens my browser for me to approve, saves the API key and installs the Sendpository skill - then follow the skill's migration guide (references/migrate.md). If you can't run commands, read https://www.sendpository.com/agents/sendpository/references/migrate.md instead.
1. API differences
| Resend | Sendpository | |
|---|---|---|
| Endpoint | POST https://api.resend.com/emails | POST https://api.sendpository.com/v1/emails |
| Body | JSON: from, to, subject, html, text, cc, bcc, reply_to, headers, attachments, scheduled_at | The same field names. to, cc, bcc and reply_to take arrays; a single string is accepted for to. |
| Tags | Array of { name, value } | Plain object: { "type": "receipt" } |
| Templates | react (Node SDK) or a stored template id with variables | Send rendered HTML. React Email works: render the component to HTML and pass it as html. |
| Idempotency | Idempotency-Key header | Idempotency-Key header - a replay returns the original id |
| Batch | POST /emails/batch | POST https://api.sendpository.com/v1/emails/batch, up to 100 messages, each accepted or rejected on its own |
| Response | { "id": "..." } | { "id": "..." } |
2. Authentication differences
| Resend | Sendpository | |
|---|---|---|
| API key | Authorization: Bearer re_... | Authorization: Bearer sp_... - create a key with the sending permission for the app, full_access only where you manage domains or webhooks by API. |
Create the key under API keys in the dashboard. It is shown once, so put it straight into SENDPOSITORY_API_KEY in your secrets manager. Keep the Resend key until the switch is finished, then revoke it.
3. Domain setup
Add your sending domain in Sendpository and publish the records it shows: a 2048-bit DKIM key signed as your own domain, plus SPF and a bounce MX on a sp. subdomain so your existing records are never edited. DMARC is recommended; if you already publish one, leave it as it is.
- Add the same domain in Sendpository. Its SPF and bounce records go on a sp. subdomain, so they don't collide with anything Resend asked you to publish.
- The DKIM record has a different selector, so it sits alongside Resend's. Both can exist while you switch over.
Not sure what’s published already? Run the domain through the email DNS checker.
4. Webhooks
| Resend | Sendpository | |
|---|---|---|
| Verification | Svix: svix-id, svix-timestamp and svix-signature headers | One Sendpository-Signature header: t=<unix>,v1=<HMAC-SHA256 of "t.rawBody">. The SDK's verifyWebhook checks it and rejects anything older than five minutes. |
Event names
| Resend event | Sendpository event |
|---|---|
| email.sent | email.sent |
| email.delivered | email.delivered |
| email.delivery_delayed | email.delayed |
| email.bounced | email.bounced |
| email.complained | email.complained |
| email.failed | email.failed |
| email.opened | email.opened |
| email.clicked | email.clicked |
| email.scheduled | email.scheduled |
| email.received | email.received |
The payload is { type, created_at, data } with data.email_id identifying the message.
Full payloads and verification code are in the webhooks guide.
5. Error handling
| Resend | Sendpository | |
|---|---|---|
| Error shape | An error object with a name and a message. | { "error": { "type": "domain_not_verified", "message": "..." } } |
Map your checks on Resend's error name to our error.type. validation_error, rate_limit_exceeded and not_found keep their meaning. Only rate_limit_exceeded (wait for Retry-After) and internal_error are worth retrying automatically; the errors reference lists every type.
6. Code migration
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.emails.send({
from: "Acme <billing@yourdomain.com>",
to: ["customer@example.com"],
subject: "Your receipt",
html: "<p>Thanks for your order.</p>",
tags: [{ name: "type", value: "receipt" }],
});import { Sendpository, SendpositoryError } from "sendpository";
const sendpository = new Sendpository(process.env.SENDPOSITORY_API_KEY);
try {
const { id } = await sendpository.emails.send(
{
from: "Acme <billing@mail.yourdomain.com>",
to: ["customer@example.com"],
subject: "Your receipt",
html: "<p>Thanks for your order.</p>",
text: "Thanks for your order.",
tags: { type: "receipt" },
},
// A retried job returns the first message's id instead of sending twice.
{ idempotencyKey: "receipt-10428" },
);
console.log(id);
} catch (err) {
if (err instanceof SendpositoryError) console.error(err.type, err.message);
throw err;
}Watch out for
- Rewrite tags from [{ name, value }] to { name: value } - an array is rejected.
- Replace the Svix verification with verifyWebhook from the sendpository package; the Svix headers will be absent.
- Resend's SDK returns { data, error }. Sendpository's SDK throws a SendpositoryError instead - wrap sends in try/catch.
- Rename email.delivery_delayed to email.delayed in your event switch.
7. Pricing
| Resend | Sendpository | |
|---|---|---|
| Published pricing | Free: 3,000 emails a month, capped at 100 a day, one domain. Paid plans start at $20 a month for 50,000 emails. | 6,000 emails a month (up to 200 a day), free forever. Current plans |
Resend pricing from their own pages, checked 2026-09-23. Sendpository's free plan is larger: 6,000 emails a month (up to 200 a day), against Resend's 3,000 a month.
8. Things to test before switching
Specific to leaving Resend:
- Re-send a message whose tags you had set as an array. Resend takes `[{name, value}]`; ours takes a plain object, and a tag silently dropped is a filter that stops matching in the logs.
- Replace the Svix signature check. Resend signs through Svix with its own header set; ours is a single `Sendpository-Signature` with the timestamp inside the signed content, so the old verification code will reject every delivery until it is swapped.
- Confirm anything built on React Email still renders. The templates are yours and keep working - they just produce HTML you now pass as `html` rather than handing the component to the SDK.
True of any migration:
- Send to a real Gmail and a real Outlook address and open Show original: SPF, DKIM and DMARC should all say PASS, with DKIM signed as your own domain.
- Send a message with an attachment and one with an inline image, and check both render.
- Trigger a bounce (an address at a domain you control with no mailbox) and confirm your webhook handler receives email.bounced and marks the address.
- Send the same request twice with one Idempotency-Key and confirm only one email arrives.
- Point a staging environment at the new API key first, and watch the Logs page for 4xx responses before switching production.
- Keep the old provider's DNS records until the new domain has verified and a day of production mail has been delivered - removing them first breaks mail still in flight.
Questions
Can I keep React Email after moving from Resend?
Yes. Render the component with React Email's render function and pass the resulting HTML as the html field. Sendpository sends whatever HTML you give it.
Do I need to change my webhook handler?
Yes, the signature check. Resend signs with Svix headers; Sendpository sends one Sendpository-Signature header, verified with verifyWebhook from the sendpository package. Event names are the same except email.delivery_delayed, which is email.delayed.
Can I run Resend and Sendpository side by side?
Yes. The DKIM records use different selectors and Sendpository's SPF and bounce records live on a subdomain, so both can be verified at once while you move traffic over.
Resend API details are from their documentation, checked 2026-09-25: resend.com, resend.com, resend.com.
Keep reading
Sendpository vs Resend
Where each is the better choice, side by side.
Migrate from SendGrid
An afternoon for most codebases - the request shape is the main rewrite.
Migrate from Postmark
An hour or two - the request maps field for field.
Migrate from Mailgun
An afternoon - mostly swapping form-data for JSON.
Send your first email in minutes
6,000 emails a month (up to 200 a day), free forever. No card needed.