Scribase Mail Studio is a separate, open-source product (caelum0x/scribase-mail-studio,
AGPL-3.0, hosted at mail.scribase.com) that serves a Resend-compatible API at
mail-api.scribase.com. It is not part of the per-project Control API described
elsewhere in these docs — a Mail Studio API key authenticates against Mail
Studio directly, the same way a Resend API key authenticates against
api.resend.com.
Existing code written against the resend SDK or Resend's REST API keeps
working unchanged: swap the API key and point the client at Mail Studio's base
URL (see Migrate from Resend below).
HTTP API
The routes below are served at mail-api.scribase.com, in the same request
and response shapes Resend uses:
| Resource | Routes |
|---|---|
| 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 |
Not yet implemented (kept honest on purpose, not hidden): automations and
custom events (/automations, /events), the Inboxes API for agent inboxes,
OAuth apps acting on a user account, domain claim (/domains/:id/claim), and
enforced TLS per domain (the setting is stored; delivery is opportunistic TLS).
Sending an email
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 { Resend } from 'resend';
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>',
});Before sending real traffic, send to the test addresses
delivered@test.scribase.com, bounced@test.scribase.com, and
complained@test.scribase.com — each produces the matching event without
sending real mail, which is useful in CI.
Domains
Add and verify the domain you send from before sending real traffic:
curl -X POST https://mail-api.scribase.com/domains \
-H "Authorization: Bearer $SCRIBASE_MAIL_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "acme.com"}'Add the DKIM and SPF records the response returns at your DNS provider, keep your existing DMARC record, then confirm:
curl -X POST https://mail-api.scribase.com/domains/<id>/verify \
-H "Authorization: Bearer $SCRIBASE_MAIL_KEY"Webhooks
Webhook payloads (email.sent, delivered, bounced, complained,
opened, clicked, received, and more) are signed in the Svix format —
svix-id, svix-timestamp, svix-signature headers — so resend.webhooks.verify()
and the svix library verify them unchanged:
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');
}Migrate from Resend
There is no separate import tool for Resend, by design: Mail Studio serves the same request and response shapes Resend does, so moving over is a configuration change, not a data migration.
- Create an account or self-host. Sign up at
mail.scribase.comfor the hosted service, or clonecaelum0x/scribase-mail-studioand run it with Docker on your own servers. Both expose the same API. - Add and verify your sending domain (see Domains above). Keep your existing DMARC record.
- Create an API key in the Mail Studio dashboard and store it as a new
secret, for example
SCRIBASE_MAIL_KEY. Keys are sent the same way as before:Authorization: Bearer <key>. - Point the SDK at the new base URL. The
resendNode SDK takes abaseUrloption and also readsRESEND_BASE_URL— either works, and nothing else in your sending code changes:Terminal RESEND_BASE_URL=https://mail-api.scribase.com - Send a test email to the test addresses above before real traffic.
- Move webhooks. Re-create the webhook endpoints (dashboard or
POST /webhooks) and store the newwhsec_...signing secret. - Switch traffic, then watch the first sends. 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.
See the full walkthrough with copy-paste Node, curl, and Python examples at scribase.com/mail/migrate-from-resend.
Self-hosting
git clone https://github.com/caelum0x/scribase-mail-studio
cd scribase-mail-studio
# follow the self-host section of the README