#Billing
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 withclient_manager,client_admin, orpartner_adminscope for the requestedclient_id. Billing routes use the shared authentication dependency, whereX-API-Keyis evaluated beforeAuthorization: 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_ISSUERZITADEL_AUDIENCEZITADEL_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.
| Surface | API routes | Source router | Auth / roles | Key inputs | Response / error envelope | Portal consumer |
|---|---|---|---|---|---|---|
| Billing overview | GET /v1/billing/overview | api/routers/billing_router.py | Bearer client_admin, client_manager, partner_admin; internal operators with explicit client_id | Query client_id | {billing}; Billing {code,message} errors | lib/aidial-api/billing-client.ts, /api/billing/overview, partner monitor analytics |
| Invoice list | GET /v1/billing/invoices | api/routers/billing_router.py | Bearer customer/partner billing roles; internal operators with explicit client_id | Query client_id, page_size, page_token, status_in | {invoices} envelope; Billing {code,message} errors | lib/aidial-api/billing-client.ts, /api/billing/invoices |
| Invoice recipients | GET /v1/billing/invoice-recipients, POST /v1/billing/invoice-recipients/unmask | api/routers/billing_router.py | Masked read: client_admin, client_manager, partner_admin; unmask: client_admin only | Query/body client_id | Masked or raw recipient envelope; unmask audit stores count-only metadata | lib/aidial-api/billing-client.ts, /api/billing/invoice-recipients* |
| Invoice PDF | GET /v1/billing/invoices/{invoice_id}/pdf | api/routers/billing_router.py | Bearer customer/partner billing roles | Path invoice_id, query client_id | PDF stream; Billing/proxy errors | lib/aidial-api/billing-client.ts, /api/billing/invoices/[invoiceId]/pdf |
| Stripe portal session | POST /v1/billing/portal-session | api/routers/billing_router.py | Bearer customer/partner billing roles | Body client_id, return_to | Allowlisted session fields; Billing {code,message} errors | lib/aidial-api/billing-client.ts, /api/billing/portal-session |
| Project billing helpers | GET /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.py | Legacy API-key project routes | Path project_id, API-key client scope, date range or billing fields | Project router detail/error envelope | Legacy 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
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
client_id | query | string | Yes | - | Client identifier to resolve within the authenticated portal assignment |
#Response
| Field | Type | Description |
|---|---|---|
billing | object | Billing overview object |
billing.client_id | string | Client identifier |
billing.billing_period_start | string | Current billing period start timestamp in ISO 8601 UTC |
billing.billing_period_end | string | Current billing period end timestamp in ISO 8601 UTC |
billing.plan_name | string | Current billing plan name |
billing.currency | string | Billing currency (ISO 4217) |
billing.monthly_fee_amount | number | Monthly subscription fee in major currency units |
billing.usage_status | string | Usage data availability: available or unavailable |
billing.included_minutes | number or null | Minutes included in plan, when available |
billing.used_minutes | number or null | Minutes used this billing period, when usage lookup succeeds |
billing.remaining_minutes | number or null | Remaining included minutes, when available |
billing.estimated_overage_amount | number or null | Estimated overage charges in major currency units |
billing.invoice_status | string | Invoice status: current, action_required, overdue, or unavailable |
billing.next_invoice_date | string or null | Next invoice timestamp in ISO 8601 UTC |
billing.last_invoice_total | number or null | Last invoice total in major currency units |
billing.metered_usage_lines | array | Polymorphic metered usage lines for active usage-based meters |
billing.metered_usage_lines[].meter_id | string | minutes or sms_segments |
billing.metered_usage_lines[].plan_type | string | Always metered |
billing.metered_usage_lines[].total_quantity | integer | Metered quantity for the current billing period |
billing.metered_usage_lines[].unit_label | string | Display unit label |
billing.metered_usage_lines[].per_unit_rate_cents | integer | Per-unit rate in minor currency units |
billing.metered_usage_lines[].currency | string | Billing currency (ISO 4217) |
billing.metered_usage_lines[].estimated_charges_cents | integer | Estimated metered charges in minor currency units |
billing.metered_usage_lines[].usage_freshness_utc | string | Timestamp of the latest usage aggregation in ISO 8601 UTC |
billing.metered_usage_lines[].usage_stale | boolean | Whether the usage line is stale or not yet aggregated |
billing.metered_usage_lines[].stale_reason | string | provider_webhook_lag, aggregation_pending, or none |
billing.metered_usage_lines[].by_project | array | Per-project quantity attribution when available |
billing.metered_usage_lines[].by_project[].project_id | string | Project identifier |
billing.metered_usage_lines[].by_project[].project_name | string or null | Project display name when available |
billing.metered_usage_lines[].by_project[].quantity | integer | Project-attributed quantity |
billing.metered_usage_lines[].by_project[].estimated_charges_cents | integer | Project-attributed estimated charge |
billing.metered_usage_lines[].message_count | integer or null | Message count for SMS usage lines, when available |
billing.metered_usage_lines[].failed_count | integer | Failed usage-event count |
billing.metered_usage_lines[].failed_reasons | object | Failed usage-event counts by reason |
billing.metered_usage | object or null | Deprecated minutes-only metered usage summary kept temporarily for portal compatibility |
billing.metered_usage.plan_type | string | Always metered |
billing.metered_usage.total_metered_minutes | integer | Cumulative metered minutes for the period |
billing.metered_usage.per_minute_rate_cents | integer | Metered per-minute rate in minor currency units |
billing.metered_usage.currency | string | Billing currency (ISO 4217) |
billing.metered_usage.estimated_metered_charges_cents | integer | Estimated metered charges in minor currency units |
billing.metered_usage.usage_freshness_utc | string | Timestamp of the latest usage aggregation in ISO 8601 UTC |
billing.metered_usage.usage_stale | boolean | Whether the metered usage summary is stale or not yet aggregated |
billing.active_discounts | array or null | Active discount summaries when discount lookup succeeds |
billing.active_discounts[].discount_id | integer | Discount identifier |
billing.active_discounts[].discount_type | string | percentage_monthly_fee or fixed_monthly_fee |
billing.active_discounts[].discount_value | integer | Percentage value or fixed amount in minor currency units, depending on discount_type |
billing.active_discounts[].currency | string | Discount currency (ISO 4217) |
billing.active_discounts[].effective_start_date | string | Discount start date |
billing.active_discounts[].effective_end_date | string or null | Discount end date |
billing.active_discounts[].description | string | Display description for the discount |
billing.discounted_monthly_fee_amount | number or null | Discounted monthly fee when active discounts can be applied |
billing.discount_calculation_available | boolean | Whether discount lookup and calculation completed |
billing.active_adjustments | array or null | Active pricing adjustments when adjustment lookup succeeds |
billing.active_adjustments[].id | string | Adjustment identifier |
billing.active_adjustments[].adjustment_type | string | override, credit, surcharge, or promotion |
billing.active_adjustments[].promotion_sub_type | string or null | replacement_fee, recurring_credit, or null |
billing.active_adjustments[].amount_cents | integer | Configured adjustment amount in minor currency units |
billing.active_adjustments[].currency | string | Adjustment currency (ISO 4217) |
billing.active_adjustments[].effective_start_date | string or null | Adjustment start date |
billing.active_adjustments[].effective_end_date | string or null | Adjustment end date |
billing.active_adjustments[].reason | string | Adjustment reason |
billing.active_adjustments[].net_effect_cents | integer | Net billing effect in minor currency units |
billing.adjusted_monthly_fee_cents | integer or null | Monthly fee after active pricing adjustments, in minor currency units |
billing.adjustment_calculation_available | boolean | Whether 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:
curl -X GET "https://api.aidial.ai/v1/billing/overview?client_id=client_example_001" \
-H "Authorization: Bearer YOUR_PORTAL_JWT"Response (200 OK):
{
"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
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
client_id | query | string | Conditional | - | 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_size | query | integer | No | 20 | Results per page (1-100) |
page_token | query | string | No | - | Opaque cursor from the previous response. Use it with the same page_size. |
status_in | query | string | No | - | Single invoice status filter: draft, open, paid, uncollectible, or void. Repeated values and comma-joined multi-value filters are not supported. |
#Response
| Field | Type | Description |
|---|---|---|
invoices | object | Invoice list wrapper |
invoices.items | array | List of invoice summary objects |
invoices.items[].invoice_id | string | Unique invoice identifier |
invoices.items[].invoice_month | string | Invoice month (YYYY-MM) |
invoices.items[].invoice_number | string or null | Invoice number |
invoices.items[].currency | string | Currency (ISO 4217) |
invoices.items[].subtotal_amount | number | Subtotal before tax in major currency units |
invoices.items[].tax_amount | number | Tax amount in major currency units |
invoices.items[].total_amount | number | Total amount in major currency units |
invoices.items[].amount_paid | number | Amount paid in major currency units |
invoices.items[].amount_due | number | Amount outstanding in major currency units |
invoices.items[].status | string | Status: draft, open, paid, uncollectible, or void |
invoices.items[].due_at | string or null | Payment due timestamp in ISO 8601 UTC |
invoices.items[].paid_at | string or null | Payment timestamp in ISO 8601 UTC |
invoices.items[].issued_at | string or null | Invoice finalisation timestamp in ISO 8601 UTC. Null for draft invoices and never falls back to invoice creation time |
invoices.items[].pdf_available | boolean | Whether PDF download is available |
invoices.items[].metered_line_items | array or null | Metered invoice line items, when present |
invoices.items[].metered_line_items[].description | string | Metered line item description |
invoices.items[].metered_line_items[].quantity | integer | Metered quantity |
invoices.items[].metered_line_items[].unit_label | string | Unit label, such as minutes |
invoices.items[].metered_line_items[].unit_amount_cents | integer | Unit amount in minor currency units |
invoices.items[].metered_line_items[].total_cents | integer | Line total in minor currency units |
invoices.items[].metered_line_items[].currency | string | Currency (ISO 4217) |
invoices.items[].discount_line_items | array or null | Discount line items, when present |
invoices.items[].discount_line_items[].description | string | Discount line item description |
invoices.items[].discount_line_items[].discount_type | string | percentage_monthly_fee or fixed_monthly_fee |
invoices.items[].discount_line_items[].base_amount_cents | integer | Discount base amount in minor currency units |
invoices.items[].discount_line_items[].savings_amount_cents | integer | Discount savings amount in minor currency units |
invoices.items[].discount_line_items[].net_amount_cents | integer | Net amount after discount in minor currency units |
invoices.items[].discount_line_items[].currency | string | Currency (ISO 4217) |
invoices.items[].discount_line_items[].pro_rated | boolean | Whether the discount was pro-rated |
invoices.items[].discount_line_items[].effective_days | integer | Number of effective discount days |
invoices.items[].discount_line_items[].period_days | integer | Number of days in the invoice period |
invoices.items[].adjustment_line_items | array or null | Pricing adjustment invoice line items, when present |
invoices.items[].adjustment_line_items[].adjustment_id | string | Pricing adjustment identifier |
invoices.items[].adjustment_line_items[].adjustment_type | string | override, credit, surcharge, or promotion |
invoices.items[].adjustment_line_items[].description | string | Pricing adjustment line item description |
invoices.items[].adjustment_line_items[].amount_cents | integer | Line amount in minor currency units |
invoices.items[].adjustment_line_items[].currency | string | Currency (ISO 4217) |
invoices.page_size | integer | Results per page |
invoices.has_more | boolean | Whether more pages exist |
invoices.next_page_token | string or null | Opaque cursor for the next page |
#Example
Request:
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):
{
"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
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
client_id | query | string | Conditional | - | 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
| Field | Type | Description |
|---|---|---|
recipients | array | Recipient category rows |
recipients[].category | string | invoices_and_receipts or plan_billing_changes |
recipients[].emails_masked | array | Masked email addresses for this category |
recipients[].count | integer | Number of email addresses in this category |
partial | boolean | true only when the Stripe customer-email lookup degraded; tenant-settings email absence is not a partial response |
#Example
Request:
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):
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
client_id | string | No | Client identifier to resolve within the authenticated portal assignment. If omitted, the bearer assignment client is used |
#Response
| Field | Type | Description |
|---|---|---|
recipients | array | Recipient category rows |
recipients[].category | string | invoices_and_receipts or plan_billing_changes |
recipients[].emails | array | Raw email addresses for this category |
recipients[].count | integer | Number of email addresses in this category |
partial | boolean | true 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:
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):
{
"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
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
invoice_id | path | string | Yes | - | Unique invoice identifier. Valid characters are letters, numbers, ., _, and -. |
client_id | query | string | Yes | - | 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:
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_toURL is where the user is redirected after they finish in the billing portal. The returnedexpires_atvalue is a short server-side UX hint for the portal redirect flow.
#Request Body
| Field | Type | Required | Description |
|---|---|---|---|
client_id | string | Yes | Client identifier to resolve within the authenticated portal assignment (1-255 characters) |
return_to | string | Yes | HTTPS URL to redirect to after the billing portal session (1-2048 characters). The URL must not include username/password credentials or a fragment. |
#Response
| Field | Type | Description |
|---|---|---|
session_url | string | Stripe billing portal URL for browser redirect |
expires_at | string | Session expiry hint timestamp in ISO 8601 UTC |
#Example
Request:
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):
{
"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_managerandclient_admincan access their ownclient_id.partner_adminmust request aclient_idassigned to the bearer subject for the authenticateddata_env.client_adminis required to unmask billing invoice recipient emails.aidial_adminandaidial_operatoraccess 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 receive404. - Out-of-scope billing resources return
404 Not Foundto 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:
| Route | Method | Purpose |
|---|---|---|
/v1/projects/{project_id}/billing-config | GET | Minimal project billing configuration for caching and cost calculations |
/v1/projects/{project_id}/billing-summary | GET | Project billing summary for a date range |
/v1/projects/{project_id} | PUT | Updates 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.
#Related Pages
- Authentication - API key and bearer authentication
- Projects - project billing configuration
- Error Responses - error format details
- Rate Limiting - handling 429 responses