Skip to content
SaaS4Builders
API Reference

Invoices API

Tenant invoice endpoints: listing with status filtering, single invoice details with line items, and PDF download via proxied streaming.

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:sanctumtenant.resolvetenant.memberonboarding.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:

StatusDescriptionFinal?
draftInvoice is being prepared, not yet sentNo
openInvoice has been issued and is awaiting paymentNo
paidPayment receivedYes
voidInvoice has been voided (cancelled before payment)Yes
uncollectiblePayment has been deemed uncollectibleYes

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:

ParamTypeDefaultDescription
statusstringFilter by invoice status: draft, open, paid, void, uncollectible
pageint1Page number
per_pageint25Items 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
  }
}
Invoice list endpoints do not use Spatie QueryBuilder. Filtering is limited to the 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

FieldTypeDescription
idUUIDInvoice identifier
tenant_idUUIDOwning tenant
subscription_idUUID|nullAssociated subscription
stripe_invoice_idstring|nullStripe invoice reference (e.g., in_1abc...)
stripe_payment_intent_idstring|nullStripe payment intent reference
numberstringDisplay number (e.g., INV-2026-001)
statusenumSee Invoice Statuses
subtotalmoneyPre-tax amount (amount_cents + currency)
taxmoneyTax amount
totalmoneyTotal including tax
issue_datedateIssue date (YYYY-MM-DD format)
due_datedate|nullPayment due date
paid_atdatetime|nullWhen payment was received (ISO-8601)
pdf_urlstring|nullRelative URL to download the PDF (null if not yet generated)
billing_infoobjectBilling details snapshot: name, email, address
linesarrayInvoice line items (see below)
created_atdatetimeISO-8601
updated_atdatetimeISO-8601
The 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.

FieldTypeDescription
idUUIDLine item identifier
invoice_idUUIDParent invoice
descriptionstringHuman-readable description (e.g., "Pro Plan - March 2026")
typeenumsubscription, proration, or adjustment
quantityintNumber of units
unit_pricemoneyPrice per unit (amount_cents + currency)
amountmoneyTotal line amount (quantity × unit_price)
plan_idUUID|nullAssociated plan (for subscription/proration lines)
meter_idUUID|nullAssociated meter (for usage-based lines)
period_startdatetime|nullStart of the billing period for this line
period_enddatetime|nullEnd of the billing period for this line

Line types:

TypeDescription
subscriptionRegular subscription charge
prorationCredit or charge from a mid-cycle plan change
adjustmentManual 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:

StatusDescription
404Invoice not found, or PDF has not been generated yet
502Failed to fetch PDF from Stripe (stripe_managed mode only)
In 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/.

In 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

StatusScenario
401Missing or invalid authentication token
403Authenticated user is not a member of the tenant
404Invoice not found, or PDF not yet generated
502Failed 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