Last updated: 2026-03-057 min read

Migrate from Resend to Truncus

Most migrations take under 30 minutes. The API shape is similar — the key differences are EU data residency, required idempotency keys, and the sandbox mode.

Why switch?

Feature Resend Truncus
Data residency US (Virginia) EU (Frankfurt, eu-west-1)
Multi-tenant isolation Separate accounts Native tenant_id
Circuit breaker No Yes — transparent bounce/complaint protection
Sandbox mode No first-class support X-Truncus-Sandbox: true or sandbox=True in SDK
Dry-run endpoint No POST /api/v1/emails/validate
MCP server No @truncus/mcp-server
OpenAPI spec Yes Yes — /api/v1/openapi.json

Step 1 — Get an API key

  1. Sign up at truncus.co
  2. Dashboard → Settings → API Keys → Create
  3. Copy your tr_live_... key

Step 2 — Verify your domain

The domain verification flow is identical to Resend. Go to Dashboard → Domains, add your domain, and copy the three DNS records (SPF, DKIM, DMARC). Most DNS providers propagate in under 5 minutes.

Step 3 — Update your code

Node.js / TypeScript

Before (Resend):

import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

await resend.emails.send({
  from: 'hello@mail.yourdomain.com',
  to: 'user@example.com',
  subject: 'Welcome!',
  html: '<p>Hello!</p>',
});

After (Truncus):

import { Truncus } from '@truncus/email';

const truncus = new Truncus({ apiKey: process.env.TRUNCUS_API_KEY });

await truncus.sendEmail({
  from: 'hello@mail.yourdomain.com',
  to: 'user@example.com',
  subject: 'Welcome!',
  html: '<p>Hello!</p>',
  // idempotencyKey auto-generated if omitted
});

Install the SDK:

npm install @truncus/email
# Uninstall Resend:
npm uninstall resend

Python

pip install truncus
pip uninstall resend
from truncus import Truncus

client = Truncus(api_key="tr_live_...")

result = client.send(
    to="user@example.com",
    from_="hello@mail.yourdomain.com",
    subject="Welcome!",
    html="<p>Hello!</p>",
)

curl

Change the endpoint and header name:

curl -X POST https://truncus.co/api/v1/emails/send \
  -H "Authorization: Bearer $TRUNCUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "to": "user@example.com",
    "from": "hello@mail.yourdomain.com",
    "subject": "Welcome!",
    "html": "<p>Hello!</p>"
  }'

API mapping

Resend endpoint Truncus endpoint Notes
POST /emails POST /api/v1/emails/send Add Idempotency-Key header
GET /emails/:id GET /api/v1/emails/:id Same shape, camelCase timestamps
POST /batch POST /api/v1/emails/batch Max 100 per request (same)
GET /domains GET /api/v1/domains Same
n/a POST /api/v1/emails/validate Truncus-only: dry-run validation
n/a GET /api/v1/stats Truncus-only: aggregated sending stats

Key differences

Idempotency-Key header (required)

Truncus requires an Idempotency-Key header on every send request. This prevents duplicate sends on network retries — a hard lesson from production outages at scale.

The Node.js and Python SDKs auto-generate a UUID v4 if you don't supply one. For raw HTTP calls, include the header explicitly:

-H "Idempotency-Key: order-123-welcome-email"

Use a stable business key (not a random UUID) for operations you might retry: order-{id}-{event}. This guarantees exactly-once delivery even across retries and server restarts.

Sandbox mode

Add X-Truncus-Sandbox: true to any request for a full dry-run — validates payload, checks domain, checks suppression, enforces rate limits, but never touches SES:

curl -X POST https://truncus.co/api/v1/emails/send \
  -H "Authorization: Bearer $TRUNCUS_API_KEY" \
  -H "X-Truncus-Sandbox: true" \
  -H "Idempotency-Key: test-$(uuidgen)" \
  -d '{ "to": "...", "from": "...", "subject": "...", "html": "..." }'

Or use the dedicated validate endpoint (equivalent):

curl -X POST https://truncus.co/api/v1/emails/validate \
  -H "Authorization: Bearer $TRUNCUS_API_KEY" \
  -H "Idempotency-Key: test-$(uuidgen)" \
  -d '{ "to": "...", "from": "...", "subject": "...", "html": "..." }'

Rate limit headers

Every 2xx response includes rate limit headers so you can implement adaptive backoff:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1741180800

Multi-tenant apps

Pass tenant_id to scope suppression lists per customer. Resend requires separate accounts for this; Truncus handles it natively:

await truncus.sendEmail({
  to: 'user@example.com',
  from: 'hello@mail.yourdomain.com',
  subject: 'Welcome!',
  html: '<p>Hello!</p>',
  tenantId: 'customer-abc123',
});

Webhook migration

Update your webhook endpoint URL in the Truncus dashboard. The payload shape is similar but the signature verification method differs:

import crypto from 'crypto';

function verifyWebhook(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Suppression list import

Import your Resend suppression list via the dashboard (Dashboard → Suppression → Import) or API:

curl -X POST https://truncus.co/api/v1/suppression/import \
  -H "Authorization: Bearer $TRUNCUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["bounce@example.com", "unsubscribed@example.com"]}'

Environment variable update

# Remove
RESEND_API_KEY=re_xxx

# Add
TRUNCUS_API_KEY=tr_live_xxx

Need help?

Email hello@truncus.co or open the chat in your dashboard. Migrations are supported — we'll help you verify the DNS records and confirm your first send.

Migrate from Resend to Truncus | Truncus Manual