DoConure

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

CallScopeWhat it does
POST /api/v1/bulk/composebulk:submit and documents:composeSubmit a batch, inline or by fileId. Answers 202 with the job.
POST /api/v1/bulk/signbulk:submit and signatures:sesSubmit a batch of envelopes to create and send (signing in bulk). Answers 202 with the job.
POST /api/v1/bulk/flowbulk: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/filesbulk:submitUpload a JSONL or CSV batch, get a fileId. Answers 201.
GET /api/v1/bulk/jobs/{id}bulk:readThe job: state, counts, credits and, once it is over, the reconciliation. Poll this.
GET /api/v1/bulk/jobs/{id}/itemsbulk:readThe result of each document, in order, in pages.
GET /api/v1/bulk/jobsbulk:readYour organisation’s jobs, newest first (status, limit up to 100).
GET /api/v1/bulk/jobs/{id}/resultsbulk:read and documents:downloadThe result bundle: a ZIP of every produced PDF with a manifest and a results.csv.
POST /api/v1/bulk/jobs/{id}/cancelbulk:submitStop a job.
POST /api/v1/bulk/jobs/{id}/retrybulk:submitPut 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 }
}
  • settings is the same object a single call takes and applies to every item. Each item’s data is what you would send as data to a single call.
  • externalId is 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 items or fileId, never both and never neither (BULK_INPUT_INVALID).
  • options.maxItems can 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 data are 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-Encoding is 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 twenty details.problems, each naming its line). A file over its byte limit is JOB_TOO_LARGE (1812).
  • A file is used by one job. An upload that is not used before expiresAt is deleted; using it afterwards, or twice, is BULK_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.

HeaderBecomes
externalIdThe item’s own reference (not a payload field).
customerdata.customer, text.
customer.namedata.customer.name: a dot nests.
lines.0.sku, lines.1.skuA list: data.lines[0].sku, data.lines[1].sku. Indexes run 0, 1, 2… with no gap.
total:number, paid:bool, lines:jsonA 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_INVALID and 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.code BULK_INPUT_INVALID (1107) and its row and line: a cell that is not the number its header says, the wrong number of cells, a duplicate or invalid externalId, 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" }
}
  • settings is the shared part of a single envelope’s settings (isTest, level, language, subject, documents, …). An item’s own documents / documentId / subject / message are laid over it, and the result is validated as the very request a single call takes. The same contract for every signer is therefore settings.documents once and signers per item.
  • Sources are a composed documentId or a PDF you uploaded once (POST /api/v1/sign/files, then fileId). An inline fileBase64 is refused in a batch: upload it once.
  • A CSV works too: externalId,signers.A.email,signers.A.fullName with 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 isTest batch spends one test item per envelope and never credit.
  • settings.callbackUrl is the callback of each envelope; the top-level callbackUrl is the completion webhook of the batch.
  • The token needs bulk:submit and signatures: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 } ] } } }
  ]
}
  • settings is the composition half of a single flow’s settings and applies to every item; signature is the shared part of its signature block (everything but signers). An item brings its data and its signers, and may lay its own subject, message and documents (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 creditsConsumed counts it, through any number of retries), and a retry does not charge it again. An isTest batch spends one test item per flow and never credit.
  • The token needs bulk:submit and flow:*.

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 statusWhen
succeededEvery item delivered a document.
partially_succeededSome delivered, some failed. The job itself is not an error: read the items.
failedNo item delivered a document.
cancelledYou 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 index order (the position in your batch, from 0). status filters by item status; limit is up to 500.
  • To read the next page, send after = the nextAfter you were given. When nextAfter is null, that was the last page.
  • A delivered item carries its documentId, downloadUrl, page count, size and SHA-256. A failed one carries the stable error.code and number (see Outcomes and error codes), and retryable says 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:read and documents:download — it hands out the bytes the per-document download does. Another organisation’s job is a 404.
  • It exists only once the job is over and while its results are kept; otherwise BULK_RESULTS_UNAVAILABLE (1504) with details.reason job_not_finished or results_expired. A document your own retention has deleted is listed with file: 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’s range.nextAfter is what to send next).
  • Entry names are the item’s index, never what you wrote; results.csv prefixes 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: truenot_retryable
its payload is still held (an organisation that composes without keeping data holds none)payload_not_kept
it has run fewer than 3 timesattempts_exhausted
no paid provider request of it has an unknown outcomeoutcome_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:

FieldMeaning
creditsReservedWhat was set aside for the batch at admission: one document’s price per item.
creditsConsumedWhat the delivered documents actually cost.
creditsHeldStill reserved for items not yet finished. 0 once the job is over.
creditsReleasedGiven 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.

OutcomeCharge
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 templateNot charged; its credit goes back.
An item fails for a platform reasonNot charged; its credit goes back.
An item is cancelled before it started, or is still waiting at the job deadlineNot charged; its credit goes back.
An item is deliveredCharged one document.
An isTest itemUses 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.

LimitDefaultRefused with
Items in an inline batch500BULK_ITEM_LIMIT_EXCEEDED
Items in an uploaded file2,000BULK_ITEM_LIMIT_EXCEEDED
Size of an inline body / of an uploaded file10 MiB / 25 MB (no request may be larger)JOB_TOO_LARGE / PAYLOAD_TOO_LARGE
Items in an isTest batch5BULK_ITEM_LIMIT_EXCEEDED
Jobs running at once2BULK_CONCURRENCY_LIMIT_REACHED
Items waiting across your jobs20,000BULK_CONCURRENCY_LIMIT_REACHED
Submissions per minute6BULK_CONCURRENCY_LIMIT_REACHED
Uploaded files not yet used5BULK_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.