#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 |
| Metered aggregation evidence | GET /v1/billing/metered/aggregation-records | api/routers/billing_router.py | Bearer aidial_admin, aidial_operator only | Query client_id; environment comes only from the authenticated context | {aggregation_records}; non-operator/API-key/inaccessible scope is 404 | Internal operator use only |
| Metered reconciliation evidence | GET /v1/billing/metered/reconciliation-records | api/routers/billing_router.py | Bearer aidial_admin, aidial_operator only | Query client_id; environment comes only from the authenticated context | {reconciliation_records}; non-operator/API-key/inaccessible scope is 404 | Internal operator use only |
| 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.chargeability | string | Server decision: flat or hybrid |
billing.chargeability_status | string | Evidence state: available, pending, or unavailable |
billing.chargeability_label | string | FIXED FOUNDATION ONLY or HYBRID |
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[].included_units | integer | Verified tenant-level allowance for this meter |
billing.metered_usage_lines[].overage_units | integer | Exact max(total_quantity - included_units, 0) |
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 | Exact tenant-level graduated overage cents (overage_units * per_unit_rate_cents) |
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[].attribution_status | string | Required completeness state: available or unavailable |
billing.metered_usage_lines[].by_project | array | Required project quantity rows; maximum 50 |
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[].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 |
#Chargeability and unavailable states
chargeability is server-derived. hybrid requires all of the following for the exact case-sensitive (client_id, data_env) scope at the time of the read:
- active local
minutesandsms_segmentsconfiguration rows with the approved
allowance, integer cents rate, AUD currency, effective interval, Stripe IDs, event names, and source-contract revision, with no active unscoped legacy row for either client/meter;
- one active Stripe subscription containing exactly three approved items:
exactly one monthly AUD 24900-cent licensed foundation item and the two meter-specific items below;
- matching active metered subscription items and prices with two graduated
tiers, a zero-priced allowance tier, an infinite per-unit overage tier, no flat amount or quantity transformation, monthly metered recurrence, distinct active meters/event names, and sum aggregation; and
- the implemented S-BI18 existing-record reader plus no pending, flagged,
unverified, or correction-pending evidence.
Stripe or product metadata by itself never activates hybrid. Missing, stale, unsafe, or conflicting evidence returns flat, suppresses every usage and overage claim, sets usage_status to unavailable, empties metered_usage_lines, and leaves the legacy usage fields null. Exact AiDial is additionally pinned to flat / FIXED FOUNDATION ONLY until a separate activation is approved.
The repository registers S-BI18-existing-record-v1 for both usage lanes but has no runtime Stripe-meter evidence loader. Consequently, the current runtime still cannot return production hybrid: exact configured scopes are flat/unavailable, and source readiness alone cannot unlock the remaining gates. This is an explicit production activation prerequisite, not a fallback.
#Approved hybrid calculation contract
The reusable, not-yet-activated hybrid reference is AUD 249.00 per subscription period, 500 connected-call minutes followed by AUD 0.59 per billable minute, and 100 outbound SMS segments followed by AUD 0.20 per segment. Each meter uses integer minor units:
overage_units = max(total_units - included_units, 0)
overage_cents = overage_units * rate_per_unit_centsVoice units use each eligible unique room's complete stored AiDial session duration_seconds; the reader does not reconstruct or truncate that stored duration at transfer. The exact worker waits one full hour beyond a window boundary before freezing voice usage, longer than the maximum portal-authored 30-minute call limit. Future activation must prove that every eligible runtime project enforces that limit; unlimited or legacy-unbounded projects remain unavailable. Duration is floored to whole seconds; null/non-positive duration is excluded and each positive eligible call contributes max(1, ceil(floor(duration_seconds) / 60)) before tenant pooling. SMS units use the existing positive stored segment_count once per eligible delivered/read logical send at its first delivery timestamp. An otherwise eligible terminal row with a missing/non-positive count makes the closed window unavailable; it never substitutes a quantity and never fetches one from a carrier. Runtime uniqueness of room SIDs and outbound logical-send keys plus half-open attribution prevents the same source identity appearing in adjacent windows.
Both readers use only exact get_aidial_db_pool(client_id, data_env) routes and closed UTC [start,end) windows. The ledger retains exact scope, window, revision, quantity, source total, selected-record count, and privacy-safe source checksum. Neither an API nor runtime database migration is required. S-BI18 adds no runtime producer/schema, HMAC, compatibility reader, or provider lookup.
Exact SMS activation also requires routed production-shaped query-plan evidence that the combined delivered/read selection completes inside the 750 ms statement timeout. S-BI18 deliberately adds no runtime index migration.
Submission uses immutable deltas for closed, half-open UTC hourly windows—not cumulative snapshots. A backlog is processed one oldest eligible UTC-hour window at a time; it is never collapsed into one later event. The provider identifier binds the exact tenant, meter, data environment, both Stripe period bounds, and both window bounds. Unchanged retries reuse that identifier. Duplicate-like, transport-failed, or otherwise uncertain exact submissions stop after the first unproven attempt and remain pending. A synchronous Stripe response proves acceptance only when it is a billing.meter_event object that exactly echoes the identifier, event name, customer, quantity, and timestamp. The event-summary recovery reader is not implemented in this release. A re-query that changes quantity or checksum becomes correction-pending and cannot mint or submit a second identifier. Exact zero windows are stored without a Stripe event and are still rechecked for late authoritative evidence under the same correction rule. Approved monetary corrections use the existing signed pricing-adjustment workflow; the billing worker does not send negative, top-up, or corrective meter events.
Legacy null-environment configuration and history remain readable for audit, but both schedulers and direct worker entry points reject those flat scopes before any Stripe access. No new legacy meter event is submitted. Existing legacy rows remain outside exact environment-scoped cumulative and reconciliation queries. An exact config is additionally blocked unless a meter-specific authoritative reader is registered in code, so adding config text or a revision label cannot route it through a legacy source query.
Exact reconciliation recomputes a complete authoritative period manifest, compares every frozen window's bounds, quantity, and checksum with stored evidence, sums provider-verified immutable deltas, and then compares source to stored and stored to Stripe. A missing, duplicate, changed, or unresolved window is marked correction-pending and cannot reconcile as a match. Tolerance is always zero and only two zero discrepancies can be match; incomplete source evidence is unverified. A separate legacy reconciliation path compares null-scoped ledger and Stripe totals at zero tolerance without claiming an authoritative source leg. The current authoritative reader prerequisite means production reconciliation cannot yet reach match for the new exact scope.
Migration 0104 adds explicit provider_acceptance_verified provenance. Every historical row defaults to false without rewriting its provider reference or financial history. A new exact row may set the flag only after the complete Stripe response echo above is validated; the retained stripe_meter_event_id column then stores the echoed caller identifier and must equal idempotency_key. Only verified rows contribute submitted totals. Verified acceptance is monotonic and cannot be cleared or replaced.
For any future verified hybrid line, attribution_status: "available" means the gross integer project quantities reconcile exactly to the tenant total. available with by_project: [] is confirmed zero; unavailable requires an empty array and is not a zero claim. Project rows contain only project_id, optional project_name, and quantity. Allowance, currency, rate, estimated charge, invoice totals, discounts, and tax remain tenant-level only.
#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,
"chargeability": "flat",
"chargeability_status": "unavailable",
"chargeability_label": "FIXED FOUNDATION ONLY",
"usage_status": "unavailable",
"included_minutes": null,
"used_minutes": null,
"remaining_minutes": null,
"estimated_overage_amount": null,
"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 or null | Stripe unit amount in minor currency units; null for a tiered price with no flat unit amount and never coerced to zero |
invoices.items[].metered_line_items[].total_cents | integer | Stripe's actual non-negative line total in minor currency units; never reconstructed from quantity and unit amount |
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 |
Metered invoice lines preserve Stripe's actual integer quantity and line total. A missing, fractional, or negative quantity/total is invalid billing evidence; the API fails closed rather than clamping or calculating a substitute. A null unit amount is valid for tiered usage and remains null.
#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.
If the tenant has an active dedicated billing-portal configuration record, the API creates the session with that dedicated Stripe Billing Portal configuration. The configuration identifier is server-only: callers cannot select or override it. The API refuses a configured record when its declared sandbox/live mode does not match the billing-provider mode configured for the serving environment. Tenants without an active record continue to use the Stripe account's default portal configuration.
#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.
Chargeability evidence and operator records are exact-case scoped to the authenticated (client_id, data_env). The current overview does not project legacy routed call duration or locally estimated SMS segments as chargeable; those sources cannot populate allowance, rate, remaining usage, or overage money. Stripe invoice line values remain provider data and do not make the separate hybrid activation gate true.
#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