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

# Plan limits and the publish gate

> Storefront caps, product caps, the pre-paywall publish block, and the 207 Multi-Status path when a manifest exceeds plan caps.

# Plan limits

Every Marea user is on a plan. The plan determines:

1. How many storefronts the user can have
2. How many products per storefront
3. Whether the user can publish at all (pre-paywall accounts cannot)
4. Per-transaction platform fee, multi-branch availability, and monthly-orders visibility (covered below)

Your agent should read the user's plan from `GET /v1/me` (`plan.tier` + `plan.limits`) and surface `_links.upgradeUrl` if a higher tier is required. The numbers below are the canonical caps from `plan-limits.ts`; custom plans can override the storefront cap via `planQuantity`, and the `/v1/me` response always reflects the live cap.

<Note>
  The API surfaces a **coarse-grained 4-value tier** (`free`, `basic`, `pro`, `business`) for SDK stability. Internally there are more numeric sub-plans (e.g. `business_200`, `business_500`, `agency_growth`) that collapse to `"business"` on the wire. The numeric `planQuantity` field carries the true storefront ceiling for custom plans.
</Note>

## The `plan.limits` shape

`/v1/me` returns this exact shape — fields are not pluralized the way you might expect:

```json theme={null}
{
  "plan": {
    "tier": "free",
    "limits": {
      "storefronts": 1,
      "products": 30,
      "publishable": true
    }
  },
  "planQuantity": null
}
```

| Field                | Meaning                                                                                                   |
| -------------------- | --------------------------------------------------------------------------------------------------------- |
| `limits.storefronts` | Maximum storefronts the user can own.                                                                     |
| `limits.products`    | Maximum products **per storefront** (each storefront gets its own bucket).                                |
| `limits.publishable` | `true` if the plan permits making a storefront publicly available. Only `false` for pre-paywall accounts. |
| `planQuantity`       | If non-null, the per-account override for `limits.storefronts` (custom/agency plans).                     |

## Per-plan caps

| Surfaced tier        | Internal plans                                 | `storefronts` | `products` per storefront      | `publishable` |
| -------------------- | ---------------------------------------------- | ------------- | ------------------------------ | ------------- |
| `free` (pre-paywall) | `NO_ACTIVO` (9)                                | 1             | 2,000 (drafts only — see note) | **false**     |
| `free`               | `FREE_NEW` (8)                                 | 1             | 30                             | true          |
| `free`               | `FREE_OLD` (2, legacy)                         | 3             | 30                             | true          |
| `basic`              | `BASIC_MONTHLY` (4) / `BASIC_YEARLY` (5)       | 3             | 60                             | true          |
| `pro`                | `PRO_MONTHLY` (1) / `PRO_YEARLY` (3)           | 15            | 200                            | true          |
| `business`           | `BUSINESS_MONTHLY` (6) / `BUSINESS_YEARLY` (7) | 50            | 2,000                          | true          |
| `business`           | `BUSINESS_200` (11)                            | 200           | 2,000                          | true          |
| `business`           | `BUSINESS_500` (12)                            | 500           | 2,000                          | true          |
| `business`           | `BUSINESS_1000` (13)                           | 1,000         | 2,000                          | true          |
| `business`           | `AGENCY` (20, legacy)                          | 20            | 2,000                          | true          |
| `business`           | `AGENCY_MONTHLY` (21) / `AGENCY_YEARLY` (22)   | 5,000         | 2,000                          | true          |

<Warning>
  **Pre-paywall accounts.** A pre-paywall user (`NO_ACTIVO`) collapses to `"free"` on the wire but `limits.publishable === false`. The product cap shows as 2,000 to permit full authoring preview before checkout, but `POST .../publish` returns **402** and orders are blocked upstream. Use `plan.limits.publishable` — never the tier name — to decide whether a publish call will succeed.
</Warning>

## Beyond storefronts + products

`plan-limits.ts` also encodes several plan-derived behaviors. Most are not surfaced verbatim in `/v1/me.plan.limits` today (the public limits object is the locked 3-field shape above), but they govern what the user can do once they hit the dashboard. Surface these to the user when an upgrade CTA needs context:

| Capability                              | Free (`NO_ACTIVO`/`FREE_*`) | Basic   | Pro     | Business  | Agency    |
| --------------------------------------- | --------------------------- | ------- | ------- | --------- | --------- |
| Platform fee (Stripe + PayPal)          | 2.5%                        | 1.9%    | 1.5%    | 0.9%      | 0%        |
| Multi-branch (locations per storefront) | 0 (off)                     | 0 (off) | 0 (off) | 3         | unlimited |
| Monthly orders visible in dashboard     | 30 (`NO_ACTIVO` = 0)        | 200     | 1,000   | unlimited | unlimited |

Monthly-order visibility gates dashboard surface — orders still arrive via WhatsApp regardless. The `0` for `NO_ACTIVO` means dashboard order intake is fully disabled until the user picks a plan.

## Scopes vs. plan

Plan tier does **not** determine the scopes a user key holds. Scopes are set per-key at issuance (`catalog:read`, `catalog:write`, `storefront:publish`). The publish scope passes the auth layer for any plan, but the `storefronts/:id/publish` handler then re-checks `plan.limits.publishable` and returns 402 for pre-paywall accounts.

In other words: the scope says "this key is permitted to attempt publishing", and the plan says "this account is permitted to actually publish". Both must be true.

## What happens at the limit

### 402 `plan_blocks_publish` — pre-paywall accounts only

`POST /v1/storefronts/:id/publish` returns **402** when the user is in the pre-paywall state (`NO_ACTIVO`, plan 9). Once any paid plan or legacy free plan is active, this endpoint stops rejecting.

```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"
    }
  }
}
```

Surface `upgrade.upgradeUrl` to the user as a CTA. Do not retry without a plan change.

<Note>
  `currentPlan: "free"` here is the coarse-grained tier surface — pre-paywall and `FREE_*` both collapse to `"free"` so upgrade copy stays consistent. The authoritative signal is `plan.limits.publishable` from `/v1/me`, not the tier string.
</Note>

### 207 `products_over_limit` — bulk manifest exceeds product cap

`POST /v1/storefronts` accepts a manifest with up to 100 products in a single call. If the user's plan caps lower than the manifest count, the storefront is still created **up to the cap** and the response status is **207 Multi-Status**:

```json theme={null}
{
  "storefront": { "id": "stf_xxx", "...": "..." },
  "errors": [
    {
      "type": "plan_limit",
      "code": "products_over_limit",
      "message": "Plan limit of 30 products reached. This product was not created.",
      "param": "products",
      "doc": "https://docs.mareaalcalina.com/errors/products_over_limit",
      "recoverable": true,
      "recovery": {
        "skippedCount": 12,
        "skippedProducts": [
          { "index": 30, "title": "Tacos al pastor" },
          { "index": 31, "title": "Tacos de chorizo" }
        ],
        "upgrade": {
          "currentPlan": "free",
          "requiredPlan": "basic",
          "upgradeUrl": "https://mareaalcalina.com/upgrade?planSource=api"
        }
      }
    }
  ]
}
```

<Note>
  The `message` is emitted per skipped product (same string for each row), not pre-aggregated. Code that branches on `message` will get one identical string per row in `errors[]` — use `recovery.skippedCount` for the total.
</Note>

The storefront is real and usable. `errors[].recovery.skippedProducts` tells your agent exactly which items were dropped, with the original manifest index. Surface the upgrade CTA — once the user upgrades, retry the dropped products via `POST /v1/storefronts/:storefrontId/products`.

### 402 `plan_max_products_reached` — individual product POST hit the cap

Once the storefront exists, individual `POST /v1/storefronts/:storefrontId/products` calls that would push past the per-storefront product cap return a plain **402** with the same `upgrade` shape:

```json theme={null}
{
  "error": {
    "type": "plan_limit",
    "code": "plan_max_products_reached",
    "message": "Plan limit of 30 products reached. Upgrade to add more.",
    "param": "products",
    "recoverable": true,
    "upgrade": {
      "currentPlan": "free",
      "requiredPlan": "basic",
      "upgradeUrl": "https://mareaalcalina.com/upgrade?planSource=api"
    }
  }
}
```

Note the different `code` from the bulk path: single-product POSTs do **not** return 207 — there's nothing partial to report.

## Reading the user's current plan

```http theme={null}
GET /v1/me HTTP/1.1
Authorization: Bearer mk_user_xxxxxxxxxxxxxxxx
```

```json theme={null}
{
  "id": "usr_xxx",
  "type": "user",
  "plan": {
    "tier": "free",
    "limits": { "storefronts": 1, "products": 30, "publishable": true }
  },
  "planQuantity": null,
  "_links": {
    "upgradeUrl": "https://mareaalcalina.com/upgrade?planSource=api",
    "dashboardUrl": "https://mareaalcalina.com/dashboard"
  }
}
```

Check `plan.limits.publishable` before calling publish, and compare current storefront / product counts against `plan.limits.{storefronts, products}` before bulk-creating to avoid the 207 round-trip.

## What this page does NOT cover

* **Pricing.** Pricing, billing cycles, and promotional terms live at [mareaalcalina.com](https://mareaalcalina.com). The numbers above are the technical caps; what each plan costs is on the public pricing page.
* **Plan sub-tiers on the wire.** Business and Agency have multiple internal sub-tiers (200 / 500 / 1,000 storefronts, monthly / yearly cadences, legacy variants). Agents see the public 4-value tier surface; the underlying numeric plan ID is not exposed. The true storefront ceiling for custom plans is in `planQuantity`.
* **Cache TTLs.** `/v1/me` reads the user doc through a 30-second in-instance cache (BL-4). A plan change made through the dashboard becomes visible to the API within 30 seconds without any explicit invalidation.
