Migrate from Resend

For teams coming from Resend. Sendoka's email API is deliberately close — most code needs minimal changes, and you pick up SMS + platform mode for free.

Field mapping

Resend Sendoka
POST /emails POST /api/v1/emails
from, to, subject, html, text Same
cc, bcc Same
reply_to reply_to
tags: [{ name, value }] tags: [string] + metadata: { ... }
attachments: [{ filename, content, contentType }] attachments: [{ filename, content, content_type }] (snake_case)
headers: { ... } headers: { ... }
scheduled_at: "in 1 hour" scheduled_at: "2026-04-22T11:14:00Z" (ISO UTC, no relative strings)

Code diff

// Resend
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
  from: "you@yourdomain.com",
  to: ["user@example.com"],
  subject: "Welcome",
  html: "<p>Hi</p>",
});

// Sendoka — plain HTTP
await fetch("https://api.sendoka.com/api/v1/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SENDOKA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "you@yourdomain.com",
    to: ["user@example.com"],
    subject: "Welcome",
    html: "<p>Hi</p>",
  }),
});

What's different

  • Snake_case in JSON. content_type, scheduled_at, reply_to, track_opens, track_clicks.
  • Tags are strings + metadata object, not { name, value } pairs. Migrate tag names to the tags: string[] array and key-value tags to metadata.
  • No relative scheduled_at. Pass absolute ISO UTC.
  • Idempotency is a header, not a body field: Idempotency-Key: <uuid>.
  • Batch endpoint returns per-row { index, id, status, error }, not an array of ids. Check results[i].status === "sent".
  • Broadcasts → audiences. POST /api/v1/audiences + POST /audiences/:id/contacts + POST /audiences/:id/send.
  • Webhooks are HMAC-signed with t={unix}, v1={hex} format — verify with your stack's HMAC primitive (Node crypto.createHmac, Python hmac).

Domain verification

Same mechanism (SES DKIM/SPF). Dashboard → Domains → Add. CNAMEs are in the same format, but you'll need to point them at Sendoka's SES account, not Resend's. Old Resend CNAMEs can be removed after your Sendoka domain verifies.

Keep them both live for a week if you want to do a gradual cutover — send test traffic to Sendoka, compare deliverability, flip the remaining traffic.

What you gain

  • SMS. POST /api/v1/sms with the same key.
  • Platform mode. Sub-organize your own customers as tenants — isolated suppressions, webhooks, domains.
  • Scheduled sends + cancellation. DELETE /api/v1/emails/:id while scheduled.
  • Per-key rate limits + IP CIDR allowlists.
  • Audit log of key/domain/webhook changes.

Gotchas

  • React Email templates work unchanged — render to HTML string on your side, pass as html.
  • Idempotency window is 24h — Resend's is 6h. Replays from that gap are now safe.
  • Webhook retry is exponential backoff, max 5 attempts — matches Resend's envelope.