Supercommerce API Docs
Store API

Invoice Module — Storefront

HTTP surface for a customer to check, generate, stream, and download the GST tax invoice(s) for their own order — async generation with SSE readiness notification.

HTTP surface for customers to access the invoice for an order they placed. Because invoices are issued per vendor, a multi-vendor order has one invoice per seller; the order-level endpoints cover all vendors merged. 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.

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

Scoped to the caller's own orders. A non-owned or unknown order id returns 404 (never 403), so other customers' orders can't be probed for existence. An order still in pending_payment returns status: "none" — invoices are issued once the order is confirmed.


Conventions

Authentication

All endpoints require a Better-Auth customer session. The order must belong to the caller. 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.

Customers cannot regenerate an invoice (no regenerate endpoint at this role).


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 /store/orders/:orderId/invoice/status
GET /store/orders/:orderId/vendors/: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 /store/orders/:orderId/invoice/generate
POST /store/orders/:orderId/vendors/:orderVendorId/invoice/generate

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

Always returns HTTP 200. Response (data field):

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

Download invoice PDF

GET /store/orders/:orderId/invoice
GET /store/orders/:orderId/vendors/: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.

  • Order-level: merges every vendor's PDF into one. Filename: invoice-{orderNumber}.pdf.
  • Sub-order-level: one vendor's invoice. Filename: invoice-{serial}.pdf (e.g. invoice-INV-2526-000001.pdf).

Stream readiness (SSE)

GET /store/orders/:orderId/invoice/stream
GET /store/orders/:orderId/vendors/: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