Reading the rows
GET /v1/email/batches/{batch_id}/recipients returns the batch’s rows in the order they were uploaded, 50 at a time.
not_ready_reason
The one answer to “why is there no checkbox next to this row”. It has exactly two forms:
to that resolved to something that is not an address looks like.
The check runs over the merged space, so it does not care whether a value came from your CSV, from the task or from an edit. Fix it either way and the reason goes away.
Editing a cell
PATCH /v1/email/batches/{batch_id}/recipients/{recipient_id} writes into overrides, which is a separate layer from what the model wrote.
- An edit is stored apart from what the model wrote and merged last.
outputstill holds the model’s version, so “what the task produced” and “what you actually sent” stay separately answerable after the fact. - A re-draft cannot overwrite an edit, because a drafting pass writes
outputand neveroverrides. - An empty value clears the edit rather than sending an empty field. The row falls back to whatever was underneath. The way to send nothing is to skip the row.
- Keys are column names, so they follow the same rule as a variable name: letters, digits and underscores, starting with a letter or an underscore.
overrides under the column the mapping names, and the row becomes sendable — the merged space does not record which layer a value came from, and nothing downstream can tell.
A row that has already been queued or sent can no longer be edited; the request is a 409 naming the status. Rows in pending, drafting, draft, draft_failed and skipped are all editable.
Skip, restore, retry
These three are reversible, so they take a shorthand thatsend does not.
Each takes either
recipient_ids or selection: "all_eligible", which means every row the verb can act on — draft for skip, skipped for restore, draft_failed for retry.
rejected, each with its row_number and a sentence — this row is sent: only a drafted row can be skipped.
retry is the one verb that revives a batch that has already finished drafting. A drafted (or stopped) batch goes back to drafting, picks up exactly the rows you retried, and stops again when they land. Nothing else brings a finished batch back to work.
Sending
POST /v1/email/batches/{batch_id}/send takes between 1 and 50 recipient ids, named one by one.
Fifty, named, every time
There is noselection shorthand on send, no bulk endpoint, and there will not be one. A five-thousand-row batch cannot leave the building without a hundred deliberate calls, each of which should follow a read of the fifty drafts it covers.
That friction is the entire structural safeguard on this feature. Do not automate around it: a loop that pages through every row and sends each page is the same product as a bulk endpoint, which is the thing this design exists to refuse. The reversible verbs are where bulk belongs.
In the dashboard, the review table pages fifty rows at a time for the same reason: Select the 50 drafts on this page ticks what is in front of you, and the send button says how many are ticked and which address they go from.
The sender, for this call only
from_email, from_name and reply_to on a send override the batch’s own for that call and nothing else. Each one you omit falls back to the batch’s. None of them writes back, so the next send starts from the batch’s defaults again.
An overridden from_email is checked against the Resend account’s verified domains before anything is queued — the same check the batch’s own address gets. The account itself is not overridable: a different Resend account is a different credential, a different rate limit and a different suppression list, which is a different batch.
What comes back
affected is how many rows were queued. Everything else comes back in rejected with its row number and why — a row that is not in this batch, a row that is not a reviewable draft, or a row whose mapped fields do not resolve. A rejected row stays a draft: fix it and send it again.
Sending the same rows twice matches nothing the second time and returns affected: 0 rather than an error. Naming the same id twice in one call is a 400.
The rows leave one at a time, send_sleep_seconds apart (1 second by default, up to 60). At one second, fifty rows take about fifty seconds.
One address, once per batch
An address is mailed at most once per batch. The second row carrying it settles as
skipped with skip_reason: duplicate_recipient and is never sent.- There is no cross-batch deduplication. The same person on two lists is emailed twice, once per batch. Keeping that from happening is your side of the line.
- Talqing keeps no suppression list. Resend does: it maintains one on your account and automatically suppresses hard bounces and spam complaints, so an address that bounced permanently stops receiving. That list lives in your Resend dashboard, not here.
Row statuses
The dashboard shows a different word for one of these; the API string is what you filter on.
A row still
pending on a batch that was cancelled or stopped reads as canceled. That is computed on read, not written to the row, and it is why filtering for canceled on a live batch matches nothing.
What freezes once anything has been sent
The moment one row reachesqueued, sending, sent or send_failed, five fields on the batch stop being editable:
Identity is frozen because rows already delivered would no longer describe what they were sent as. Pacing stays open because it is exactly what someone watching a batch wants to change. A
PATCH that touches a frozen field returns a 400 naming the fields.
start_at is a separate case: it moves only while the batch is still scheduled.
Next
Monitoring a batch
Statuses, counts,
next_draft_at, self-stopping and cost.Research and outreach
A task that researches a lead, feeding a batch end to end.