Scribase Mail docs

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

ResendScribase Mail
Free plan3,000 emails/month, 100/day3,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 emailsyou build themPOST /auth/magic-link, POST /auth/otp, JWT + JWKS included
Test modetest addressessk_test_ keys: simulated sends, no domain needed, not metered
Stackcloseda 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 endpointScribase MailNotes
POST /emailssamefrom, to, subject, html/text, cc, bcc, reply_to, scheduled_at, headers, attachments, tags; Idempotency-Key header honored for 24 hours
POST /emails/batchsameup to 100 emails, no attachments
GET /emails/{id}sameincludes last_event
PATCH /emails/{id}samereschedule with scheduled_at
POST /emails/{id}/cancelsame
GET /emailssamelimit, after, before
POST /domains, GET /domains, GET /domains/{id}, PATCH /domains/{id}, DELETE /domains/{id}, POST /domains/{id}/verifysamerecords array uses Resend's record/name/type/value/status keys
POST /api-keys, GET /api-keys, DELETE /api-keys/{id}samepermission: full_access or sending_access, optional domain_id
POST /webhooks, GET /webhooks, GET /webhooks/{id}, PATCH /webhooks/{id}, DELETE /webhooks/{id}samesecret is whsec_...; headers webhook-id, webhook-timestamp, webhook-signature plus svix-* aliases
POST /emails/{id}/eventsextensionreport a bounce, complaint or delivery from your own processor
POST /auth/magic-link, POST /auth/otp, GET /auth/verify, /.well-known/jwks.jsonextensionpasswordless sign-in; see Magic links and OTP
/contacts, /audiences, /broadcasts, /templates, /emails/receivingnot yet404 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

  1. 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 uses resend._domainkey), and the return-path sub-domain defaults to send (change it with return_path if Resend already uses send). See Domains and DNS.
  2. Verify: POST /domains/{id}/verify, then GET /domains/{id} until status is verified.
  3. Create keys with the same permission split you use today (sending_access + domain_id for app servers, full_access for admin tooling).
  4. Recreate webhook endpoints with POST /webhooks (same URLs, same event list). Store the new signing_secret. Your handler can verify against both secrets during the overlap: try { verify(resendSecret) } catch { verify(scribaseSecret) }.
  5. Switch staging: set RESEND_BASE_URL / baseUrl and the API key, send one email to yourself, confirm DKIM shows d=yourdomain in the received headers and the webhook arrived.
  6. Copy suppressions: export Resend's suppression list and re-report each address with POST /emails/{id}/events is not needed; instead send us the list and we import it into your tenant, so you never email someone who bounced on Resend.
  7. Switch production: same two variables. Keep the Resend key alive for 7 days as a rollback.
  8. 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.