Move from Resend without rewriting your email code
Scribase Mail Studio serves the Resend REST API shapes at mail-api.scribase.com, and the same code runs when you self-host. Seven steps, most of them configuration.
Scribase Mail Studio is independent and not affiliated with Resend. It is a fork of useSend under AGPL-3.0.
Step 1
Create an account or self-host
Sign up at https://mail.scribase.com for the hosted service, or clone the repository and run it with Docker on your own servers. Both expose the same API.
Step 2
Add and verify your sending domain
In the dashboard (Domains) or with POST /domains, add the domain you send from. Add the DKIM and SPF records it shows at your DNS provider, then press Verify or call POST /domains/:id/verify. Keep your existing DMARC record.
Step 3
Create an API key
Create a key in the dashboard (API keys) and store it as a new secret, for example SCRIBASE_MAIL_KEY. Keys are sent the same way as before: Authorization: Bearer <key>.
Step 4
Point the SDK at the new base URL
The resend Node SDK takes a baseUrl option and also reads RESEND_BASE_URL. Either one works; nothing else in your sending code changes.
Node, resend SDK 6.xts import { Resend } from 'resend'; // Before: const resend = new Resend(process.env.RESEND_API_KEY); const resend = new Resend(process.env.SCRIBASE_MAIL_KEY, { baseUrl: 'https://mail-api.scribase.com', }); await resend.emails.send({ from: 'Acme <hello@acme.com>', to: ['you@example.com'], subject: 'It works', html: '<p>Sent through Scribase Mail Studio.</p>', });Step 5
Send a test email to the test addresses
Before real traffic, send to delivered@test.scribase.com, bounced@test.scribase.com, complained@test.scribase.com. They produce the matching delivered, bounced and complained events without sending real mail, which is useful in CI.
Step 6
Move webhooks
Create the webhook endpoints again (dashboard or POST /webhooks) and store the new whsec_ signing secret. Payloads are signed in the Svix format with svix-id, svix-timestamp and svix-signature headers, so resend.webhooks.verify() or the svix library keeps working.
Verifying a webhook in a Next.js route handlerts import { Resend } from 'resend'; const resend = new Resend(process.env.SCRIBASE_MAIL_KEY, { baseUrl: 'https://mail-api.scribase.com' }); export async function POST(req: Request) { const payload = await req.text(); const event = resend.webhooks.verify({ payload, headers: { id: req.headers.get('svix-id')!, timestamp: req.headers.get('svix-timestamp')!, signature: req.headers.get('svix-signature')!, }, webhookSecret: process.env.SCRIBASE_MAIL_WEBHOOK_SECRET!, // whsec_... }); // event.type: 'email.delivered', 'email.bounced', 'email.received', ... return new Response('ok'); }Step 7
Switch traffic, then watch the first sends
Deploy with the new key and base URL. The dashboard shows each email with its events and the API request log (GET /logs). New senders start on warm-up caps that rise as sending stays clean.
No SDK required
Any HTTP client works. These are the same request your code sends to Resend today, with a different host and key.
curl -X POST https://mail-api.scribase.com/emails \
-H "Authorization: Bearer $SCRIBASE_MAIL_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@acme.com>",
"to": ["you@example.com"],
"subject": "It works",
"html": "<p>Sent through Scribase Mail Studio.</p>"
}'import os
import requests
response = requests.post(
"https://mail-api.scribase.com/emails",
headers={"Authorization": f"Bearer {os.environ['SCRIBASE_MAIL_KEY']}"},
json={
"from": "Acme <hello@acme.com>",
"to": ["you@example.com"],
"subject": "It works",
"html": "<p>Sent through Scribase Mail Studio.</p>",
},
timeout=30,
)
response.raise_for_status()
print(response.json()["id"])# No code change: the resend SDK reads this variable
RESEND_BASE_URL=https://mail-api.scribase.comWhat is supported
Every route below is served at mail-api.scribase.com in the Resend request and response shape, with Resend-style error bodies.
| Emails | POST /emails · POST /emails/batch · GET /emails · GET /emails/:id · PATCH /emails/:id · POST /emails/:id/cancel · GET /emails/:id/attachments |
| Received emails | GET /emails/received · GET /emails/received/:id · GET /emails/received/:id/attachments |
| Domains | POST /domains · GET /domains · GET /domains/:id · PATCH /domains/:id · DELETE /domains/:id · POST /domains/:id/verify |
| API keys | POST /api-keys · GET /api-keys · DELETE /api-keys/:id |
| Contacts | GET, POST /contacts · GET, PATCH, DELETE /contacts/:id · GET, PUT /contacts/:id/topics/:topicId |
| Audiences and segments | /audiences (CRUD + contacts) · /segments (CRUD + contacts) · /contact-properties (CRUD) |
| Topics | GET, POST /topics · GET, PATCH, DELETE /topics/:id |
| Broadcasts | CRUD /broadcasts · POST /broadcasts/:id/send · POST /broadcasts/:id/cancel · POST /broadcasts/:id/duplicate |
| Templates | CRUD /templates · POST /templates/:id/publish · POST /templates/:id/duplicate · GET /templates/:id/versions |
| Webhooks | CRUD /webhooks · POST /webhooks/:id/signing-secret/rotate · GET /webhooks/:id/events |
| Suppressions and logs | GET, POST /suppressions · DELETE /suppressions/:email · GET /logs · GET /logs/:id |
Sends accept scheduled_at, attachments (content or path), inline images by content_id, tags, reply_to, headers and the Idempotency-Key header.
Not supported yet
- Automations and custom events (/automations, /events)
- The Inboxes API for agent inboxes
- OAuth apps acting on a user account
- Domain claim (/domains/:id/claim)
- Enforced TLS per domain: the setting is stored, delivery is opportunistic TLS
Missing something you rely on? Open an issue on GitHub.