Resend API notes
What the Resend adapter does under the hood — the endpoints, the limits, the quirks worth knowing, and the capabilities it declares.
Capabilities
| Capability | Value | Consequence |
|---|---|---|
one_off_send | true | Can be the one-off sender. |
broadcast_send | false | No bulk API, so broadcasts fan out through the paced queue — with a full report. |
custom_headers | true | Our headers are passed through. |
injects_unsubscribe_headers | false | Nothing is auto-injected, so the RFC 8058 headers are entirely ours. |
send_time_suppression | true | Silent, region-wide drops for bounces and complaints. |
suppression_read | :none | No API at all. The nightly suppression sync doesn't run. |
suppression_webhooks | true | The only route for Resend-side suppression. |
engagement_webhooks | false | Per-domain tracking stays off — tracking is ours. |
push_unsubscribe | false | No write API. The standing guard covers it. |
remote_delete | false | GDPR erase can't remove the address Resend-side. |
Sending
| Case | Endpoint |
|---|---|
| One message | POST /emails |
| More than one | POST /emails/batch, chunked at 100 |
Rate limit: 10 requests/second, team-wide. The adapter declares
emails_per_minute: 600, and the sender's pacer counts every send
against that budget — one message per request through the queue, so 600 a minute
is the honest ceiling.
Headers
"headers": {
"List-Unsubscribe": "<https://your-instance.com/u/SIGNED_TOKEN>",
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
}
Resend injects nothing, so these are ours. The token is the same signed token the
subscription page takes, which is what attributes a header unsubscribe to the
exact email it came from. Both halves matter: without
List-Unsubscribe-Post, a mail client shows a link rather than its own
unsubscribe button.
Transactional mail carries no unsubscribe header. You can't opt out of your own password reset.
Webhooks
| Resend event | Normalizes to |
|---|---|
email.delivered | delivered |
email.bounced | bounced |
email.complained | complained |
email.suppressed | suppressed |
| anything else | acknowledged and ignored |
A bounce's type maps to our bounce kind: Permanent →
hard (suppresses the person), Transient → soft (doesn't — a full
mailbox is a try-again, not a dead address).
Signed with Svix: HMAC-SHA256 over svix-id.svix-timestamp.body,
base64, compared in constant time, with a five-minute freshness window so a
captured request can't be replayed tomorrow. Multiple v1,…
signatures are accepted so a secret can be rotated without dropping anything
mid-flight. See Webhooks for the shared
contract.
Quirks
- The
User-Agentheader is mandatory. Resend answers 403 without one, which is a confusing way to learn you forgot it. - Idempotency keys expire after 24 hours. Fine — they exist to make one retry of one send safe, not to deduplicate forever.
- The batch endpoint drops attachments. Mimeo doesn't use attachments.
- DKIM is 1024-bit only.
- Accepted ≠ delivered. A suppressed address gets a success-shaped response and no email.
Credentials
| Setting | What |
|---|---|
provider.resend.api_key | The API key. |
provider.resend.webhook_secret | The Svix signing secret, whsec_…. |
Both are entered on the Settings page and encrypted in the instance database, and neither ever appears in a definitions repo.
verify_config calls GET /domains and fails
when no domain is verified, naming the ones still pending — reporting a good
connection that would refuse every send isn't a verification.
See also: Resend setup · Webhooks · Write your own adapter