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
- Sign up at truncus.co
- Dashboard → Settings → API Keys → Create
- 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.