Migrate from Resend
Move from Resend to Scribase Mail in minutes. Endpoint and SDK comparison table, the one-line baseUrl change for the resend npm package, and a step-by-step checklist that keeps your webhooks working.
Scribase Mail speaks the same HTTP API as Resend: same paths, same request bodies, same { "id": "..." } answers, same { statusCode, name, message } errors, same Svix-style webhook signatures. Most teams migrate by changing one URL and one API key. The rest of this page is for the details you want to check before flipping production.
Why move
| Resend | Scribase Mail | |
|---|---|---|
| Free plan | 3,000 emails/month, 100/day | 3,000 emails/month, 100/day, 1 domain |
| Pro | $20 = 50,000, $0.90 per extra 1,000 | $20 = 50,000, $0.80 per extra 1,000 |
| Scale | $90 = 100,000 | $90 = 150,000, $0.60 per extra 1,000 |
| Sign-in emails | you build them | POST /auth/magic-link, POST /auth/otp, JWT + JWKS included |
| Test mode | test addresses | sk_test_ keys: simulated sends, no domain needed, not metered |
| Stack | closed | a single Rust service; the dashboard shows every event and webhook attempt |
Full numbers on Limits and pricing.
Option 1: keep the resend npm package, change the base URL
The official Resend Node SDK accepts a custom base URL, either as a constructor option or through the RESEND_BASE_URL environment variable (checked in resend-node's src/resend.ts: constructor(readonly key?: string, options?: ResendOptions) with ResendOptions { baseUrl?: string }, defaulting to process.env.RESEND_BASE_URL || "https://api.resend.com").
import { Resend } from "resend";
const resend = new Resend(process.env.SCRIBASE_MAIL_API_KEY, {
baseUrl: "https://mail-api.scribase.com",
});
const { data, error } = await resend.emails.send({
from: "Acme <hello@acme.com>",
to: ["jane@example.com"],
subject: "Welcome",
html: "<p>Hi Jane</p>",
});
Or with no code change at all:
RESEND_API_KEY=sk_... # your Scribase Mail key
RESEND_BASE_URL=https://mail-api.scribase.com
resend.emails.send / batch.send / get / update / cancel, resend.domains.*, resend.apiKeys.*, resend.webhooks.* and resend.webhooks.verify() keep working. Methods for products we have not shipped yet (contacts, audiences, broadcasts, templates, inbound) return a 404 not_found until they land.
Other Resend SDKs (Python, Go, Ruby, PHP, Rust, Java, .NET) each expose a base URL setting too, usually named base_url, BaseURL or api_url. Point it at https://mail-api.scribase.com.
Option 2: switch to @scribase/mail
Same { data, error } return shape, zero dependencies, runs on Node 18+, Bun, Deno and Cloudflare Workers, and adds the auth and JWT helpers.
- import { Resend } from "resend";
- const resend = new Resend(process.env.RESEND_API_KEY);
+ import { ScribaseMail } from "@scribase/mail";
+ const resend = new ScribaseMail(process.env.SCRIBASE_MAIL_API_KEY);
const { data, error } = await resend.emails.send({ from, to, subject, html });
Differences worth knowing: webhooks.verify(payload, headers, secret) is async (it uses WebCrypto) and takes positional arguments; domains.remove / apiKeys.remove / webhooks.remove are the delete methods; emails.batch([...]) and batch.send([...]) are both available. Everything else matches. See TypeScript SDK.
Endpoint comparison
| Resend endpoint | Scribase Mail | Notes |
|---|---|---|
POST /emails | same | from, to, subject, html/text, cc, bcc, reply_to, scheduled_at, headers, attachments, tags; Idempotency-Key header honored for 24 hours |
POST /emails/batch | same | up to 100 emails, no attachments |
GET /emails/{id} | same | includes last_event |
PATCH /emails/{id} | same | reschedule with scheduled_at |
POST /emails/{id}/cancel | same | |
GET /emails | same | limit, after, before |
POST /domains, GET /domains, GET /domains/{id}, PATCH /domains/{id}, DELETE /domains/{id}, POST /domains/{id}/verify | same | records array uses Resend's record/name/type/value/status keys |
POST /api-keys, GET /api-keys, DELETE /api-keys/{id} | same | permission: full_access or sending_access, optional domain_id |
POST /webhooks, GET /webhooks, GET /webhooks/{id}, PATCH /webhooks/{id}, DELETE /webhooks/{id} | same | secret is whsec_...; headers webhook-id, webhook-timestamp, webhook-signature plus svix-* aliases |
POST /emails/{id}/events | extension | report a bounce, complaint or delivery from your own processor |
POST /auth/magic-link, POST /auth/otp, GET /auth/verify, /.well-known/jwks.json | extension | passwordless sign-in; see Magic links and OTP |
/contacts, /audiences, /broadcasts, /templates, /emails/receiving | not yet | 404 not_found |
Webhook event names are identical: email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.failed, email.opened, email.clicked. Payload shape { type, created_at, data: { email_id, from, to, subject, tags, ... } } is the same, so a handler written for Resend parses ours.
Error names you may match on in code are the same strings: missing_api_key, validation_error, missing_required_field, restricted_api_key, rate_limit_exceeded, not_found, invalid_idempotent_request, application_error.
Step by step
- Create your domain on Scribase Mail and publish the DNS records next to the Resend ones. Both can coexist: the DKIM selector is
scribase._domainkey(Resend usesresend._domainkey), and the return-path sub-domain defaults tosend(change it withreturn_pathif Resend already usessend). See Domains and DNS. - Verify:
POST /domains/{id}/verify, thenGET /domains/{id}untilstatusisverified. - Create keys with the same permission split you use today (
sending_access+domain_idfor app servers,full_accessfor admin tooling). - Recreate webhook endpoints with
POST /webhooks(same URLs, same event list). Store the newsigning_secret. Your handler can verify against both secrets during the overlap:try { verify(resendSecret) } catch { verify(scribaseSecret) }. - Switch staging: set
RESEND_BASE_URL/baseUrland the API key, send one email to yourself, confirm DKIM showsd=yourdomainin the received headers and the webhook arrived. - Copy suppressions: export Resend's suppression list and re-report each address with
POST /emails/{id}/eventsis not needed; instead send us the list and we import it into your tenant, so you never email someone who bounced on Resend. - Switch production: same two variables. Keep the Resend key alive for 7 days as a rollback.
- Delete the Resend webhook endpoints and keys, then the domain, once logs show 7 days of clean sending here.
What you do not get yet
Contacts, audiences, segments, broadcasts, templates and inbound receiving are on the roadmap, in that order. If you depend on them today, keep that part on Resend and move transactional + auth email now; both can run on the same domain with different selectors.