Invoices API
The invoice API provides read-only access to a tenant's invoices. Invoices are sourced from Stripe (in the default stripe_managed billing mode) or from the local database (in platform_managed mode). The API abstracts this difference — your frontend code works the same regardless of billing mode.
Base path: /api/v1/tenant/{tenantId}/...Middleware: auth:sanctum → tenant.resolve → tenant.member → onboarding.complete
All invoice endpoints are tenant-scoped. Any authenticated tenant member can access their tenant's invoices — no additional permissions are required.
Invoice Statuses
Every invoice has a status that reflects its payment lifecycle:
| Status | Description | Final? |
|---|---|---|
draft | Invoice is being prepared, not yet sent | No |
open | Invoice has been issued and is awaiting payment | No |
paid | Payment received | Yes |
void | Invoice has been voided (cancelled before payment) | Yes |
uncollectible | Payment has been deemed uncollectible | Yes |
Final statuses cannot transition to other states. Only draft and open invoices can be voided.
These statuses are defined in backend/app/Domain/Billing/Enums/InvoiceStatus.php.
List Invoices
GET /api/v1/tenant/{tenantId}/invoices
Returns a paginated list of invoices for the tenant, sorted by issue date (newest first).
Auth: Bearer token + tenant member + onboarding complete
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter by invoice status: draft, open, paid, void, uncollectible |
page | int | 1 | Page number |
per_page | int | 25 | Items per page (max: 100) |
Response (200):
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "550e8400-e29b-41d4-a716-446655440001",
"subscription_id": "550e8400-e29b-41d4-a716-446655440002",
"stripe_invoice_id": "in_1abc123def456",
"stripe_payment_intent_id": "pi_1abc123def456",
"number": "INV-2026-001",
"status": "paid",
"subtotal": {
"amount_cents": 2999,
"currency": "EUR"
},
"tax": {
"amount_cents": 570,
"currency": "EUR"
},
"total": {
"amount_cents": 3569,
"currency": "EUR"
},
"issue_date": "2026-03-01",
"due_date": "2026-03-31",
"paid_at": "2026-03-01T10:30:00.000000Z",
"pdf_url": "/api/v1/tenant/550e8400-.../invoices/550e8400-.../pdf",
"billing_info": {
"name": "Acme Corp",
"email": "billing@acme.com",
"address": {
"country": "DE",
"postal_code": "10115"
}
},
"lines": [
{
"id": "660e8400-e29b-41d4-a716-446655440003",
"invoice_id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Pro Plan - March 2026",
"type": "subscription",
"quantity": 1,
"unit_price": {
"amount_cents": 2999,
"currency": "EUR"
},
"amount": {
"amount_cents": 2999,
"currency": "EUR"
},
"plan_id": "770e8400-e29b-41d4-a716-446655440004",
"meter_id": null,
"period_start": "2026-03-01T00:00:00.000000Z",
"period_end": "2026-03-31T23:59:59.000000Z",
"created_at": "2026-03-01T10:30:00.000000Z",
"updated_at": "2026-03-01T10:30:00.000000Z"
}
],
"created_at": "2026-03-01T10:30:00.000000Z",
"updated_at": "2026-03-01T10:30:00.000000Z"
}
],
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 25,
"to": 25,
"total": 112
}
}
status query parameter. Results are always sorted by issue_date descending (newest first).Get Invoice
GET /api/v1/tenant/{tenantId}/invoices/{invoiceId}
Returns a single invoice with its line items.
Auth: Bearer token + tenant member + onboarding complete
Response (200):
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "550e8400-e29b-41d4-a716-446655440001",
"subscription_id": "550e8400-e29b-41d4-a716-446655440002",
"stripe_invoice_id": "in_1abc123def456",
"stripe_payment_intent_id": "pi_1abc123def456",
"number": "INV-2026-001",
"status": "paid",
"subtotal": {
"amount_cents": 2999,
"currency": "EUR"
},
"tax": {
"amount_cents": 570,
"currency": "EUR"
},
"total": {
"amount_cents": 3569,
"currency": "EUR"
},
"issue_date": "2026-03-01",
"due_date": "2026-03-31",
"paid_at": "2026-03-01T10:30:00.000000Z",
"pdf_url": "/api/v1/tenant/550e8400-.../invoices/550e8400-.../pdf",
"billing_info": {
"name": "Acme Corp",
"email": "billing@acme.com",
"address": {
"country": "DE",
"postal_code": "10115"
}
},
"lines": [
{
"id": "660e8400-e29b-41d4-a716-446655440003",
"invoice_id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Pro Plan - March 2026",
"type": "subscription",
"quantity": 5,
"unit_price": {
"amount_cents": 2999,
"currency": "EUR"
},
"amount": {
"amount_cents": 14995,
"currency": "EUR"
},
"plan_id": "770e8400-e29b-41d4-a716-446655440004",
"meter_id": null,
"period_start": "2026-03-01T00:00:00.000000Z",
"period_end": "2026-03-31T23:59:59.000000Z",
"created_at": "2026-03-01T10:30:00.000000Z",
"updated_at": "2026-03-01T10:30:00.000000Z"
}
],
"created_at": "2026-03-01T10:30:00.000000Z",
"updated_at": "2026-03-01T10:30:00.000000Z"
}
}
Response (404): If the invoice does not exist or belongs to a different tenant.
Invoice Resource Fields
| Field | Type | Description |
|---|---|---|
id | UUID | Invoice identifier |
tenant_id | UUID | Owning tenant |
subscription_id | UUID|null | Associated subscription |
stripe_invoice_id | string|null | Stripe invoice reference (e.g., in_1abc...) |
stripe_payment_intent_id | string|null | Stripe payment intent reference |
number | string | Display number (e.g., INV-2026-001) |
status | enum | See Invoice Statuses |
subtotal | money | Pre-tax amount (amount_cents + currency) |
tax | money | Tax amount |
total | money | Total including tax |
issue_date | date | Issue date (YYYY-MM-DD format) |
due_date | date|null | Payment due date |
paid_at | datetime|null | When payment was received (ISO-8601) |
pdf_url | string|null | Relative URL to download the PDF (null if not yet generated) |
billing_info | object | Billing details snapshot: name, email, address |
lines | array | Invoice line items (see below) |
created_at | datetime | ISO-8601 |
updated_at | datetime | ISO-8601 |
issue_date and due_date fields use date-only format (YYYY-MM-DD), not full ISO-8601 datetimes. This differs from other date fields in the API.Invoice Line Fields
Each invoice contains one or more line items describing what was billed.
| Field | Type | Description |
|---|---|---|
id | UUID | Line item identifier |
invoice_id | UUID | Parent invoice |
description | string | Human-readable description (e.g., "Pro Plan - March 2026") |
type | enum | subscription, proration, or adjustment |
quantity | int | Number of units |
unit_price | money | Price per unit (amount_cents + currency) |
amount | money | Total line amount (quantity × unit_price) |
plan_id | UUID|null | Associated plan (for subscription/proration lines) |
meter_id | UUID|null | Associated meter (for usage-based lines) |
period_start | datetime|null | Start of the billing period for this line |
period_end | datetime|null | End of the billing period for this line |
Line types:
| Type | Description |
|---|---|
subscription | Regular subscription charge |
proration | Credit or charge from a mid-cycle plan change |
adjustment | Manual adjustment or one-time charge |
Download Invoice PDF
GET /api/v1/tenant/{tenantId}/invoices/{invoiceId}/pdf
Downloads the invoice as a PDF file. The response is a streamed binary download.
Auth: Bearer token + tenant member + onboarding complete
Response (200):
Content-Type: application/pdf
Content-Disposition: attachment; filename="invoice-INV-2026-001.pdf"
[binary PDF data]
Error Responses:
| Status | Description |
|---|---|
| 404 | Invoice not found, or PDF has not been generated yet |
| 502 | Failed to fetch PDF from Stripe (stripe_managed mode only) |
stripe_managed mode, the PDF is fetched from Stripe's servers and proxied through the backend. This avoids CORS issues and keeps Stripe URLs out of the frontend. The proxy has a 30-second timeout.Billing Mode Abstraction
The invoice endpoints use a strategy pattern to abstract the data source. The controller injects an InvoiceQueryInterface that resolves to the correct implementation based on the configured billing mode:
stripe_managed(default) — Invoices are fetched from the Stripe API and mapped to in-memory models. Stripe is the source of truth for all invoice data including PDFs.platform_managed— Invoices are stored in the local database and PDFs are served from local storage.
This abstraction is transparent to your frontend — the API response shape is identical regardless of mode. The implementation lives in backend/app/Infrastructure/Billing/Queries/.
stripe_managed mode, deep pagination (beyond a few pages) may be limited because Stripe uses cursor-based pagination internally. The adapter fetches up to 100 items per Stripe API call and slices them for page-based pagination.Error Responses
| Status | Scenario |
|---|---|
| 401 | Missing or invalid authentication token |
| 403 | Authenticated user is not a member of the tenant |
| 404 | Invoice not found, or PDF not yet generated |
| 502 | Failed to fetch PDF from external source (Stripe) |
What's Next
- Usage & Meters API — Usage tracking and quota enforcement endpoints
- Tenants & Teams API — Tenant management, team members, and invitations
- Invoices — How the invoice system works and how invoices are synced
- Webhooks — Webhook events that create and update invoices
Subscriptions API
Tenant subscription endpoints: checkout, cancellation, plan changes, proration preview, billing portal, seats, entitlements, and onboarding flow.
Usage & Meters API
Usage tracking endpoints: recording events with idempotency, viewing usage summaries and meter details, quota enforcement, and cost estimation.