feat(billing,admin): canonical usage-cap + promo openapi types (#35)

Curry-precise, code-faithful types for the spend-cap / promo money surface,
aligned to the HIP-0127 primitive algebra (Meter=usage, Policy=cap verdict,
Money=cents, Schedule=period window).

billing/openapi.yaml:
- /v1/billing/spend-alerts CRUD (list, create, patch, delete) +
  /v1/billing/spend-alerts/authorize (the per-request cap verdict the cloud
  metering gate consumes).
- SpendAlert (scope {project,service}, threshold cap cents, enforce, softPct,
  rateLimitRpm, derived period/resetsAt/periodSpentCents/over/warn),
  SpendAlertCreate, SpendAlertUpdate, CapVerdict, CapReason. spend_cap and
  insufficient_balance are DISTINCT reasons.
- Faithful to commerce api/billing/spend_alerts.go + spend_cap.go and the
  cloud clients/metering verdict.

admin/openapi.yaml:
- /v1/admin/promos CRUD; Promo {percentOff,start,end,plans,active},
  PromoCreate, PromoUpdate — the admin-set discount window.

git/openapi.yaml: single-quote a 400 description whose bare commas made the
merged master fail OpenAPI 3.1 structural validation (same flow-scalar bug
class avoided in the new specs).

Regenerated hanzo.yaml + CAPABILITIES.md; full master validates OpenAPI 3.1.
This commit is contained in:
2026-07-18 09:32:33 -07:00
committed by GitHub
parent 4bac9b547c
commit 7114ce1322
5 changed files with 890 additions and 5 deletions
+19
View File
@@ -2,6 +2,25 @@
## v1.0.0
### Usage-cap + promo canonical types (HIP-0127 primitive algebra)
Curry-precise, code-faithful types for the spend-cap / promo money surface,
aligned to the primitive algebra (Meter = usage measured, Policy = cap-deny
verdict, Money = cents, Schedule = period window).
- `billing/openapi.yaml` — `/v1/billing/spend-alerts` self-service CRUD
(`get` list, `post`, `patch {id}`, `delete {id}`) plus
`/v1/billing/spend-alerts/authorize` (the per-request cap verdict the cloud
metering gate consumes). Schemas `SpendAlert` (scope + `threshold` cap cents
+ `enforce` + derived `period`/`resetsAt`/`periodSpentCents`/`over`/`warn`),
`SpendAlertCreate`, `SpendAlertUpdate`, `CapVerdict`, and `CapReason` — where
`spend_cap` and `insufficient_balance` are DISTINCT reasons. Shapes match
commerce `api/billing/spend_alerts.go` + `spend_cap.go` and the cloud
`clients/metering` verdict.
- `admin/openapi.yaml` — `/v1/admin/promos` admin-set discount CRUD with the
`Promo` (`percentOff`, `start`, `end`, `plans`, `active`), `PromoCreate`,
and `PromoUpdate` schemas.
### 2026-07-11 — git: SSH transport, client-less push, ZAP note, sshUrl
Extended `git/openapi.yaml` to match the new native-git surface in
+100
View File
@@ -38,6 +38,8 @@ tags:
description: Fleet compute and o11y aggregates
- name: Audit
description: Tamper-evident audit trail
- name: Promotions
description: Admin-set, limited-time plan discount windows
security:
- bearerAuth: []
paths:
@@ -280,6 +282,68 @@ paths:
'200': { description: Analytics data, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/AnalyticsData' } } } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/admin/promos:
get:
tags: [Promotions]
summary: List discount promos
operationId: adminListPromos
description: The admin-set, limited-time plan discount windows (e.g. a 50%-off launch promo).
responses:
'200': { description: Promo rows, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { type: array, items: { $ref: '#/components/schemas/Promo' } }, data2: { type: integer } } } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
post:
tags: [Promotions]
summary: Create a discount promo
operationId: adminCreatePromo
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/PromoCreate' }
responses:
'201': { description: Created, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/Promo' } } } } } }
'400': { description: 'Invalid request — percentOff out of [0,100] or end <= start', content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string } } } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/admin/promos/{id}:
get:
tags: [Promotions]
summary: Get one discount promo
operationId: adminGetPromo
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'200': { description: Promo, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/Promo' } } } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Promo not found }
patch:
tags: [Promotions]
summary: Update a discount promo
operationId: adminUpdatePromo
description: Partial update — only fields present in the body change.
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/PromoUpdate' }
responses:
'200': { description: Updated, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/Promo' } } } } } }
'400': { description: Invalid request }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Promo not found }
delete:
tags: [Promotions]
summary: Delete a discount promo
operationId: adminDeletePromo
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'200': { description: Deleted, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string } } } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Promo not found }
components:
securitySchemes:
bearerAuth:
@@ -716,3 +780,39 @@ components:
count: { type: integer, format: int64 }
head: { type: string }
brokenAt: { type: integer, format: int64 }
Promo:
type: object
description: |
An admin-set, limited-time discount window applied to plan pricing (e.g. a
50%-off launch promo), configured at admin.hanzo.ai via /v1/admin/promos.
The discount is LIVE only when `active` is true AND now is within
[start, end). `plans` scopes it to specific plan ids (empty = all plans).
properties:
id: { type: string, description: Server-assigned id. }
percentOff: { type: number, format: double, minimum: 0, maximum: 100, description: 'Percent off list price (50 = 50% off).' }
start: { type: string, format: date-time, description: 'Window start, inclusive (UTC).' }
end: { type: string, format: date-time, description: 'Window end, exclusive (UTC).' }
plans: { type: array, items: { type: string }, description: Plan ids the promo applies to; empty = all plans. }
active: { type: boolean, description: 'Admin on/off toggle; the discount applies only when this is true and now is within [start, end).' }
required: [id, percentOff, start, end, active]
PromoCreate:
type: object
description: Create a discount promo. `id` is server-assigned and ignored on input.
properties:
percentOff: { type: number, format: double, minimum: 0, maximum: 100 }
start: { type: string, format: date-time }
end: { type: string, format: date-time }
plans: { type: array, items: { type: string }, description: Plan ids; empty = all plans. }
active: { type: boolean, default: true }
required: [percentOff, start, end]
additionalProperties: false
PromoUpdate:
type: object
description: Partial update — only the fields present are changed.
properties:
percentOff: { type: number, format: double, minimum: 0, maximum: 100 }
start: { type: string, format: date-time }
end: { type: string, format: date-time }
plans: { type: array, items: { type: string } }
active: { type: boolean }
additionalProperties: false
+168
View File
@@ -23,6 +23,8 @@ servers:
tags:
- name: Billing
description: Per-org usage, balance, GPU launch gate, and saved cards
- name: Spend Caps
description: Per-scope spend alerts, hard caps, rate limits, and the per-request cap verdict
security:
- bearerAuth: []
paths:
@@ -95,6 +97,86 @@ paths:
'200': { description: Saved cards, content: { application/json: { schema: { $ref: '#/components/schemas/PaymentMethods' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/billing/spend-alerts:
get:
tags: [Spend Caps]
summary: List spend alerts / caps
operationId: billingListSpendAlerts
description: The caller org's spend-alert rows (soft budgets, hard caps, rate limits) with DERIVED period spend and over/warn flags for the current UTC month. Org-scoped from the validated token — never another tenant's rows.
responses:
'200': { description: Spend-alert rows, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/SpendAlert' } } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
post:
tags: [Spend Caps]
summary: Create a spend alert / cap
operationId: billingCreateSpendAlert
description: |
Create a soft alert or, with `enforce: true`, a HARD per-scope spend cap. The
owning org is pinned server-side from the validated token (never a body/query
field). At least one of `threshold` (> 0) or `rateLimitRpm` (> 0) is required.
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/SpendAlertCreate' }
responses:
'201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/SpendAlert' } } } }
'400': { description: 'Invalid request — no limit set, negative threshold, softPct out of range, or org row limit (200) reached', content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string } } } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/billing/spend-alerts/authorize:
get:
tags: [Spend Caps]
summary: Per-request spend-cap verdict
operationId: billingAuthorizeSpendCap
description: |
The per-request cap verdict the cloud metering gate consumes (service-to-service).
Evaluates EVERY covering spend-alert row for the (project, service) scope over the
current UTC month (most-restrictive-wins) and denies with reason `spend_cap` when a
hard-enforced cap would be exceeded; fails CLOSED if a hard row's spend is unknown.
Balance is NOT checked here — the DISTINCT `insufficient_balance` reason is layered
by the cloud money-path composite, not this endpoint.
parameters:
- { name: user, in: query, schema: { type: string }, description: Org billing key (equals the org slug). }
- { name: project, in: query, schema: { type: string }, description: 'Scope project axis; "" or "default" = the org-wide default scope.' }
- { name: service, in: query, schema: { type: string }, description: Scope service axis (server-derived route/provider). }
- { name: amount, in: query, schema: { type: integer, format: int64 }, description: 'Proposed charge in USD cents to test against the cap (0 = a pure "already over?" check).' }
- { name: pv, in: query, schema: { type: string, enum: ['0', '1'] }, description: '1 = the project axis is bound to a VALIDATED claim (hardens project-scoped caps); otherwise a project-scoped cap degrades to a soft warn (anti-spoof).' }
- { name: currency, in: query, schema: { type: string, default: usd } }
responses:
'200': { description: Cap verdict, content: { application/json: { schema: { $ref: '#/components/schemas/CapVerdict' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/billing/spend-alerts/{id}:
patch:
tags: [Spend Caps]
summary: Update a spend alert / cap
operationId: billingUpdateSpendAlert
description: Partial update of a spend-alert row the caller's org owns — only fields present in the body change; the rest are preserved. A foreign id 404s (tenant-isolated).
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/SpendAlertUpdate' }
responses:
'200': { description: Updated, content: { application/json: { schema: { $ref: '#/components/schemas/SpendAlert' } } } }
'400': { description: Invalid request (negative threshold / softPct out of range) }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Spend alert not found }
delete:
tags: [Spend Caps]
summary: Delete a spend alert / cap
operationId: billingDeleteSpendAlert
description: Delete a spend-alert row the caller's org owns. A foreign id 404s.
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'204': { description: Deleted }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Spend alert not found }
components:
securitySchemes:
bearerAuth:
@@ -159,3 +241,89 @@ components:
properties:
data: { type: array, items: { $ref: '#/components/schemas/PaymentMethod' } }
additionalProperties: true
SpendAlert:
type: object
description: |
A tenant-configured budget on cumulative spend for a SCOPE (project,
service). It is a soft alert by default; with `enforce: true` it is a HARD
per-scope spend cap — once `period` spend for the scope would exceed
`threshold`, the metering gate denies with reason `spend_cap` (402). Period
spend is DERIVED on demand from the append-only usage ledger for the current
UTC month; the row carries NO running total, so a cap change takes effect on
the next request and can never drift from the debited spend. Scope axes use
"" as a wildcard: ("","") is the org-wide default row, (P,"") every service
in project P, ("",S) service S in any project.
properties:
id: { type: string }
userId: { type: string, description: 'The org the cap is scoped to — server-pinned to the validated X-Org-Id, never a client field.' }
title: { type: string }
threshold: { type: integer, format: int64, description: 'The scope''s spend cap / alert ceiling in USD cents (Money). 0 = no cap (a rate-limit-only row).' }
currency: { type: string, default: usd }
project: { type: string, description: 'Scope project axis; "" = wildcard (the org-wide default row).' }
service: { type: string, description: 'Scope service axis (server-derived route/provider); "" = wildcard (all services).' }
enforce: { type: boolean, description: 'false (default) = SOFT (warn only, never blocks); true = HARD cap (402 on breach).' }
softPct: { type: integer, minimum: 1, maximum: 100, description: Soft-warn threshold as a percent of `threshold` (unset/0 defaults to 80). }
rateLimitRpm: { type: integer, description: Requests-per-minute ceiling for the scope (429 when exceeded). 0 = none. }
period: { type: string, pattern: '^\d{4}-\d{2}$', description: 'The UTC calendar month the cap is measured over, e.g. "2026-07" (Schedule window; resets on the 1st).' }
resetsAt: { type: string, format: date-time, description: Midnight UTC on the first of the next month — when period spend resets to 0. }
periodSpentCents: { type: integer, format: int64, description: 'DERIVED spend for the scope in `period`, in USD cents, summed from the deduped usage ledger. Absent if the aggregation errored.' }
over: { type: boolean, description: 'DERIVED — true iff a further billable request would breach the cap (the SAME boundary `enforce` uses, so it never drifts from the verdict).' }
warn: { type: boolean, description: DERIVED — true iff period spend has reached the soft-warn threshold (`softPct` of `threshold`). }
triggeredAt: { type: string, description: ISO timestamp the alert last fired; empty if never. }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
required: [id, userId, threshold, currency, project, service, enforce, softPct, rateLimitRpm]
additionalProperties: true
SpendAlertCreate:
type: object
description: |
Create a spend alert / cap. The owning org is pinned server-side from the
validated token — a body `userId` is ignored. At least one of `threshold`
(> 0) or `rateLimitRpm` (> 0) must be set.
properties:
title: { type: string }
threshold: { type: integer, format: int64, minimum: 0, description: Spend cap ceiling in USD cents. }
currency: { type: string, default: usd }
project: { type: string, description: 'Scope project axis; "" or "default" = org-wide.' }
service: { type: string, description: 'Scope service axis; "" = all services.' }
enforce: { type: boolean, default: false, description: true = HARD cap; false = soft warn. }
softPct: { type: integer, minimum: 0, maximum: 100, description: Soft-warn percent; 0 defaults to 80. }
rateLimitRpm: { type: integer, minimum: 0, description: Per-minute request ceiling; 0 = none. }
additionalProperties: false
SpendAlertUpdate:
type: object
description: Partial update — only the fields present are changed; absent fields are preserved (a PATCH that flips `enforce` must not wipe `threshold`).
properties:
title: { type: string }
threshold: { type: integer, format: int64, minimum: 0 }
project: { type: string }
service: { type: string }
enforce: { type: boolean }
softPct: { type: integer, minimum: 0, maximum: 100 }
rateLimitRpm: { type: integer, minimum: 0 }
additionalProperties: false
CapReason:
type: string
description: |
The cap/funds verdict reason. "" = allowed (funded and under every covering
cap). `spend_cap` = funded but a HARD-enforced per-scope cap would be exceeded
(the tenant's OWN policy ceiling). `insufficient_balance` = out of prepaid
funds. `spend_cap` and `insufficient_balance` are DISTINCT — one is policy,
one is funds. The `/spend-alerts/authorize` endpoint (a pure cap check) emits
only "" or `spend_cap`; the cloud money-path composite (metering) adds
`insufficient_balance`.
enum: ['', spend_cap, insufficient_balance]
CapVerdict:
type: object
description: |
The per-request spend-cap verdict for a (project, service) scope, evaluated
over the org's spend-alert rows (most-restrictive-wins). Fails CLOSED: if a
hard-enforceable row's spend cannot be computed the verdict denies.
properties:
allow: { type: boolean, description: Whether the request is permitted. }
reason: { $ref: '#/components/schemas/CapReason' }
capCents: { type: integer, format: int64, description: The tightest violated cap's ceiling in USD cents (0 when allowed). }
spentCents: { type: integer, format: int64, description: Period spend for the scope in USD cents at decision time. }
warnPct: { type: integer, description: 'Utilization percent of the most-utilized covering cap when at/over its soft threshold (0 otherwise, and 0 on a deny).' }
required: [allow, reason]
additionalProperties: true
+1 -1
View File
@@ -123,7 +123,7 @@ paths:
schema: { $ref: '#/components/schemas/PushRequest' }
responses:
'200': { description: Pushed, content: { application/json: { schema: { $ref: '#/components/schemas/PushResult' } } } }
'400': { description: Invalid branch, path, files, or encoding }
'400': { description: 'Invalid branch, path, files, or encoding' }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/git/keys:
+602 -4
View File
@@ -1507,6 +1507,166 @@ paths:
type: integer
'401':
$ref: '#/components/responses/admin_Unauthorized'
/v1/admin/promos:
get:
tags:
- Promotions
summary: List discount promos
operationId: admin_adminListPromos
description: The admin-set, limited-time plan discount windows (e.g. a 50%-off launch promo).
responses:
'200':
description: Promo rows
content:
application/json:
schema:
type: object
properties:
status:
type: string
msg:
type: string
data:
type: array
items:
$ref: '#/components/schemas/admin_Promo'
data2:
type: integer
'401':
$ref: '#/components/responses/admin_Unauthorized'
post:
tags:
- Promotions
summary: Create a discount promo
operationId: admin_adminCreatePromo
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/admin_PromoCreate'
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
properties:
status:
type: string
msg:
type: string
data:
$ref: '#/components/schemas/admin_Promo'
'400':
description: "Invalid request \u2014 percentOff out of [0,100] or end <= start"
content:
application/json:
schema:
type: object
properties:
status:
type: string
msg:
type: string
'401':
$ref: '#/components/responses/admin_Unauthorized'
/v1/admin/promos/{id}:
get:
tags:
- Promotions
summary: Get one discount promo
operationId: admin_adminGetPromo
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Promo
content:
application/json:
schema:
type: object
properties:
status:
type: string
msg:
type: string
data:
$ref: '#/components/schemas/admin_Promo'
'401':
$ref: '#/components/responses/admin_Unauthorized'
'404':
description: Promo not found
patch:
tags:
- Promotions
summary: Update a discount promo
operationId: admin_adminUpdatePromo
description: "Partial update \u2014 only fields present in the body change."
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/admin_PromoUpdate'
responses:
'200':
description: Updated
content:
application/json:
schema:
type: object
properties:
status:
type: string
msg:
type: string
data:
$ref: '#/components/schemas/admin_Promo'
'400':
description: Invalid request
'401':
$ref: '#/components/responses/admin_Unauthorized'
'404':
description: Promo not found
delete:
tags:
- Promotions
summary: Delete a discount promo
operationId: admin_adminDeletePromo
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Deleted
content:
application/json:
schema:
type: object
properties:
status:
type: string
msg:
type: string
'401':
$ref: '#/components/responses/admin_Unauthorized'
'404':
description: Promo not found
/v1/admin/referrals:
get:
operationId: referrals_adminListReferrals
@@ -7193,6 +7353,172 @@ paths:
$ref: '#/components/schemas/billing_PaymentMethods'
'401':
$ref: '#/components/responses/billing_Unauthorized'
/v1/billing/spend-alerts:
get:
tags:
- Spend Caps
summary: List spend alerts / caps
operationId: billing_billingListSpendAlerts
description: "The caller org's spend-alert rows (soft budgets, hard caps, rate limits) with DERIVED period spend and\
\ over/warn flags for the current UTC month. Org-scoped from the validated token \u2014 never another tenant's rows."
responses:
'200':
description: Spend-alert rows
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/billing_SpendAlert'
'401':
$ref: '#/components/responses/billing_Unauthorized'
post:
tags:
- Spend Caps
summary: Create a spend alert / cap
operationId: billing_billingCreateSpendAlert
description: 'Create a soft alert or, with `enforce: true`, a HARD per-scope spend cap. The
owning org is pinned server-side from the validated token (never a body/query
field). At least one of `threshold` (> 0) or `rateLimitRpm` (> 0) is required.
'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/billing_SpendAlertCreate'
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/billing_SpendAlert'
'400':
description: "Invalid request \u2014 no limit set, negative threshold, softPct out of range, or org row limit (200)\
\ reached"
content:
application/json:
schema:
type: object
properties:
status:
type: string
msg:
type: string
'401':
$ref: '#/components/responses/billing_Unauthorized'
/v1/billing/spend-alerts/authorize:
get:
tags:
- Spend Caps
summary: Per-request spend-cap verdict
operationId: billing_billingAuthorizeSpendCap
description: "The per-request cap verdict the cloud metering gate consumes (service-to-service).\nEvaluates EVERY covering\
\ spend-alert row for the (project, service) scope over the\ncurrent UTC month (most-restrictive-wins) and denies\
\ with reason `spend_cap` when a\nhard-enforced cap would be exceeded; fails CLOSED if a hard row's spend is unknown.\n\
Balance is NOT checked here \u2014 the DISTINCT `insufficient_balance` reason is layered\nby the cloud money-path\
\ composite, not this endpoint.\n"
parameters:
- name: user
in: query
schema:
type: string
description: Org billing key (equals the org slug).
- name: project
in: query
schema:
type: string
description: Scope project axis; "" or "default" = the org-wide default scope.
- name: service
in: query
schema:
type: string
description: Scope service axis (server-derived route/provider).
- name: amount
in: query
schema:
type: integer
format: int64
description: Proposed charge in USD cents to test against the cap (0 = a pure "already over?" check).
- name: pv
in: query
schema:
type: string
enum:
- '0'
- '1'
description: 1 = the project axis is bound to a VALIDATED claim (hardens project-scoped caps); otherwise a project-scoped
cap degrades to a soft warn (anti-spoof).
- name: currency
in: query
schema:
type: string
default: usd
responses:
'200':
description: Cap verdict
content:
application/json:
schema:
$ref: '#/components/schemas/billing_CapVerdict'
'401':
$ref: '#/components/responses/billing_Unauthorized'
/v1/billing/spend-alerts/{id}:
patch:
tags:
- Spend Caps
summary: Update a spend alert / cap
operationId: billing_billingUpdateSpendAlert
description: "Partial update of a spend-alert row the caller's org owns \u2014 only fields present in the body change;\
\ the rest are preserved. A foreign id 404s (tenant-isolated)."
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/billing_SpendAlertUpdate'
responses:
'200':
description: Updated
content:
application/json:
schema:
$ref: '#/components/schemas/billing_SpendAlert'
'400':
description: Invalid request (negative threshold / softPct out of range)
'401':
$ref: '#/components/responses/billing_Unauthorized'
'404':
description: Spend alert not found
delete:
tags:
- Spend Caps
summary: Delete a spend alert / cap
operationId: billing_billingDeleteSpendAlert
description: Delete a spend-alert row the caller's org owns. A foreign id 404s.
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'204':
description: Deleted
'401':
$ref: '#/components/responses/billing_Unauthorized'
'404':
description: Spend alert not found
/v1/billing/usage:
get:
tags:
@@ -29517,10 +29843,7 @@ paths:
schema:
$ref: '#/components/schemas/git_PushResult'
'400':
description: Invalid branch
path: null
files: null
or encoding: null
description: Invalid branch, path, files, or encoding
'401':
$ref: '#/components/responses/git_Unauthorized'
/v1/git/usage:
@@ -58670,6 +58993,99 @@ components:
type: boolean
updated:
type: string
admin_Promo:
type: object
description: 'An admin-set, limited-time discount window applied to plan pricing (e.g. a
50%-off launch promo), configured at admin.hanzo.ai via /v1/admin/promos.
The discount is LIVE only when `active` is true AND now is within
[start, end). `plans` scopes it to specific plan ids (empty = all plans).
'
properties:
id:
type: string
description: Server-assigned id.
percentOff:
type: number
format: double
minimum: 0
maximum: 100
description: Percent off list price (50 = 50% off).
start:
type: string
format: date-time
description: Window start, inclusive (UTC).
end:
type: string
format: date-time
description: Window end, exclusive (UTC).
plans:
type: array
items:
type: string
description: Plan ids the promo applies to; empty = all plans.
active:
type: boolean
description: Admin on/off toggle; the discount applies only when this is true and now is within [start, end).
required:
- id
- percentOff
- start
- end
- active
admin_PromoCreate:
type: object
description: Create a discount promo. `id` is server-assigned and ignored on input.
properties:
percentOff:
type: number
format: double
minimum: 0
maximum: 100
start:
type: string
format: date-time
end:
type: string
format: date-time
plans:
type: array
items:
type: string
description: Plan ids; empty = all plans.
active:
type: boolean
default: true
required:
- percentOff
- start
- end
additionalProperties: false
admin_PromoUpdate:
type: object
description: "Partial update \u2014 only the fields present are changed."
properties:
percentOff:
type: number
format: double
minimum: 0
maximum: 100
start:
type: string
format: date-time
end:
type: string
format: date-time
plans:
type: array
items:
type: string
active:
type: boolean
additionalProperties: false
admin_RawList:
type: object
description: "Verbatim IAM list envelope \u2014 `data` carries the raw upstream rows."
@@ -61598,6 +62014,48 @@ components:
type: integer
format: int64
additionalProperties: true
billing_CapReason:
type: string
description: "The cap/funds verdict reason. \"\" = allowed (funded and under every covering\ncap). `spend_cap` = funded\
\ but a HARD-enforced per-scope cap would be exceeded\n(the tenant's OWN policy ceiling). `insufficient_balance` =\
\ out of prepaid\nfunds. `spend_cap` and `insufficient_balance` are DISTINCT \u2014 one is policy,\none is funds.\
\ The `/spend-alerts/authorize` endpoint (a pure cap check) emits\nonly \"\" or `spend_cap`; the cloud money-path\
\ composite (metering) adds\n`insufficient_balance`.\n"
enum:
- ''
- spend_cap
- insufficient_balance
billing_CapVerdict:
type: object
description: 'The per-request spend-cap verdict for a (project, service) scope, evaluated
over the org''s spend-alert rows (most-restrictive-wins). Fails CLOSED: if a
hard-enforceable row''s spend cannot be computed the verdict denies.
'
properties:
allow:
type: boolean
description: Whether the request is permitted.
reason:
$ref: '#/components/schemas/billing_CapReason'
capCents:
type: integer
format: int64
description: The tightest violated cap's ceiling in USD cents (0 when allowed).
spentCents:
type: integer
format: int64
description: Period spend for the scope in USD cents at decision time.
warnPct:
type: integer
description: Utilization percent of the most-utilized covering cap when at/over its soft threshold (0 otherwise,
and 0 on a deny).
required:
- allow
- reason
additionalProperties: true
billing_GpuChargeRequest:
type: object
properties:
@@ -61647,6 +62105,146 @@ components:
items:
$ref: '#/components/schemas/billing_PaymentMethod'
additionalProperties: true
billing_SpendAlert:
type: object
description: "A tenant-configured budget on cumulative spend for a SCOPE (project,\nservice). It is a soft alert by\
\ default; with `enforce: true` it is a HARD\nper-scope spend cap \u2014 once `period` spend for the scope would exceed\n\
`threshold`, the metering gate denies with reason `spend_cap` (402). Period\nspend is DERIVED on demand from the append-only\
\ usage ledger for the current\nUTC month; the row carries NO running total, so a cap change takes effect on\nthe\
\ next request and can never drift from the debited spend. Scope axes use\n\"\" as a wildcard: (\"\",\"\") is the\
\ org-wide default row, (P,\"\") every service\nin project P, (\"\",S) service S in any project.\n"
properties:
id:
type: string
userId:
type: string
description: "The org the cap is scoped to \u2014 server-pinned to the validated X-Org-Id, never a client field."
title:
type: string
threshold:
type: integer
format: int64
description: The scope's spend cap / alert ceiling in USD cents (Money). 0 = no cap (a rate-limit-only row).
currency:
type: string
default: usd
project:
type: string
description: Scope project axis; "" = wildcard (the org-wide default row).
service:
type: string
description: Scope service axis (server-derived route/provider); "" = wildcard (all services).
enforce:
type: boolean
description: false (default) = SOFT (warn only, never blocks); true = HARD cap (402 on breach).
softPct:
type: integer
minimum: 1
maximum: 100
description: Soft-warn threshold as a percent of `threshold` (unset/0 defaults to 80).
rateLimitRpm:
type: integer
description: Requests-per-minute ceiling for the scope (429 when exceeded). 0 = none.
period:
type: string
pattern: ^\d{4}-\d{2}$
description: The UTC calendar month the cap is measured over, e.g. "2026-07" (Schedule window; resets on the 1st).
resetsAt:
type: string
format: date-time
description: "Midnight UTC on the first of the next month \u2014 when period spend resets to 0."
periodSpentCents:
type: integer
format: int64
description: DERIVED spend for the scope in `period`, in USD cents, summed from the deduped usage ledger. Absent
if the aggregation errored.
over:
type: boolean
description: "DERIVED \u2014 true iff a further billable request would breach the cap (the SAME boundary `enforce`\
\ uses, so it never drifts from the verdict)."
warn:
type: boolean
description: "DERIVED \u2014 true iff period spend has reached the soft-warn threshold (`softPct` of `threshold`)."
triggeredAt:
type: string
description: ISO timestamp the alert last fired; empty if never.
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
required:
- id
- userId
- threshold
- currency
- project
- service
- enforce
- softPct
- rateLimitRpm
additionalProperties: true
billing_SpendAlertCreate:
type: object
description: "Create a spend alert / cap. The owning org is pinned server-side from the\nvalidated token \u2014 a body\
\ `userId` is ignored. At least one of `threshold`\n(> 0) or `rateLimitRpm` (> 0) must be set.\n"
properties:
title:
type: string
threshold:
type: integer
format: int64
minimum: 0
description: Spend cap ceiling in USD cents.
currency:
type: string
default: usd
project:
type: string
description: Scope project axis; "" or "default" = org-wide.
service:
type: string
description: Scope service axis; "" = all services.
enforce:
type: boolean
default: false
description: true = HARD cap; false = soft warn.
softPct:
type: integer
minimum: 0
maximum: 100
description: Soft-warn percent; 0 defaults to 80.
rateLimitRpm:
type: integer
minimum: 0
description: Per-minute request ceiling; 0 = none.
additionalProperties: false
billing_SpendAlertUpdate:
type: object
description: "Partial update \u2014 only the fields present are changed; absent fields are preserved (a PATCH that flips\
\ `enforce` must not wipe `threshold`)."
properties:
title:
type: string
threshold:
type: integer
format: int64
minimum: 0
project:
type: string
service:
type: string
enforce:
type: boolean
softPct:
type: integer
minimum: 0
maximum: 100
rateLimitRpm:
type: integer
minimum: 0
additionalProperties: false
billing_UsageLedger:
type: object
properties: