Tenant-facing API
Every route below is reached with a tenant credential (an fdk_-prefixed
API key, Authorization: Bearer <key>), resolved to a tenant_ref
per credential-carries-ownership — never a path or body parameter. This page
describes the real route handlers as they exist in the modules today;
it is not a hand-written OpenAPI spec, and it will grow as the
headless SDK effort formalizes it.
This page is maintained by hand and can drift. The machine-generated, always-current contract for the public storefront and merchant
fdk_surface is the OpenAPI document the API serves directly: browse the interactive reference athttps://api.ruo.pro/docsand fetch the raw spec athttps://api.ruo.pro/openapi.json(ADR 0007). Internal operator routes are deliberately not part of that contract and are undocumented there.
Self-serve store management — stores module
/v1/my/* — store-owner routes, authenticated by verifyTenantAuth
(the tenant credential, rate-limited on verification attempts before the DB
lookup — an fdk_ key is online-guessable with no limiter otherwise).
| Method | Path | Purpose |
|---|---|---|
GET | /v1/my/store | Fetch the caller’s own store. |
GET | /v1/my/store/manifest | Fetch the caller’s store manifest (theme tokens + sections). |
PUT | /v1/my/store/manifest | Update the caller’s store manifest. |
GET | /v1/my/site | Site overview: active manifest, latest draft, version history. |
PUT | /v1/my/site/draft | Save a draft (new version row; pointer untouched). |
POST | /v1/my/site/publish | Publish (or roll back to) an existing version. |
POST | /v1/my/site/preview-token | Mint a signed token to preview one version. |
POST | /v1/my/domains | Register a custom domain for domain-verification (DCV). |
GET | /v1/my/domains | List the caller’s registered domains. |
GET | /v1/my/domains/:id | Fetch one domain’s verification status. |
POST | /v1/my/domains/:id/verify | Trigger (re-)verification of a domain. On success, this is the only write path that creates the Host -> tenant_ref mapping row the storefront renderer resolves against (see Tenancy). |
DELETE | /v1/my/domains/:id | Remove a registered domain. |
GET | /v1/my/credentials | List the caller’s own API keys — metadata only (id, label, scopes, timestamps), never key material. Scope manage:own_credentials. |
POST | /v1/my/credentials | Mint a new API key. The issued scopes must be a subset of the issuing key’s scopes (else 403 scope_escalation); the plaintext key appears in the 201 body once. Scope manage:own_credentials. |
POST | /v1/my/credentials/:id/revoke | Revoke a key — a recorded state change, never a delete. Self-revocation is allowed. Scope manage:own_credentials. |
POST | /v1/my/credentials/initial | Bootstrap the first fdk_ key for a session-verified owner (session-gated, rate-limited, not manage:own_credentials-scoped). |
The draft-to-publish-to-rollback lifecycle, the manifest shape, signed preview tokens, and the RUO claim-scan gate are covered in full on the Site builder page.
Merchant API — merchant-api module
/v1/merchant/* — owns no Postgres schema of its own; it is pure
composition over several other modules’ published repo factories
(createStoresRepo, createCommerceRepo, createOutboundWebhooksRepo, and
the persons/merchant-billing repos), each scoped to the credential’s
tenant_ref. Scopes are enforced per route via requireScope(...).
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /v1/merchant/orders | read:own_orders | List the caller’s orders. |
GET | /v1/merchant/orders/:id | read:own_orders | Fetch one order with its line items and refund records (refunds: [] = verified none; null = the refund read failed). |
GET | /v1/merchant/catalog | read:own_catalog | List the caller’s catalog. |
GET | /v1/merchant/customers | read:own_customers | Search the caller’s customers (with per-customer stats). |
GET | /v1/merchant/customers/resolve | read:own_customers | Batch-resolve customer refs to {name, email}. |
GET | /v1/merchant/customers/:id | read:own_customers | One customer’s profile and merged timeline. |
GET | /v1/merchant/billing/invoices | read:own_billing | List the caller’s billing invoices. |
GET | /v1/merchant/billing/invoices/:id | read:own_billing | One invoice with its line items. |
GET | /v1/merchant/billing/statement | read:own_billing | Outstanding balance (a read whose failure surfaces as an error, never a laundered $0). See Merchant billing. |
GET | /v1/merchant/webhooks | manage:own_webhooks | List the caller’s registered outbound webhook endpoints. |
POST | /v1/merchant/webhooks | manage:own_webhooks | Register an outbound webhook endpoint. |
DELETE | /v1/merchant/webhooks/:id | manage:own_webhooks | Remove a webhook endpoint. |
POST | /v1/merchant/webhooks/:id/test | manage:own_webhooks | Fire a real signed test delivery (event type platform.test.v1, payload test: true) at the endpoint. Returns the endpoint’s honest status + latency, or the connection failure; the attempt is recorded in the delivery ledger. Failed tests never retry and never count toward auto-pause. |
GET | /v1/merchant/deliveries | manage:own_webhooks | List recent delivery attempts (the outbound dispatch ledger). |
Merchant order actions — checkout module
/v1/merchant/orders/:id/* — the money-path inverse actions on the
merchant’s OWN orders (scope manage:own_orders, distinct from
read:own_orders so a read-only key can never move money). Both drive the
same executors as the operator refund/cancel routes; the card charge is
derived server-side from the order (no charge_id in the request) and the
idempotency keys are deterministic per order, so a retried click replays
instead of refunding twice.
| Method | Path | Scope | Purpose |
|---|---|---|---|
POST | /v1/merchant/orders/:id/refund | manage:own_orders | Full refund — card charge plus any store-credit/gift-card tenders. |
POST | /v1/merchant/orders/:id/cancel | manage:own_orders | Cancel an unfulfilled order (releases reserved inventory, refunds any captured payment). A shipped/delivered order is refused with order_fulfilled_use_returns — use a return. |
Checkout — checkout module
/v1/tenants/:tenant/checkout* — an honesty note on tenancy: unlike the
fdk_ routes above, the checkout placement route does not derive the
tenant from the credential. The x-storefront-token accepted on the bare
checkout path (requireCheckoutCaller) is a platform-wide but
checkout-only credential — the tenant is taken from the :tenant path
segment. Folding it into per-tenant fdk_ credentials with a narrow
checkout scope is planned but not shipped. The order-action routes beneath
it (/checkout/orders/*) are operator-token-gated today. All mutating
routes require an Idempotency-Key header — a retried request with the
same key replays the original outcome rather than double-charging (see
Money invariants).
| Method | Path | Purpose |
|---|---|---|
POST | /v1/tenants/:tenant/checkout | Cart -> priced order (pending_payment) -> payment intent -> capture -> placed, or a classified decline. Body: customer_ref, items[], optional card, coupon_code, referral_code, apply_store_credit, gift_card_code, subscribe, expedited, shipping_address, email, ip, and the store’s shipping/tax policy blocks. |
POST | /v1/tenants/:tenant/checkout/preview | Price a cart without placing it — same pricing engine, no payment. |
POST | /v1/tenants/:tenant/checkout/orders/:id/refund | Refund a placed order’s charge. In the public spec and wrapped by the SDK, but gated by the operator token today — the storefront token does not reach it. Merchant-credentialed refunds go through /v1/merchant/orders/:id/refund. Body: charge_id, optional reason, optional amount_cents for a partial refund. |
POST | /v1/tenants/:tenant/checkout/orders/:id/cancel | Cancel an order (operator-gated today; merchants use /v1/merchant/orders/:id/cancel). |
POST | /v1/tenants/:tenant/checkout/orders/:id/edit | Edit a placed order’s lines, repricing and settling the charge difference (operator-gated today). |
POST | /v1/tenants/:tenant/checkout/returns/:id/approve | Approve a requested return (RMA). Operator-gated. |
POST | /v1/tenants/:tenant/checkout/returns/:id/receive | Mark an approved return received. Operator-gated. |
POST | /v1/tenants/:tenant/checkout/returns/:id/reject | Reject a requested return. Operator-gated. |
Public storefront catalog — commerce module
/v1/tenants/:tenant/catalog/* — no operator token, no tenant
credential. This is the one deliberately public read surface: a
storefront’s product catalog. A draft/archived product, or a
draft/suspended tenant’s catalog, is unreachable here even by direct
ID/slug guess — enforced at the query layer, not just the UI.
| Method | Path | Purpose |
|---|---|---|
GET | /v1/tenants/:tenant/catalog/products | List a tenant’s live catalog (products/variants, integer-cents prices). |
GET | /v1/tenants/:tenant/catalog/products/:slug | Fetch one product by slug. |
GET | /v1/tenants/:tenant/catalog/search | Faceted catalog search (q, collection, price_min_cents, price_max_cents, limit, offset). |
GET | /v1/tenants/:tenant/catalog/collections | List the tenant’s collections. |
GET | /v1/tenants/:tenant/catalog/collections/:slug | Fetch one collection (and its products) by slug. |
A note on throttling: these public catalog reads are currently
unthrottled — there is no per-caller rate limit on them today. Some
management and operator routes do return 429 with a retry-after header
when a limiter is spent. Client code should handle 429 defensively
(respect retry-after, back off) everywhere, rather than assuming any
route’s current behavior is permanent.
This is the exact route the storefront fleet renderer’s manifest-source
resolves against for host -> tenant lookups, and the route ADR 0003 commits
to as contract identity for the future headless SDK: the renderer binds
to the same types, same tenancy resolution, and same error surface a
headless caller would use — it just invokes them in-process today instead
of over the network.
The product shape
Each catalog product carries integer-cents prices and its variants. Media is returned in two forms so no consumer has to re-implement asset resolution:
| Field | Type | Notes |
|---|---|---|
id, tenant_ref, slug, name | string | Identity. slug is the lookup key for the by-slug route. |
description | string | null | Honest absence, not an empty string. |
status | draft | active | archived | Only active products are reachable on the public surface. |
image_asset_ref | string | null | The raw, portable media ref: asset:<storeKey>/<sha256>.<ext> (or null). |
image_url | string | null | image_asset_ref resolved to a fetchable URL — prefer this. |
is_subscription | boolean | FD subscribe-and-save eligibility. |
subscription_interval | day|week|month|year | null | Set only when is_subscription. |
subscription_interval_count | int | null | e.g. 1 = every interval. |
subscription_price_cents | int | null | Subscribe-and-save price. |
variants[] | array | Each: id, sku, label (nullable), price_cents, currency. |
Products that carry a gallery also return an images[] array; each entry has a
raw asset_ref and, alongside it, the resolved url — the same two-form
pattern as the top-level fields.
{
"product": {
"slug": "bpc-157",
"name": "BPC-157",
"status": "active",
"image_asset_ref": "asset:ruo-demo/3f8a…e1.png",
"image_url": "https://assets.ruo.pro/stores/ruo-demo/assets/3f8a…e1.png",
"is_subscription": true,
"subscription_interval": "month",
"subscription_interval_count": 1,
"subscription_price_cents": 4499,
"variants": [
{ "id": "var_…", "sku": "NOVA-BPC-5MG", "label": "5mg vial", "price_cents": 4999, "currency": "USD" }
]
}
}How image resolution works
The platform resolves a <store> ref to a fetchable URL by rewrite, with no
call into the media module (the module-boundary rule holds — see
Media):
asset:<store>/<sha256>.<ext> -> <ASSET_BASE_URL>/stores/<store>/assets/<sha256>.<ext>The rewrite is driven by the API’s ASSET_BASE_URL. It is honest about
absence: when no image is set, the base is unconfigured, or the ref is
malformed, image_url is null — never a broken or guessed URL. Clients
should prefer image_url and fall back to resolving image_asset_ref
themselves only if they have a reason to.
What’s not here yet
payments and inventory have no tenant-self-serve routes yet — every
route in those modules today is operator-gated (x-operator-token), because
no consuming module calls them on a tenant’s behalf yet. persons is the
same: operator-gated, consumed by other modules and operator tooling, not
directly by an end shopper with a tenant key.