Sending email
API base URL: https://api.relayloft.com
POST /emails
Authenticate with Authorization: Bearer mr_.... The response is { "id": "<email id>" }. The email is queued and sent within seconds; its progress shows under Emails in the portal and in webhook events.
| Field | Notes |
|---|---|
from | Required. you@domain or Name <you@domain>, on one of your verified domains (and allowed by the key, if the key is limited to some domains). |
to, cc, bcc | A string or an array. At most 50 recipients across all three. |
reply_to | A string or an array. |
subject | Required. One line, at most 900 characters. |
html, text | At least one. With only html, a plain-text part is made for you. |
headers | Extra headers as an object. Addressing, content and authentication headers (such as From, Message-ID, DKIM-Signature) can't be set. |
attachments | Up to 32. Each has filename and either content (base64) or path (an https URL we download; at most 10 per request). Optional content_type and content_id for inline images. |
tags | Up to 50 { "name", "value" } pairs (letters, digits, _ and -). They come back in webhook events. |
scheduled_at | Send later, up to 30 days ahead. See Scheduling. |
template | { "id": "<id or alias>", "variables": { "KEY": "value" } } instead of html and text. See Templates. |
The whole email, attachments included, can be up to 7 MB.
Scheduling
Set scheduled_at (scheduledAt in the Node SDK) to send later. The email is accepted and stored at once, then usually sent within a minute or two of its time. When a lot of email comes due at once, it goes out at a few hundred a minute, shared fairly between senders, so a large batch can take several minutes to finish. It can be up to 30 days ahead. Each email in a batch can have its own.
- ISO 8601:
2026-08-05T11:52:01Z,2026-08-05T13:52:01+02:00. Without an offset it's UTC. in 5 min,in 1 hour,in 2 days,in 1 week(minutes, hours, days or weeks;minandhrwork too).tomorrow at 9am,today at 17:30,Friday at 3pm ET. The time needsam/pmor a 24-hourHH:MM. A weekday is the next one: today if the time hasn't passed, otherwise next week.- Time zones:
UTC/GMT,ET,CT,MT,PT(and theirEST/EDT-style names),AKT,HST,BST,CET,EET,JST,SGT,AET,NZST. A zone name means that region's local time, daylight saving included:9am ESTin July is 9am in New York.CSTis US Central time (Chicago), not China; for China use an ISO offset such as+08:00. No zone means UTC.
Anything else is refused with 422, never guessed; so is a time in the past. An ISO time up to a minute ago sends at once. Read the time back with GET /emails/:id: scheduled_at, and last_event is scheduled until it's sent. An email.scheduled webhook fires when it's accepted; its data also has scheduled_at.
The email counts toward your daily and monthly limits of the day and month (UTC) it's due, reserved when it's accepted. So a day can be booked up ahead of time: once email scheduled for a day reaches a limit, more for that day is refused with 429 (daily_quota_exceeded or monthly_quota_exceeded) and the message names the day; schedule it for another day, or cancel something scheduled for then. Test addresses can't be scheduled.
Reschedule or cancel
Both need a Full access key and work only while the email is still scheduled; after that they get 422. Cancel also works on an email that came due while sending was paused and is being held unsent. You can also do both from the email's page in the portal.
await resend.emails.update({ id, scheduledAt: "in 1 hour" }); // PATCH /emails/:id { "scheduled_at": "in 1 hour" }
await resend.emails.cancel(id); // POST /emails/:id/cancelA new time can be at most 30 days after the email was first accepted. Moving it to another day moves its sends to that day's limits; if that day is full, the reschedule is refused with 429 and the email keeps its time. Canceling gives its sends back to the limits of the day and month it was counted in. Once canceled, it can't be rescheduled.
If a key leaks: revoke the key and cancel its scheduled email. Revoking stops new requests, but email it already scheduled still goes out unless you tick Also cancel them when you revoke it on the API keys page. (Leave it unticked when you revoke an old key after rotating it.)
POST /emails/batch
An array of up to 100 emails in one call. By default it's all or nothing: if any email is invalid, none is sent. The response is { "data": [{ "id": "..." }, ...] }, in the same order.
With the header x-batch-validation: permissive (resend.batch.send(emails, { batchValidation: "permissive" })), the valid emails are sent and the others reported: { "data": [...], "errors": [{ "index": 1, "message": "…" }] }, always with status 200. Limits on the whole request (quota, rate, test-address caps) still refuse the whole batch. With an Idempotency-Key, a retry returns the same result, errors included: send the failed emails again under a new key.
Idempotency
Send an Idempotency-Key header (up to 256 characters) to make retries safe. For 24 hours, the same key with the same body returns the original response without sending again; the same key with a different body gets 409.
curl -X POST https://api.relayloft.com/emails \
-H "Authorization: Bearer $RESEND_API_KEY" \
-H "Idempotency-Key: welcome-user-123" \
-H "Content-Type: application/json" \
-d '{"from":"hello@mail.yourcompany.com","to":"user@example.com","subject":"Welcome","text":"Hi!"}'Test addresses
Send to these to test without sending real email. They work in the sandbox too.
delivered@simulator.relayloft.com: sent, then delivered.bounced@simulator.relayloft.com: sent, then a permanent bounce.complained@simulator.relayloft.com: delivered, then a spam complaint.
Add a label to tell tests apart: delivered+signup@simulator.relayloft.com. Up to 50 test recipients per request; an hourly allowance depends on your plan. A request can't mix test addresses and real ones.
Suppressed addresses
An address that bounced permanently or complained is suppressed: later emails to it are accepted but not sent to that address. If every recipient is suppressed the email ends as suppressed (with an email.suppressed event). You can see and remove suppressions in the portal.
Tracking
Open and click tracking are off by default. Turn them on per domain: on the domain's page in the portal, or with PATCH /domains/:id and open_tracking / click_tracking (resend.domains.update). They apply to email sent after the change, HTML only; the text part is never changed.
- Opens: a hidden 1×1 image from
https://relayloft.usis added at the end of the HTML. Each load is anemail.openedevent (at most one per email every 10 minutes). Opens are approximate: Apple Mail Privacy Protection and some mail providers load images for every email whether or not it's read, and many readers block images, so an open can be counted that wasn't one, or missed. - Clicks: each
http/httpslink (up to 200 different URLs per email; the same URL twice shares one redirect) goes throughhttps://relayloft.us/c/…, which records anemail.clickedevent and redirects to your URL (at most one event per link a minute; every click is still redirected). Click tracking is available once support has approved your account (out of the sandbox); open tracking works from the start.
Left as they are (not tracked):
- Links to
mailto:,tel:and#anchors. Relative links (/pricing) and template placeholders. - Any link with
data-resend-track="false"orclicktracking="off". - URLs longer than 2,048 characters.
- Links that could mislead a reader about where they go: URLs with user info (
https://brand.example@other.example), links to an IP address instead of a name, and international domain names that mix alphabets in one part (a Cyrillic letter among Latin ones).
Per email, not per recipient. An email to several people has one set of links and one pixel, so its opens and clicks are counted per email, not per recipient, and the events don't say which recipient opened or clicked. To track each person, send each one their own email (a batch does that).
Limits. At most 100 opens and clicks per email per day are recorded (so one email can't flood your events and webhooks), one open per email every 10 minutes and one click per link a minute. The tracking host also turns away more than 600 requests a minute from one network (an IP address, or an IPv6 /64); a reader past that still gets the image, and a link they already opened in the last minute still redirects.
What's stored. The time, and for a click the link, the reader's IP address and user agent (in the event and its webhook), kept with the email for 30 days. For the redirects, each link's full URL, including query strings, is kept for 180 days, so links in older email keep working; a click on one after the email is gone redirects without recording anything. Links go through the shared tracking host; a custom tracking subdomain (tracking_subdomain) isn't available. Test addresses are never tracked (nothing is sent).
Links that carry secrets (a password reset, a magic sign-in link, anything with a token in it) should be marked data-resend-track="false": otherwise the full URL is stored for 180 days and passes through the tracking host.
Support can turn off a single tracked link, or click tracking for a whole account, when a link is reported as abuse: it then shows a "disabled" page instead of redirecting.