Guides
Migrate from Resend
Move from Resend to Pidgely - switch to the Pidgely SDK (same method names), move domains, webhooks, contacts and suppressions, and the differences to know.
Pidgely serves the Resend REST API at the same paths, with no version prefix. Requests, responses, status codes, the error body ({"statusCode","name","message"}), the Idempotency-Key header and cursor pagination all match. To migrate, switch to the Pidgely SDK for your language (same method names) and swap the API key; your code barely changes.
Step by step
- Create an account and an API key. Pidgely keys look like
pdg_xxxxxxxx_xxxx…; the SDKs don't check there_prefix. - Add your domain (
POST /domainsor the dashboard) and publish the records it returns: DKIMTXT, the return-pathMX+ SPFTXT, and optionally DMARC. The return path defaults tobounces.<domain>; pass"custom_return_path": "send"to reuse the label you had on Resend. You can keep the Resend records in place during the switch: the DKIM selectors differ, so both coexist. - Verify the domain (
POST /domains/{id}/verify). - Switch your code to the Pidgely SDK (or, temporarily, point the Resend SDK at Pidgely) and deploy.
- Recreate webhooks with
POST /webhooksand update the signing secret in your receiver. Verification code doesn't change. - Import contacts and segments if you use broadcasts (
POST /contactsis an upsert, so re-running an import is safe), and copy your suppression list (POST /suppressions/batch/add) so bounced and unsubscribed addresses stay suppressed. - Recreate templates and publish them; aliases keep your send code unchanged.
Switch to the Pidgely SDK (recommended)
The official Pidgely SDKs mirror the Resend SDKs' method names, arguments and { data, error } results, so switching is a change of package, import and key:
- import { Resend } from 'resend';
- const resend = new Resend(process.env.RESEND_API_KEY);
+ import { Pidgely } from 'pidgely';
+ const resend = new Pidgely(process.env.PIDGELY_API_KEY);
const { data, error } = await resend.emails.send({ from, to, subject, html });npm uninstall resend && npm install pidgely(or the package for your language).- Rename the import and swap the
re_…key for yourpdg_…key. - If you verify webhooks with the SDK,
awaittheverifycall (it is async in the Pidgely SDK).
You also get Pidgely additions such as automatic retries, emails.attachments, apiKeys.update, broadcasts.cancel/duplicate and suppressions.*.
Keep the Resend SDK for now (base URL per SDK)
If you can't change dependencies yet, the official Resend SDKs also work against Pidgely: set their base URL to https://api.pidgely.com and use a Pidgely key. Plan to switch to the Pidgely SDK afterwards.
| SDK | Override | Example |
|---|---|---|
Node.js (resend) |
env RESEND_BASE_URL, or new Resend(key, { baseUrl }) |
RESEND_BASE_URL=https://api.pidgely.com RESEND_API_KEY=pdg_… node app.js |
Python (resend) |
env RESEND_API_URL, or resend.api_url = "…" |
resend.api_url = "https://api.pidgely.com" |
Go (resend-go) |
env RESEND_BASE_URL (read at init), or client.BaseURL, _ = url.Parse("https://api.pidgely.com/") |
the trailing slash matters when you set BaseURL directly |
PHP / Laravel (resend-php, resend-laravel) |
env RESEND_BASE_URL (host or full URL) |
RESEND_BASE_URL=https://api.pidgely.com/ |
Ruby / Rails (resend) |
env RESEND_BASE_URL (read at load time) |
RESEND_BASE_URL=https://api.pidgely.com/ |
Rust (resend-rs) |
env RESEND_BASE_URL, or ConfigBuilder::new(key).base_url(url) |
.base_url("https://api.pidgely.com".parse()?) |
Java (resend-java) |
Resend.builder().apiKey(key).baseUrl("https://api.pidgely.com").build() |
|
.NET (Resend) |
ResendClientOptions.ApiUrl |
services.Configure<ResendClientOptions>(o => { o.ApiUrl = "https://api.pidgely.com"; o.ApiToken = key; }); |
Elixir (community resend package) |
not verified: check your package version for a base URL option, or call the API with Req/Finch |
|
| cURL / raw HTTP | replace https://api.resend.com |
curl https://api.pidgely.com/emails -H "Authorization: Bearer pdg_…" |
SMTP
| Resend | Pidgely |
|---|---|
host smtp.resend.com |
smtp.pidgely.com |
| ports 465/2465 (TLS), 587/2587 (STARTTLS) | same |
user resend, password = API key |
user pidgely (resend also works), password = API key |
Resend-Idempotency-Key header |
same, or Pidgely-Idempotency-Key |
See SMTP relay.
Webhooks
Webhooks created through POST /webhooks use Resend's event names (email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.failed, email.suppressed, domain.updated, contact.created/updated/deleted) and Resend's payload ({type, created_at, data: {email_id, to[], …}}). They are signed with the same Svix / Standard Webhooks scheme: svix-id, svix-timestamp and svix-signature headers, plus the webhook-* equivalents. resend.webhooks.verify(…) and the Svix libraries therefore work with your whsec_… secret. See Webhooks.
Supported resources
| Resource | Endpoints | Notes |
|---|---|---|
| Emails | send, batch (x-batch-validation: strict|permissive), retrieve, list, update (scheduled_at), cancel, attachments (list/retrieve) |
scheduled_at accepts ISO 8601 and natural language (in 1 hour, tomorrow at 9am). Attachments: base64 content or remote path (fetched with SSRF protection, 40 MB budget). react is rendered by the Node SDK as usual. |
| Domains | create, list, retrieve, update (tls), delete, verify |
tls: enforced is honoured by the MTA. region is informative. |
| API keys | create (full_access/sending_access, domain_id), list, update (name), delete |
|
| Webhooks | create, list, retrieve, update, delete, rotate signing secret | |
| Suppressions | add, batch add/remove, list (origin), retrieve/remove by id or email |
Adds the unsubscribe origin. |
| Contacts | create, list (segment_id), retrieve/update/delete by id or email, segment membership |
unsubscribed stays in sync with the suppression list both ways. |
| Segments | create, list, retrieve, update, delete, list contacts | |
| Audiences (legacy) | create, list, retrieve, delete, audience-scoped contacts | Aliases of segments over global contacts. |
| Broadcasts | create (send, scheduled_at), list, retrieve, update (drafts), delete, send, cancel, duplicate |
{{{FIRST_NAME|there}}}, {{{contact.<property>}}}, {{{RESEND_UNSUBSCRIBE_URL}}} and the alias {{{PIDGELY_UNSUBSCRIBE_URL}}}; RFC 8058 one-click unsubscribe on every message. |
| Templates | create, list, retrieve, update, delete, publish, duplicate; template: {id, variables} in send |
Draft plus published versions; typed variables with fallbacks. |
Not supported yet
Each of these fails loudly rather than silently, so a migration can't misbehave unnoticed:
- Open and click tracking.
open_tracking/click_trackingare accepted and reported asfalse.email.opened/email.clickedwebhooks can be created but never fire. - Topics. A non-empty
topic_idortopicsis rejected with 422. - Inbound and receiving. Inboxes, received emails,
capabilities.receiving: enabled(rejected) andemail.received. - Other endpoints. Automations, events, contact properties as a separate resource, contact imports, logs, OAuth,
POST /emails/{id}/share,GET /emails/metrics, broadcast recipients and clicked links, domain claims, and webhook event history and replay.
Differences worth knowing
- Delete endpoints return
200with{"object","id","deleted":true}, as Resend does. The native/v1API returns204. - Validation errors are
422 validation_error. A From domain that is not verified, a plan limit, or a paused account returns403 validation_error. Asending_accesskey on a full-access endpoint returns401 restricted_api_key. GET /emails/{id}reads from the primary database, so it is consistent immediately after a send. List endpoints may use a read replica.Idempotent-Replayed: truemarks a replayed idempotent response.email.sentmeans accepted and queued;email.deliveredconfirms the receiving server accepted it.- Suppressions apply to every stream: an unsubscribe also stops transactional mail to that address.