Commerce
Selling: checkout, subscriptions, invoices, spend alerts, payment webhooks and the storefront catalog.
Also for this capability: API · CLI · MCP · SDKs
Selling: checkout, subscriptions, invoices, spend alerts, payment webhooks and the storefront catalog.
| Base URL | https://api.hanzo.ai |
| Operations | 180 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1220 · Commerce — The Merchant Half — Draft · read the specification →
/v1/commerce is selling: checkout, subscriptions, invoices, spend alerts,
payment webhooks, the storefront and its catalog, carts, priced SKUs and the
typed payment door. It is hanzoai/cloud apps/commerce, which mounts the
hanzoai/commerce module natively on the cloud's own router — one router, one
specificity space, zero handler adaptation (apps/commerce/mount.go:3-14).
This HIP states the target surface — one root for every merchant noun — and the
boundary with billing, the customer money door HIP-0018 declares: commerce is
the merchant half and the store it owns; billing is the address a customer's
money questions are answered at.
Motivation
The app answers at eight top-level roots today (manifest/apps.go:116), which
puts a cart, a storefront and a payment in three different generated client
classes for one subsystem. HIP-0139 §7.1's default closes that: an app with one
store is one capability, however many nouns it answers for.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 One root
Every route commerce serves MUST be under /v1/commerce:
/v1/commerce/cart— the shopper's basket (from/v1/cart; HIP-1002)./v1/commerce/catalog— the merchant catalog rows in commerce's store, merging the SuperAdmin CRUD now at/v1/catalog/*./v1/commerce/payments— the typed payment door (from/v1/payments; HIP-1005)./v1/commerce/plans— plan ROWS in commerce's datastore (from/v1/plans/{entries,seed}). The/v1/plansroot is the plans capability's; these rows are not that catalog and take commerce's prefix./v1/commerce/store— storefronts, listings, checkout (from/v1/store; HIP-1006).- the tenant-admin surface, from
/_/commerce— HIP-0139 §3.3, nothing outside/v1; same audience, same binary. This half of the fold lands in thehanzoai/commercemodule, which registers those routes (one module release).
Where the router still serves the old spellings, the pairs are the commerce
lines in cloud's openapi/misfiled.txt, and each closes by fold, never alias.
§2 The billing boundary
Commerce MUST NOT serve /v1/billing. That address is billing's (HIP-0018),
answered over the plane; commerce keeps the store and publishes the plane
operations — balance, the prepaid gate, credit, usage, transactions, spend and
scope rules (apps/commerce/mount.go:204-210) — that the money door and every
metered surface debit through. One store, one publisher, one address that is
somebody else's.
§3 The store it owns
Per-tenant merchant and money stores under <DataDir>/commerce
(apps/commerce/mount.go:319-323), encrypted under the process master key when
the sqlcipher codec is linked. The at-rest posture is decided in exactly one
function (apps/commerce/mount.go:161-170): a production build refuses to open
money data unencrypted, and a pure-Go dev build gets the module's documented
unencrypted dev store rather than no money plane at all.
The customer ledger of record is NOT here: EmbedConfig.Ledger injects
apps/finance, so a credit minted by a settled charge lands in the one ledger
(apps/commerce/mount.go:6-8).
§4 Tenant
The paying org is read from the validated principal (HIP-0026) and never from a
request field — a field is caller-supplied, and an org read from one is a
cross-tenant write the caller asserted for itself
(apps/commerce/payments.go:25-30). A card payment taken here can only credit
the caller's own org.
§5 Price, and the screen on the mint
The surface is free, in those words: Price: cloud.Free
(plugin/commerce/main.go:21) — it is the path to payment itself. The meter
downstream of the edge is the one §2 publishes.
Both doors that mint spendable balance from a settled charge — the browser
top-up and the typed payment op — MUST wrap their handler, not their route, with
the one risk screen (apps/commerce/mount.go, the credit-screen note;
apps/commerce/risk.go): a typed op is projected four ways and only the handler
is the point all four run through.
§6 Events and observability
No commerce.<noun>.<verb> events reach the platform bus, so a customer's
webhooks (HIP-1310) receive nothing from it directly. A settled charge is
stated once, as the analytics event order_completed through the same capture
core POST /v1/event reaches (apps/commerce/emit.go:24-31, :58, :80) —
counted by the same lenses, forwarded by the same destinations fan-out. The
emit is detached and bounded: a conversion row is expendable, a settled payment
is not. Beyond the request span every route gets, it emits nothing.
§7 Typed and declared
The operations this repo owns are typed: payments, invoices, cart, the health
probe and the plane ops. The embedded module's surface is declared with prose
beside the live route (apps/commerce/describe.go:3-14), which cannot add an
operation, only explain one that exists. Each declared route is a typing the
module still owes — the honest typed op exports a value-taking core from the
module first — not a route that can never be typed
(apps/commerce/mount.go, the module-handler note).
§8 Stage and upstream
The stage is ga — the manifest row declares none, and absent is ga
(HIP-0139 §8).
It derives from hanzoai/commerce (pinned v1.50.58 in cloud's go.mod:18),
dual-licensed MIT / Apache-2.0. The whole module survives in HEAD — routes,
datastore, middleware — embedded in-process. PAN-touching paths relay to the
out-of-process Payments/Vault; this binary holds tokens and intent ids only
and is not in PCI-DSS scope (apps/commerce/mount.go:16-19).
Rationale
The alternative to the fold is a split into cart, store, payments and catalog capabilities. All of them read and write commerce's one datastore, so the split is four apps on one store — the defect HIP-0139 §7.2 refuses by name. The nouns stay; they become path segments of the app that owns their rows.
Security Considerations
The wrong implementation hands an attacker the mint. A screen mounted on the router instead of the handler leaves the same authorized deposit reachable unscreened through the tool projection; an org taken from a request field turns a card payment into a cross-tenant credit; a per-config at-rest posture lets a production build quietly write plaintext money data where the per-build decision in §3 refuses to boot.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | commerce at its own prefix | 180 operations |
| CLI | hanzo commerce … | 162 of 180 — the CLI pins the document on its own clock |
| SDK | CommerceApi in every published client | 178 of 180 — the clients are generated at their own release |
| MCP | tool commerce on https://api.hanzo.ai/v1/mcp | 215 operations, 76 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/commerce/org, operation get_commerce_org:
hanzo commerce orgimport { Configuration, CommerceApi } from 'hanzoai';
const api = new CommerceApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getCommerceOrg();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import CommerceApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = CommerceApi(client).get_commerce_org()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.CommerceAPI.GetCommerceOrg(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, commerce_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = commerce_api::get_commerce_org(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.CommerceApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new CommerceApi(client).getCommerceOrg();curl https://api.hanzo.ai/v1/commerce/org \
-H "Authorization: Bearer $HANZO_API_KEY"Tool commerce, op get_commerce_org — POST the JSON-RPC envelope to https://api.hanzo.ai/v1/mcp.
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "commerce",
"arguments": {
"op": "get_commerce_org",
"input": {}
}
}
}'Endpoints
| Endpoint | What it does |
|---|---|
POST /v1/commerce/cart/{id}/discard | Discard a cart the shopper abandoned |
POST /v1/commerce/cart/{id}/item | Set one item's quantity in a cart; zero removes it |
GET /v1/commerce/cart/{id} | Read one cart with its lines and totals |
POST /v1/commerce/cart | Open a cart for a shopper to fill |
GET /v1/commerce/catalog/entries | The raw catalog entries, including the unpublished ones |
POST /v1/commerce/catalog/entries | Add a catalog entry |
POST /v1/commerce/catalog/models/refresh | Refresh the model catalog by reading the upstream provider |
POST /v1/commerce/catalog/models | Land a syncer's view of the model catalog: upstream costs and machine facts |
POST /v1/commerce/catalog/seed | Seed the embedded catalog, without disturbing edits already made |
GET /v1/commerce/catalog | The public product catalog projection for a brand |
GET /v1/commerce/collection/{collectionid} | Fetch one collection |
POST /v1/commerce/collection/{collectionid} | Method-override tunnel for a collection — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/collection/{collectionid} | Replace a collection outright |
PATCH /v1/commerce/collection/{collectionid} | Change part of a collection |
DELETE /v1/commerce/collection/{collectionid} | Delete a collection, keeping a recoverable copy |
GET /v1/commerce/collection/ | List your org's collections, as a page |
POST /v1/commerce/collection/ | Create a collection |
GET /v1/commerce/currencies | The reference currency list the price and settings pickers render |
GET /v1/commerce/deposits | Read the crypto deposit watcher's runtime state, asset by asset |
GET /v1/commerce/disclosure/{disclosureid} | Fetch one disclosure |
POST /v1/commerce/disclosure/{disclosureid} | Method-override tunnel for a disclosure — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/disclosure/{disclosureid} | Replace a disclosure outright |
PATCH /v1/commerce/disclosure/{disclosureid} | Change part of a disclosure |
DELETE /v1/commerce/disclosure/{disclosureid} | Delete a disclosure, keeping a recoverable copy |
GET /v1/commerce/disclosure/ | List your org's disclosures, as a page |
POST /v1/commerce/disclosure/ | Create a disclosure |
GET /v1/commerce/discount/{discountid} | Fetch one discount |
POST /v1/commerce/discount/{discountid} | Method-override tunnel for a discount — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/discount/{discountid} | Replace a discount outright |
PATCH /v1/commerce/discount/{discountid} | Change part of a discount |
DELETE /v1/commerce/discount/{discountid} | Delete a discount, keeping a recoverable copy |
GET /v1/commerce/discount/ | List your org's discounts, as a page |
POST /v1/commerce/discount/ | Create a discount |
GET /v1/commerce/health | Answers ok whenever the commerce subsystem is mounted. |
GET /v1/commerce/movie/{movieid} | Fetch one movie |
POST /v1/commerce/movie/{movieid} | Method-override tunnel for a movie — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/movie/{movieid} | Replace a movie outright |
PATCH /v1/commerce/movie/{movieid} | Change part of a movie |
DELETE /v1/commerce/movie/{movieid} | Delete a movie, keeping a recoverable copy |
GET /v1/commerce/movie/ | List your org's movies, as a page |
POST /v1/commerce/movie/ | Create a movie |
GET /v1/commerce/note/{noteid} | Fetch one note |
POST /v1/commerce/note/{noteid} | Method-override tunnel for a note — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/note/{noteid} | Replace a note outright |
PATCH /v1/commerce/note/{noteid} | Change part of a note |
DELETE /v1/commerce/note/{noteid} | Delete a note, keeping a recoverable copy |
GET /v1/commerce/note/ | List your org's notes, as a page |
POST /v1/commerce/note/ | Create a note |
GET /v1/commerce/org | The public org configuration a checkout page boots from |
GET /v1/commerce/payments/{id} | Read one settled payment by its id |
POST /v1/commerce/payments | Take a card payment and credit the org's balance |
PUT /v1/commerce/plans/entries/{slug} | Edit a plan, leaving the fields you omit alone |
DELETE /v1/commerce/plans/entries/{slug} | Remove a plan from the authority |
GET /v1/commerce/plans/entries | The raw plan authority rows |
POST /v1/commerce/plans/entries | Add a subscription plan |
POST /v1/commerce/plans/seed | Seed the embedded plan catalog, without overwriting administrative edits |
GET /v1/commerce/product/{productid} | Fetch one product |
POST /v1/commerce/product/{productid} | Method-override tunnel for a product — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/product/{productid} | Replace a product outright |
PATCH /v1/commerce/product/{productid} | Change part of a product |
DELETE /v1/commerce/product/{productid} | Delete a product, keeping a recoverable copy |
GET /v1/commerce/product/ | List your org's products, as a page |
POST /v1/commerce/product/ | Create a product |
PUT /v1/commerce/rates/entries/{product}/{meter} | Edit a rate, and mark it as operator-set |
DELETE /v1/commerce/rates/entries/{product}/{meter} | Remove a rate outright |
GET /v1/commerce/rates/entries | List what one unit of each metered thing costs |
POST /v1/commerce/rates/entries | Add a rate |
POST /v1/commerce/rates/import | Load the published price document, reconciling rather than replacing |
GET /v1/commerce/return/{returnid} | Fetch one return |
POST /v1/commerce/return/{returnid} | Method-override tunnel for a return — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/return/{returnid} | Replace a return outright |
PATCH /v1/commerce/return/{returnid} | Change part of a return |
DELETE /v1/commerce/return/{returnid} | Delete a return, keeping a recoverable copy |
GET /v1/commerce/return/ | List your org's returns, as a page |
POST /v1/commerce/return/ | Create a return |
GET /v1/commerce/saleschannel/{saleschannelid} | Fetch one sales channel |
POST /v1/commerce/saleschannel/{saleschannelid} | Method-override tunnel for a sales channel — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/saleschannel/{saleschannelid} | Replace a sales channel outright |
PATCH /v1/commerce/saleschannel/{saleschannelid} | Change part of a sales channel |
DELETE /v1/commerce/saleschannel/{saleschannelid} | Delete a sales channel, keeping a recoverable copy |
GET /v1/commerce/saleschannel/ | List your org's sales channels, as a page |
POST /v1/commerce/saleschannel/ | Create a sales channel |
GET /v1/commerce/stocklocation/{stocklocationid} | Fetch one stock location |
POST /v1/commerce/stocklocation/{stocklocationid} | Method-override tunnel for a stock location — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/stocklocation/{stocklocationid} | Replace a stock location outright |
PATCH /v1/commerce/stocklocation/{stocklocationid} | Change part of a stock location |
DELETE /v1/commerce/stocklocation/{stocklocationid} | Delete a stock location, keeping a recoverable copy |
GET /v1/commerce/stocklocation/ | List your org's stock locations, as a page |
POST /v1/commerce/stocklocation/ | Create a stock location |
POST /v1/commerce/store/{storeid}/authorize/{orderid} | Authorize an order that already exists, holding the funds without settling them |
POST /v1/commerce/store/{storeid}/authorize | Authorize a new order against a storefront, holding the funds without settling them |
GET /v1/commerce/store/{storeid}/bundle/{key} | Fetch a bundle as this storefront sells it |
POST /v1/commerce/store/{storeid}/capture/{orderid} | Capture a previously authorized order and settle the payment |
POST /v1/commerce/store/{storeid}/charge | Authorize and capture a new order in one call |
POST /v1/commerce/store/{storeid}/checkout/authorize/{orderid} | Authorize an existing order, holding the funds — the checkout spelling |
POST /v1/commerce/store/{storeid}/checkout/authorize | Authorize a new order against a storefront, holding the funds — the checkout spelling |
POST /v1/commerce/store/{storeid}/checkout/capture/{orderid} | Capture a previously authorized order and settle it — the checkout spelling |
POST /v1/commerce/store/{storeid}/checkout/charge | Authorize and capture a new order in one call — the checkout spelling |
POST /v1/commerce/store/{storeid}/checkout/paypal/cancel/{payKey} | PayPal cancel by pay key — refuses, exactly as the unprefixed address does |
POST /v1/commerce/store/{storeid}/checkout/paypal/confirm/{payKey} | PayPal confirm by pay key — refuses, exactly as the unprefixed address does |
POST /v1/commerce/store/{storeid}/checkout/paypal/pay | Start a PayPal authorization for a new order — the checkout spelling |
GET /v1/commerce/store/{storeid}/listing/{key} | Fetch one listing override, by item id or by its slug or SKU |
POST /v1/commerce/store/{storeid}/listing/{key} | Add a listing override under a new key |
PUT /v1/commerce/store/{storeid}/listing/{key} | Upsert a listing override |
PATCH /v1/commerce/store/{storeid}/listing/{key} | Confirm a listing override exists and re-save the store |
DELETE /v1/commerce/store/{storeid}/listing/{key} | Remove a listing override |
GET /v1/commerce/store/{storeid}/listing | The storefront's whole listing override map |
POST /v1/commerce/store/{storeid}/paypal/cancel/{payKey} | PayPal cancel by pay key — refuses, because a pay key alone does not identify the order |
POST /v1/commerce/store/{storeid}/paypal/confirm/{payKey} | PayPal confirm by pay key — refuses, because a pay key alone does not identify the order |
POST /v1/commerce/store/{storeid}/paypal/pay | Start a PayPal authorization for a new order |
GET /v1/commerce/store/{storeid}/product/{key} | Fetch a product as this storefront sells it |
POST /v1/commerce/store/{storeid}/trial | Start this store's no-card trial on the entry plan |
GET /v1/commerce/store/{storeid}/variant/{key} | Fetch a variant as this storefront sells it |
GET /v1/commerce/store/{storeid} | Fetch one storefront |
POST /v1/commerce/store/{storeid} | Method-override tunnel for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/store/{storeid} | Replace a storefront outright |
PATCH /v1/commerce/store/{storeid} | Change part of a storefront |
DELETE /v1/commerce/store/{storeid} | Delete a storefront, keeping a recoverable copy |
GET /v1/commerce/store/ | List your org's storefronts as a page |
POST /v1/commerce/store/ | Create a storefront |
GET /v1/commerce/store/access | Whether a store is entitled to trade, and why |
GET /v1/commerce/store/current | Resolve your org's active storefront without naming an id |
POST /v1/commerce/store/token | Mint your org's least-privilege storefront read key |
GET /v1/commerce/submission/{submissionid} | Fetch one submission |
POST /v1/commerce/submission/{submissionid} | Method-override tunnel for a submission — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/submission/{submissionid} | Replace a submission outright |
PATCH /v1/commerce/submission/{submissionid} | Change part of a submission |
DELETE /v1/commerce/submission/{submissionid} | Delete a submission, keeping a recoverable copy |
GET /v1/commerce/submission/ | List your org's submissions, as a page |
POST /v1/commerce/submission/ | Create a submission |
GET /v1/commerce/subscriber/{subscriberid} | Fetch one subscriber |
POST /v1/commerce/subscriber/{subscriberid} | Method-override tunnel for a subscriber — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/subscriber/{subscriberid} | Replace a subscriber outright |
PATCH /v1/commerce/subscriber/{subscriberid} | Change part of a subscriber |
DELETE /v1/commerce/subscriber/{subscriberid} | Delete a subscriber, keeping a recoverable copy |
GET /v1/commerce/subscriber/ | List your org's subscribers, as a page |
POST /v1/commerce/subscriber/ | Create a subscriber |
GET /v1/commerce/tokentransaction/{tokentransactionid} | Fetch one token transaction |
POST /v1/commerce/tokentransaction/{tokentransactionid} | Method-override tunnel for a token transaction — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/tokentransaction/{tokentransactionid} | Replace a token transaction outright |
PATCH /v1/commerce/tokentransaction/{tokentransactionid} | Change part of a token transaction |
DELETE /v1/commerce/tokentransaction/{tokentransactionid} | Delete a token transaction, keeping a recoverable copy |
GET /v1/commerce/tokentransaction/ | List your org's token transactions, as a page |
POST /v1/commerce/tokentransaction/ | Create a token transaction |
GET /v1/commerce/transfer/{transferid} | Fetch one transfer |
POST /v1/commerce/transfer/{transferid} | Method-override tunnel for a transfer — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/transfer/{transferid} | Replace a transfer outright |
PATCH /v1/commerce/transfer/{transferid} | Change part of a transfer |
DELETE /v1/commerce/transfer/{transferid} | Delete a transfer, keeping a recoverable copy |
GET /v1/commerce/transfer/ | List your org's transfers, as a page |
POST /v1/commerce/transfer/ | Create a transfer |
GET /v1/commerce/variant/{variantid} | Fetch one variant |
POST /v1/commerce/variant/{variantid} | Method-override tunnel for a variant — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/variant/{variantid} | Replace a variant outright |
PATCH /v1/commerce/variant/{variantid} | Change part of a variant |
DELETE /v1/commerce/variant/{variantid} | Delete a variant, keeping a recoverable copy |
GET /v1/commerce/variant/ | List your org's variants, as a page |
POST /v1/commerce/variant/ | Create a variant |
GET /v1/commerce/wallet/{walletid} | Fetch one wallet |
POST /v1/commerce/wallet/{walletid} | Method-override tunnel for a wallet — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/wallet/{walletid} | Replace a wallet outright |
PATCH /v1/commerce/wallet/{walletid} | Change part of a wallet |
DELETE /v1/commerce/wallet/{walletid} | Delete a wallet, keeping a recoverable copy |
GET /v1/commerce/wallet/ | List your org's wallets, as a page |
POST /v1/commerce/wallet/ | Create a wallet |
GET /v1/commerce/watchlist/{watchlistid} | Fetch one watchlist |
POST /v1/commerce/watchlist/{watchlistid} | Method-override tunnel for a watchlist — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/watchlist/{watchlistid} | Replace a watchlist outright |
PATCH /v1/commerce/watchlist/{watchlistid} | Change part of a watchlist |
DELETE /v1/commerce/watchlist/{watchlistid} | Delete a watchlist, keeping a recoverable copy |
GET /v1/commerce/watchlist/ | List your org's watchlists, as a page |
POST /v1/commerce/watchlist/ | Create a watchlist |
GET /v1/commerce/webhook/{webhookid} | Fetch one webhook |
POST /v1/commerce/webhook/{webhookid} | Method-override tunnel for a webhook — for clients that cannot send PUT, PATCH or DELETE |
PUT /v1/commerce/webhook/{webhookid} | Replace a webhook outright |
PATCH /v1/commerce/webhook/{webhookid} | Change part of a webhook |
DELETE /v1/commerce/webhook/{webhookid} | Delete a webhook, keeping a recoverable copy |
GET /v1/commerce/webhook/ | List your org's webhooks, as a page |
POST /v1/commerce/webhook/ | Create a webhook |
POST /v1/commerce/webhooks/{provider} | Payment-provider webhook intake for settlement and subscription lifecycle events |
How is this guide?