Skip to main content

Billing Service — API Contracts

Status: populated Owner: Platform Engineering + Finance Last updated: 2026-04-18 Companion: APPLICATION_LOGIC · ADR-0001 Kong

All REST endpoints are admin/internal only; fronted by Kong. No public customer-facing billing API — customer-portal reads invoices and usage via these same endpoints (with account.admin scope).

1. Usage Query API

GET /v1/billing/usage

Returns aggregated usage for the caller's account (or a target account for platform.admin).

Query params:

  • accountId (platform.admin only)
  • from ISO date (required)
  • to ISO date (required)
  • granularity hour|day|month (default day)
  • operatorId (optional filter)

Response 200:

{
"accountId": "uuid",
"currency": "USD",
"from": "2026-03-01",
"to": "2026-03-31",
"granularity": "day",
"buckets": [
{
"period": "2026-03-01",
"messageCount": 15200,
"segmentCount": 16800,
"totalCustomerPrice": "304.00",
"totalOperatorCost": "168.00",
"margin": "136.00"
}
],
"totals": {
"messageCount": 450000,
"segmentCount": 495000,
"totalCustomerPrice": "9000.00",
"totalOperatorCost": "4950.00",
"margin": "4050.00"
}
}

2. Invoice API

GET /v1/billing/invoices

Paginated list. Params: accountId (admin), status, from, to, cursor, limit (max 50).

Response 200:

{
"data": [
{
"invoiceId": "uuid",
"accountId": "uuid",
"periodStart": "2026-03-01",
"periodEnd": "2026-03-31",
"totalMessages": 450000,
"subtotalAmount": "9000.00",
"currency": "USD",
"status": "FINALIZED",
"generatedAt": "2026-04-01T00:10:00Z"
}
],
"nextCursor": "opaque",
"total": 12
}

GET /v1/billing/invoices/{invoiceId}/download

Returns a presigned S3 URL (TTL 15 min).

Response 200:

{ "url": "https://s3.../invoices/...", "expiresAt": "2026-04-18T10:15:00Z" }

POST /v1/admin/invoices/{invoiceId}/void

Finance admin only (role platform.finance). Body: { "reason": "string" }. Response 200: updated invoice record.

3. Pricing Admin API

GET /v1/admin/pricing

List pricing tables. Params: accountTier, operatorId, activeOnly=true.

POST /v1/admin/pricing

Create pricing table entry.

{
"accountTier": "GROWTH",
"operatorId": "op-uk-001",
"direction": "MT",
"pricingModel": "PER_SEGMENT",
"unitPrice": "0.0200",
"currency": "USD",
"effectiveFrom": "2026-05-01"
}

Response 201: created PricingTable record.

PATCH /v1/admin/pricing/{id}

Updates effectiveTo of existing row and creates a successor row (versioned update). Body: same as POST; effectiveFrom must be after existing row's effectiveFrom.

DELETE /v1/admin/pricing/{id}

Soft-deletes by setting effectiveTo = today. Response 204.

4. Operator Cost Admin API

GET /v1/admin/operator-costs

List. Params: operatorId, activeOnly=true.

POST /v1/admin/operator-costs

{
"operatorId": "op-uk-001",
"direction": "MT",
"costPerSegment": "0.0100",
"currency": "USD",
"effectiveFrom": "2026-05-01"
}

Response 201.

PATCH /v1/admin/operator-costs/{id}

Versioned update — same pattern as pricing PATCH.

5. Internal / Operational

EndpointCallerPurpose
GET /health/liveK8sLiveness
GET /health/readyK8sReadiness (PG + Redis + NATS reachable)
GET /metricsPrometheusMetrics scrape

6. Error Response Shape

{
"type": "https://errors.ghasi.io/billing/PRICING_NOT_FOUND",
"title": "No active pricing rule found",
"status": 404,
"code": "PRICING_NOT_FOUND",
"detail": "No pricing_tables row for accountTier=GROWTH, operatorId=op-xx-999, direction=MT",
"requestId": "req_01H..."
}

7. Auth & Scopes (Kong-enforced)

ScopeEndpoints
billing:readGET /v1/billing/usage, GET /v1/billing/invoices, download
billing:adminAll /v1/admin/* endpoints
platform.financevoid invoice