Internal Documentation internal
TalkIDE internal documentation

Authenticated user retrieves a paginated list of their Stripe invoices, sorted by date descending. Each invoice includes a direct download link (Stripe hosted_invoice_url). Requires a valid JWT access token.

  • Invoice data is fetched live from the Stripe Invoices API using the user’s stripe_customer_id — not stored in TalkIDE DB.
  • If stripe_customer_id is null (user has never performed any billing action), an empty list is returned without hitting Stripe.
  • Pagination uses Stripe’s cursor-based starting_after parameter wrapped in a page-friendly envelope.
  • Invoice statuses: paid, open, void, uncollectible. FE renders a status pill for each.
  • PDF download is handled via Stripe hosted_invoice_url (external Stripe-hosted page) — no proxy through TalkIDE BE.
  • Related: UC-10001 — payment method registration. UC-10006invoice.* webhook events.
sequenceDiagram
    actor User

    User->>+FE: opens Profile → Billing & Usage → Invoices section

    FE->>FE: onMounted — trigger invoices load (page 1)

    FE->>+BE: GET /api/v1/users/me/invoices?limit=10 <br> Authorization: Bearer {accessToken}

    BE->>BE: validate JWT access token
    alt access token invalid or missing
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
    end

    BE->>DB: SELECT stripe_customer_id FROM users WHERE id = userId

    alt stripe_customer_id is null
        BE-->>FE: 200 OK <br> InvoiceListResponse (empty items, hasMore=false)
    end

    BE->>Stripe: stripe.invoices.list({ customer: customerId, limit: 10, starting_after: cursor })
    Stripe-->>BE: { data: [Invoice...], has_more: boolean }

    BE->>-FE: 200 OK <br> InvoiceListResponse

    FE->>-User: render invoices table

    opt User clicks "Load more"
        User->>+FE: clicks "Load more" button

        FE->>+BE: GET /api/v1/users/me/invoices?limit=10&startingAfter={lastInvoiceId} <br> Authorization: Bearer {accessToken}

        BE->>Stripe: stripe.invoices.list({ customer: customerId, limit: 10, starting_after: lastInvoiceId })
        Stripe-->>BE: { data: [Invoice...], has_more: boolean }

        BE->>-FE: 200 OK <br> InvoiceListResponse

        FE->>-User: append invoices to table
    end

    opt User clicks "Download" on an invoice
        User->>FE: clicks "Download" link
        FE->>User: window.open(hostedInvoiceUrl, "_blank")
        Note over User: Stripe-hosted invoice PDF opens in new tab
    end

GET /api/v1/users/me/invoices Query parameters:

ParameterTypeRequiredDefaultNote
limitintegerno10Max 50
startingAfterstringnoStripe invoice ID cursor for pagination

200 OK InvoiceListResponse:

{
  "items": [
    {
      "id": "in_1PqXyz...",
      "number": "TALKIDE-0001",
      "date": "2026-05-01T00:00:00Z",
      "amountDue": 2900,
      "currency": "usd",
      "status": "paid",
      "hostedInvoiceUrl": "https://invoice.stripe.com/i/acct_xxx/test_live_xxx"
    },
    {
      "id": "in_1PqAbc...",
      "number": "TALKIDE-0002",
      "date": "2026-04-01T00:00:00Z",
      "amountDue": 1500,
      "currency": "usd",
      "status": "open",
      "hostedInvoiceUrl": "https://invoice.stripe.com/i/acct_xxx/test_live_yyy"
    }
  ],
  "hasMore": false,
  "lastId": "in_1PqAbc..."
}

Field notes:

  • amountDue — integer, amount in the smallest currency unit (cents for USD). FE divides by 100 for display: $29.00.
  • currency — ISO 4217 lowercase (e.g. usd). FE uses for display formatting.
  • status — one of: paid, open, void, uncollectible.
  • hostedInvoiceUrl — Stripe-hosted invoice URL. FE opens in new tab. May be null for draft invoices (not applicable in production flow).
  • lastId — ID of the last item in items; pass as startingAfter to fetch the next page. null when items is empty.

200 OK InvoiceListResponse (empty — no stripe_customer_id or no invoices):

{
  "items": [],
  "hasMore": false,
  "lastId": null
}

400 Bad Request (invalid query params) ErrorResponse:

{
  "status": 400,
  "code": "VALIDATION_ERROR",
  "message": "Validation failed",
  "errors": [
    { "field": "limit", "message": "must be between 1 and 50" }
  ]
}

401 Unauthorized ErrorResponse:

{
  "status": 401,
  "code": "AUTHENTICATION_FAILED",
  "message": "Access token is missing or invalid"
}

502 Bad Gateway (Stripe API unreachable) ErrorResponse:

{
  "status": 502,
  "code": "STRIPE_UNAVAILABLE",
  "message": "Payment provider is temporarily unavailable. Please try again."
}

Frontend

Validations

Field / ControlConstraintsNote
limit (query param)integer 1–50Hardcoded to 10 in MVP FE; no user-facing control
startingAfternon-empty string if providedTaken from lastId of previous response

UX Guidelines

Invoices table in BillingSection.vue:

ColumnContent
Invoice #number (monospace)
DateFormatted from date (e.g. “May 1, 2026”)
AmountamountDue / 100 formatted as currency (e.g. “$29.00”)
StatusPill: paid = green, open = amber, void = neutral (fg-3), uncollectible = rose
Download”Download PDF” link → window.open(hostedInvoiceUrl, '_blank'). Hidden when hostedInvoiceUrl is null.

Pagination: “Load more” button below table, visible only when hasMore === true. On click: append items, update lastId cursor.

Empty state: “No invoices yet.” text in fg-3.

Loading state: Skeleton rows (3 rows) during initial load.

Error state (502): Toast (rose, 5 s): “Could not load invoices. Please try again.”

Table container: border-radius: 12px, each row border-bottom: 1px solid var(--line-1), last row no border.

Backend

Validations

FieldConstraintsNote
JWT Authorization headernot_blank, valid signature, not expired401 if missing/invalid
limitinteger, 1–50400 VALIDATION_ERROR if outside range
startingAfteroptional string; if provided must be non-blank400 if blank string provided

Test Cases

GIVENWHENTHEN
Authenticated user with no stripe_customer_idGET /invoices is called200 OK with empty items list, hasMore=false, lastId=null
Authenticated user with stripe_customer_id but no Stripe invoicesGET /invoices is called200 OK with empty items list (Stripe returns empty array)
Authenticated user with 3 Stripe test invoices (status: paid, open, void)GET /invoices?limit=10 is called200 OK; all 3 invoices returned; correct status, amountDue, hostedInvoiceUrl for each
Authenticated user with 15 Stripe invoices, limit=10GET /invoices?limit=10 is called200 OK; 10 items returned; hasMore=true; lastId = ID of 10th invoice
Second page requested using lastId from first pageGET /invoices?limit=10&startingAfter=<lastId> is called200 OK; remaining 5 items returned; hasMore=false
limit=51 (exceeds max)GET /invoices?limit=51 is called400 VALIDATION_ERROR returned
No Authorization headerGET /invoices is called401 AUTHENTICATION_FAILED returned
Stripe API unreachableGET /invoices is called502 STRIPE_UNAVAILABLE returned

Was this page helpful?

Thanks for the feedback.