Skip to main content

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-Key returns 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 failed with code envelope_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.

POST /v1/bulk-sends
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).

GET /v1/bulk-sends/{id}
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 statusMeaning
queuedAccepted, not yet started
processingRows are being sent
completedEvery row sent
completed_with_errorsSome rows sent, some failed
failedNo rows sent
Row statusMeaning
queuedNot yet processed
sentEnvelope created + sent; envelope_id is populated
failedSee error.code / error.message (e.g. envelope_quota_exceeded, validation_error)

3. With the SDKs

Node
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`);
Python
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.