Compare commits

...
Author SHA1 Message Date
hanzoandhanzo-dev 3937ea0ead spec: the document catches up to two money-mint removals already in the source
Two subsets on main describe behaviour their own packages no longer have, and
both lies are about MONEY moving. The source removed the mints; the committed
projections were never regenerated, so the published API kept advertising them.

  POST /v1/marketing/promos/{code}/redeem said it credits the discount value to
  the org's wallet through the finance ledger. It does not: the redemption is a
  row, and credit is an admin decision against an auditable ledger. Its plan and
  seats inputs are gone too -- the plan is derived from the org's live paid
  subscription and can never be named by the caller, and the seat count is the
  single-seat floor, so neither can inflate a recorded figure.

  POST /v1/admin/referrals/sweep said qualifying grants the referrer and the
  referee their bonuses, backed against a platform reserve fund. Qualifying now
  moves no money at all: it records that an attribution became a real customer,
  and what is owed for that is an affiliate payable in commerce. GET /v1/referrals
  no longer credits by the act of loading the page, and the admin bonuses read
  carries no amounts because the package issues none.

Regenerated, not edited: describe for each of the two apps, then the weave.
Nothing about the wire changes -- the published (path, method, operationId) set
is identical at 2352 operations, no operationId moved, and the floor does not
move in either direction. Exactly five operations change their prose, all of
them in these two packages.

Separate from the tier fix beside it: that one adds an operation's missing
sentence, this one corrects sentences that are false about money.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 00:02:07 -07:00
hanzoandhanzo-dev 492722073d commerce: the per-request tier read says what it answers, and the document records it
GET /v1/billing/tier is registered on the billing read chain and the ai router
reads it on every request to pick that caller's rate-limit tier, but the handler
lives in the vendored hanzoai/commerce module, so zipdoc has no doc comment in
this repo to lift. It reached the published surface saying nothing about itself,
which is what openapi.Complete refuses:

    commerce: 1 operation(s) say nothing about themselves:
      GET /v1/billing/tier

An untyped route states its prose with openapi.Describe beside the route, so
that is where it is stated, in describe.go with the rest of the billing reads.
The sentence a caller needs is that the balance to gate on is effectiveAvailable
and not prepaidAvailable: granted credits spend too, credits first, so an
account funded only by a grant reads zero prepaid while holding real spendable
credit -- and with the daily term zero on every tier there is nothing behind it.

No money logic is touched: describe.go is prose. The two zipdoc_gen.go files
were already stale on main against their own sources and are regenerated, not
edited -- commerce had a Describe for a POST /finance/starter that no longer
exists, projects was missing two that do.

The set of published (path, method, operationId) changes by exactly one:
2351 -> 2352 operations, GET /v1/billing/tier added, none removed, no
operationId changed and no other operation's summary, description or tags
touched. The floor moves only upward (billing 26 -> 27).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 00:01:03 -07:00
8 changed files with 160 additions and 228 deletions
+21
View File
@@ -137,6 +137,27 @@ func describeBilling() {
"rather than a silently blank list. It is a catalog read, not an entitlement read — "+
"it says what may be bought, never what this caller has.")
openapi.Describe("/v1/billing/tier", http.MethodGet,
"The subject's plan tier and the balance a metered call is admitted on",
"Answers one subject's resolved tier — name, display name, agent ceiling and allowed "+
"models — with the balance that admits their next metered call: prepaidAvailable, "+
"creditsRemaining, dailyRemaining and the effectiveAvailable those fold into. The ai "+
"router reads it per request to pick that caller's rate-limit tier. It sits on the "+
"org-resolving chain because a tier is org state, and the subject keys are pinned to "+
"the validated caller before the handler runs, so a browser read is always the "+
"caller's own; user is required, which only a service-to-service caller can omit and "+
"be refused 400 for. The tier is an upstream tier claim, or an explicit tier "+
"override, when either is present — that is the service-to-service contract — and is "+
"otherwise DERIVED from the org's active and trialing subscriptions, the highest one "+
"winning, its paid-ness read from the plan catalog by slug rather than from the "+
"subscription's own stored copy. The rule to get right is effectiveAvailable and not "+
"prepaidAvailable: granted credits spend too, credits first, so an account funded "+
"only by a grant reads zero prepaid while holding real spendable credit — and with "+
"the daily term zero on every tier there is no free allowance behind it, so a "+
"zero-balance account is gated. A subscription-store error answers 500 rather than "+
"downgrading to free, so a transient failure never reports a paid subscriber as "+
"unsubscribed.")
openapi.Describe("/v1/billing/alerts", http.MethodGet,
"List your org's spend caps and rate limits",
"Returns the caps and alerts keyed to the caller's own billing subject, each with its "+
-3
View File
@@ -37,9 +37,6 @@ func init() {
zip.Describe("POST /finance/scope-rules", zip.Doc{
Description: "Lists the caller org's per-scope request-rate ceilings, so the edge limiter in\nanother process can enforce a budget whose rows it cannot open.\n\nIt takes NO input: the org rides the caller and there is nothing else to name,\nso one org can never read another's ceilings. Only rows that SET a ceiling are\nreturned — a spend-alert row with no rate limit is a spend cap, a different\npolicy answered by a different op, and shipping it here would make the limiter\nweigh rules that bind nothing.\n\nThe row scan is the org's WHOLE policy set — the same query commerce's own cap\nverdict runs (loadOrgScopes), bounded the same way. The retired HTTP read went\nthrough the per-subject list instead, so the rate ceiling and the spend cap\ncould in principle bind on different rows; one query is what makes that\nimpossible rather than merely unlikely.\n\nA named handler, not a closure, so zipdoc can lift this prose into the registry.",
})
zip.Describe("POST /finance/starter", zip.Doc{
Description: "Grants a new account its opening welcome credit and answers the amount granted.\n\nGRANTED ONCE, whoever asks. The idempotency key is the ACCOUNT and nothing\nelse, so asking twice — from two processes, after a restart, or concurrently —\ngrants exactly once; that property lives in the ledger, which dedups inside the\nsame transaction as the insert, and this op only carries the question across.\n\nThe org is the CALLER'S and can never be named in the input; an empty subject\ngrants to the org's own account. It is published because the grant runs as\nmiddleware on EVERY app's chain while the ledger has one writer and lives here\n— a grant that could not reach it left new orgs unfunded and correctly\npaywalled for a reason nobody had chosen.\n\nA named handler, not a closure, so zipdoc can lift this prose into the registry.",
})
zip.Describe("POST /finance/txns", zip.Doc{
Description: "Lists the caller org's ledger entries — id, kind, ref, memo, amount and\ntimestamp — most recent first and bounded to one page. It is the movement list\nbehind the customer-facing transactions, credits and receipts pages, all three\nof which read this one list.\n\nIt takes NO input: the org comes from the caller and there is nothing else to\nname, so one org can never read another's entries. Unlike the usage read the\namount crosses as a DECIMAL STRING with its currency rather than a bare\nquantity, because an entry is a movement a customer reads rather than a number\na gate does arithmetic on. A ledger implementation that cannot list entries is\nan error, not an empty page.\n\nA named handler, not a closure, so zipdoc can lift this prose into the registry.",
})
+6
View File
@@ -172,6 +172,12 @@ func init() {
"projectsUpdate.visibility": "Visibility flips an existing project between \"public\" and \"private\". Same\nONE rule as at create: public is free, private needs a paid plan.",
},
})
zip.Describe("POST /sites/resolve", zip.Doc{
Description: "Answers the multi-tenant product URL (<slug>.hanzo.app) and\nbound custom domains. Not-found is `Found:false`, never an error: the edge\nturns that into an honest 404, and an error into a 503. Collapsing the two\nwould serve 404s for real live sites during a transient failure.",
})
zip.Describe("POST /sites/resolve-org", zip.Doc{
Description: "Is the first-party path: it NEVER falls back to\nunique-across-orgs, so an internal host is served only by our own project and\nnever a customer's same-named one.",
})
zip.Describe("POST /v1/platform/sites", zip.Doc{
Description: "Creates a project — the handle a site is deployed and served\nunder — and answers 201 with it in `draft`.\n\n`name` is required; `slug` is derived from the name when omitted and is the\nidentifier that matters — it becomes the S3 key segment, the public host\n`<slug>.hanzo.app`, and the handle every later call addresses, so it must\nmatch `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$` and may not be a reserved label\nsuch as `api` or `admin`. `framework` is a build hint from a closed set,\ndefaulting to `static`; it never gates a deploy, it only tells CI how to build\na linked repo.\n\nTwo defaults are worth knowing: the analytics beacon is ON unless `analytics`\nis explicitly false, and `visibility` is `public` unless asked otherwise.\nPublishing publicly is free; PRIVATE is the paid feature, and an unfunded org\nasking for it is refused rather than quietly published as public. Creation\nalso provisions the project's data space and a canonical git repo, both\nbest-effort — neither can fail the create.\n\nScope: a validated principal is required (403 without one) and the project is\ncreated in THAT principal's org. The slug is unique per org, so a slug already\nused in the caller's own org is a 409 while the same slug in another org is\nirrelevant.",
Fields: map[string]string{
+96 -132
View File
@@ -4580,28 +4580,22 @@ components:
instrument:
description: |-
Instrument identifies the payment method. It is the anti-farming key: one
redemption per instrument, fleet-wide.
redemption per instrument, fleet-wide, and it is REQUIRED — an absent
instrument is refused, never waved through.
type: string
plan:
description: 'Plan is the plan being redeemed against: pro, max or team.'
type: string
seats:
description: |-
Seats is the Team seat count; 0 means 1. Seats beyond the promo's
teamSeatCap bill at list.
type: integer
type: object
RedeemResult:
properties:
alreadyRedeemed:
description: |-
AlreadyRedeemed is true when this org had already taken the promo and the
call was an idempotent replay — nothing was credited a second time.
call was an idempotent replay.
type: boolean
chargeCents:
description: |-
ChargeCents is what month one costs after the discount, DiscountCents the
credit that produced it.
discount that produced it. Both are quoted figures against the org's
derived plan — NOTHING WAS CREDITED and no wallet moved.
type: integer
discountCents:
type: integer
@@ -4613,17 +4607,18 @@ components:
code:
description: Code is the promo redeemed.
type: string
creditCents:
discountCents:
description: |-
CreditCents is the discount value credited to the org's wallet — the promo
is realized as a NON-CASH credit, not a subscription coupon.
DiscountCents is the month-one discount this redemption CLAIMS, in USD
cents. It is a recorded figure, NOT a balance: nothing was credited and no
wallet moved. An admin granting against this claim is what would make it
money, and that decision happens on the admin surface, not here.
type: integer
creditEntryId:
description: CreditEntryID is the finance ledger entry that credit landed
in.
type: string
plan:
description: Plan and Seats are what was redeemed against.
description: |-
Plan and Seats are what was redeemed against. Both are DERIVED server-side
— Plan from the org's live paid subscription, Seats from claimSeats — and
neither is ever read from the request.
type: string
redeemedAt:
description: RedeemedAt is unix seconds.
@@ -7586,7 +7581,7 @@ components:
adminBonusDirectory:
properties:
referrals:
description: Referrals is every referral in the ledger, both orgs exposed.
description: Referrals is every referral in the directory, both orgs exposed.
items:
$ref: '#/components/schemas/adminReferralView'
type: array
@@ -7721,11 +7716,6 @@ components:
createdAt:
description: CreatedAt is when the referral was recorded, as a Unix timestamp.
type: integer
creditedAt:
description: |-
CreditedAt is when the bonuses were latched and granted, as a Unix
timestamp; 0 until they are.
type: integer
id:
description: ID is the referral's handle.
type: string
@@ -7734,36 +7724,14 @@ components:
QualifiedAt is when the referee first made metered spend, as a Unix
timestamp; 0 while still pending.
type: integer
refereeGrantCents:
description: |-
RefereeGrantCents is what the referee was granted, in USD cents; 0 until the
referral is credited.
type: integer
refereeOrg:
description: RefereeOrg is the org that signed up with it.
type: string
refereeTxn:
description: |-
RefereeTxn is the commerce ledger transaction that carried the referee's
grant, omitted until one exists.
type: string
referrerGrantCents:
description: |-
ReferrerGrantCents is what the referrer was granted, in USD cents; 0 until
the referral is credited.
type: integer
referrerOrg:
description: ReferrerOrg is the org whose code was used.
type: string
referrerTxn:
description: |-
ReferrerTxn is the commerce ledger transaction that carried the referrer's
grant, omitted until one exists.
type: string
status:
description: |-
Status is the referral's lifecycle state: "signup", "qualified" or
"credited".
description: 'Status is the referral''s lifecycle state: "signup" or "qualified".'
type: string
type: object
adminReportData:
@@ -7796,22 +7764,14 @@ components:
type: object
adminSummary:
properties:
credited:
description: Credited is how many have paid both bonuses.
type: integer
grantedCents:
description: |-
GrantedCents is the promo credit granted across BOTH sides of every
referral, in USD cents — the program's total liability to date.
type: integer
qualified:
description: Qualified is how many have qualified but are not yet credited.
description: Qualified is how many referees have made metered spend.
type: integer
signup:
description: Signup is how many are recorded but not yet qualified.
type: integer
total:
description: Total is every referral in the ledger.
description: Total is every referral in the directory.
type: integer
type: object
advanceIn:
@@ -18385,16 +18345,6 @@ components:
createdAt:
description: CreatedAt is when the referral was recorded, as a Unix timestamp.
type: integer
creditedAt:
description: |-
CreditedAt is when the bonuses were latched and granted, as a Unix
timestamp; 0 until they are. It is the at-most-once latch.
type: integer
creditsCents:
description: |-
CreditsCents is what I earned from this referral, in USD cents. It is 0
until the referee qualifies.
type: integer
id:
description: ID is the referral's handle.
type: string
@@ -18408,8 +18358,8 @@ components:
type: string
status:
description: |-
Status is the referral's lifecycle state: "signup" until the referee
makes metered spend, then "qualified", then "credited".
Status is the referral's lifecycle state: "signup" until the referee makes
metered spend, then "qualified".
type: string
type: object
myReferrals:
@@ -18422,29 +18372,15 @@ components:
counts:
$ref: '#/components/schemas/statusCounts'
description: Counts tallies this org's referrals by status.
creditsEarnedCents:
description: |-
CreditsEarnedCents is the total promo credit this org has earned as the
REFERRER, in USD cents.
type: integer
link:
description: Link is the shareable signup link carrying the code, on the
brand's own host.
type: string
refereeBonusCents:
description: RefereeBonusCents is what a referee is granted on qualification,
in USD cents.
type: integer
referrals:
description: Referrals is one row per org that signed up with this code.
items:
$ref: '#/components/schemas/myReferralView'
type: array
referrerBonusCents:
description: |-
ReferrerBonusCents is what the referrer is granted when a referee qualifies,
in USD cents.
type: integer
type: object
namespaceCreateIn:
properties:
@@ -35841,11 +35777,8 @@ components:
type: object
statusCounts:
properties:
credited:
description: Credited is how many referrals have paid both bonuses.
type: integer
qualified:
description: Qualified is how many referees have spent but are not yet credited.
description: Qualified is how many referees have made metered spend.
type: integer
signup:
description: Signup is how many referees have signed up but not yet spent.
@@ -36411,10 +36344,9 @@ components:
type: object
sweepResult:
properties:
credited:
description: |-
Credited is how many of those referrals qualified on this pass and were
granted their bonuses.
qualified:
description: Qualified is how many of those referrals qualified on this
pass.
type: integer
swept:
description: Swept is how many pending referrals were checked.
@@ -41097,13 +41029,13 @@ paths:
/v1/admin/referrals/bonuses:
get:
description: |-
Returns every one-time referral bonus in the ledger with a fleet summary.
Returns every referral edge in the directory with a fleet summary.
SuperAdmin only, fail-closed. This is the ONE-TIME BONUS ledger — who referred
whom, what each side was granted and which ledger transactions carried it. The
cross-tenant referral ANALYTICS board (top referrers, conversion, multi-level
accrual liability) is a different surface, GET /v1/admin/referrals, owned by the
affiliates subsystem over the shared attribution spine.
SuperAdmin only, fail-closed. This is the ATTRIBUTION directory — who referred
whom and whether that referee became a customer. It carries no amounts because
this package issues none. The cross-tenant referral ANALYTICS board (top
referrers, conversion) is a different surface, GET /v1/admin/referrals, owned by
the affiliates subsystem over the shared attribution spine.
operationId: get_v1_admin_referrals_bonuses
parameters:
- description: |-
@@ -41124,25 +41056,24 @@ paths:
schema:
$ref: '#/components/schemas/adminBonusesEnvelope'
description: ok
summary: Returns every one-time referral bonus in the ledger with a fleet summary.
summary: Returns every referral edge in the directory with a fleet summary.
tags:
- admin
x-app: referrals
/v1/admin/referrals/sweep:
post:
description: |-
Qualify-checks every pending referral and grants the ones that now qualify.
Qualify-checks every pending referral and advances the ones that now qualify.
SuperAdmin only, fail-closed. This is the cron path: a referee QUALIFIES once
they have made metered spend — the honest signal that they actually used the
product rather than merely claiming a welcome grant — and qualifying grants the
referrer and the referee their bonuses in one latched step.
SuperAdmin only, fail-closed. This is the cron path, and the ONLY path that
advances a referral: a referee QUALIFIES once they have made metered spend — the
honest signal that they actually used the product rather than merely signing up.
The grant is backed against the platform reserve fund before it is latched, so
an empty fund leaves the referral honestly pending rather than minting unbacked
credit, and the latch makes it at-most-once: this sweep, a concurrent sweep and
the lazy check on GET /v1/referrals can never double-pay. One pass is bounded,
so a large backlog drains over several runs instead of wedging one request.
Qualifying moves NO money. It records that an attribution became a real customer;
what is owed for that is an affiliate payable in commerce, settled by wire or to a
connected wallet. One pass is bounded, so a large backlog drains over several runs
instead of wedging one request, and the latch makes the transition at-most-once
under a concurrent sweep.
It reads nothing from the caller — the counters it returns are the whole result.
operationId: post_v1_admin_referrals_sweep
@@ -41153,7 +41084,7 @@ paths:
schema:
$ref: '#/components/schemas/sweepEnvelope'
description: ok
summary: Qualify-checks every pending referral and grants the ones that now
summary: Qualify-checks every pending referral and advances the ones that now
qualify.
tags:
- admin
@@ -47955,6 +47886,32 @@ paths:
tags:
- billing
x-app: commerce
/v1/billing/tier:
get:
description: 'Answers one subject''s resolved tier — name, display name, agent
ceiling and allowed models — with the balance that admits their next metered
call: prepaidAvailable, creditsRemaining, dailyRemaining and the effectiveAvailable
those fold into. The ai router reads it per request to pick that caller''s
rate-limit tier. It sits on the org-resolving chain because a tier is org
state, and the subject keys are pinned to the validated caller before the
handler runs, so a browser read is always the caller''s own; user is required,
which only a service-to-service caller can omit and be refused 400 for. The
tier is an upstream tier claim, or an explicit tier override, when either
is present — that is the service-to-service contract — and is otherwise DERIVED
from the org''s active and trialing subscriptions, the highest one winning,
its paid-ness read from the plan catalog by slug rather than from the subscription''s
own stored copy. The rule to get right is effectiveAvailable and not prepaidAvailable:
granted credits spend too, credits first, so an account funded only by a grant
reads zero prepaid while holding real spendable credit — and with the daily
term zero on every tier there is no free allowance behind it, so a zero-balance
account is gated. A subscription-store error answers 500 rather than downgrading
to free, so a transient failure never reports a paid subscriber as unsubscribed.'
operationId: get_v1_billing_tier
summary: The subject's plan tier and the balance a metered call is admitted
on
tags:
- billing
x-app: commerce
/v1/billing/topup/token:
post:
description: 'Charges the single-use card token for the given amount and credits
@@ -69649,13 +69606,22 @@ paths:
/v1/marketing/promos/{code}/redeem:
post:
description: |-
Redeems the promo for the caller's org, crediting the discount
value to its wallet through the finance ledger. Three guards run under one
lock so the cap cannot be raced past: the fleet-wide redemption cap, one
redemption per org, and one per payment instrument.
Records the caller org's claim on a promo. NOTHING IS CREDITED:
the redemption is a row, and credit into an org is an admin decision made on
the admin surface against an auditable ledger.
The plan is DERIVED from the org's live ACTIVE/TRIALING paid subscription and
can never be named by the caller — an org with no qualifying subscription is
refused, and so is one whose subscription cannot be read. The seat count is
the single-seat floor (claimSeats), so the recorded figure has no input that
can inflate it.
Guards run under one lock so the cap cannot be raced past: the fleet-wide
redemption cap, one redemption per org, one per payment instrument (REQUIRED),
and the per-redemption ceiling.
It is IDEMPOTENT: an org that already redeemed gets its original redemption
back with alreadyRedeemed true and is not credited twice.
back with alreadyRedeemed true.
operationId: post_v1_marketing_promos_code_redeem
parameters:
- description: Code is the promo code from the path.
@@ -69671,8 +69637,6 @@ paths:
example:
code: first1000
instrument: pm_1QxYz2AbCdEf
plan: pro
seats: 1
schema:
$ref: '#/components/schemas/RedeemInput'
required: true
@@ -69683,8 +69647,7 @@ paths:
schema:
$ref: '#/components/schemas/RedeemResult'
description: ok
summary: Redeems the promo for the caller's org, crediting the discount value
to its wallet through the finance ledger.
summary: Records the caller org's claim on a promo.
tags:
- marketing
x-app: marketing
@@ -86777,15 +86740,15 @@ paths:
Returns the caller's referral code, share link and the referrals they have made.
The code is a stable, deterministic function of the org, so the link in this
response is the same one every time. Each row carries the referee, its status and
the credit this org earned from it; creditsEarnedCents is their sum.
response is the same one every time. Each row carries the referee and the status
of that attribution.
The read is self-updating: before listing, it runs the qualify check over this
org's still-pending referees, so a referee who has since made metered spend is
credited by the act of the referrer loading their page. That check is
best-effort and bounded — a commerce hiccup leaves the referral pending for the
next check rather than failing the page — and the grant is latched at-most-once,
so this path and the admin sweep can never double-pay.
IT IS A PURE READ. It advances no referral, grants nothing and deposits nothing
— a GET reports state, it never changes it. Qualification is the admin sweep's
job (POST /v1/admin/referrals/sweep). The one row this handler can write is the
caller's OWN code-directory entry (EnsureCode), which materialises a value
deriveCode already computes deterministically from the org id so the code has an
O(1) reverse lookup; it carries no money, no referral state and no other tenant.
operationId: get_v1_referrals
responses:
"200":
@@ -86812,9 +86775,10 @@ paths:
call returns the referral already on file with created=false and 200, where the
first call answers 201.
Recording a referral grants nothing. Both bonuses are granted later, when the
referee actually makes metered spend — see GET /v1/referrals and
POST /v1/admin/referrals/sweep.
Recording a referral grants nothing, and neither does anything downstream of it:
the edge later advances to qualified when the referee makes metered spend
(POST /v1/admin/referrals/sweep), and that is the end of it. No credit is ever
issued from this package.
operationId: post_v1_referrals_claim
requestBody:
content:
@@ -96503,8 +96467,8 @@ tags:
a cloud binary with the money, ingest and telemetry callbacks cloud BUILDS but
cannot INSTALL.
name: rag
- description: Package referrals is credit for both sides when someone you refer actually
spends.
- description: 'Package referrals is referral ATTRIBUTION: who referred whom, and
whether that referee ever became a real customer.'
name: referrals
- description: 'Package registry is your container and package registry: push images,
pull them back, see what you store.'
+3 -3
View File
@@ -1,6 +1,6 @@
{
"paths": 1696,
"operations": 2351,
"paths": 1697,
"operations": 2352,
"products": {
"admin": 87,
"ads": 7,
@@ -20,7 +20,7 @@
"balancers": 4,
"base": 1,
"benchmark": 6,
"billing": 26,
"billing": 27,
"blueprint": 3,
"books": 25,
"bot": 11,
+10
View File
@@ -265,6 +265,16 @@
]
}
},
"/v1/billing/tier": {
"get": {
"operationId": "get_v1_billing_tier",
"summary": "The subject's plan tier and the balance a metered call is admitted on",
"description": "Answers one subject's resolved tier — name, display name, agent ceiling and allowed models — with the balance that admits their next metered call: prepaidAvailable, creditsRemaining, dailyRemaining and the effectiveAvailable those fold into. The ai router reads it per request to pick that caller's rate-limit tier. It sits on the org-resolving chain because a tier is org state, and the subject keys are pinned to the validated caller before the handler runs, so a browser read is always the caller's own; user is required, which only a service-to-service caller can omit and be refused 400 for. The tier is an upstream tier claim, or an explicit tier override, when either is present — that is the service-to-service contract — and is otherwise DERIVED from the org's active and trialing subscriptions, the highest one winning, its paid-ness read from the plan catalog by slug rather than from the subscription's own stored copy. The rule to get right is effectiveAvailable and not prepaidAvailable: granted credits spend too, credits first, so an account funded only by a grant reads zero prepaid while holding real spendable credit — and with the daily term zero on every tier there is no free allowance behind it, so a zero-balance account is gated. A subscription-store error answers 500 rather than downgrading to free, so a transient failure never reports a paid subscriber as unsubscribed.",
"tags": [
"billing"
]
}
},
"/v1/billing/topup/token": {
"post": {
"operationId": "post_v1_billing_topup_token",
+9 -23
View File
@@ -749,8 +749,8 @@
"/v1/marketing/promos/{code}/redeem": {
"post": {
"operationId": "post_v1_marketing_promos_code_redeem",
"summary": "Redeems the promo for the caller's org, crediting the discount value to its wallet through the finance ledger.",
"description": "Redeems the promo for the caller's org, crediting the discount\nvalue to its wallet through the finance ledger. Three guards run under one\nlock so the cap cannot be raced past: the fleet-wide redemption cap, one\nredemption per org, and one per payment instrument.\n\nIt is IDEMPOTENT: an org that already redeemed gets its original redemption\nback with alreadyRedeemed true and is not credited twice.",
"summary": "Records the caller org's claim on a promo.",
"description": "Records the caller org's claim on a promo. NOTHING IS CREDITED:\nthe redemption is a row, and credit into an org is an admin decision made on\nthe admin surface against an auditable ledger.\n\nThe plan is DERIVED from the org's live ACTIVE/TRIALING paid subscription and\ncan never be named by the caller — an org with no qualifying subscription is\nrefused, and so is one whose subscription cannot be read. The seat count is\nthe single-seat floor (claimSeats), so the recorded figure has no input that\ncan inflate it.\n\nGuards run under one lock so the cap cannot be raced past: the fleet-wide\nredemption cap, one redemption per org, one per payment instrument (REQUIRED),\nand the per-redemption ceiling.\n\nIt is IDEMPOTENT: an org that already redeemed gets its original redemption\nback with alreadyRedeemed true.",
"tags": [
"marketing"
],
@@ -771,9 +771,7 @@
"application/json": {
"example": {
"code": "first1000",
"instrument": "pm_1QxYz2AbCdEf",
"plan": "pro",
"seats": 1
"instrument": "pm_1QxYz2AbCdEf"
},
"schema": {
"$ref": "#/components/schemas/RedeemInput"
@@ -1798,16 +1796,8 @@
"type": "string"
},
"instrument": {
"description": "Instrument identifies the payment method. It is the anti-farming key: one\nredemption per instrument, fleet-wide.",
"description": "Instrument identifies the payment method. It is the anti-farming key: one\nredemption per instrument, fleet-wide, and it is REQUIRED — an absent\ninstrument is refused, never waved through.",
"type": "string"
},
"plan": {
"description": "Plan is the plan being redeemed against: pro, max or team.",
"type": "string"
},
"seats": {
"description": "Seats is the Team seat count; 0 means 1. Seats beyond the promo's\nteamSeatCap bill at list.",
"type": "integer"
}
},
"type": "object"
@@ -1815,11 +1805,11 @@
"RedeemResult": {
"properties": {
"alreadyRedeemed": {
"description": "AlreadyRedeemed is true when this org had already taken the promo and the\ncall was an idempotent replay — nothing was credited a second time.",
"description": "AlreadyRedeemed is true when this org had already taken the promo and the\ncall was an idempotent replay.",
"type": "boolean"
},
"chargeCents": {
"description": "ChargeCents is what month one costs after the discount, DiscountCents the\ncredit that produced it.",
"description": "ChargeCents is what month one costs after the discount, DiscountCents the\ndiscount that produced it. Both are quoted figures against the org's\nderived plan — NOTHING WAS CREDITED and no wallet moved.",
"type": "integer"
},
"discountCents": {
@@ -1837,16 +1827,12 @@
"description": "Code is the promo redeemed.",
"type": "string"
},
"creditCents": {
"description": "CreditCents is the discount value credited to the org's wallet — the promo\nis realized as a NON-CASH credit, not a subscription coupon.",
"discountCents": {
"description": "DiscountCents is the month-one discount this redemption CLAIMS, in USD\ncents. It is a recorded figure, NOT a balance: nothing was credited and no\nwallet moved. An admin granting against this claim is what would make it\nmoney, and that decision happens on the admin surface, not here.",
"type": "integer"
},
"creditEntryId": {
"description": "CreditEntryID is the finance ledger entry that credit landed in.",
"type": "string"
},
"plan": {
"description": "Plan and Seats are what was redeemed against.",
"description": "Plan and Seats are what was redeemed against. Both are DERIVED server-side\n— Plan from the org's live paid subscription, Seats from claimSeats — and\nneither is ever read from the request.",
"type": "string"
},
"redeemedAt": {
+15 -67
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "Hanzo Cloud API",
"description": "Package referrals is credit for both sides when someone you refer actually spends.",
"description": "Package referrals is referral ATTRIBUTION: who referred whom, and whether that referee ever became a real customer.",
"version": "v1"
},
"servers": [
@@ -22,8 +22,8 @@
"/v1/admin/referrals/bonuses": {
"get": {
"operationId": "get_v1_admin_referrals_bonuses",
"summary": "Returns every one-time referral bonus in the ledger with a fleet summary.",
"description": "Returns every one-time referral bonus in the ledger with a fleet summary.\n\nSuperAdmin only, fail-closed. This is the ONE-TIME BONUS ledger — who referred\nwhom, what each side was granted and which ledger transactions carried it. The\ncross-tenant referral ANALYTICS board (top referrers, conversion, multi-level\naccrual liability) is a different surface, GET /v1/admin/referrals, owned by the\naffiliates subsystem over the shared attribution spine.",
"summary": "Returns every referral edge in the directory with a fleet summary.",
"description": "Returns every referral edge in the directory with a fleet summary.\n\nSuperAdmin only, fail-closed. This is the ATTRIBUTION directory — who referred\nwhom and whether that referee became a customer. It carries no amounts because\nthis package issues none. The cross-tenant referral ANALYTICS board (top\nreferrers, conversion) is a different surface, GET /v1/admin/referrals, owned by\nthe affiliates subsystem over the shared attribution spine.",
"tags": [
"admin"
],
@@ -55,8 +55,8 @@
"/v1/admin/referrals/sweep": {
"post": {
"operationId": "post_v1_admin_referrals_sweep",
"summary": "Qualify-checks every pending referral and grants the ones that now qualify.",
"description": "Qualify-checks every pending referral and grants the ones that now qualify.\n\nSuperAdmin only, fail-closed. This is the cron path: a referee QUALIFIES once\nthey have made metered spend — the honest signal that they actually used the\nproduct rather than merely claiming a welcome grant — and qualifying grants the\nreferrer and the referee their bonuses in one latched step.\n\nThe grant is backed against the platform reserve fund before it is latched, so\nan empty fund leaves the referral honestly pending rather than minting unbacked\ncredit, and the latch makes it at-most-once: this sweep, a concurrent sweep and\nthe lazy check on GET /v1/referrals can never double-pay. One pass is bounded,\nso a large backlog drains over several runs instead of wedging one request.\n\nIt reads nothing from the caller — the counters it returns are the whole result.",
"summary": "Qualify-checks every pending referral and advances the ones that now qualify.",
"description": "Qualify-checks every pending referral and advances the ones that now qualify.\n\nSuperAdmin only, fail-closed. This is the cron path, and the ONLY path that\nadvances a referral: a referee QUALIFIES once they have made metered spend — the\nhonest signal that they actually used the product rather than merely signing up.\n\nQualifying moves NO money. It records that an attribution became a real customer;\nwhat is owed for that is an affiliate payable in commerce, settled by wire or to a\nconnected wallet. One pass is bounded, so a large backlog drains over several runs\ninstead of wedging one request, and the latch makes the transition at-most-once\nunder a concurrent sweep.\n\nIt reads nothing from the caller — the counters it returns are the whole result.",
"tags": [
"admin"
],
@@ -78,7 +78,7 @@
"get": {
"operationId": "get_v1_referrals",
"summary": "Returns the caller's referral code, share link and the referrals they have made.",
"description": "Returns the caller's referral code, share link and the referrals they have made.\n\nThe code is a stable, deterministic function of the org, so the link in this\nresponse is the same one every time. Each row carries the referee, its status and\nthe credit this org earned from it; creditsEarnedCents is their sum.\n\nThe read is self-updating: before listing, it runs the qualify check over this\norg's still-pending referees, so a referee who has since made metered spend is\ncredited by the act of the referrer loading their page. That check is\nbest-effort and bounded — a commerce hiccup leaves the referral pending for the\nnext check rather than failing the page — and the grant is latched at-most-once,\nso this path and the admin sweep can never double-pay.",
"description": "Returns the caller's referral code, share link and the referrals they have made.\n\nThe code is a stable, deterministic function of the org, so the link in this\nresponse is the same one every time. Each row carries the referee and the status\nof that attribution.\n\nIT IS A PURE READ. It advances no referral, grants nothing and deposits nothing\n— a GET reports state, it never changes it. Qualification is the admin sweep's\njob (POST /v1/admin/referrals/sweep). The one row this handler can write is the\ncaller's OWN code-directory entry (EnsureCode), which materialises a value\nderiveCode already computes deterministically from the org id so the code has an\nO(1) reverse lookup; it carries no money, no referral state and no other tenant.",
"tags": [
"referrals"
],
@@ -100,7 +100,7 @@
"post": {
"operationId": "post_v1_referrals_claim",
"summary": "Records that the caller's org signed up through a referral code.",
"description": "Records that the caller's org signed up through a referral code.\n\nThe REFEREE is the validated caller, never a client field, and the referrer is\nresolved from the code — so a caller can only ever attach THEMSELVES to someone\nelse's code. Referring yourself is 400 and an unknown code is 404.\n\nIt is idempotent and first-touch: an org can be referred once, ever. A repeat\ncall returns the referral already on file with created=false and 200, where the\nfirst call answers 201.\n\nRecording a referral grants nothing. Both bonuses are granted later, when the\nreferee actually makes metered spend — see GET /v1/referrals and\nPOST /v1/admin/referrals/sweep.",
"description": "Records that the caller's org signed up through a referral code.\n\nThe REFEREE is the validated caller, never a client field, and the referrer is\nresolved from the code — so a caller can only ever attach THEMSELVES to someone\nelse's code. Referring yourself is 400 and an unknown code is 404.\n\nIt is idempotent and first-touch: an org can be referred once, ever. A repeat\ncall returns the referral already on file with created=false and 200, where the\nfirst call answers 201.\n\nRecording a referral grants nothing, and neither does anything downstream of it:\nthe edge later advances to qualified when the referee makes metered spend\n(POST /v1/admin/referrals/sweep), and that is the end of it. No credit is ever\nissued from this package.",
"tags": [
"referrals"
],
@@ -137,7 +137,7 @@
"adminBonusDirectory": {
"properties": {
"referrals": {
"description": "Referrals is every referral in the ledger, both orgs exposed.",
"description": "Referrals is every referral in the directory, both orgs exposed.",
"items": {
"$ref": "#/components/schemas/adminReferralView"
},
@@ -177,10 +177,6 @@
"description": "CreatedAt is when the referral was recorded, as a Unix timestamp.",
"type": "integer"
},
"creditedAt": {
"description": "CreditedAt is when the bonuses were latched and granted, as a Unix\ntimestamp; 0 until they are.",
"type": "integer"
},
"id": {
"description": "ID is the referral's handle.",
"type": "string"
@@ -189,32 +185,16 @@
"description": "QualifiedAt is when the referee first made metered spend, as a Unix\ntimestamp; 0 while still pending.",
"type": "integer"
},
"refereeGrantCents": {
"description": "RefereeGrantCents is what the referee was granted, in USD cents; 0 until the\nreferral is credited.",
"type": "integer"
},
"refereeOrg": {
"description": "RefereeOrg is the org that signed up with it.",
"type": "string"
},
"refereeTxn": {
"description": "RefereeTxn is the commerce ledger transaction that carried the referee's\ngrant, omitted until one exists.",
"type": "string"
},
"referrerGrantCents": {
"description": "ReferrerGrantCents is what the referrer was granted, in USD cents; 0 until\nthe referral is credited.",
"type": "integer"
},
"referrerOrg": {
"description": "ReferrerOrg is the org whose code was used.",
"type": "string"
},
"referrerTxn": {
"description": "ReferrerTxn is the commerce ledger transaction that carried the referrer's\ngrant, omitted until one exists.",
"type": "string"
},
"status": {
"description": "Status is the referral's lifecycle state: \"signup\", \"qualified\" or\n\"credited\".",
"description": "Status is the referral's lifecycle state: \"signup\" or \"qualified\".",
"type": "string"
}
},
@@ -222,16 +202,8 @@
},
"adminSummary": {
"properties": {
"credited": {
"description": "Credited is how many have paid both bonuses.",
"type": "integer"
},
"grantedCents": {
"description": "GrantedCents is the promo credit granted across BOTH sides of every\nreferral, in USD cents — the program's total liability to date.",
"type": "integer"
},
"qualified": {
"description": "Qualified is how many have qualified but are not yet credited.",
"description": "Qualified is how many referees have made metered spend.",
"type": "integer"
},
"signup": {
@@ -239,7 +211,7 @@
"type": "integer"
},
"total": {
"description": "Total is every referral in the ledger.",
"description": "Total is every referral in the directory.",
"type": "integer"
}
},
@@ -285,14 +257,6 @@
"description": "CreatedAt is when the referral was recorded, as a Unix timestamp.",
"type": "integer"
},
"creditedAt": {
"description": "CreditedAt is when the bonuses were latched and granted, as a Unix\ntimestamp; 0 until they are. It is the at-most-once latch.",
"type": "integer"
},
"creditsCents": {
"description": "CreditsCents is what I earned from this referral, in USD cents. It is 0\nuntil the referee qualifies.",
"type": "integer"
},
"id": {
"description": "ID is the referral's handle.",
"type": "string"
@@ -306,7 +270,7 @@
"type": "string"
},
"status": {
"description": "Status is the referral's lifecycle state: \"signup\" until the referee\nmakes metered spend, then \"qualified\", then \"credited\".",
"description": "Status is the referral's lifecycle state: \"signup\" until the referee makes\nmetered spend, then \"qualified\".",
"type": "string"
}
},
@@ -322,40 +286,24 @@
"$ref": "#/components/schemas/statusCounts",
"description": "Counts tallies this org's referrals by status."
},
"creditsEarnedCents": {
"description": "CreditsEarnedCents is the total promo credit this org has earned as the\nREFERRER, in USD cents.",
"type": "integer"
},
"link": {
"description": "Link is the shareable signup link carrying the code, on the brand's own host.",
"type": "string"
},
"refereeBonusCents": {
"description": "RefereeBonusCents is what a referee is granted on qualification, in USD cents.",
"type": "integer"
},
"referrals": {
"description": "Referrals is one row per org that signed up with this code.",
"items": {
"$ref": "#/components/schemas/myReferralView"
},
"type": "array"
},
"referrerBonusCents": {
"description": "ReferrerBonusCents is what the referrer is granted when a referee qualifies,\nin USD cents.",
"type": "integer"
}
},
"type": "object"
},
"statusCounts": {
"properties": {
"credited": {
"description": "Credited is how many referrals have paid both bonuses.",
"type": "integer"
},
"qualified": {
"description": "Qualified is how many referees have spent but are not yet credited.",
"description": "Qualified is how many referees have made metered spend.",
"type": "integer"
},
"signup": {
@@ -388,8 +336,8 @@
},
"sweepResult": {
"properties": {
"credited": {
"description": "Credited is how many of those referrals qualified on this pass and were\ngranted their bonuses.",
"qualified": {
"description": "Qualified is how many of those referrals qualified on this pass.",
"type": "integer"
},
"swept": {