Bulk composition, signing and flow
Compose many documents from one template, or send many envelopes for signature, with a single request. A batch is accepted whole or refused whole, runs in the background, and you read the result of every item back as it finishes. Only what was delivered is charged.
Every document is rendered by exactly the code that renders a single
POST /api/v1/compose/documents/generate,
so a document in a batch is the same document you would have got one at a time,
at the same price. A signing batch is the same: every envelope is
made and sent by the code behind a single
POST /api/v1/sign/envelopes?send=true, with the
same validation, the same defaults and gates, the same refusals and the same error
codes. A batch is never a way round a rule: whatever a single call
would refuse, a batch refuses with the same code, and what a single call costs, an
item costs.
The calls
| Call | Scope | What it does |
|---|---|---|
POST /api/v1/bulk/compose | bulk:submit and documents:compose | Submit a batch, inline or by fileId. Answers 202 with the job. |
POST /api/v1/bulk/sign | bulk:submit and signatures:ses | Submit a batch of envelopes to create and send (signing in bulk). Answers 202 with the job. |
POST /api/v1/bulk/flow | bulk:submit and flow:* | Submit a batch of flows: each item composes a document and puts it out for signature (Document Fly in bulk). Answers 202 with the job. |
POST /api/v1/bulk/files | bulk:submit | Upload a JSONL or CSV batch, get a fileId. Answers 201. |
GET /api/v1/bulk/jobs/{id} | bulk:read | The job: state, counts, credits and, once it is over, the reconciliation. Poll this. |
GET /api/v1/bulk/jobs/{id}/items | bulk:read | The result of each document, in order, in pages. |
GET /api/v1/bulk/jobs | bulk:read | Your organisation’s jobs, newest first (status, limit up to 100). |
GET /api/v1/bulk/jobs/{id}/results | bulk:read and documents:download | The result bundle: a ZIP of every produced PDF with a manifest and a results.csv. |
POST /api/v1/bulk/jobs/{id}/cancel | bulk:submit | Stop a job. |
POST /api/v1/bulk/jobs/{id}/retry | bulk:submit | Put the retryable failed items back, as new attempts in the same job. |
The documents themselves are fetched with
GET /api/v1/compose/documents/{id} (scope
documents:download); each item carries its documentId
and downloadUrl. Composition batches need the Composer Fly switch of your
organisation to be on and signing batches need the Signer Fly switch, like the
single calls; the job and the upload are shared, so a Signer Fly customer without
Composer Fly can bulk-sign and read its own jobs.
Each bulk operation is also a purchase of its own. On top of the product it
belongs to (Composer Fly for /bulk/compose, Signer Fly for /bulk/sign, Document
Fly for /bulk/flow), your organisation needs the matching bulk option, which we switch on
for you. Without it the call is answered 403 FEATURE_NOT_ENABLED with
details.feature set to bulkCompose, bulkSign or bulkFlow; nothing is
reserved and nothing is charged. GET /api/v1/usage lists what your organisation holds under
organisation.entitlements.
Submitting a batch
POST /api/v1/bulk/compose
Authorization: Bearer lpdc_...
Idempotency-Key: month-end-2026-09
{
"settings": { "templateId": "invoice-standard-8bc99b" },
"items": [
{ "externalId": "inv-1001", "data": { "customer": { "name": "Acme Srl" }, "total": "1.240,00" } },
{ "externalId": "inv-1002", "data": { "customer": { "name": "Beta SpA" }, "total": "380,00" } }
],
"options": { "maxItems": 500 }
}
settingsis the same object a single call takes and applies to every item. Each item’sdatais what you would send asdatato a single call.externalIdis your own reference for the item. It is optional, and unique within the batch. It comes back on the item so you can match results to your records.- Send either
itemsorfileId, never both and never neither (BULK_INPUT_INVALID). options.maxItemscan lower the item limit for this request, never raise it. A batch over the limit is refused whole, never trimmed.- Unknown keys anywhere in the body outside an item’s
dataare refused, as everywhere in this API.
The answer is 202 Accepted, with a Location header
pointing at the job. The settings come back settled, and the job is in
output.bulk:
202 Accepted
Location: /api/v1/bulk/jobs/6d1e...
{
"settings": { "templateId": "invoice-standard-8bc99b", "...": "as settled" },
"output": {
"status": "pending",
"bulk": {
"jobId": "6d1e...",
"operation": "compose",
"status": "queued",
"isTest": false,
"templateId": "invoice-standard-8bc99b",
"templateVersion": 3,
"totalItems": 2,
"counts": { "queued": 2, "processing": 0, "succeeded": 0, "failed": 0, "cancelled": 0 },
"creditsReserved": 10,
"creditsConsumed": 0,
"creditsHeld": 10,
"creditsReleased": 0,
"links": { "self": "/api/v1/bulk/jobs/6d1e...", "items": "/api/v1/bulk/jobs/6d1e.../items" },
"...": "createdAt, startedAt, completedAt, deadlineAt, resultsExpireAt, error"
},
"credits": { "used": 0, "left": 1237 },
"warnings": [],
"error": null
}
}
output.bulk is the fourth key of output and appears only on
answers under /api/v1/bulk. The three product sections
(compose, sign, flow) keep their place and
are null there.
What a batch cannot ask for
A batch is refused whole with FEATURE_NOT_AVAILABLE_IN_BULK (1010),
before anything is reserved, if its settings ask for a qualified seal
(qeSeal), a qualified timestamp (qualifiedTimestamp), a
document password, a responseFormat other than
pdf, or a filename. details.fields names each. A seal or a
timestamp is charged after the fact and could overdraw what was reserved; a
password cannot be kept in a stored job; an item has no response of its own and
one filename would name many files. Send those documents one at a time.
Uploading a large batch
Above the inline limit, upload the items as a JSONL file (one JSON object per
line, {"externalId": "...", "data": {...}}) — or as a
CSV — and submit with fileId:
POST /api/v1/bulk/files
Authorization: Bearer lpdc_...
Idempotency-Key: month-end-2026-09-file
Content-Type: application/x-ndjson
{"externalId":"inv-1001","data":{"customer":{"name":"Acme Srl"}}}
{"externalId":"inv-1002","data":{"customer":{"name":"Beta SpA"}}}
201 Created
{ "output": { "bulk": { "file": {
"fileId": "f41a...", "items": 2, "bytes": 131,
"sha256": "...", "expiresAt": "2026-10-03T09:14:02Z" } }, "...": "..." } }
POST /api/v1/bulk/compose
{ "settings": { "templateId": "invoice-standard-8bc99b" }, "fileId": "f41a..." }
- Send the lines as the raw request body. Nothing is decompressed: a
Content-Encodingis refused. - The whole file is validated before anything is stored, and a JSONL is refused
whole if any line is wrong (
BULK_INPUT_INVALID, with up to twentydetails.problems, each naming itsline). A file over its byte limit isJOB_TOO_LARGE(1812). - A file is used by one job. An upload that is not used before
expiresAtis deleted; using it afterwards, or twice, isBULK_FILE_NOT_FOUND(1503).
CSV input
A spreadsheet export is a batch too. Upload it to the same
POST /api/v1/bulk/files with Content-Type: text/csv (or
?format=csv) and submit the fileId as for a JSONL. It is read
into exactly the same items, so everything after the reader — the limits, the
reservation, the rendering, the charging — is the same.
| Header | Becomes |
|---|---|
externalId | The item’s own reference (not a payload field). |
customer | data.customer, text. |
customer.name | data.customer.name: a dot nests. |
lines.0.sku, lines.1.sku | A list: data.lines[0].sku, data.lines[1].sku. Indexes run 0, 1, 2… with no gap. |
total:number, paid:bool, lines:json | A typed cell. Without a suffix a cell is text. :number is digits with an optional minus and a dot for decimals; :bool is true/false, yes/no, 1/0; :json is a JSON value. |
- The first row is the header. An empty cell is absent (the field is not sent), never an empty string, and a list element whose cells are all empty is dropped.
- Delimiter: comma, semicolon, tab or pipe, read from the header
line, or given with
?delimiter=comma|semicolon|tab|pipe. It is read once, at upload, and stored with the file. - Encoding: UTF-8 (a byte-order mark is fine), or
?encoding=iso-8859-1/windows-1252— what an Excel in Italy still writes. It is never guessed: a file that is not valid in its encoding is refused with the byte it failed on. - A bad file is refused whole with
BULK_INPUT_INVALIDand nothing is stored: empty, NUL bytes, a header that does not parse (a duplicate or unnamed column, an unknown type suffix, a list with a gap), a malformed quote, more rows than the file limit, or no usable row at all. - A bad row does not refuse the others. It becomes a failed item
with
error.codeBULK_INPUT_INVALID(1107) and itsrowandline: a cell that is not the number its header says, the wrong number of cells, a duplicate or invalidexternalId, a row over the per-item size. It is never reserved and never charged, and it is not retryable (fix the row and send it in a new batch). - Formulas: what you send is never evaluated or altered; a cell that
starts with
=,+,-or@stays text, because it goes into a PDF. Every CSV we write (results.csv) prefixes such text with an apostrophe so a spreadsheet cannot run it. - The columns are bounded (200, nested at most 6 deep, list index at most 99) and nothing is decompressed.
Signing in bulk
POST /api/v1/bulk/sign sends one envelope per item. It is
POST /bulk/compose with a different item, so everything on this page — the
202 and the job, admission whole or not at all, one reservation per item, the
worker, the states, Idempotency-Key, the limits, CSV, the bundle, the retry and the
webhook — applies as it is. An item is made and sent by the same service a single
POST /sign/envelopes?send=true uses.
POST /api/v1/bulk/sign
Authorization: Bearer lpdc_...
Idempotency-Key: nda-2026-q4
{
"settings": {
"documents": [ { "fileId": "9c2e..." } ],
"subject": "NDA 2026"
},
"items": [
{ "externalId": "C-001",
"signers": { "A": { "email": "anna@example.com", "fullName": "Anna Rossi" } } },
{ "externalId": "C-002", "subject": "NDA 2026 - special terms",
"documents": [ { "fileId": "b17a..." } ],
"signers": { "A": { "email": "bruno@example.com", "fullName": "Bruno Neri" } } }
],
"callbackUrl": { "onSuccess": "https://example.com/hooks/bulk" }
}
settingsis the shared part of a single envelope’ssettings(isTest,level,language,subject,documents, …). An item’s owndocuments/documentId/subject/messageare laid over it, and the result is validated as the very request a single call takes. The same contract for every signer is thereforesettings.documentsonce andsignersper item.- Sources are a composed
documentIdor a PDF you uploaded once (POST /api/v1/sign/files, thenfileId). An inlinefileBase64is refused in a batch: upload it once. - A CSV works too:
externalId,signers.A.email,signers.A.fullNamewith the shared settings in the request. A row with an invalid address is a failed, uncharged item. - One signature is reserved per item at admission and charged when that envelope is
sent; an item that fails is refunded. An
isTestbatch spends one test item per envelope and never credit. settings.callbackUrlis the callback of each envelope; the top-levelcallbackUrlis the completion webhook of the batch.- The token needs
bulk:submitandsignatures:ses: a batch never gets round a scope.
Refused whole, with the codes a single call gives: FEATURE_NOT_AVAILABLE_IN_TEST_MODE
(1009) for a paid feature with isTest; SIGNATURE_LEVEL_UNAVAILABLE for AES or
QES; INVALID_REQUEST_BODY for a missing subject or a malformed signer;
QUOTA_EXCEEDED. A seal or timestamp — asked for, or required by your
organisation — is refused with FEATURE_NOT_AVAILABLE_IN_BULK (1010): it is
charged at completion and cannot be reserved up front. A source that cannot be used
(STORED_FILE_NOT_FOUND) fails its item, uncharged, with the single call’s code.
Each item says which envelope it made (envelopeId); follow it with
GET /api/v1/sign/envelopes/{id}.
Document Fly in bulk
POST /api/v1/bulk/flow runs one flow per item: the document is
composed from the template and put out for signature in the same step, exactly as a single
POST /api/v1/flow does it. Everything on this page applies: the 202 and the job, admission
whole or not at all, the worker, the states, Idempotency-Key, the limits, CSV, the bundle, the
retry and the webhook.
POST /api/v1/bulk/flow
Authorization: Bearer lpdc_...
Idempotency-Key: contracts-2026-q4
{
"settings": { "templateId": "tpl_..." },
"signature": { "subject": "Service agreement" },
"items": [
{ "externalId": "C-001",
"data": { "customer": "Acme S.r.l." },
"signers": { "A": { "email": "anna@example.com", "fullName": "Anna Rossi",
"fields": [ { "page": 1, "x": 0.1, "y": 0.8, "width": 0.3, "height": 0.08 } ] } } }
]
}
settingsis the composition half of a single flow’ssettingsand applies to every item;signatureis the shared part of its signature block (everything butsigners). An item brings itsdataand itssigners, and may lay its ownsubject,messageanddocuments(annexes) over the shared block.- Each item reserves two things at admission, a document and a signature, as a single flow
does, and both are charged when the item is delivered. An item that fails is refunded; a flow
whose document was composed before its envelope was refused has charged that document once (the
job’s
creditsConsumedcounts it, through any number of retries), and a retry does not charge it again. AnisTestbatch spends one test item per flow and never credit. - The token needs
bulk:submitandflow:*.
Refused whole, with the codes a single flow gives: FEATURE_NOT_AVAILABLE_IN_TEST_MODE for a paid
feature with isTest; SIGNATURE_LEVEL_UNAVAILABLE for AES or QES;
DATALESS_NOT_AVAILABLE; QUOTA_EXCEEDED; FEATURE_NOT_ENABLED for an SMS code or
a signature level your organisation has not bought. A seal or timestamp is refused with
FEATURE_NOT_AVAILABLE_IN_BULK. Each item says which document and which envelope it made.
The job
A job moves queued → running → one of
succeeded, partially_succeeded, failed or
cancelled. Poll on output.status: it is pending while the job can still
change, then succeeded only when every item delivered, and
failed for anything else (a partial result, a cancelled job, a failed
one), so read output.bulk.status and the items to tell those apart. Do not
infer the state from the counts.
| Final status | When |
|---|---|
succeeded | Every item delivered a document. |
partially_succeeded | Some delivered, some failed. The job itself is not an error: read the items. |
failed | No item delivered a document. |
cancelled | You asked for it to stop; this wins over the other three. |
Each item is queued, processing, succeeded,
failed or cancelled. counts has one number for each,
totalItems is what you submitted, and pending is
queued plus processing.
Results are kept for the retention period shown in
resultsExpireAt (set once the job is over); afterwards
resultsExpired is true. Fetch what you need before then.
Reading the results
GET /api/v1/bulk/jobs/6d1e.../items?status=failed&limit=100&after=41
{ "output": { "bulk": {
"jobId": "6d1e...", "...": "the job, as above",
"items": [
{ "index": 57, "externalId": "inv-1058", "status": "failed",
"documentId": null, "downloadUrl": null, "creditsUsed": 0,
"error": { "code": "MISSING_TEMPLATE_FIELD", "number": 1401,
"message": "...", "retryable": false },
"warnings": [], "...": "attempts, pageCount, sizeBytes, sha256, durationMs, startedAt, finishedAt" }
],
"page": { "limit": 100, "after": 41, "status": "failed", "nextAfter": 98 } } } }
- Items come in
indexorder (the position in your batch, from 0).statusfilters by item status;limitis up to 500. - To read the next page, send
after= thenextAfteryou were given. WhennextAfterisnull, that was the last page. - A delivered item carries its
documentId,downloadUrl, page count, size and SHA-256. A failed one carries the stableerror.codeand number (see Outcomes and error codes), andretryablesays whether the same item is worth sending again. - The platform never retries an item for you. You ask for it, with
the retry call; an item that is not retryable, or whose payload
is not kept, you resubmit in a new batch (reuse its
externalId).
The result bundle
GET /api/v1/bulk/jobs/{id}/results returns one ZIP once the job is over:
documents/<index>.pdf for every produced file, manifest.json and
results.csv (per item: status, attempts, documentId / envelopeId, the
file’s SHA-256, page count, credits used, the error code and number, whether it is
retryable) and the job’s credits. A signing job’s bundle holds the manifest and
the CSV only.
- It needs
bulk:readanddocuments:download— it hands out the bytes the per-document download does. Another organisation’s job is a404. - It exists only once the job is over and while its results are kept; otherwise
BULK_RESULTS_UNAVAILABLE(1504) withdetails.reasonjob_not_finishedorresults_expired. A document your own retention has deleted is listed withfile: null. - It is built one document at a time into a temporary file and capped. A job whose
documents are over the cap is
JOB_TOO_LARGE; fetch it in ranges with?after=<index>&limit=<n>(the manifest’srange.nextAfteris what to send next). - Entry names are the item’s index, never what you wrote;
results.csvprefixes an apostrophe to any text that a spreadsheet could read as a formula. - Downloading it costs nothing and is audited. For one document for somebody who has
no key, mint a signed download link for its
documentId.
Retrying failed items
POST /api/v1/bulk/jobs/{id}/retry puts the retryable failed items of a
finished job back to work, as new attempts in the same job (attempts counts
them, retryCount counts the calls). Each item is admitted like a new one: its
own reservation, the same limits and the same worker; a retry that cannot be paid for is refused
whole and changes nothing. The answer is 202 with the job and
retry: {requeued, skippedCount, skipped[]}.
| An item is put back when… | otherwise it is skipped as |
|---|---|
its failure says retryable: true | not_retryable |
| its payload is still held (an organisation that composes without keeping data holds none) | payload_not_kept |
| it has run fewer than 3 times | attempts_exhausted |
| no paid provider request of it has an unknown outcome | outcome_unknown: never repeated, it waits for its reconciliation |
Nothing retryable is BULK_RETRY_NOT_POSSIBLE (1505) with details.reason
(nothing_retryable, job_not_finished, job_cancelled,
results_expired, payload_not_kept) and the skipped items. It is idempotent: a
second call while the first one’s items run finds the job unfinished and changes nothing, and
with an Idempotency-Key the first answer is replayed. It needs bulk:submit and the scope of the job’s operation (documents:compose, or
signatures:ses for a signing job; isTest for a test job). The job ends again,
and tells you again.
Completion webhook
Add "callbackUrl": {"onSuccess": "https://...", "onError": "https://..."} to a
compose, sign or flow batch and you are told when the job ends, instead of polling.
onSuccess is called when every item delivered, onError for every other end
(partially_succeeded, failed, cancelled). It is
the same delivery as a signature callback: https only, signed (X-Signer-Signature:
HMAC-SHA256 of the raw body under your organisation’s callback secret), never to a private
or non-public address, no redirects followed, retried on the same backoff (up to about two days), listed in
the webhook log and replayable there. Headers add
X-Signer-Bulk-Job. To authenticate at your receiver, name a
callback environment and send paths:
"callbackUrl": {"env": "prod", "onSuccess": "/hooks/bulk"}; an unknown
env is refused at submission, before anything is reserved.
{
"bulkJobId": "6d1e...", "operation": "compose", "status": "partially_succeeded",
"isTest": false, "totalItems": 100, "counts": { "succeeded": 97, "failed": 3, "...": "..." },
"creditsConsumed": 485, "creditsReleased": 15, "retryCount": 0,
"reconciliation": { "...": "as in the job" }, "links": { "self": "...", "items": "...", "results": "..." },
"event": { "id": "evt_...", "type": "bulk.partially_succeeded", "occurredAt": "...", "schema": 1 }
}
event.id is stable across retries: deduplicate on it. The body names the job and
the numbers and carries no item data; fetch the items, paginated. A job that is retried
and ends again sends a new event. The URL is never echoed back by the API.
Credits: credits.total and credits.block
Like every answer, a bulk answer carries output.credits as
{used, left}. For a job, used is the total of what its
items settled so far (the sum of the per-item creditsUsed; 0 for
isTest), and left is your balance read from the ledger
now — never your previous balance minus used, which is wrong while
the job holds a reservation.
Inside output.bulk the same money is broken down for the job:
| Field | Meaning |
|---|---|
creditsReserved | What was set aside for the batch at admission: one document’s price per item. |
creditsConsumed | What the delivered documents actually cost. |
creditsHeld | Still reserved for items not yet finished. 0 once the job is over. |
creditsReleased | Given back: failed or cancelled items. |
Once the job is over it also carries a reconciliation:
"reconciliation": {
"requested": 100, "succeeded": 97,
"failed": { "platform": 1, "customer": 1 },
"cancelled": 1,
"creditsReserved": 500, "creditsConsumed": 485, "creditsReleased": 15,
"chargedFailureClasses": []
}
creditsConsumed + creditsReleased = creditsReserved. The failures are
split into platform (ours: an internal error, a rendering fault, a job
deadline) and customer (the input) so you can see which is which;
neither is charged.
What is charged
A document is charged if, and only if, it was delivered. A bulk job costs exactly what the same documents sent one at a time would.
| Outcome | Charge |
|---|---|
| The batch is refused (malformed, over a limit, not enough credit, no capacity) | Nothing is reserved, nothing is charged. |
| An item fails on its data or its template | Not charged; its credit goes back. |
| An item fails for a platform reason | Not charged; its credit goes back. |
| An item is cancelled before it started, or is still waiting at the job deadline | Not charged; its credit goes back. |
| An item is delivered | Charged one document. |
An isTest item | Uses the test allowance, never credit. See below. |
The credit for the whole batch is reserved in one step before any document is
rendered, so two batches that together are more than your balance cannot both be
admitted: the second is refused with QUOTA_EXCEEDED (see
Outcomes and error codes) and nothing is left behind. It is
reserved per item, and each item settles on its own as it finishes.
Cancelling
POST /api/v1/bulk/jobs/{id}/cancel stops the job. Items that have not
started are cancelled and their credit is released at once; an item being
rendered right now finishes and is charged if it delivers. The answer is the
job (cancelRequested is true). Cancelling a job that is already
over changes nothing and answers with it. The call takes an
Idempotency-Key like the others.
Idempotency
Submitting, uploading a file, cancelling and retrying all take an
Idempotency-Key, and for a submit you should always send one. With it,
a retry of a request that timed out returns the same job (the answer
carries Idempotent-Replay: true) instead of admitting and charging a
second batch. The same key with a different body is
IDEMPOTENCY_KEY_REUSED. The rules are those of
every other call; use a key from your own domain, such as a
batch or period reference.
Testing: isTest
With "isTest": true in settings the whole batch runs in the
sandbox: no credit is spent and the documents are marked as tests. The batch draws
one test-allowance item per document from the same pool as single test
calls, all or nothing, and is capped at a small number of items per batch (the
current default is 5). A test batch over the cap is
BULK_ITEM_LIMIT_EXCEEDED. Test items are scheduled behind production work.
Dataless organisations
If your organisation composes dataless, a bulk
batch still keeps its documents for your normal retention — a batch answers with no
bytes, so you fetch them afterwards — and the answer carries the warning
DOCUMENT_STORED_FOR_BULK (2203). The data you sent is not recorded; each
item’s payload is wiped when the item finishes.
Limits
These are the current defaults. They are working values and may change; the ones that apply to your organisation can be lower, never higher. A request is refused whole, with nothing reserved, when it is over any of them.
| Limit | Default | Refused with |
|---|---|---|
| Items in an inline batch | 500 | BULK_ITEM_LIMIT_EXCEEDED |
| Items in an uploaded file | 2,000 | BULK_ITEM_LIMIT_EXCEEDED |
| Size of an inline body / of an uploaded file | 10 MiB / 25 MB (no request may be larger) | JOB_TOO_LARGE / PAYLOAD_TOO_LARGE |
Items in an isTest batch | 5 | BULK_ITEM_LIMIT_EXCEEDED |
| Jobs running at once | 2 | BULK_CONCURRENCY_LIMIT_REACHED |
| Items waiting across your jobs | 20,000 | BULK_CONCURRENCY_LIMIT_REACHED |
| Submissions per minute | 6 | BULK_CONCURRENCY_LIMIT_REACHED |
| Uploaded files not yet used | 5 | BULK_CONCURRENCY_LIMIT_REACHED |
The effective item limit is the lowest of what you ask in
options.maxItems, your organisation’s own limit and the platform’s;
details.requestedItems and details.allowedItems say by how much
you were over. BULK_CONCURRENCY_LIMIT_REACHED is retryable and carries
Retry-After: details.what says which limit it was. A job still
running six hours after it was admitted times out its remaining items
(BULK_JOB_TIMED_OUT, not charged). Items are rendered one after the other
and share capacity with everybody else’s requests, so a large batch is a matter of
minutes, not seconds; the platform pauses a job rather than failing it when it is
busy. If bulk is switched off for the whole service, a new batch is refused with
SERVICE_PAUSED.
Scopes
A compose batch needs bulk:submit and documents:compose; a
signing batch needs bulk:submit and signatures:ses; a flow batch needs
bulk:submit and flow:*. The first
authorises running a batch, the second authorises the operation under it, and a refusal
(TOKEN_SCOPE_NOT_ALLOWED, 403) names the first scope missing in details.missingScopes.
The bulk scope group is: bulk:submit to submit, upload, cancel and retry, bulk:read to read jobs,
items and the result bundle (which also needs documents:download), or bulk:*
for both. Ticking bulk:submit does not tick the operation’s scope for you: tick both on the
API keys page. Tokens issued before bulk existed do not
get these scopes; issue a new token with them ticked.
Another organisation’s job or file answers 404, exactly like one that does
not exist.
In the console
A batch is a list of single calls, and the console shows it that way. Every
document a compose batch made is a row of Documents and every
envelope a signing batch sent is a row of Signatures, marked
Batch with a link to its batch; the filter above each list shows all rows,
only single calls or only batches. A batch started with an API key is there without
anything to set up (Documents, then Batches, at
/tenant/batches). The results of a batch are kept for the retention
period; after that its documents are still in Documents, but no longer marked.
The batch page shows its status, its type, how many items are done, failed,
cancelled and pending, a progress bar, and the credits reserved, used and returned
(the same figures as output.credits; a test batch says so and shows no
credit). Every item links to its own document or signature request, and a failed one
shows its stable error code, its number and its message. Anyone who can see
Documents can read the page and download the results ZIP. A coordinator can also
retry the failed items (only the retryable ones, as the API does),
cancel a running batch (what had not started stops and its credit
returns) and send the completion webhook again; each of the three is
recorded in the audit trail under the person’s name.
Not built yet
- No seal, timestamp or password on a bulk document or a bulk envelope: they are charged or kept after the fact and cannot be reserved up front (send those one at a time).
- No AES or QES, in a batch or singly (
SIGNATURE_LEVEL_UNAVAILABLE). - A signed download link for the whole bundle: the ZIP is fetched with your key; a link is minted per document.