Supercommerce API Docs
Vendor API

Invoice Module — Vendor

HTTP surface for a vendor to check, generate, stream, download, and regenerate the GST tax invoice for their own sub-order — async generation with SSE readiness notification.

HTTP surface for a vendor to access the invoice for one of their own sub-orders. Each vendor is the seller of record on its invoice (its own GSTIN), so a vendor only ever sees its own sub-order's invoice — never a sibling vendor's. Generation is lazy and async: the first POST …/generate call enqueues a background render job; an SSE stream reports when it is ready; repeat downloads stream the cached PDF instantly. Vendors may also regenerate their own invoice — it re-renders against the same, immutable serial.

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

Scoped to the active vendor (resolveActiveVendorId). A sub-order belonging to another vendor returns 404. An order still in pending_payment returns status: "none".


Conventions

Authentication

All endpoints require a Better-Auth vendor session. The sub-order (orderVendorId) must belong to the active vendor. The SSE stream (/stream) is authenticated via the session cookie, which the browser EventSource sends automatically.

Response shape

  • Status / generate endpoints return the standard JSON envelope: { data, message, statusCode }.
  • Download (GET …/invoice) streams raw application/pdf bytes with Content-Disposition: attachment — no JSON envelope.
  • SSE stream (GET …/invoice/stream) returns text/event-stream — no JSON envelope.

Generation flow

POST …/invoice/generate

       ├─ status: "ready"  ──►  GET …/invoice  (streams cached PDF)

       └─ status: "pending"  ──►  open GET …/invoice/stream

                                         ├─ { status:"pending", heartbeat:true }  (periodic)
                                         ├─ { status:"ready", downloadUrl }  ──►  GET …/invoice
                                         └─ { status:"failed", error }

On page load, call GET …/invoice/status to render the current state without triggering a render.


Endpoints

Check invoice status

GET /vendor/orders/:orderVendorId/invoice/status

Returns the current status without enqueuing anything. Safe to call on every page load.

Response (data field):

{
  "status": "ready" | "pending" | "failed" | "none",
  "invoiceNumber": "INV-2526-000001" | null,
  "downloadUrl": "…"  | null,   // set only when ready
  "streamUrl":  "…"  | null,   // set only when pending or failed
  "jobId":      "…"  | null
}

status: "none" is returned when the parent order is still in pending_payment or no generate has been requested yet.


Enqueue invoice generation

POST /vendor/orders/:orderVendorId/invoice/generate

Enqueues the render job if not already cached. Idempotent — concurrent first-generation requests collapse onto one job.

Request body (optional):

{ "regenerate": true }

Setting regenerate: true forces a fresh render against the existing, immutable serial and overwrites the cached PDF (e.g. to correct a stale invoice after a vendor-profile GSTIN change). Omit the body (or set regenerate: false) for normal generation. This is the only way a vendor regenerates — there is no separate regenerate endpoint on the vendor surface.

Always returns HTTP 200. Response (data field):

{
  "status": "ready" | "pending",
  "jobId": "…",
  "downloadUrl": "…" | null,   // set when ready
  "streamUrl":   "…" | null    // set when pending
}

Download my sub-order's invoice

GET /vendor/orders/:orderVendorId/invoice

Streams the cached application/pdf when the invoice is ready. Returns 409 while still generating — call POST …/generate first, then wait for the SSE ready frame before downloading.

Filename: invoice-{serial}.pdf (e.g. invoice-INV-2526-000001.pdf).


Stream readiness (SSE)

GET /vendor/orders/:orderVendorId/invoice/stream

Opens a Server-Sent Events connection (text/event-stream). Each data: frame is JSON:

// Periodic heartbeat while rendering
{ "status": "pending", "heartbeat": true }

// Terminal: ready
{ "status": "ready", "downloadUrl": "…" }

// Terminal: render failed
{ "status": "failed", "error": "…" }

The stream closes automatically after ~60 s. If no terminal frame was received, re-check status with GET …/invoice/status and reconnect if still pending.

On this page