Supercommerce API Docs
Admin API

Bulk Invoices — Admin

HTTP surface for an admin to batch-generate the invoices for many orders at once — across every vendor on them — merged into a single downloadable PDF, with async status polling and retry of failed orders.

HTTP surface behind Print Invoice: an admin selects whole orders and requests one merged invoice PDF. Each selected order is expanded into its invoiceable sub-orders — across every vendor on it — and a BullMQ worker generates (or reuses the cached) invoice for each, then concatenates them into a single file. A batch finishes ready as soon as at least one invoice succeeded; it only finishes failed when every one did. Failed orders can be retried without re-selecting the batch.

Source: api-modules/invoice/src/controllers/admin-bulk-invoices.controller.ts.

Admin batches are platform-scoped (bulk_invoice_job.vendor_id IS NULL) and visible to every admin who can read invoices, so ops can pick up a colleague's batch. A vendor's own batch id returns 404 here, and an admin batch id returns 404 on the vendor surface — neither side can probe the other for existence.

Note the input difference from the vendor surface: this endpoint takes orderIds (parent orders), where POST /vendor/bulk-invoices takes orderVendorIds (sub-orders). A vendor only ever sees their own sub-orders; an admin works in whole orders.


Conventions

Authentication

All endpoints require a Better-Auth admin session and a role granting the matching permission.

PermissionGrants
invoice:viewList batches, read one batch's detail, download the PDF.
invoice:bulkStart a batch and retry its failed orders.
invoice:regenerateStart a batch with regenerate: true (on top of invoice:bulk).

Response shape

  • Create / retry / status endpoints return the standard JSON envelope: { data, message, statusCode }.
  • List returns the standard paginated envelope: { data, message, statusCode, metadata: { total, limit, offset, hasMore } }.
  • Download (GET …/:id/download) streams raw application/pdf bytes with Content-Disposition: attachment — no JSON envelope.

Generation flow

POST /admin/bulk-invoices  { orderIds: [...] }


  each order expanded into its invoiceable sub-orders
  (cancelled ones skipped, unless every sub-order on that order is cancelled)


  bulk_invoice_job created (status: pending) — one item per sub-order


  background worker generates each invoice, then merges the
  successful PDFs into one file

       ├─ at least one invoice succeeded  ──►  status: "ready"   (fileName/fileSize set)
       └─ every invoice failed            ──►  status: "failed"

GET /admin/bulk-invoices           — poll the batch list (or a single id) for status
GET /admin/bulk-invoices/:id/download   — download the merged PDF once ready
POST /admin/bulk-invoices/:id/retry     — re-run only the failed orders in the background

Rendering happens in the worker role (APP_ROLE=worker), never in the API process. If no worker is consuming the bulk-invoice queue, a batch stays pending indefinitely — the admin UI flags a batch that has been queued for over five minutes rather than polling forever.


Endpoints

Start a batch

POST /admin/bulk-invoices · invoice:bulk

Request body:

{
  "orderIds": ["…", "…"], // 1–100 parent order ids
  "regenerate": false     // optional, defaults to false
}
FieldEffect
regenerate: false (default)A sub-order whose invoice PDF is already cached is reused as-is. Only sub-orders without a PDF are rendered.
regenerate: trueEvery selected sub-order is re-rendered from current data. Requires invoice:regenerate on top of invoice:bulk403 otherwise.

On a regenerate, each invoice keeps its existing invoice number and its issuance-time seller snapshot (a tax invoice's number and issuing entity are immutable once issued). Everything else is rebuilt from current data — tax rows, store branding, invoice presentation config — and the stored PDF is overwritten. regeneratedAt is stamped on the invoice record; generatedAt never moves. This is the batch-scale equivalent of POST /admin/orders/:orderId/invoice/regenerate.

The flag is persisted on the batch, so POST …/:id/retry re-renders on the same terms as the original pass.

Expansion rules and limits:

  • Per order, the non-cancelled sub-orders are invoiced; if every sub-order on that order is cancelled, all of them are, since a cancelled order keeps its tax invoice on record (GST reverses via a credit note, not by withdrawing the invoice).
  • Sub-orders are ordered by the parent's placedAt, then the sub-order's createdAt and id, so the merged PDF's page order is reproducible across downloads.
  • At most 100 orders per request, expanding to at most 200 invoices — both bounds exist because the merge runs in memory. Exceeding either returns 400.
  • An orderId that does not exist fails the whole request with 400.
  • An order still in pending_payment has no invoice yet; it is recorded as a failed line item with a readable reason rather than rejecting the batch.

Response (data field) — a batch row:

{
  "id": "…",
  "status": "pending" | "processing" | "ready" | "failed",
  "orderCount": 24,          // sub-orders (invoices), not parent orders
  "successCount": 0,
  "failedCount": 0,
  "fileName": null,
  "fileSize": null,
  "retryCount": 0,
  "regenerate": false,        // the batch re-rendered already-cached invoices
  "error": null,
  "createdAt": "…",
  "startedAt": null,
  "completedAt": null,
  "downloadUrl": null,        // set only when status = ready
  "requestedBy": { "id": "…", "name": "…" } // null once that user is deleted
}

List batches

GET /admin/bulk-invoices · invoice:view

Standard offset/limit pagination (querySchema) plus an optional status filter. Rows are the same shape as the create response, newest first, and span every admin's batches.


Batch status + per-order breakdown

GET /admin/bulk-invoices/:id · invoice:view

Same row shape as above, plus an items array — one entry per sub-order. orderId lets a client group the items back under the order the admin selected:

{
  "...": "batch row fields",
  "items": [
    {
      "orderVendorId": "…",
      "orderId": "…" | null,
      "orderNumber": "ORD-2026-00000123" | null,
      "status": "pending" | "success" | "failed",
      "error": "…" | null
    }
  ]
}

Retry failed orders

POST /admin/bulk-invoices/:id/retry · invoice:bulk

Allowed only when the batch is ready or failed and has at least one failed item — returns 409 otherwise (including while a batch is still processing). Resets every failed item back to pending and re-runs them in the background; the merged PDF is rebuilt from the full current set of successful invoices (previously-succeeded ones are not re-rendered), so a partially-fixed retry still produces one complete file.

Response: the updated batch row (status: "processing", retryCount incremented).


Download the merged PDF

GET /admin/bulk-invoices/:id/download · invoice:view

Streams the merged application/pdf once the batch is ready. Returns 404 otherwise (not yet ready, or the batch failed outright).

Filename: the batch's stored fileName (bulk-invoices-{id}.pdf).

On this page