Drafting produces drafts and stops. Nothing has been emailed, and nothing will be until a person reads the rows and names the ones to send. This page is that step. It is also the only safeguard this feature has, which is why it is shaped the way it is: at most 50 rows per send, each named by id, with no bulk form of the call.

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.
Each row carries the merged column space and the three layers that made it, separately.

not_ready_reason

The one answer to “why is there no checkbox next to this row”. It has exactly two forms:
The first names the mapped field and, in brackets, the column the mapping points at — so you can tell “the task returned nothing” from “the mapping names the wrong column”. The second is what a 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.
Four rules:
  • An edit is stored apart from what the model wrote and merged last. output still 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 output and never overrides.
  • 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.
This is the answer to a row whose task returned nothing for the address. Put the address in 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 that send 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.
Nothing moves silently. Rows that were in the wrong state come back in 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

This mails real people and cannot be undone, recalled or edited afterwards. There is no unsend, no queue you can drain and no scheduled delay to change your mind in — a queued row is on its way within seconds.
POST /v1/email/batches/{batch_id}/send takes between 1 and 50 recipient ids, named one by one.

Fifty, named, every time

There is no selection 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.
The check happens when the row is claimed for sending, not at upload, and it has to: the address may not exist until the task drafts the row, so there is nothing to compare at create time. A batch fed phone numbers and told to find the email address behind each one is exactly this case. Two limits worth stating plainly:
  • 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 reaches queued, 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.
If you are ever tempted to force a stuck send through by hand, read this first. Every row carries an idempotency key that is the row’s own id, permanently, and Resend honours it for 24 hours. Inside that window a re-run is free — Resend replays the original response and nobody is mailed twice. Past it the key has expired, and re-running the send is a second real email to everyone still sending. Contact hello@talqing.com rather than guessing at the window.

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.