#Billing

16 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
Metered aggregation evidenceGET /v1/billing/metered/aggregation-recordsapi/routers/billing_router.pyBearer aidial_admin, aidial_operator onlyQuery client_id; environment comes only from the authenticated context{aggregation_records}; non-operator/API-key/inaccessible scope is 404Internal operator use only
Metered reconciliation evidenceGET /v1/billing/metered/reconciliation-recordsapi/routers/billing_router.pyBearer aidial_admin, aidial_operator onlyQuery client_id; environment comes only from the authenticated context{reconciliation_records}; non-operator/API-key/inaccessible scope is 404Internal operator use only
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.chargeabilitystringServer decision: flat or hybrid
billing.chargeability_statusstringEvidence state: available, pending, or unavailable
billing.chargeability_labelstringFIXED FOUNDATION ONLY or HYBRID
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[].included_unitsintegerVerified tenant-level allowance for this meter
billing.metered_usage_lines[].overage_unitsintegerExact max(total_quantity - included_units, 0)
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_centsintegerExact tenant-level graduated overage cents (overage_units * per_unit_rate_cents)
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[].attribution_statusstringRequired completeness state: available or unavailable
billing.metered_usage_lines[].by_projectarrayRequired project quantity rows; maximum 50
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[].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

#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 minutes and sms_segments configuration 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:

text
overage_units = max(total_units - included_units, 0)
overage_cents = overage_units * rate_per_unit_cents

Voice 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:

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,
    "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

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_centsinteger or nullStripe 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_centsintegerStripe's actual non-negative line total in minor currency units; never reconstructed from quantity and unit amount
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

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:

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.

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

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.

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:

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.