SignEnvoy docs
Sign in Start in the sandbox

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

  1. 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.
  2. 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 with subscription-ending-limit.
  3. 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.
  4. Track with GET /v1/bulk-sends/{id}: per-row status and delivery_state (sent, opened, signed, completed, bounced, rejected), updated from the same event stream webhooks use.
  5. 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 example 3.
  • 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.