SignEnvoy docs

Bulk send

A bulk send takes a template and a list of rows, one per document, and sends each row as a normal signature request. Every row is validated before anything moves, the monthly allowance is reserved for the whole batch at dispatch, rows go out at a steady rate, and each row reports the same delivery states your webhook endpoints receive.

What it is for

Transactional documents 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 documents 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 allowance is reserved for every row as one block. If fewer documents remain than rows, the whole batch is refused with 402 and nothing is sent.
  3. Rows send at 10 rows every 2 seconds per workspace. Each row becomes a document 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 their allowance refunded; documents 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://signenvoystaging.fly.dev/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": [
    {"row_no": 1, "status": "queued", "document_id": null, "delivery_state": null,
     "error": null, "sent_at": null, "recipients": [{"name": "Ada Lovelace", "email": "ada@example.com"}]},
    {"row_no": 2, "status": "queued", "document_id": null, "delivery_state": null,
     "error": null, "sent_at": null, "recipients": [{"name": "Grace Hopper", "email": "grace@example.com"}]}
  ]
}

Batch ids are plain UUIDs; only documents 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": "https://docs.signenvoy.com/errors/bulk-rows-invalid",
  "title": "Some bulk rows are invalid",
  "status": 422,
  "detail": "2 of 120 rows are invalid; nothing was sent.",
  "docs_url": "https://docs.signenvoy.com/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 (from senv 0.2.0) 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,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 documents at once.

Dispatch and the allowance

Dispatch reserves one document of your monthly allowance per row, as a block, before the first row sends. If fewer documents remain than the batch has rows, the batch is refused whole with envelope-allowance-exhausted (402) and nothing is sent. This holds even when auto-advance is on: batches never move a workspace to the next band on their own. Upgrade first, then dispatch.

Rows that never produce a sent document are refunded: a row that fails at the engine, and every queued row of a canceled batch. A batch that is already dispatching when the cap is reached completes; in-flight sends are never cut off.

Dispatching a batch that is not ready answers bulk-not-ready (409) with the batch's current status.

Watch progress

curl https://signenvoystaging.fly.dev/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 document: it has its own page, timeline, downloads, and events. Its delivery_state follows the document's events without polling each document yourself. The batch's own status moves ready to dispatching to done (or failed when every row failed, or canceled).

Batches in the documents 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 documents. The dashboard shows one row per batch the same way.

From the command line

From senv 0.2.0:

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. Until then, the API calls above are the path.

Test mode

A batch created with a test key creates test documents: no mail is sent (the captured emails appear under Settings, Test mode), sealed PDFs carry the watermark, and nothing counts toward the allowance or the daily review threshold.