`GET /api/marketplace/purchases/{id}/invoices` — #1111 FUND 3: the billing history of one subscription licence, newest first.
const url = 'https://example.com/api/marketplace/purchases/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/invoices';const options = {method: 'GET', headers: {cookie: 'supacloud_session=<supacloud_session>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url https://example.com/api/marketplace/purchases/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/invoices \ --cookie supacloud_session=<supacloud_session>Workspace-scoped like the cancellation, and for the same reason: the entitlement belongs to the workspace that paid, so its billing record does too.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Responses
Section titled “Responses”#1111 — one row per Stripe invoice OUTCOME on a subscription purchase.
Two jobs in one row: it is the webhook idempotency key (UNIQUE (provider_invoice_id, outcome) — a replayed invoice.paid inserts nothing,
so nothing is booked twice) and the per-period audit trail, linking the paid
period to the seller ledger row it produced.
object
The seller-earnings row this period booked. None for a failed invoice
and for a platform-as-seller sale (#928 writes no payout row).
paid | payment_failed.
The negative ledger row the reversal booked, if any.
#1125 — set once this period was refunded. Also the idempotency guard:
the reversal is applied by a CAS on refunded_at IS NULL, so Stripe’s
repeated charge.refunded deliveries cannot reverse a period twice.
The CUMULATIVE amount Stripe last reported as refunded for this period. Stripe grows this figure across partial refunds, so the stored value is what lets a LARGER later refund extend the reversal instead of being dropped as a replay.
Examplegenerated
[ { "amount_minor": 1, "created_at": "2026-04-15T12:00:00Z", "currency": "example", "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "ledger_entry_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "outcome": "example", "period_end": "2026-04-15T12:00:00Z", "period_start": "2026-04-15T12:00:00Z", "provider_invoice_id": "example", "purchase_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "refund_ledger_entry_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "refunded_at": "2026-04-15T12:00:00Z", "refunded_minor": 1 }]Authentication required
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Purchase belongs to another workspace
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Purchase not found
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Structured server error
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}