Plan limits
Every Marea user is on a plan. The plan determines:
- How many storefronts the user can have
- How many products per storefront
- Whether the user can publish at all (pre-paywall accounts cannot)
- 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.
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.
The plan.limits shape
/v1/me returns this exact shape — fields are not pluralized the way you might expect:
{
"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 |
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.
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.
{
"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.
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.
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:
{
"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"
}
}
}
]
}
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.
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:
{
"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
GET /v1/me HTTP/1.1
Authorization: Bearer mk_user_xxxxxxxxxxxxxxxx
{
"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. 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.