Guides
Bulk send
A bulk send takes a template and a list of rows, one per envelope, and sends each row as a normal signature request. Every row is validated before anything moves, rows go out at a steady rate, and each row reports the same delivery states your webhook endpoints receive.
What it is for
Transactional envelopes your recipients expect: renewals, onboarding packets, annual acknowledgements, anything each person already has a reason to sign. The acceptable use policy bars unsolicited or promotional content dressed as a signature request, and batches receive proportionally stricter automated review than single sends. A workspace that sends more than 2,000 envelopes in one day is flagged for review; the flag never stops a batch.
The flow
- Create the batch with
POST /v1/bulk-sends: the template and the rows. Every row is checked against the template's definition. If any row is invalid, the request answers 422 with a per-row list and nothing is stored as sendable. - Dispatch with
POST /v1/bulk-sends/{id}/dispatch: the batch is queued. It is never refused for its size: there is no allowance to run out of, and each envelope is billed when it completes. A batch dispatches whole or not at all, never half. One exception: a workspace whose subscription is ending this month can send only what its minimum still covers, and rows past that fail withsubscription-ending-limit. - Rows send at 10 rows every 2 seconds per workspace. Each row becomes an envelope with its own
doc_id, created and sent in one step. - Track with
GET /v1/bulk-sends/{id}: per-row status anddelivery_state(sent, opened, signed, completed, bounced, rejected), updated from the same event stream webhooks use. - Cancel with
POST /v1/bulk-sends/{id}/cancel: rows still queued are canceled and never sent; envelopes already sent stay sent.
Create a batch
Rows carry real people for each of the template's roles, in signing
order, and optional prefills keyed by field name (or by index when a
field has no name). A row with a recipient count that does not match
the template, a malformed email, an unknown prefill key, a missing
prefill for a read-only field, or an authentication level other than
link or otp is invalid.
curl -X POST https://app-staging.signenvoy.com/v1/bulk-sends \
-H "Authorization: Bearer $SENV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template_id": "tpl_7Q2mK9...",
"source_filename": "renewals-2026.csv",
"rows": [
{"recipients": [{"name": "Ada Lovelace", "email": "ada@example.com"}],
"form_values": {"po_number": "PO-1041"}},
{"recipients": [{"name": "Grace Hopper", "email": "grace@example.com"}],
"form_values": {"po_number": "PO-1042"}}
]
}'
HTTP/1.1 201 Created
{
"id": "5b1f1c3e-9f3a-4a6e-9a7e-2c1d4e5f6a7b",
"template_id": "tpl_7Q2mK9...",
"mode": "live",
"source_filename": "renewals-2026.csv",
"row_count": 2,
"status": "ready",
"sent_count": 0, "failed_count": 0, "canceled_count": 0,
"review_flagged": false,
"created_at": "2026-08-23T12:00:00+00:00",
"dispatched_at": null, "finished_at": null,
"rows": null
}
The creation response omits row detail. Use GET /v1/bulk-sends/{id} to retrieve populated rows.
Batch ids are plain UUIDs; only envelopes and templates carry the
doc_ and tpl_ prefixes. A batch takes at most
5,000 rows; more answers
bulk-too-many-rows.
When a row is invalid
The whole request is refused. The batch is stored with status
invalid so you can read it back, but it cannot be
dispatched; fix the rows and create a new batch.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "/problems/bulk-rows-invalid",
"title": "Some rows are invalid",
"status": 422,
"detail": "2 of 120 row(s) failed validation. Nothing was sent; fix the rows and upload again.",
"docs_url": "https://staging.signenvoy-docs-b4g.pages.dev/errors/bulk-rows-invalid",
"bulk_send_id": "5b1f1c3e-9f3a-4a6e-9a7e-2c1d4e5f6a7b",
"rows": [
{"row_no": 4, "errors": ["recipients[0].email: not a valid email address"]},
{"row_no": 5, "errors": ["form_values: unknown field key(s) 'po_numbre'"]}
]
}
See bulk-rows-invalid
for the full list of reasons.
The CSV column rule
The dashboard's bulk flow and senv bulk create take a CSV and turn it into the rows above. Both
read the same columns, so a file prepared for one works in the other.
recipient1_name,recipient1_email,recipient2_name,recipient2_email, and so on: one pair per role of the template, numbered by signing position starting at 1.- One column per field name for prefills, for example
po_number. A field without a name is addressed by its index, for example3. - UTF-8 encoding; the header row is required; every row needs every recipient column filled.
recipient1_name,recipient1_email,recipient2_name,recipient2_email,po_number
Ada Lovelace,ada@example.com,Miguel Ortega,m.ortega@example.com,PO-1041
Grace Hopper,grace@example.com,Miguel Ortega,m.ortega@example.com,PO-1042
That file becomes two rows, each with two recipients and
form_values: {"po_number": "..."}. Unknown columns are
reported per row rather than ignored, because a silently dropped column
is how a prefill goes missing on 400 envelopes at once.
Dispatch and what it costs
A batch is never refused for volume: envelopes are priced, not capped, so a batch that is larger than usual costs more. Each row's envelope is billed only when everyone on it has signed, like any other envelope. The review screen states that cost before the button.
A batch of any size does cross the fair-use review threshold if it lands a month's volume in a day. That flags the workspace for review; it is not a limit and it never stops the send.
A row that never produces a sent envelope (one that fails at the engine,
or a queued row of a canceled batch) is never billed: nothing is billed
until an envelope completes. The one limit is a workspace whose
subscription is ending this month: each row is checked as it sends, and
rows past what its minimum still covers fail with
subscription-ending-limit.
Dispatching a batch that is not ready answers
bulk-not-ready (409)
with the batch's current status.
Watch progress
curl https://app-staging.signenvoy.com/v1/bulk-sends/5b1f1c3e-9f3a-4a6e-9a7e-2c1d4e5f6a7b?status=failed \
-H "Authorization: Bearer $SENV_API_KEY"
?status= filters rows (queued, sending,
sent, failed, canceled). A row's
document_id is a normal envelope: it has its own page,
timeline, downloads, and events. Its delivery_state follows
the envelope's events without polling each envelope yourself. The batch's
own status moves ready to dispatching to
done (or failed when every row failed, or
canceled).
Batches in the envelopes list
GET /v1/documents leaves batch members out by default, so a
400-row batch does not bury everything else. Pass
?batch=<id> to list exactly that batch's envelopes. The
dashboard shows one row per batch the same way.
From the command line
senv bulk create tpl_7Q2mK9... renewals-2026.csv --dispatch
senv bulk status 5b1f1c3e-9f3a-4a6e-9a7e-2c1d4e5f6a7b --failed
senv bulk cancel 5b1f1c3e-9f3a-4a6e-9a7e-2c1d4e5f6a7b
create parses the CSV locally with the column rule above and
renders invalid rows as a table; --dispatch asks for
confirmation before sending unless --yes is given.
Test mode
A batch created with a test key creates test envelopes: mail is sent only to your own workspace's members, and everyone's mail is captured under Settings, Test mode. Sealed PDFs carry the watermark, and nothing is counted, billed, or included in the daily review threshold.