Hanzo AI

Stripe

Stripe takes payments and bills for them. Here that is /v1/commerce (181 operations) for taking the money and /v1/billing (45) for what is owed and what is left, with the payer resolved from the key instead of carried as a customer id.

Stripe is a payments API with a billing product on top: charge a card, hold a customer, run subscriptions and invoices against that customer, meter usage, pay out through Connect. Two capabilities answer it here — /v1/commerce (181 operations) takes money and runs the storefront, /v1/billing (45) owns what a customer owes, holds, and has left to spend.

The structural difference is who the payer is. Every Stripe money object names a cus_… you created and must keep in step with a pm_…, a price_… and a sub_…; here the paying org comes from the validated key and is never a field in the request. A charge is an amount, a currency and a card token.

Start here

Mint a key, read which processor account and environment your org is on, then charge the card token a browser mints against it.

# 1. mint a key — sk- belongs on a server, pk- is safe in a browser
curl -sS -X POST https://api.hanzo.ai/v1/account/keys \
  -H "Authorization: Bearer $HANZO_SESSION" \
  -H 'Content-Type: application/json' \
  -d '{"type":"secret"}'

# 2. the public processor ids a browser tokenizes with, and which bucket you are in
curl -sS https://api.hanzo.ai/v1/billing/settings \
  -H "Authorization: Bearer $HANZO_API_KEY"

# 3. charge the single-use token that came back from the browser
curl -sS -X POST https://api.hanzo.ai/v1/commerce/payments \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "amountCents": 5000,
    "currency": "usd",
    "sourceId": "cnon:card-nonce-from-the-browser",
    "idempotencyKey": "first-charge-1"
  }'

Step 2 names the processor account the charge will land on and live says whether step 3 moves real money; step 3 answers a ledger transaction id, processorRef for reconciling at the processor, and test echoing that same bucket. No cus_…, no pm_…, no price_… had to exist first — the org credited is the key that made the call, so $50 cannot land in someone else's books.

Core capabilities

CapabilityWhat it doesOperations
/v1/commerceTakes the money: payments, cart, checkout, catalog, discounts, storefronts181
/v1/billingWhat is owed and what is left: cards on file, subscriptions, invoices, usage, ledger45
/v1/webhookEndpoints, signing secrets, delivery attempts and replays8

Nouns

The left column is Stripe's product surface, not just its payments core.

Taking money

StripeHanzo
CustomerYour org, taken from the validated key. Never a field in the request
PaymentIntent, confirmedPOST /v1/commerce/payments — amount, currency, card token, idempotency key
Charge, read backGET /v1/commerce/payments/{id}, by the ledger transaction id
PaymentMethod attached to a customerPOST /v1/billing/methods — vaulted at the processor, always the caller's own
SetupIntent, to charge laterThe same POST /v1/billing/methods. A card already on file answers 200, a new one 201
Publishable key for ElementsGET /v1/billing/settings — the ids a browser tokenizes with, and which environment
Checkout SessionPOST /v1/commerce/cart, then POST /v1/commerce/store/{storeid}/checkout/charge
Authorize now, capture later.../checkout/authorize, then .../checkout/capture/{orderid}
Cart line editPOST /v1/commerce/cart/{id}/item — quantity is the result, not a delta; zero removes
Coupon and promotion code/v1/commerce/discount/ — flat, percent, free shipping, free item or bulk
Balance and balance transactionsGET /v1/billing/balance, GET /v1/billing/transactions

Subscriptions, invoices and metering

StripeHanzo
Product and Price/v1/commerce/product/ and /v1/commerce/variant/; the sellable catalog is GET /v1/billing/plans
SubscriptionPOST /v1/billing/subscribe/card, listed at GET /v1/billing/subscriptions
Cancel at period endPOST /v1/billing/subscriptions/{id}/cancelatPeriodEnd defaults true
Resume a canceled subscriptionPOST /v1/billing/subscriptions/{id}/reactivate
Trial without a cardPOST /v1/commerce/store/{storeid}/trial — seven days, entitlement per store
Invoice: draft, finalize, payPOST /v1/billing/invoices, then /issue, then /collect
Void, and the PDFPOST /v1/billing/invoices/{id}/void, GET /v1/billing/invoices/{id}/pdf
Meter events and usage recordsPriced by the rate authority at /v1/commerce/rates/entries, read back at GET /v1/billing/usage
Credit grantsGET /v1/billing/credits for every grant, GET /v1/billing/credit-balance for what is spendable
Active entitlements and featuresGET /v1/entitlement (3), plus GET /v1/commerce/store/access per storefront
Free-tier quota beside the composerGET /v1/allowance (1) — reading it does not spend
Billing portalGET /v1/team/billing/ui, the session-gated wallet page

Ledger, reporting and events

StripeHanzo
Balance transaction exportGET /v1/billing/ledger — signed double-entry postings over a 24h, 7d, 30d or 90d window
Usage grouped by productGET /v1/billing/usage?groupBy=product
The month against the planGET /v1/billing/usage/rollup — plan allotment and wallet, reported separately
Which tier, and what is leftGET /v1/billing/tier — compare effectiveAvailable against zero, nothing else
Billing alerts and spend thresholds/v1/billing/alerts, enforced per request at GET /v1/billing/alerts/authorize
Webhook endpoints/v1/webhook (8) — the signing secret leaves the server on create and on rotate only
Delivery attempts and replaysGET /v1/webhook/{id}/deliveries, POST /v1/webhook/{id}/test
Inbound provider eventsPOST /v1/commerce/webhooks/{provider} — the signature is the authentication
Sigma and ReportingGET /v1/books/gl and GET /v1/books/pnl, from the 25 accounting operations
Dashboard graphs/v1/o11y (381)

amount on a usage row is whole cents, rounded; decimal beside it is the same debit exact, to 18 places. Sum decimal. A page of sub-cent inference calls totals correctly there and totals zero in amount, and that difference is real money.

Marketplace, risk and identity

StripeHanzo
Radar fraud scoringPOST /v1/risk/score — your org's own model, from /v1/risk (11)
Radar rulesGET and PUT /v1/risk/policy
Chargeback and refund outcomes as training signalPOST /v1/risk/learn — a label carries seen as well as at, so a chargeback landing 90 days late cannot leak backwards
Identity verification sessionsPOST /v1/compliance/verifications (17), ruled on at /{id}/decision
Connect account onboarding/v1/compliance/subjects and /v1/compliance/accreditation
Marketplace listings and installs/v1/marketplace (6)
Transfer to a connected account/v1/commerce/transfer/ — a record that a payable was paid, not an executor
PayoutsGET /v1/billing/payouts — amount, destination, status, failure reason
Revenue share to partners/v1/treasury (2) for the reserve and its policy, /v1/affiliate (10) for earnings
Crypto acceptancePOST /v1/billing/crypto/deposit, assets at GET /v1/billing/crypto/options
Wire top-upGET /v1/billing/wire — the serving brand's bank, and your own payment reference
Machine-to-machine payment receiptGET /v1/x402/settlements/{id} (1)

The call

Charging a customer $50. Stripe needs three identifiers to exist and agree:

curl -sS https://api.stripe.com/v1/customers \
  -u "$STRIPE_SECRET_KEY:" \
  -d [email protected]

curl -sS https://api.stripe.com/v1/payment_methods/pm_1234/attach \
  -u "$STRIPE_SECRET_KEY:" \
  -d customer=cus_1234

curl -sS https://api.stripe.com/v1/payment_intents \
  -u "$STRIPE_SECRET_KEY:" \
  -H "Idempotency-Key: $KEY" \
  -d amount=5000 \
  -d currency=usd \
  -d customer=cus_1234 \
  -d payment_method=pm_1234 \
  -d confirm=true

Hanzo:

curl -sS -X POST https://api.hanzo.ai/v1/commerce/payments \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "amountCents": 5000,
    "currency": "usd",
    "sourceId": "cnon:card-nonce-from-the-browser",
    "idempotencyKey": "'"$KEY"'"
  }'

Three identifiers become none. The org credited is the validated principal and is not readable from the body, so a payment can only ever credit the account of whoever made the call — there is no customer to mistype into someone else's books. The amount is bounded server-side, floor 1andceiling1 and ceiling 5,000 by default, and the charge is risk-screened before the card is touched: 403 is a refusal, 503 is a screen that could not reach a decision and is worth retrying, and no money moved either way. The receipt carries processorRef for reconciliation against the processor and test for which bucket it credited, so a sandbox charge can never be read as a live one. Omitting idempotencyKey is survivable rather than dangerous: the server falls back to a key derived from amount and currency, and a double-submit inside 15 minutes collapses onto one charge.

Invoicing is the same collapse. Stripe builds the invoice from items you create first, then finalizes, then pays:

curl -sS https://api.stripe.com/v1/invoiceitems -u "$STRIPE_SECRET_KEY:" \
  -d customer=cus_1234 -d amount=250000 -d currency=usd \
  -d description="Advisory retainer"
curl -sS https://api.stripe.com/v1/invoices -u "$STRIPE_SECRET_KEY:" -d customer=cus_1234
curl -sS https://api.stripe.com/v1/invoices/in_1234/finalize -u "$STRIPE_SECRET_KEY:"
curl -sS https://api.stripe.com/v1/invoices/in_1234/pay -u "$STRIPE_SECRET_KEY:"
curl -sS -X POST https://api.hanzo.ai/v1/billing/invoices \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "userId": "u_412",
    "currency": "usd",
    "lines": [{"description":"Advisory retainer — August","amount":250000}]
  }'

curl -sS -X POST "https://api.hanzo.ai/v1/billing/invoices/$ID/issue" \
  -H "Authorization: Bearer $HANZO_API_KEY"

curl -sS -X POST "https://api.hanzo.ai/v1/billing/invoices/$ID/collect" \
  -H "Authorization: Bearer $HANZO_API_KEY"

The lines are the invoice: subtotal and amount due are computed from them, so there is no total to send and none to disagree with itself. collect runs one waterfall — credit grants, then prepaid balance, then the card on file — and answers creditUsedCents, balanceUsedCents and cardChargedCents on a single receipt, where Stripe's customer balance, credit grants and card are three things configured apart and applied apart. A decline is paid=false with a reason and the invoice still open, not an error, because a decline is a normal business outcome and dunning must stay retryable. Only a successful collection is sealed, so retrying a paid invoice replays the receipt rather than charging again.

What does not carry

Refunds and disputes are settled at the processor. No route reverses a settled charge. void covers a draft or issued invoice and refuses a paid one — money moved, and the correction for that is a refund, not an erasure. /v1/commerce/return/ records the RMA, with the money living in its line items and fulfillment pricing rather than as a refund amount on the return itself. /v1/risk/learn takes dispute and chargeoff as label sources, but there is no evidence-submission route behind them. Reconcile with processorRef.

Connect's payout rail is a record here, not an executor. GET /v1/billing/payouts reads outbound payouts; creating one is not on this surface. /v1/commerce/transfer/ is the annotation a human writes after paying out of band, it moves no money, and nothing enforces uniqueness on its reference — post the same transfer twice and the payable settles twice. The pieces around the rail do exist: /v1/marketplace for listings, /v1/treasury for the reserve and revenue-share policy, /v1/affiliate for partner earnings, /v1/compliance for KYC and KYB of a counterparty. The money leaving is the part you still run at your bank.

Test mode is not a second key. Stripe hands out sk_test_ and sk_live_ and you switch by swapping a string. Here there is one key, and POST /v1/billing/mode moves an org between sandbox and real money — platform authority, never the org itself, because an org that could put itself in test mode could take priced work for free. Every payment receipt states test outright, so the two can never be confused after the fact.

Products and variants are not kept in step for you. A product carries its variants as a denormalized JSON snapshot alongside the standalone /v1/commerce/variant/ rows, and writing one does not update the other. Stripe's price hangs off product by reference and cannot drift. Pick which side is authoritative and write both.

Sales tax is a number on the cart, not a filing product. taxCents stays zero until checkout resolves the shopper's region, and the total is subtotal plus shipping plus tax. There is no registrations-and-returns surface on this API; /v1/books and GET /v1/billing/ledger give you the books to file from.

You do not post your own meter events. Stripe lets you meter your product's own units and price them against a price_…. Here the rate authority at /v1/commerce/rates/entries is SuperAdmin-only — a rate is cross-tenant money — and the platform's meter prices calls as they happen, with GET /v1/billing/usage reading the charged rows back. POST /v1/usage is a different thing: it ingests a developer's own AI-account samples. To bill your own units, put them on invoice lines with quantity and unitPrice.

How is this guide?