#Billing

10 min read

Retrieve tenant billing overviews, invoices, invoice recipients, invoice PDFs, and Stripe billing portal sessions for authorised portal users.

  • Production API: https://api.aidial.ai
  • Version prefix: /v1

This page omits environment-specific host names, internal topology, and deployment details.

Billing authentication: Customer billing endpoints on this page require Authorization: Bearer <jwt> for a portal user with client_manager, client_admin, or partner_admin scope for the requested client_id. Billing routes use the shared authentication dependency, where X-API-Key is evaluated before Authorization: Bearer; do not send both headers on billing requests, because the API key will be used and bearer-only billing routes will fail closed.

Bearer JWT validation requires the API runtime to have all of these settings configured, with no fallback defaults:

  • ZITADEL_ISSUER
  • ZITADEL_AUDIENCE
  • ZITADEL_JWKS_URL

Billing errors: Billing service and validation errors use a domain-specific error format ({code, message}) instead of the general error shape. Authentication failures raised before the billing route runs use the general error shape. See Error Responses for details.

#Implemented Route Map

This table is the customer/Portal-facing inventory for published Billing surfaces. The detailed contracts below remain the source for field-level examples.

SurfaceAPI routesSource routerAuth / rolesKey inputsResponse / error envelopePortal consumer
Billing overviewGET /v1/billing/overviewapi/routers/billing_router.pyBearer client_admin, client_manager, partner_admin; internal operators with explicit client_idQuery client_id{billing}; Billing {code,message} errorslib/aidial-api/billing-client.ts, /api/billing/overview, partner monitor analytics
Invoice listGET /v1/billing/invoicesapi/routers/billing_router.pyBearer customer/partner billing roles; internal operators with explicit client_idQuery client_id, page_size, page_token, status_in{invoices} envelope; Billing {code,message} errorslib/aidial-api/billing-client.ts, /api/billing/invoices
Invoice recipientsGET /v1/billing/invoice-recipients, POST /v1/billing/invoice-recipients/unmaskapi/routers/billing_router.pyMasked read: client_admin, client_manager, partner_admin; unmask: client_admin onlyQuery/body client_idMasked or raw recipient envelope; unmask audit stores count-only metadatalib/aidial-api/billing-client.ts, /api/billing/invoice-recipients*
Invoice PDFGET /v1/billing/invoices/{invoice_id}/pdfapi/routers/billing_router.pyBearer customer/partner billing rolesPath invoice_id, query client_idPDF stream; Billing/proxy errorslib/aidial-api/billing-client.ts, /api/billing/invoices/[invoiceId]/pdf
Stripe portal sessionPOST /v1/billing/portal-sessionapi/routers/billing_router.pyBearer customer/partner billing rolesBody client_id, return_toAllowlisted session fields; Billing {code,message} errorslib/aidial-api/billing-client.ts, /api/billing/portal-session
Project billing helpersGET /v1/projects/{project_id}/billing-config, GET /v1/projects/{project_id}/billing-summary, billing fields on PUT /v1/projects/{project_id}api/routers/projects_router.pyLegacy API-key project routesPath project_id, API-key client scope, date range or billing fieldsProject router detail/error envelopeLegacy runtime/config consumers; not a current customer Portal page BFF route

#GET /v1/billing/overview

Returns a billing overview for your tenant.

Authentication: Bearer required (client_manager, client_admin, or partner_admin; internal aidial_admin and aidial_operator reads require an explicit client_id)

#Parameters

NameInTypeRequiredDefaultDescription
client_idquerystringYes-Client identifier to resolve within the authenticated portal assignment

#Response

FieldTypeDescription
billingobjectBilling overview object
billing.client_idstringClient identifier
billing.billing_period_startstringCurrent billing period start timestamp in ISO 8601 UTC
billing.billing_period_endstringCurrent billing period end timestamp in ISO 8601 UTC
billing.plan_namestringCurrent billing plan name
billing.currencystringBilling currency (ISO 4217)
billing.monthly_fee_amountnumberMonthly subscription fee in major currency units
billing.usage_statusstringUsage data availability: available or unavailable
billing.included_minutesnumber or nullMinutes included in plan, when available
billing.used_minutesnumber or nullMinutes used this billing period, when usage lookup succeeds
billing.remaining_minutesnumber or nullRemaining included minutes, when available
billing.estimated_overage_amountnumber or nullEstimated overage charges in major currency units
billing.invoice_statusstringInvoice status: current, action_required, overdue, or unavailable
billing.next_invoice_datestring or nullNext invoice timestamp in ISO 8601 UTC
billing.last_invoice_totalnumber or nullLast invoice total in major currency units
billing.metered_usage_linesarrayPolymorphic metered usage lines for active usage-based meters
billing.metered_usage_lines[].meter_idstringminutes or sms_segments
billing.metered_usage_lines[].plan_typestringAlways metered
billing.metered_usage_lines[].total_quantityintegerMetered quantity for the current billing period
billing.metered_usage_lines[].unit_labelstringDisplay unit label
billing.metered_usage_lines[].per_unit_rate_centsintegerPer-unit rate in minor currency units
billing.metered_usage_lines[].currencystringBilling currency (ISO 4217)
billing.metered_usage_lines[].estimated_charges_centsintegerEstimated metered charges in minor currency units
billing.metered_usage_lines[].usage_freshness_utcstringTimestamp of the latest usage aggregation in ISO 8601 UTC
billing.metered_usage_lines[].usage_stalebooleanWhether the usage line is stale or not yet aggregated
billing.metered_usage_lines[].stale_reasonstringprovider_webhook_lag, aggregation_pending, or none
billing.metered_usage_lines[].by_projectarrayPer-project quantity attribution when available
billing.metered_usage_lines[].by_project[].project_idstringProject identifier
billing.metered_usage_lines[].by_project[].project_namestring or nullProject display name when available
billing.metered_usage_lines[].by_project[].quantityintegerProject-attributed quantity
billing.metered_usage_lines[].by_project[].estimated_charges_centsintegerProject-attributed estimated charge
billing.metered_usage_lines[].message_countinteger or nullMessage count for SMS usage lines, when available
billing.metered_usage_lines[].failed_countintegerFailed usage-event count
billing.metered_usage_lines[].failed_reasonsobjectFailed usage-event counts by reason
billing.metered_usageobject or nullDeprecated minutes-only metered usage summary kept temporarily for portal compatibility
billing.metered_usage.plan_typestringAlways metered
billing.metered_usage.total_metered_minutesintegerCumulative metered minutes for the period
billing.metered_usage.per_minute_rate_centsintegerMetered per-minute rate in minor currency units
billing.metered_usage.currencystringBilling currency (ISO 4217)
billing.metered_usage.estimated_metered_charges_centsintegerEstimated metered charges in minor currency units
billing.metered_usage.usage_freshness_utcstringTimestamp of the latest usage aggregation in ISO 8601 UTC
billing.metered_usage.usage_stalebooleanWhether the metered usage summary is stale or not yet aggregated
billing.active_discountsarray or nullActive discount summaries when discount lookup succeeds
billing.active_discounts[].discount_idintegerDiscount identifier
billing.active_discounts[].discount_typestringpercentage_monthly_fee or fixed_monthly_fee
billing.active_discounts[].discount_valueintegerPercentage value or fixed amount in minor currency units, depending on discount_type
billing.active_discounts[].currencystringDiscount currency (ISO 4217)
billing.active_discounts[].effective_start_datestringDiscount start date
billing.active_discounts[].effective_end_datestring or nullDiscount end date
billing.active_discounts[].descriptionstringDisplay description for the discount
billing.discounted_monthly_fee_amountnumber or nullDiscounted monthly fee when active discounts can be applied
billing.discount_calculation_availablebooleanWhether discount lookup and calculation completed
billing.active_adjustmentsarray or nullActive pricing adjustments when adjustment lookup succeeds
billing.active_adjustments[].idstringAdjustment identifier
billing.active_adjustments[].adjustment_typestringoverride, credit, surcharge, or promotion
billing.active_adjustments[].promotion_sub_typestring or nullreplacement_fee, recurring_credit, or null
billing.active_adjustments[].amount_centsintegerConfigured adjustment amount in minor currency units
billing.active_adjustments[].currencystringAdjustment currency (ISO 4217)
billing.active_adjustments[].effective_start_datestring or nullAdjustment start date
billing.active_adjustments[].effective_end_datestring or nullAdjustment end date
billing.active_adjustments[].reasonstringAdjustment reason
billing.active_adjustments[].net_effect_centsintegerNet billing effect in minor currency units
billing.adjusted_monthly_fee_centsinteger or nullMonthly fee after active pricing adjustments, in minor currency units
billing.adjustment_calculation_availablebooleanWhether adjustment lookup and calculation completed

Usage values are derived from the resolved tenant and data environment. Included-minute usage is queried from the routed tenant data store for the resolved (client_id, data_env). If that routed usage lookup is unavailable, the overview still returns billing plan and invoice data with usage_status: "unavailable" and usage amount fields set to null. Metered usage lines come from the API host's local API metadata store aggregation records.

#Example

Request:

Bash
curl -X GET "https://api.aidial.ai/v1/billing/overview?client_id=client_example_001" \
  -H "Authorization: Bearer YOUR_PORTAL_JWT"

Response (200 OK):

JSON
{
  "billing": {
    "client_id": "client_example_001",
    "billing_period_start": "2026-03-01T00:00:00Z",
    "billing_period_end": "2026-04-01T00:00:00Z",
    "plan_name": "Professional",
    "currency": "AUD",
    "monthly_fee_amount": 299.0,
    "usage_status": "available",
    "included_minutes": 500.0,
    "used_minutes": 473.3,
    "remaining_minutes": 26.7,
    "estimated_overage_amount": 0.0,
    "invoice_status": "current",
    "next_invoice_date": "2026-04-01T00:00:00Z",
    "last_invoice_total": 299.0,
    "metered_usage_lines": [],
    "metered_usage": null,
    "active_discounts": [],
    "discounted_monthly_fee_amount": null,
    "discount_calculation_available": true,
    "active_adjustments": [],
    "adjusted_monthly_fee_cents": null,
    "adjustment_calculation_available": true
  }
}

#GET /v1/billing/invoices

Returns a paginated list of invoices.

Authentication: Bearer required (client_manager, client_admin, or partner_admin; internal aidial_admin and aidial_operator reads require an explicit client_id)

#Parameters

NameInTypeRequiredDefaultDescription
client_idquerystringConditional-Client identifier to resolve within the authenticated portal assignment. Required for partner_admin, aidial_admin, and aidial_operator; optional for client_manager and client_admin, where omission uses the bearer assignment client
page_sizequeryintegerNo20Results per page (1-100)
page_tokenquerystringNo-Opaque cursor from the previous response. Use it with the same page_size.
status_inquerystringNo-Single invoice status filter: draft, open, paid, uncollectible, or void. Repeated values and comma-joined multi-value filters are not supported.

#Response

FieldTypeDescription
invoicesobjectInvoice list wrapper
invoices.itemsarrayList of invoice summary objects
invoices.items[].invoice_idstringUnique invoice identifier
invoices.items[].invoice_monthstringInvoice month (YYYY-MM)
invoices.items[].invoice_numberstring or nullInvoice number
invoices.items[].currencystringCurrency (ISO 4217)
invoices.items[].subtotal_amountnumberSubtotal before tax in major currency units
invoices.items[].tax_amountnumberTax amount in major currency units
invoices.items[].total_amountnumberTotal amount in major currency units
invoices.items[].amount_paidnumberAmount paid in major currency units
invoices.items[].amount_duenumberAmount outstanding in major currency units
invoices.items[].statusstringStatus: draft, open, paid, uncollectible, or void
invoices.items[].due_atstring or nullPayment due timestamp in ISO 8601 UTC
invoices.items[].paid_atstring or nullPayment timestamp in ISO 8601 UTC
invoices.items[].issued_atstring or nullInvoice finalisation timestamp in ISO 8601 UTC. Null for draft invoices and never falls back to invoice creation time
invoices.items[].pdf_availablebooleanWhether PDF download is available
invoices.items[].metered_line_itemsarray or nullMetered invoice line items, when present
invoices.items[].metered_line_items[].descriptionstringMetered line item description
invoices.items[].metered_line_items[].quantityintegerMetered quantity
invoices.items[].metered_line_items[].unit_labelstringUnit label, such as minutes
invoices.items[].metered_line_items[].unit_amount_centsintegerUnit amount in minor currency units
invoices.items[].metered_line_items[].total_centsintegerLine total in minor currency units
invoices.items[].metered_line_items[].currencystringCurrency (ISO 4217)
invoices.items[].discount_line_itemsarray or nullDiscount line items, when present
invoices.items[].discount_line_items[].descriptionstringDiscount line item description
invoices.items[].discount_line_items[].discount_typestringpercentage_monthly_fee or fixed_monthly_fee
invoices.items[].discount_line_items[].base_amount_centsintegerDiscount base amount in minor currency units
invoices.items[].discount_line_items[].savings_amount_centsintegerDiscount savings amount in minor currency units
invoices.items[].discount_line_items[].net_amount_centsintegerNet amount after discount in minor currency units
invoices.items[].discount_line_items[].currencystringCurrency (ISO 4217)
invoices.items[].discount_line_items[].pro_ratedbooleanWhether the discount was pro-rated
invoices.items[].discount_line_items[].effective_daysintegerNumber of effective discount days
invoices.items[].discount_line_items[].period_daysintegerNumber of days in the invoice period
invoices.items[].adjustment_line_itemsarray or nullPricing adjustment invoice line items, when present
invoices.items[].adjustment_line_items[].adjustment_idstringPricing adjustment identifier
invoices.items[].adjustment_line_items[].adjustment_typestringoverride, credit, surcharge, or promotion
invoices.items[].adjustment_line_items[].descriptionstringPricing adjustment line item description
invoices.items[].adjustment_line_items[].amount_centsintegerLine amount in minor currency units
invoices.items[].adjustment_line_items[].currencystringCurrency (ISO 4217)
invoices.page_sizeintegerResults per page
invoices.has_morebooleanWhether more pages exist
invoices.next_page_tokenstring or nullOpaque cursor for the next page

#Example

Request:

Bash
curl -X GET "https://api.aidial.ai/v1/billing/invoices?client_id=client_example_001&page_size=2" \
  -H "Authorization: Bearer YOUR_PORTAL_JWT"

Response (200 OK):

JSON
{
  "invoices": {
    "items": [
      {
        "invoice_id": "in_example_001",
        "invoice_month": "2026-02",
        "invoice_number": "INV-2026-0042",
        "currency": "AUD",
        "subtotal_amount": 299.0,
        "tax_amount": 29.9,
        "total_amount": 328.9,
        "amount_paid": 328.9,
        "amount_due": 0.0,
        "status": "paid",
        "due_at": "2026-03-15T00:00:00Z",
        "paid_at": "2026-03-10T01:00:00Z",
        "issued_at": "2026-03-01T00:00:00Z",
        "pdf_available": true,
        "metered_line_items": null,
        "discount_line_items": null,
        "adjustment_line_items": null
      }
    ],
    "page_size": 2,
    "has_more": false,
    "next_page_token": null
  }
}

#GET /v1/billing/invoice-recipients

Returns masked billing invoice recipient email rows for the resolved tenant.

Authentication: Bearer required (client_manager, client_admin, or partner_admin)

#Parameters

NameInTypeRequiredDefaultDescription
client_idquerystringConditional-Client identifier to resolve within the authenticated portal assignment. Required for partner_admin; optional for client_manager and client_admin, where omission uses the bearer assignment client

#Response

FieldTypeDescription
recipientsarrayRecipient category rows
recipients[].categorystringinvoices_and_receipts or plan_billing_changes
recipients[].emails_maskedarrayMasked email addresses for this category
recipients[].countintegerNumber of email addresses in this category
partialbooleantrue only when the Stripe customer-email lookup degraded; tenant-settings email absence is not a partial response

#Example

Request:

Bash
curl -X GET "https://api.aidial.ai/v1/billing/invoice-recipients?client_id=client_example_001" \
  -H "Authorization: Bearer YOUR_PORTAL_JWT"

Response (200 OK):

JSON
{
  "recipients": [
    {
      "category": "invoices_and_receipts",
      "emails_masked": ["f******@e******.com"],
      "count": 1
    },
    {
      "category": "plan_billing_changes",
      "emails_masked": ["a******@e******.com"],
      "count": 1
    }
  ],
  "partial": false
}

#POST /v1/billing/invoice-recipients/unmask

Returns raw billing invoice recipient email rows for the resolved tenant and emits a server-side audit event.

Authentication: Bearer required (client_admin only)

#Request Body

FieldTypeRequiredDescription
client_idstringNoClient identifier to resolve within the authenticated portal assignment. If omitted, the bearer assignment client is used

#Response

FieldTypeDescription
recipientsarrayRecipient category rows
recipients[].categorystringinvoices_and_receipts or plan_billing_changes
recipients[].emailsarrayRaw email addresses for this category
recipients[].countintegerNumber of email addresses in this category
partialbooleantrue only when the Stripe customer-email lookup degraded

Unmask audit metadata includes only a privacy-safe recipient-count bucket, never raw email addresses.

#Example

Request:

Bash
curl -X POST "https://api.aidial.ai/v1/billing/invoice-recipients/unmask" \
  -H "Authorization: Bearer YOUR_PORTAL_JWT" \
  -H "Content-Type: application/json" \
  -d '{"client_id": "client_example_001"}'

Response (200 OK):

JSON
{
  "recipients": [
    {
      "category": "invoices_and_receipts",
      "emails": ["finance@example.com"],
      "count": 1
    },
    {
      "category": "plan_billing_changes",
      "emails": ["ap@example.com"],
      "count": 1
    }
  ],
  "partial": false
}

#GET /v1/billing/invoices/{invoice_id}/pdf

Downloads an invoice as a PDF file.

Authentication: Bearer required (client_manager, client_admin, or partner_admin)

#Parameters

NameInTypeRequiredDefaultDescription
invoice_idpathstringYes-Unique invoice identifier. Valid characters are letters, numbers, ., _, and -.
client_idquerystringYes-Client identifier to resolve within the authenticated portal assignment

#Response

Returns a PDF file (Content-Type: application/pdf) with Cache-Control: no-store, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, a restrictive Content-Security-Policy, and an attachment filename based on the invoice identifier. Content-Length is included when available.

#Example

Request:

Bash
curl -X GET "https://api.aidial.ai/v1/billing/invoices/in_example_001/pdf?client_id=client_example_001" \
  -H "Authorization: Bearer YOUR_PORTAL_JWT" \
  -o invoice.pdf

#POST /v1/billing/portal-session

Creates a Stripe billing portal session. The response contains a URL for redirecting the user's browser.

Authentication: Bearer required (client_manager, client_admin, or partner_admin)

Redirect pattern: Your server receives the session URL and redirects the user's browser. The return_to URL is where the user is redirected after they finish in the billing portal. The returned expires_at value is a short server-side UX hint for the portal redirect flow.

#Request Body

FieldTypeRequiredDescription
client_idstringYesClient identifier to resolve within the authenticated portal assignment (1-255 characters)
return_tostringYesHTTPS URL to redirect to after the billing portal session (1-2048 characters). The URL must not include username/password credentials or a fragment.

#Response

FieldTypeDescription
session_urlstringStripe billing portal URL for browser redirect
expires_atstringSession expiry hint timestamp in ISO 8601 UTC

#Example

Request:

Bash
curl -X POST "https://api.aidial.ai/v1/billing/portal-session" \
  -H "Authorization: Bearer YOUR_PORTAL_JWT" \
  -H "Content-Type: application/json" \
  -d '{"client_id": "client_example_001", "return_to": "https://portal.example.com/billing"}'

Response (200 OK):

JSON
{
  "session_url": "https://billing.stripe.com/p/session/example_session_id",
  "expires_at": "2026-03-16T01:00:00Z"
}

#Data Scoping

Billing responses are scoped to the authenticated portal assignment:

  • client_manager and client_admin can access their own client_id.
  • partner_admin must request a client_id assigned to the bearer subject for the authenticated data_env.
  • client_admin is required to unmask billing invoice recipient emails.
  • aidial_admin and aidial_operator access is limited to explicitly documented internal read surfaces.
  • Discount audit history is visible only to internal read roles and same-client client_admin/client_manager; client_staff, partners, API-key callers, and cross-client customer requests receive 404.
  • Out-of-scope billing resources return 404 Not Found to avoid tenant enumeration.

The billing Stripe customer mapping is stored in the local API metadata store and decrypted through server-side decryption before provider calls. Raw encrypted fields, ciphertext, wrapped keys, and Stripe customer mapping secrets are not response fields.

Usage values in the billing overview are tenant-scoped. Included-minute usage is calculated from the routed tenant data store path for the authenticated (client_id, data_env). Metered billing summary and invoice line item metadata come from billing tables in the API metadata store and Stripe invoice data.

#Project Billing Helpers

The project router still exposes legacy billing helper endpoints used by cost-calculation and reporting paths:

RouteMethodPurpose
/v1/projects/{project_id}/billing-configGETMinimal project billing configuration for caching and cost calculations
/v1/projects/{project_id}/billing-summaryGETProject billing summary for a date range
/v1/projects/{project_id}PUTUpdates project configuration, including billing fields when supplied

These routes follow the project router's auth, ownership, and error semantics rather than the /v1/billing/* route envelope.