GuidesHosted storefront

Author and publish a hosted storefront

A hosted store is data, not a fork. Everything a visitor sees — the brand, the theme tokens, and the ordered list of page sections — is one JSON document, the manifest, rendered by one multi-tenant fleet renderer. You author the manifest, save it as a draft, preview it, then publish. Publishing an older version is how you roll back.

This guide walks the whole loop with an fdk_ tenant credential. The authoring routes live under /v1/my/*; the tenant is resolved from the credential, never from a path or body parameter.

These are merchant routes on the commerce API, authenticated with your fdk_ key as Authorization: Bearer. They are separate from the checkout-only storefront token a headless frontend uses (an opaque secret, no fixed prefix, provisioned by the platform operator; platform-wide today) — see Build your own frontend.

Before you start

  • An fdk_ tenant credential with the read:own_store and write:own_store scopes (a pre-live build key carries both).
  • The API origin — https://api.ruo.pro.
  • Your tenant ref (this guide uses nova-peptide).

Steps

Read the current site

GET /v1/my/site is one honest read: the active manifest, the latest draft (the newest version the active pointer does not reference, null when the newest version is already live), and the full version history.

curl "https://api.ruo.pro/v1/my/site" -H "Authorization: Bearer fdk_your_key_here"
# -> { "active": { "version": 1, ... }, "draft": null, "versions": [ ... ] }

Author the manifest as data

The manifest names the blocks the renderer relies on — store, brand, theme, template.sections, seo, behavior — and preserves any extra properties verbatim. Each section is a registry type plus an open props bag:

{
  "brand": { "name": "Nova Peptide" },
  "theme": { "tokens": { "color.brand": "#0b6" } },
  "template": {
    "id": "base-v1",
    "sections": [
      { "type": "announcement-bar", "props": { "text": "Free shipping over $150" } },
      { "type": "header-nav", "props": {} },
      { "type": "epic-hero", "props": { "heading": "Research-grade peptides" } },
      { "type": "product-grid", "props": { "collection": "peptides" } },
      { "type": "legal-footer", "props": {} }
    ]
  }
}

See The manifest and sections for the block reference and Theming for how theme tokens map to CSS custom properties.

Save a draft

PUT /v1/my/site/draft writes a new version row without moving the active pointer, so nothing a visitor sees changes yet. It returns the version it minted:

curl -X PUT "https://api.ruo.pro/v1/my/site/draft" \
  -H "Authorization: Bearer fdk_your_key_here" \
  -H "content-type: application/json" \
  -d '{ "manifest": { "brand": { "name": "Nova Peptide" }, "template": { "sections": [] } } }'
{ "version": 2, "rowHash": "…" }

The draft save is where the RUO-claims guardrail scan runs. The scanner walks every string in the manifest tree for prohibited claim language and is fail-closed: a scanner error is a 503 (“scan did not run”), never a silent pass. A CRITICAL finding is a hard 422 and no version row is written — there is no merchant override.

⚠️

On a 422 ruo_claims_violation, fix the offending copy and re-save — do not look for a bypass header. The x-guardrails-override escape hatch exists only on the operator publish route, which is not merchant-facing. If a finding is a false positive, contact the platform operator to review.

The finding body names exactly what tripped the scan — one entry per finding with { path, term, rule, severity, context }.

Preview the draft

POST /v1/my/site/preview-token mints a short-lived signed token for one version. Nothing is written to the database — the expiry rides in the signed payload, and the token grants a render of exactly that immutable version:

curl -X POST "https://api.ruo.pro/v1/my/site/preview-token" \
  -H "Authorization: Bearer fdk_your_key_here" \
  -H "content-type: application/json" \
  -d '{ "version": 2 }'
{ "token": "…", "version": 2, "expiresAt": "2026-09-14T12:30:00.000Z" }

The renderer resolves it through the public GET /v1/preview-resolve?token=<token> route (served with Cache-Control: no-store). Expired, tampered, malformed, or unknown-version tokens all resolve to the same 404 invalid_preview_token — one code, no oracle, and no cross-tenant preview.

Publish

POST /v1/my/site/publish moves the active pointer to a version. Publishing does not re-scan — every version already passed the guardrail scan on its write:

curl -X POST "https://api.ruo.pro/v1/my/site/publish" \
  -H "Authorization: Bearer fdk_your_key_here" \
  -H "content-type: application/json" \
  -d '{ "version": 2 }'
{ "version": 2, "rowHash": "…" }

An unknown version is a 404 version_not_found. The moment this returns, the public read GET /v1/tenants/nova-peptide/site serves the new active manifest.

Roll back

Because any existing version can be published, rollback is the same route — re-publish the earlier version ({ "version": 1 }). The version table is append-only, so a rollback never destroys the version you rolled away from; publish it again to roll forward.

The public read

One unauthenticated route serves the live store: GET /v1/tenants/{tenant}/site returns the active manifest for a live store and nothing else.

{ "tenant_ref": "nova-peptide", "version": 2, "manifest": { /* … */ } }

It is fail-closed: a draft or suspended store, or an unknown tenant, is an honest 404 unknown_tenant (never an empty manifest); a failed read is a 503, never absence. The response carries an ETag (the manifest row hash) so a conditional If-None-Match request answers 304 when the active version is unchanged. The SDK’s store.site.get() wraps this exact route.