Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mareaalcalina.com/llms.txt

Use this file to discover all available pages before exploring further.

Publishing

POST /v1/storefronts/:storefrontId/publish takes a draft storefront live. It is the only operation that exposes the storefront to the public internet — every other catalog mutation only changes the draft. Required scope: storefront:publish (held by user keys after verification; not held by developer keys, not held by restricted pre-verify user keys).

Behavior

The handler enforces the following gates in this order:
OrderGateOn failure
1Plan paywall402 plan_blocks_publish if plan == NO_ACTIVO (pre-paywall state). FREE / BASIC / PRO / BUSINESS all OK.
2Ownership pre-check404 storefront_not_found (leak-less) if the storefront isn’t owned by the caller’s user
3Empty-storefront guard422 no_products if the storefront has zero products
4ToS gate451 tos_not_accepted (reserved — see ToS jurisdiction)
5Auto-versionIf versionId is omitted, a fresh version snapshot is created atomically
6Publish transactionMarks the version published; idempotent on republish of the same version (200 + same DTO)
The ownership check runs before the empty-products check so a wrong-user-key probe cannot enumerate storefront ids by status-code timing.

Request

POST /v1/storefronts/stf_abc/publish HTTP/1.1
Authorization: Bearer mk_user_...
Idempotency-Key: 2f1a8c4b-2e3a-4b9d-9f1a-8c4b2e3a4b9d
Content-Type: application/json

{}
Body is optional. Pass { "versionId": "ver_xxx" } to publish a specific named version; omit for auto-version (the common case). Include Idempotency-Key if you want the call to be safe to retry — see Safe mutations.

Successful response (200)

{
  "storefront": {
    "id": "stf_abc",
    "name": "Tacos La Marea",
    "published": true,
    "publishedDate": "2026-05-10T01:23:45Z",
    "_links": {
      "previewUrl": "https://marea.pro/preview/<token>",
      "publicUrl": "https://marea.pro/tacos-la-marea",
      "editUrl": "https://mareaalcalina.com/dashboard/menu/stf_abc"
    }
  }
}
_links.publicUrl is now non-null and stable. Surface it to the user.

Republishing

Calling publish a second time on the same storefront:
  • If the version already published matches the request, the call is idempotent: the same 200 + DTO is returned.
  • If a new version is created (auto-version with new content), a fresh public snapshot is generated.
This means an agent can call publish on every “save” without worrying about the user seeing a flicker — publish is cheap when nothing changed.

Error responses

402 plan_blocks_publish — pre-paywall account

{
  "error": {
    "type": "plan_limit",
    "code": "plan_blocks_publish",
    "message": "Your plan does not permit publishing. Upgrade to publish this storefront.",
    "recoverable": true,
    "upgrade": {
      "currentPlan": "free",
      "requiredPlan": "basic",
      "upgradeUrl": "https://mareaalcalina.com/upgrade?planSource=api"
    }
  }
}
Today only the NO_ACTIVO (pre-paywall) state hits this. Free, Basic, Pro, and Business all publish successfully. Surface upgrade.upgradeUrl as a CTA. Do not retry until the user upgrades.

422 no_products — empty storefront

{
  "error": {
    "type": "invalid_request",
    "code": "no_products",
    "message": "Cannot publish an empty storefront. Add at least one product first.",
    "recoverable": true,
    "nextActions": [
      {
        "label": "Add at least one product before publishing.",
        "method": "POST",
        "url": "/v1/storefronts/stf_abc/products"
      }
    ]
  }
}
Add at least one product, then retry publish with the same Idempotency-Key.

451 tos_not_accepted — user hasn’t accepted ToS

{
  "error": {
    "type": "tos_not_accepted",
    "code": "tos_required",
    "message": "User must accept the Terms of Service before publishing.",
    "recoverable": true,
    "nextActions": [
      {
        "label": "Open the Marea dashboard and accept the Terms of Service.",
        "method": "GET",
        "url": "https://mareaalcalina.com/dashboard"
      }
    ]
  }
}
The user must accept the ToS via the dashboard modal — the agent cannot bypass. Surface nextActions[0].url verbatim. Once accepted, retry publish with the same Idempotency-Key. See ToS jurisdiction.

404 — storefront not found OR cross-tenant access

If your mk_user_* key doesn’t own the storefront, you get 404 storefront_not_found (silent denial — not 403). See Storefronts.

What NOT to do

  • Don’t auto-publish. Publishing is destructive and user-visible — once it’s live, the URL is shared and may be indexed. Always require explicit user confirmation.
  • Don’t retry 402 / 422 / 451 without surfacing nextActions[] to the user. Each is a recoverable: true error that needs user input, not a backoff.
  • Don’t auto-accept the ToS. The 451 → modal flow is counsel-reviewed and intentional.

Verification in code

  • src/api/v1/storefronts.publish.ts — the handler with the locked step order.
  • src/utils/publish.utils.tsrunPublishTransaction.
  • src/models/plan-limits.tsisPublishable() (the 402 gate condition).