{"openapi":"3.1.0","info":{"title":"IG1 Billing","description":"Per-tenant OpenStack usage -> rated EUR -> Stripe invoices","version":"0.8.1"},"paths":{"/health/live":{"get":{"tags":["health"],"summary":"Liveness","operationId":"liveness_health_live_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health/ready":{"get":{"tags":["health"],"summary":"Readiness","description":"Ready when this pod's own CONFIG is complete. Upstream reachability is\nreported, not gated.\n\n2026-08-17 (gotcha 175). This also required `openstack_ok` — a live token\nmint against Keystone — and the deployment wires it as the kubelet's\nreadinessProbe, so a slow Keystone evicted billing from its own Service and\nconcentrated the load on whatever replica was left. The distinction that\nmatters, and the reason this is not the blanket change the api needed:\n\n  * config_ok / stripe_ok are LOCAL facts. A pod with no application\n    credential and no Stripe key genuinely cannot serve, they cannot flap,\n    and no amount of retrying fixes them — a correct readiness gate.\n  * openstack_ok is a NETWORK PROBE of somebody else. That is the cascade,\n    and it is now reported in the body instead of gating pod membership.\n\nA tenant-visible OpenStack failure still surfaces per request, which is the\nright blast radius.","operationId":"readiness_health_ready_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/usage/summary":{"get":{"tags":["usage"],"summary":"Usage Summary","description":"Per-project usage for a billing period (default: current month UTC),\nrated to EUR at per-second granularity. Customers see only their own\nmapped project; admins may pass any ?project= (default: first TENANTS\nentry — the ig1-customer-pilot).","operationId":"usage_summary_v1_usage_summary_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"project","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project"}},{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start"}},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageSummaryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/invoices":{"get":{"tags":["invoices"],"summary":"List Invoices","description":"Ledger entries. Admins see all; customers only their own project's.","operationId":"list_invoices_v1_invoices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceListResponse"}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["invoices"],"summary":"Create Invoice","description":"Admin-only: collect + rate the project's usage for the period and\ncreate a Stripe DRAFT invoice (auto_advance=False — a human sends it).\nThe ledger entry is created first, then linked to the Stripe ids.","operationId":"create_invoice_v1_invoices_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInvoiceRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LedgerEntry"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/v1/cost/breakdown":{"get":{"tags":["cost"],"summary":"Cost Breakdown","description":"Rated-usage aggregation for a period (default: current month-to-date).\n\ngroup_by=service: one row per service family (compute/volume/network).\ngroup_by=tag: rows per (tag, service) — Nova server tags + Neutron\nnetwork tags joined with the service's own OpenStack credentials;\nmulti-tagged resources contribute to each tag, untagged resources and\nvolumes land in \"untagged\" (the spec's documented v1 volume gap).\nCustomers see only their own mapped project; admins may pass ?project=.","operationId":"cost_breakdown","security":[{"HTTPBearer":[]}],"parameters":[{"name":"project","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project"}},{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start"}},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End"}},{"name":"group_by","in":"query","required":false,"schema":{"type":"string","default":"service","title":"Group By"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/compat/aws/cost-and-usage":{"get":{"tags":["cost"],"summary":"Aws Compat Cost And Usage","description":"Cost and usage in the shape AWS Cost Explorer's GetCostAndUsage returns,\nso existing FinOps tooling can read IG1 without a bespoke integration.\n\nTHIS IS A DELIBERATE, NARROW EXCEPTION to the compat-layer verdict in\ndocs/aws-migration-parity.md §4, which says the migration story is mapping,\nnot masquerade, and rules out emulating AWS APIs. §4.6 records why this one\nendpoint is different and what would have to be true to add another. Read\nthat before extending this surface.\n\nIt is an ADAPTER, not a second billing engine: the numbers come from exactly\nthe same `_collect_and_rate` + `breakdown_by_service` path that serves\n/v1/cost/breakdown, so the two endpoints cannot disagree about a period.\nA gate asserts that (tests/test_billing_aws_compat.py).\n\nWHAT IS DELIBERATELY REFUSED, rather than silently ignored:\n\n* `granularity=DAILY|HOURLY` — refused. A daily series over a caller-chosen\n  window is an unbounded fan-out against the live collector, and nothing\n  here caches. MONTHLY is the granularity this can serve honestly.\n* any `metrics` other than UnblendedCost — refused. IG1 has no blended,\n  amortised or net-amortised concept, and answering with the same number\n  under four names would be a lie told four times.\n* `group_by` other than SERVICE — refused. Cost Explorer's grouping\n  vocabulary is large; SERVICE is the one this maps to a real row set.\n\nTHE UNIT IS EUR, NOT USD. Cost Explorer callers that assume USD will read\nthis wrong, so the Unit field says EUR and the envelope repeats it. Amounts\nare decimal strings, as Cost Explorer returns them — NOT the integer cents\n/v1/budgets takes. See §2 surprise #16: `amount_cents: 100` means one euro,\nand a budget ported naively becomes 100x smaller.","operationId":"aws_compat_cost_and_usage","security":[{"HTTPBearer":[]}],"parameters":[{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start"}},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End"}},{"name":"granularity","in":"query","required":false,"schema":{"type":"string","default":"MONTHLY","title":"Granularity"}},{"name":"metrics","in":"query","required":false,"schema":{"type":"string","default":"UnblendedCost","title":"Metrics"}},{"name":"group_by","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Group By"}},{"name":"project","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/cost/forecast":{"get":{"tags":["cost"],"summary":"Cost Forecast","description":"The current-month run-rate forecast: month-to-date rated spend\nprojected linearly to month end. The method is documented in the\nenvelope (method / method_detail) — no ML theater (spec §1).","operationId":"cost_forecast","security":[{"HTTPBearer":[]}],"parameters":[{"name":"project","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/budgets":{"get":{"tags":["budgets"],"summary":"List Budgets","description":"The caller's budget thresholds. Admins see all (or one ?project=);\ncustomers only their own mapped project's.","operationId":"list_budgets","security":[{"HTTPBearer":[]}],"parameters":[{"name":"project","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BudgetListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["budgets"],"summary":"Create Budget","description":"Create a monthly budget threshold for the caller's OWN project (the\ntenant is derived from the credential only — never caller-supplied).\n\n**`amount_cents` IS CENTS. `amount_cents: 100` is EUR 1.00, not EUR 100.**\nStated here because this is where the mistake is made: a budget ported from\na tool that thinks in whole currency units becomes 100x SMALLER, and the\nsymptom is a breach event on day one rather than an error. Pass `amount_eur`\ninstead if that is what you mean — it converts. The AWS-shaped read surface\n(/v1/compat/aws/cost-and-usage, §4.6) returns decimal strings, as Cost\nExplorer does, precisely so a client cannot carry that confusion across.\nRegistered as surprise #16 in docs/aws-migration-parity.md §2.\n\nAmount as amount_cents (exact) or amount_eur (converted). The breach\nlands on the events bus as billing.budget.breached after the next\nmonth-to-date rating pass.","operationId":"create_budget","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BudgetCreateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Budget"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/budgets/{budget_id}":{"delete":{"tags":["budgets"],"summary":"Delete Budget","description":"Delete a budget. Customers may delete only their OWN project's\nbudgets — a cross-tenant id answers 404 (existence is not leaked).","operationId":"delete_budget","security":[{"HTTPBearer":[]}],"parameters":[{"name":"budget_id","in":"path","required":true,"schema":{"type":"string","title":"Budget Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks/stripe":{"post":{"tags":["webhooks"],"summary":"Stripe Webhook","description":"Stripe-signed events: payment succeeded/failed etc. update the\ninvoice ledger entry and log a customer notify event.\n\nW9 — the two ways this used to lose money:\n\n* An invoice created before a restart was simply gone, so the event was\n  acknowledged (204) and the paid/void status never applied. The ledger\n  is durable now, and a miss RELOADS from the state store before giving\n  up — that covers an event that arrived during downtime (Stripe\n  retries it) and an entry another replica wrote.\n* Applying the status in memory only. The mutation is persisted here;\n  if the store is unreachable we answer 503 so Stripe RETRIES rather\n  than dropping a payment state on the floor.\n\nA genuinely unknown invoice (never ours) still 204s — a non-2xx would\nmake Stripe retry that one forever.","operationId":"stripe_webhook_v1_webhooks_stripe_post","responses":{"204":{"description":"Successful Response"}}}},"/":{"get":{"tags":["root"],"summary":"Root","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"Budget":{"properties":{"budget_id":{"type":"string","title":"Budget Id"},"project_id":{"type":"string","title":"Project Id"},"tenant_name":{"type":"string","title":"Tenant Name","default":""},"amount_cents":{"type":"integer","minimum":0.0,"title":"Amount Cents"},"currency":{"type":"string","title":"Currency","default":"eur"},"period":{"type":"string","title":"Period","default":"monthly"},"created_by":{"type":"string","title":"Created By","default":""},"created_at":{"type":"string","format":"date-time","title":"Created At"},"last_breached_period":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Breached Period"}},"type":"object","required":["budget_id","project_id","amount_cents","created_at"],"title":"Budget"},"BudgetCreateRequest":{"properties":{"amount_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Amount Cents"},"amount_eur":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Amount Eur"},"currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Currency"},"period":{"type":"string","title":"Period","default":"monthly"}},"type":"object","title":"BudgetCreateRequest","description":"The create body — amount only; the tenant is the resolved credential's\nproject (never caller-supplied). v1 period is monthly (spec §1)."},"BudgetListResponse":{"properties":{"count":{"type":"integer","title":"Count"},"budgets":{"items":{"$ref":"#/components/schemas/Budget"},"type":"array","title":"Budgets"}},"type":"object","required":["count","budgets"],"title":"BudgetListResponse"},"CreateInvoiceRequest":{"properties":{"project_id":{"type":"string","title":"Project Id"},"period_start":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Period Start"},"period_end":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Period End"},"customer_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Email"}},"type":"object","required":["project_id"],"title":"CreateInvoiceRequest"},"ExposureUsage":{"properties":{"exposure_id":{"type":"string","title":"Exposure Id"},"name":{"type":"string","title":"Name"},"hostname":{"type":"string","title":"Hostname"},"status":{"type":"string","title":"Status"},"created":{"type":"string","format":"date-time","title":"Created"},"hours":{"type":"number","title":"Hours","default":0.0}},"type":"object","required":["exposure_id","name","hostname","status","created"],"title":"ExposureUsage"},"FipUsage":{"properties":{"fip_id":{"type":"string","title":"Fip Id"},"address":{"type":"string","title":"Address"},"status":{"type":"string","title":"Status"},"created":{"type":"string","format":"date-time","title":"Created"},"hours":{"type":"number","title":"Hours","default":0.0},"network_id":{"type":"string","title":"Network Id","default":""},"estimated":{"type":"boolean","title":"Estimated","default":true}},"type":"object","required":["fip_id","address","status","created"],"title":"FipUsage"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InstanceUsage":{"properties":{"server_id":{"type":"string","title":"Server Id"},"name":{"type":"string","title":"Name"},"status":{"type":"string","title":"Status"},"flavor":{"type":"string","title":"Flavor"},"vcpus":{"type":"integer","title":"Vcpus"},"created":{"type":"string","format":"date-time","title":"Created"},"terminated":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Terminated"},"seconds":{"type":"number","title":"Seconds","default":0.0},"vcpu_seconds":{"type":"number","title":"Vcpu Seconds","default":0.0},"storage_only_seconds":{"type":"number","title":"Storage Only Seconds","default":0.0},"estimated":{"type":"boolean","title":"Estimated","default":true}},"type":"object","required":["server_id","name","status","flavor","vcpus","created"],"title":"InstanceUsage"},"InvoiceListResponse":{"properties":{"count":{"type":"integer","title":"Count"},"invoices":{"items":{"$ref":"#/components/schemas/LedgerEntry"},"type":"array","title":"Invoices"}},"type":"object","required":["count","invoices"],"title":"InvoiceListResponse"},"LedgerEntry":{"properties":{"invoice_id":{"type":"string","title":"Invoice Id"},"project_id":{"type":"string","title":"Project Id"},"tenant_name":{"type":"string","title":"Tenant Name","default":""},"period_start":{"type":"string","format":"date-time","title":"Period Start"},"period_end":{"type":"string","format":"date-time","title":"Period End"},"currency":{"type":"string","title":"Currency","default":"eur"},"lines":{"items":{"$ref":"#/components/schemas/RatedLine"},"type":"array","title":"Lines","default":[]},"total_cents":{"type":"integer","title":"Total Cents","default":0},"status":{"type":"string","title":"Status","default":"draft"},"stripe_invoice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stripe Invoice Id"},"stripe_customer_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stripe Customer Id"},"customer_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Email"},"created_by":{"type":"string","title":"Created By","default":""},"created_at":{"type":"string","format":"date-time","title":"Created At"},"history":{"items":{"$ref":"#/components/schemas/LedgerEvent"},"type":"array","title":"History","default":[]},"notify_events":{"items":{"$ref":"#/components/schemas/LedgerEvent"},"type":"array","title":"Notify Events","default":[]}},"type":"object","required":["invoice_id","project_id","period_start","period_end","created_at"],"title":"LedgerEntry"},"LedgerEvent":{"properties":{"ts":{"type":"string","format":"date-time","title":"Ts"},"event":{"type":"string","title":"Event"},"detail":{"type":"string","title":"Detail","default":""}},"type":"object","required":["ts","event"],"title":"LedgerEvent"},"RatedLine":{"properties":{"key":{"type":"string","title":"Key"},"description":{"type":"string","title":"Description"},"quantity":{"type":"number","title":"Quantity"},"unit":{"type":"string","title":"Unit"},"unit_price_eur":{"type":"string","title":"Unit Price Eur"},"amount_eur":{"type":"string","title":"Amount Eur"},"amount_cents":{"type":"integer","title":"Amount Cents"},"estimated":{"type":"boolean","title":"Estimated","default":false}},"type":"object","required":["key","description","quantity","unit","unit_price_eur","amount_eur","amount_cents"],"title":"RatedLine"},"RatedUsage":{"properties":{"currency":{"type":"string","title":"Currency"},"lines":{"items":{"$ref":"#/components/schemas/RatedLine"},"type":"array","title":"Lines"},"total_eur":{"type":"string","title":"Total Eur"},"total_cents":{"type":"integer","title":"Total Cents"}},"type":"object","required":["currency","lines","total_eur","total_cents"],"title":"RatedUsage"},"UsageRecord":{"properties":{"project_id":{"type":"string","title":"Project Id"},"period_start":{"type":"string","format":"date-time","title":"Period Start"},"period_end":{"type":"string","format":"date-time","title":"Period End"},"instances":{"items":{"$ref":"#/components/schemas/InstanceUsage"},"type":"array","title":"Instances","default":[]},"volumes":{"items":{"$ref":"#/components/schemas/VolumeUsage"},"type":"array","title":"Volumes","default":[]},"floating_ips":{"items":{"$ref":"#/components/schemas/FipUsage"},"type":"array","title":"Floating Ips","default":[]},"edge_exposures":{"items":{"$ref":"#/components/schemas/ExposureUsage"},"type":"array","title":"Edge Exposures","default":[]},"totals":{"$ref":"#/components/schemas/UsageTotals","default":{"instance_seconds":0.0,"vcpu_seconds":0.0,"volume_gb_hours":0.0,"fip_hours":0.0,"exposure_hours":0.0}}},"type":"object","required":["project_id","period_start","period_end"],"title":"UsageRecord"},"UsageSummaryResponse":{"properties":{"project_id":{"type":"string","title":"Project Id"},"tenant_name":{"type":"string","title":"Tenant Name"},"period":{"additionalProperties":true,"type":"object","title":"Period"},"usage":{"$ref":"#/components/schemas/UsageRecord"},"rated":{"$ref":"#/components/schemas/RatedUsage"}},"type":"object","required":["project_id","tenant_name","period","usage","rated"],"title":"UsageSummaryResponse"},"UsageTotals":{"properties":{"instance_seconds":{"type":"number","title":"Instance Seconds","default":0.0},"vcpu_seconds":{"type":"number","title":"Vcpu Seconds","default":0.0},"volume_gb_hours":{"type":"number","title":"Volume Gb Hours","default":0.0},"fip_hours":{"type":"number","title":"Fip Hours","default":0.0},"exposure_hours":{"type":"number","title":"Exposure Hours","default":0.0}},"type":"object","title":"UsageTotals"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VolumeUsage":{"properties":{"volume_id":{"type":"string","title":"Volume Id"},"name":{"type":"string","title":"Name"},"status":{"type":"string","title":"Status"},"size_gb":{"type":"integer","title":"Size Gb"},"created":{"type":"string","format":"date-time","title":"Created"},"hours":{"type":"number","title":"Hours","default":0.0},"gb_hours":{"type":"number","title":"Gb Hours","default":0.0},"estimated":{"type":"boolean","title":"Estimated","default":true}},"type":"object","required":["volume_id","name","status","size_gb","created"],"title":"VolumeUsage"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}}