scribase
Migration guide

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.

  1. 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.

  2. 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.

  3. 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>.

  4. 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>',
    });
  5. 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.

  6. 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');
    }
  7. 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.

Other languages

No SDK required

Any HTTP client works. These are the same request your code sends to Resend today, with a different host and key.

curlsh
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>"
  }'
Python (requests)py
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"])
Node without a code changeenv
# No code change: the resend SDK reads this variable
RESEND_BASE_URL=https://mail-api.scribase.com
Compatibility

What is supported

Every route below is served at mail-api.scribase.com in the Resend request and response shape, with Resend-style error bodies.

EmailsPOST /emails · POST /emails/batch · GET /emails · GET /emails/:id · PATCH /emails/:id · POST /emails/:id/cancel · GET /emails/:id/attachments
Received emailsGET /emails/received · GET /emails/received/:id · GET /emails/received/:id/attachments
DomainsPOST /domains · GET /domains · GET /domains/:id · PATCH /domains/:id · DELETE /domains/:id · POST /domains/:id/verify
API keysPOST /api-keys · GET /api-keys · DELETE /api-keys/:id
ContactsGET, 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)
TopicsGET, POST /topics · GET, PATCH, DELETE /topics/:id
BroadcastsCRUD /broadcasts · POST /broadcasts/:id/send · POST /broadcasts/:id/cancel · POST /broadcasts/:id/duplicate
TemplatesCRUD /templates · POST /templates/:id/publish · POST /templates/:id/duplicate · GET /templates/:id/versions
WebhooksCRUD /webhooks · POST /webhooks/:id/signing-secret/rotate · GET /webhooks/:id/events
Suppressions and logsGET, 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.