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

# Publish a storefront

> POST .../publish with the four gates: 402 (pre-paywall), 422 (empty), 451 (ToS), 200 (live publicUrl). Idempotent on republish.

export const LLMBlock = ({action, method, path, keyType, bodyExample, summary, errors, endpoint}) => <>
    <h2>For agents</h2>
    <p>Copy this block into your AI assistant to give it everything it needs:</p>
    <pre>
      <code>{`────────────────────────────────────────────────────────────
You can ${action} via the Marea Catalog API.
Endpoint: ${method} https://api.mareaalcalina.com${path}
Header: Authorization: Bearer <${keyType}>
Body schema: ${bodyExample}
What this does: ${summary}
Errors: ${errors}
Full reference (markdown): https://docs.mareaalcalina.com/api/${endpoint}.md
────────────────────────────────────────────────────────────`}</code>
    </pre>
  </>;

# Publish a storefront

Publish is the one operation that takes a storefront from draft to public — the URL becomes shareable, indexable, and live for end customers. Required scope: `storefront:publish`. **Always require explicit user confirmation before calling** — publish is user-visible.

## Pre-flight checklist

Before calling publish:

1. The storefront must have **at least one product** (else 422 `no_products`).
2. The user must be on **any active plan** — Free, Basic, Pro, Business, or Agency. Pre-paywall accounts (just-signed-up, no plan picked) get 402.
3. The user must have **accepted the dashboard ToS modal** (else 451). The agent cannot bypass this.

Short-circuit with `GET /v1/me` if you want to surface a CTA before the publish round-trip:

```json theme={null}
{
  "plan": { "tier": "free", "limits": { "publishable": true, "products": 30, "storefronts": 1 } },
  "tosAcceptedAt": "2026-04-15T10:23:00Z"
}
```

If `plan.limits.publishable` is `false` or `tosAcceptedAt` is `null`, surface the appropriate next-action to the user before attempting publish.

## Request

`POST /v1/storefronts/{storefrontId}/publish` — body is optional.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.mareaalcalina.com/v1/storefronts/$STOREFRONT_ID/publish \
    -H "Authorization: Bearer $MAREA_USER_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $(uuidgen)" \
    -d '{}'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    `https://api.mareaalcalina.com/v1/storefronts/${storefrontId}/publish`,
    {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${userKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": crypto.randomUUID(),
      },
      body: JSON.stringify({}),
    },
  );
  const { storefront } = await res.json();
  // Surface storefront._links.publicUrl to the user.
  ```

  ```python Python theme={null}
  import uuid, requests

  res = requests.post(
      f"https://api.mareaalcalina.com/v1/storefronts/{storefront_id}/publish",
      headers={
          "Authorization": f"Bearer {user_key}",
          "Content-Type": "application/json",
          "Idempotency-Key": str(uuid.uuid4()),
      },
      json={},
  )
  storefront = res.json()["storefront"]
  ```
</CodeGroup>

Send `{ "versionId": "ver_xxx" }` to publish a specific version snapshot; omit for auto-version (the common case — Marea creates a fresh snapshot from the current draft).

## 200 OK — success

```json theme={null}
{
  "storefront": {
    "id": "stf_9a8b7c6d5e4f3a2b1c0d9e8f",
    "name": "Tacos La Marea",
    "language": "es",
    "currency": "MXN",
    "businessType": "restaurant",
    "published": true,
    "publishedDate": "2026-05-10T18:30:00.000Z",
    "_links": {
      "previewUrl": "https://marea.pro/preview/pv_8c4b2e3a4b9d",
      "publicUrl": "https://marea.pro/tacos-la-marea",
      "editUrl": "https://mareaalcalina.com/dashboard/menu/stf_9a8b7c6d5e4f3a2b1c0d9e8f"
    }
  }
}
```

Surface `_links.publicUrl` to the user — that's the share-and-print-and-QR-code URL.

## The four error gates

Every error follows the §9.6 envelope verbatim. Branch on `error.type` + `error.code`; surface `nextActions[]` to the user without paraphrasing.

### 402 — plan paywall (`plan_blocks_publish`)

The user is pre-paywall (no plan picked yet). Surface `error.upgrade.upgradeUrl` as a CTA; do not retry until the user upgrades.

```json theme={null}
{
  "error": {
    "type": "plan_limit",
    "code": "plan_blocks_publish",
    "message": "Your plan does not permit publishing. Upgrade to publish this storefront.",
    "doc": "https://docs.mareaalcalina.com/concepts/plan-limits#publish-paywall",
    "param": null,
    "requestId": "req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
    "requestLogUrl": "https://mareaalcalina.com/developers/logs/req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
    "recoverable": true,
    "retryAfterMs": null,
    "nextActions": [
      { "label": "Pick a plan to publish.", "method": null, "url": "https://mareaalcalina.com/upgrade?planSource=api" }
    ],
    "upgrade": {
      "currentPlan": "free",
      "requiredPlan": "basic",
      "upgradeUrl": "https://mareaalcalina.com/upgrade?planSource=api"
    }
  }
}
```

### 422 — empty storefront (`no_products`)

The storefront has 0 products. Use `nextActions[0].url` to add a product before retrying.

```json theme={null}
{
  "error": {
    "type": "invalid_request",
    "code": "no_products",
    "message": "Cannot publish an empty storefront. Add at least one product first.",
    "doc": "https://docs.mareaalcalina.com/concepts/publishing#no-products",
    "param": null,
    "requestId": "req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
    "requestLogUrl": "https://mareaalcalina.com/developers/logs/req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
    "recoverable": true,
    "retryAfterMs": null,
    "nextActions": [
      { "label": "Add at least one product before publishing.", "method": "POST", "url": "/v1/storefronts/stf_9a8b7c6d5e4f3a2b1c0d9e8f/products" }
    ],
    "upgrade": null
  }
}
```

### 451 — ToS not accepted (`tos_required`)

The user hasn't accepted the dashboard ToS modal. The agent **cannot** bypass — the user must complete the modal in their browser. Retry with the same `Idempotency-Key` after the user accepts.

```json theme={null}
{
  "error": {
    "type": "tos_not_accepted",
    "code": "tos_required",
    "message": "The account holder must accept the Marea Terms of Service before this storefront can be published.",
    "doc": "https://docs.mareaalcalina.com/concepts/tos-jurisdiction",
    "param": null,
    "requestId": "req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
    "requestLogUrl": "https://mareaalcalina.com/developers/logs/req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
    "recoverable": true,
    "retryAfterMs": null,
    "nextActions": [
      { "label": "Open the dashboard so the user can accept the Terms of Service.", "method": "GET", "url": "https://mareaalcalina.com/dashboard/tos" }
    ],
    "upgrade": null
  }
}
```

### 404 — not found (`storefront_not_found`)

Wrong `storefrontId`, or this user-key doesn't own it. Cross-tenant returns 404 (leak-less), not 403 — you cannot infer existence.

## Summary table

| HTTP | `error.type` / `code`                | Why                      | What to do                                                                                                           |
| ---- | ------------------------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| 402  | `plan_limit` / `plan_blocks_publish` | Pre-paywall account      | Surface `error.upgrade.upgradeUrl`; do not retry until upgrade.                                                      |
| 422  | `invalid_request` / `no_products`    | 0 products               | Use `error.nextActions[0]` (POST products URL), then retry.                                                          |
| 451  | `tos_not_accepted` / `tos_required`  | ToS modal not completed  | Surface `nextActions[0].url` (dashboard). Agent cannot bypass. Retry with same `Idempotency-Key` after user accepts. |
| 404  | `not_found` / `storefront_not_found` | Wrong id or cross-tenant | Confirm id + ownership; cannot retry blindly.                                                                        |
| 429  | `rate_limited` / `rpm_exceeded`      | Per-user `rpm: 60` cap   | Sleep `Retry-After`, retry with same key.                                                                            |

## Idempotent republishing

Calling publish on an already-published storefront with the same `versionId` is a **no-op** that returns 200 with the same DTO. To republish a new version, omit `versionId` (auto-version) — Marea creates a fresh snapshot, runs the publish, and the live URL points at the new content.

You can keep the same `Idempotency-Key` across retries of the same publish attempt; generate a fresh key for a new republish.

## Cross-references

* [Publishing concept](/concepts/publishing) — full gate semantics + idempotency rules.
* [ToS jurisdiction](/concepts/tos-jurisdiction) — what the 451 ToS modal contains.
* [Plan limits](/concepts/plan-limits) — per-plan storefront caps + the publish-block-on-pre-paywall rule.

<LLMBlock action="publish a Marea storefront to its public URL" method="POST" path="/v1/storefronts/{storefrontId}/publish" keyType="mk_user_*" bodyExample={`{}`} summary="Takes a draft storefront live. Returns 200 with storefront._links.publicUrl on success. Idempotent on republish (same versionId → no-op). Always require explicit user confirmation before calling — publish is user-visible. Pre-flight: GET /v1/me first to check plan.limits.publishable and tosAcceptedAt." errors="402 plan_limit/plan_blocks_publish (pre-paywall — surface upgrade.upgradeUrl, do not retry); 422 invalid_request/no_products (add a product first via nextActions[0]); 451 tos_not_accepted/tos_required (surface dashboard URL; agent cannot bypass; retry with same Idempotency-Key after user accepts); 404 not_found (wrong id or cross-tenant — silent); 429 rate_limited (sleep retryAfterMs)." endpoint="catalog/publish-storefront" />
