> ## 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 a storefront

> POST /v1/storefronts/:storefrontId/publish — the publish gates (402, 422, 451), the auto-version flow, and idempotent republish.

# 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:

| Order | Gate                   | On failure                                                                                                         |
| ----- | ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
| 1     | Plan paywall           | `402 plan_blocks_publish` if `plan == NO_ACTIVO` (pre-paywall state). FREE / BASIC / PRO / BUSINESS all OK.        |
| 2     | ToS gate               | `451 tos_not_accepted` if `users/{uid}.tosAcceptedAt` is null — see [ToS jurisdiction](/concepts/tos-jurisdiction) |
| 3     | Ownership pre-check    | `404 storefront_not_found` (leak-less) if the storefront isn't owned by the caller's user                          |
| 4     | Empty-storefront guard | `422 no_products` if the storefront has zero products                                                              |
| 5     | Auto-version           | If `versionId` is omitted, a fresh version snapshot is created atomically                                          |
| 6     | Publish transaction    | Marks the version published; idempotent on republish of the same version (200 + same DTO)                          |

The plan paywall and ToS gate are account-level checks that fire **before** any storefront read, so an unauthorized caller cannot probe whether a storefront id exists by status-code timing. The ownership pre-check runs **before** the empty-products check for the same reason: a wrong-user-key probe cannot distinguish "empty storefront I own" from "storefront I don't own".

## Request

```http theme={null}
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](/concepts/safe-mutations).

## Successful response (200)

```json theme={null}
{
  "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

```json theme={null}
{
  "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

```json theme={null}
{
  "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

```json theme={null}
{
  "error": {
    "type": "tos_not_accepted",
    "code": "tos_required",
    "message": "User must accept the Terms of Service before publishing.",
    "recoverable": true,
    "nextActions": [
      {
        "label": "Accept Terms of Service",
        "method": "GET",
        "url": "https://mareaalcalina.com/terms?accept=true"
      }
    ]
  }
}
```

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](/concepts/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](/concepts/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.ts` — `runPublishTransaction`.
* `src/models/plan-limits.ts` — `isPublishable()` (the 402 gate condition).
