Relayloft docsSign in

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.

FieldNotes
fromRequired. 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, bccA string or an array. At most 50 recipients across all three.
reply_toA string or an array.
subjectRequired. One line, at most 900 characters.
html, textAt least one. With only html, a plain-text part is made for you.
headersExtra headers as an object. Addressing, content and authentication headers (such as From, Message-ID, DKIM-Signature) can't be set.
attachmentsUp 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.
tagsUp to 50 { "name", "value" } pairs (letters, digits, _ and -). They come back in webhook events.
scheduled_atSend 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.

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/cancel

A 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.

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.

Left as they are (not tracked):

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.