Migrate from Resend
Keep the resend SDK. Set its base URL to Sendly's Resend dialect, swap the key, and your send calls, batches and error handling stay as they are.
Last updated 2026-09-04
What Sendly implements
Resend's POST /emails, POST /emails/batch, and GET /emails/:id. Those are the three calls a transactional integration actually makes, and all three answer in Resend's own response shapes.
| Setting | Value |
|---|---|
| Base URL | https://api.sendly.now/api/compat/resend |
| Auth | Authorization: Bearer sk_… |
| Env var (JavaScript SDK) | RESEND_BASE_URL |
| Env var (Python SDK) | RESEND_API_URL |
Both official SDKs read those environment variables, so on a deployment where configuration is environment and not code, this migration is a variable change and a redeploy with no diff at all.
The change
import { Resend } from "resend";
const resend = new Resend(process.env.SENDLY_API_KEY, {
baseUrl: "https://api.sendly.now/api/compat/resend",
});
// every call site below is untouched
await resend.emails.send({
from: "you@yourdomain.com",
to: "customer@example.com",
subject: "Hello",
html: "<p>Welcome aboard.</p>",
});import resend
resend.api_key = os.environ["SENDLY_API_KEY"]
resend.api_url = "https://api.sendly.now/api/compat/resend"
resend.Emails.send({
"from": "you@yourdomain.com",
"to": "customer@example.com",
"subject": "Hello",
"html": "<p>Welcome aboard.</p>",
})What does not come across
Four fields return a clean Resend-shaped 422 rather than being accepted and quietly dropped. If your sends use any of them, that is work to plan for, not a surprise to discover in a delivery report.
| Field | What happens | What to do instead |
|---|---|---|
scheduled_at | 422 validation_error | Scheduled sends are not supported through the compat path in this phase. Schedule on your side, or use Sendly's native send. |
attachments[].path | 422 | Sendly does not fetch attachments from a URL. Fetch the file yourself and send it inline as base64 content. |
template | 422 | A Resend template id refers to content stored in Resend. Rebuild the template in Sendly, or move the markup into the request body. |
topic_id | 422 | Broadcast topics are not supported. Use a Sendly campaign or segment. |
Two fields are partial rather than rejected. reply_to uses the first address when you pass several, and tags are combined, sanitized and capped at ten — so read back what Sendly stored rather than assuming byte equality with what you sent.
The one call that is not a send
emails.get(id) reads a message back, including its subject, body and recipient. Because that is a read of message content, it is gated on emails:read rather than on sending permission — the same rule the native API applies.
A key with only sending ticked keeps sending normally and gets a 403 on the read, in Resend's own error shape, naming the permission it is missing:
{
"statusCode": 403,
"message": "This API key is missing the required scope: emails:read",
"name": "restricted_api_key"
}Grant the key emails:read in your project's API key settings, or use a full-permission key.
Before you switch a production sender
Verify your domain and import your suppression list first, then move one slice at a time. The staged rollout plan is the seven steps we would follow, in the order that keeps each one reversible.
The full supported-field table, with every note, lives in the Resend migration guide in the docs.
Questions people ask
- Does batch send work?
- Yes, and response order is preserved, so an index into the results array still means what your code thinks it means.
- Can I do this without changing code?
- Usually yes. The JavaScript SDK honours RESEND_BASE_URL and the Python SDK honours RESEND_API_URL, so setting that variable and the Sendly key is enough on a deployment where configuration lives in the environment.
- Do I keep Resend's { data, error } handling?
- Yes. Errors come back in Resend's own { statusCode, message, name } shape with the matching status, so the JavaScript SDK's { data, error } split and the Python SDK's raised ResendError behave exactly as they do today.
- Is a compat send billed differently from a native one?
- No. It runs the same pipeline and meters the same way: one email is one email, against the same free allowance and the same per-thousand curve.
Something on this page out of date, or missing the case you are in? Write to support@sendly.now — we answer our own email. The full technical detail lives in the documentation.