Skip to content
Pidgely Docs

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.

View as Markdown

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

  1. Create an account and an API key. Pidgely keys look like pdg_xxxxxxxx_xxxx…; the SDKs don't check the re_ prefix.
  2. Add your domain (POST /domains or the dashboard) and publish the records it returns: DKIM TXT, the return-path MX + SPF TXT, and optionally DMARC. The return path defaults to bounces.<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.
  3. Verify the domain (POST /domains/{id}/verify).
  4. Switch your code to the Pidgely SDK (or, temporarily, point the Resend SDK at Pidgely) and deploy.
  5. Recreate webhooks with POST /webhooks and update the signing secret in your receiver. Verification code doesn't change.
  6. Import contacts and segments if you use broadcasts (POST /contacts is 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.
  7. Recreate templates and publish them; aliases keep your send code unchanged.

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:

DIFF
- 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 });
  1. npm uninstall resend && npm install pidgely (or the package for your language).
  2. Rename the import and swap the re_… key for your pdg_… key.
  3. If you verify webhooks with the SDK, await the verify call (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_tracking are accepted and reported as false. email.opened/email.clicked webhooks can be created but never fire.
  • Topics. A non-empty topic_id or topics is rejected with 422.
  • Inbound and receiving. Inboxes, received emails, capabilities.receiving: enabled (rejected) and email.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 200 with {"object","id","deleted":true}, as Resend does. The native /v1 API returns 204.
  • Validation errors are 422 validation_error. A From domain that is not verified, a plan limit, or a paused account returns 403 validation_error. A sending_access key on a full-access endpoint returns 401 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: true marks a replayed idempotent response.
  • email.sent means accepted and queued; email.delivered confirms the receiving server accepted it.
  • Suppressions apply to every stream: an unsubscribe also stops transactional mail to that address.