Bulk send
Bulk send creates and sends many envelopes from one template in a single call.
You submit a batch of rows — each row is one recipient set plus its own variable
values — and Permissio processes them asynchronously. Every row becomes a
fully-auditable envelope, created and sent exactly as a single
POST /v1/envelopes with immediate send would be.
- Up to 500 rows per batch.
- Asynchronous — the create call returns immediately with a batch id; you poll for per-envelope results.
- Idempotent — a retried submit with the same
Idempotency-Keyreturns the same batch instead of creating a second one. - Quota-aware — each row consumes one envelope from your plan's allotment.
Rows send until the allotment is reached; any remaining rows are reported as
failedwith codeenvelope_quota_exceeded(the batch is never rejected wholesale).
1. Create a batch
Each entry in rows mirrors a single envelope create: signers (matching the
template's roles) plus optional variables, metadata, and title.
curl https://api.permissio.us/v1/bulk-sends \
-H "Authorization: Bearer $PERMISSIO_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9b7e8f1c-3a8a-4d8b-8c34-2c4d4f8d1f72" \
-d '{
"template_id": "tpl_1a2b3c4d5e6f7a8b",
"expires_in_days": 14,
"rows": [
{
"signers": [
{ "role_name": "Signer", "email": "alice@acme.com", "name": "Alice Nguyen" }
],
"variables": { "plan": "Growth" }
},
{
"signers": [
{ "role_name": "Signer", "email": "bob@acme.com", "name": "Bob Smith" }
],
"variables": { "plan": "Scale" }
}
]
}'
The response is 202 Accepted with the batch id and status: "queued":
{
"id": "bsb_9a8b7c6d5e4f3a2b",
"status": "queued",
"template_id": "tpl_1a2b3c4d5e6f7a8b",
"total": 2,
"created_at": "2026-07-28T14:20:00.000Z"
}
2. Poll for results
Fetch the batch to watch it progress and read each envelope's outcome. Use
limit and offset to page through large batches (default 100 rows per page).
curl https://api.permissio.us/v1/bulk-sends/bsb_9a8b7c6d5e4f3a2b \
-H "Authorization: Bearer $PERMISSIO_KEY"
{
"id": "bsb_9a8b7c6d5e4f3a2b",
"status": "completed",
"template_id": "tpl_1a2b3c4d5e6f7a8b",
"total": 2,
"succeeded": 2,
"failed": 0,
"rows": [
{ "index": 0, "status": "sent", "envelope_id": "env_7f8a9b0c1d2e3f4a", "error": null },
{ "index": 1, "status": "sent", "envelope_id": "env_1b2c3d4e5f6a7b8c", "error": null }
]
}
Statuses
Batch status | Meaning |
|---|---|
queued | Accepted, not yet started |
processing | Rows are being sent |
completed | Every row sent |
completed_with_errors | Some rows sent, some failed |
failed | No rows sent |
Row status | Meaning |
|---|---|
queued | Not yet processed |
sent | Envelope created + sent; envelope_id is populated |
failed | See error.code / error.message (e.g. envelope_quota_exceeded, validation_error) |
3. With the SDKs
import { Permissio } from "permissio-sdk";
const client = new Permissio({ apiKey: process.env.PERMISSIO_KEY! });
const batch = await client.bulkSends.create({
template_id: "tpl_…",
rows: [
{ signers: [{ role_name: "Signer", email: "alice@acme.com", name: "Alice" }], variables: { plan: "Growth" } },
{ signers: [{ role_name: "Signer", email: "bob@acme.com", name: "Bob" }], variables: { plan: "Scale" } },
],
});
let result = await client.bulkSends.get(batch.id);
while (result.status === "queued" || result.status === "processing") {
await new Promise((r) => setTimeout(r, 2000));
result = await client.bulkSends.get(batch.id);
}
console.log(`${result.succeeded}/${result.total} sent`);
import time
from permissio import Permissio
client = Permissio(api_key=os.environ["PERMISSIO_KEY"])
batch = client.bulk_sends.create(
template_id="tpl_…",
rows=[
{"signers": [{"role_name": "Signer", "email": "alice@acme.com", "name": "Alice"}], "variables": {"plan": "Growth"}},
{"signers": [{"role_name": "Signer", "email": "bob@acme.com", "name": "Bob"}], "variables": {"plan": "Scale"}},
],
)
result = client.bulk_sends.get(batch["id"])
while result["status"] in ("queued", "processing"):
time.sleep(2)
result = client.bulk_sends.get(batch["id"])
print(f'{result["succeeded"]}/{result["total"]} sent')
Webhooks
Each successfully-sent row emits the same envelope.created and
envelope.sent events a single-envelope send would — so if you already consume
those webhooks, bulk-sent envelopes flow through your existing pipeline with no
changes. Subscribe to envelope.completed to know when each recipient finishes.
Notes
- Idempotency is required on
POST /v1/bulk-sends. Reuse the same key to safely retry a submit; you'll get the original batch back. - Rate limit: batch creation is capped at 10 requests/minute per key (the per-row sends inside the batch are not subject to the partner API limiter).
- Rows are processed independently — one row's validation error never blocks the others.