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

# AGENTS

# AGENTS.md — Marea Alcalina API

> Following the [agents.md spec](https://agents.md/). This file is for AI agents (Claude, GPT, Cursor, Continue.dev, generic LLM-driven tools) consuming the Marea public API. For the full doc index in agent-friendly markdown form, fetch [llms.txt](https://docs.mareaalcalina.com/llms.txt).

## What this API is for

Operations + retention API for small-business commerce. Bootstrap users, manage storefronts + products (food menus or retail / service catalogs), publish to public hosted URLs.

End-customer order channels are the operator's choice — WhatsApp, web checkout, or in-person pickup — and configurable per storefront. The API does not lock the channel.

## Auth model

The **default credential is `mk_user_*`** — one key per user, covers **all** storefronts that user owns (1 on Free, up to 5,000 on Agency Growth). The key is owner-scoped, not storefront-scoped. You do NOT issue a separate key per storefront.

A second credential, `mk_dev_*`, exists *only* for partner-style callers (agents, vertical SaaS, agencies) that bootstrap Marea accounts for *other* people. Most integrations never hold one.

**End-user AI clients do not paste a key: they connect over OAuth.** When a merchant adds Marea to Claude, ChatGPT, Cursor or any MCP client, the client discovers OAuth 2.1 + PKCE from the `WWW-Authenticate` challenge on a 401 from `POST https://mcp.mareaalcalina.com/mcp` (RFC 9728), the merchant signs in to Marea and authorizes. Consent mints a per-user `mk_user_*` behind the token, so everything below about user-key scope still applies. OAuth grants carry `marea.read`, `marea.write` and `marea.publish`, and can never call `marea.bootstrap_user`. Static keys stay the path for servers, CI and agents with no browser. See the [MCP connect guide](https://docs.mareaalcalina.com/quickstart/mcp).

Full model: [API keys](https://docs.mareaalcalina.com/concepts/keys).

* `mk_user_*` — **per-user key**, owner-scoped (covers all the user's storefronts). Either minted from the user's own dashboard, OR returned by `POST /v1/users` in the partner bootstrap response. Bootstrap-issued keys start **restricted** (only `me:verify` + `me:resendVerification`) until the 6-digit code is verified, then upgraded in-place to `catalog:read`, `catalog:write`, `storefront:publish`. Cannot bootstrap, cannot see other users' data. Default `rpd: 10000`.
* `mk_dev_*` — **partner / developer key**, held by the integrator. Holds `developer:bootstrap`, `developer:read`, `developer:issueUserKey`, `developer:webhooks`. Used to bootstrap users and manage webhook endpoints — **never** to touch a catalog. Default `rpd: 50` (configurable at issue time; the dev `rpd` IS the bootstrap cap).

Both pass as `Authorization: Bearer <key>` (or `X-API-Key: <key>` as a fallback).

## Common patterns

* **Bootstrap → verify → use.** `POST /v1/users` returns a *restricted* `mk_user_*` key + emails the user a 6-digit code. The user reads the code aloud (or the agent fetches it via Gmail-MCP), agent calls `POST /v1/users/:userId/verify`, the same key is upgraded to full per-user scope. See [Bootstrap quickstart](https://docs.mareaalcalina.com/quickstart/bootstrap).
* **Idempotency on every mutation.** Pass `Idempotency-Key: <uuid-v4>` on every `POST` and `PATCH`. Same key + same body returns the original response (replay-safe). Same key + different body returns `409 idempotency_conflict`. See [Safe mutations](https://docs.mareaalcalina.com/concepts/safe-mutations).
* **Branch on `error.type`, not `error.message`.** The `type` field is a stable 10-value enum; `message` is English today and its wording may change. See [Errors](https://docs.mareaalcalina.com/concepts/errors).
* **Surface `nextActions[]` verbatim.** Errors carry `{ label, method, url }[]` with the concrete actions the user can take (empty when there is nothing to do, for example on 429 or a validation error). Don't paraphrase; render verbatim.
* **Read rate-limit headers proactively.** `X-RateLimit-Remaining` is on every response. When you hit `429`, read `Retry-After` (seconds) and back off. Defaults: `rpm: 60`. Dev keys: `rpd: 50`. User keys: `rpd: 10000`.
* **Markdown for ingestion.** Every doc page also serves at `<page>.md` (e.g., `/concepts/errors.md`); use that variant when ingesting docs into your context — cleaner than the HTML.

## When to call which tool

REST (always available) and MCP tool (when the user has connected Marea in their AI client, over OAuth or with a static key: see the [MCP connect guide](https://docs.mareaalcalina.com/quickstart/mcp)). Ten `marea.*` tools.

| Goal | REST | MCP tool |
| - | - | - |
| Bootstrap a new user | `POST /v1/users` (dev key) | `marea.bootstrap_user` |
| Verify the 6-digit email code | `POST /v1/users/:userId/verify` | (use REST in v1) |
| Read identity / plan / budget | `GET /v1/me` | `marea.whoami` |
| Create a storefront | `POST /v1/storefronts` | `marea.create_storefront` |
| Update storefront fields | `PATCH /v1/storefronts/:id` | `marea.update_storefront` |
| Add a product | `POST /v1/storefronts/:id/products` | `marea.create_product` |
| Update a product | `PATCH /v1/storefronts/:id/products/:productId` | `marea.update_product` |
| List the user's storefronts | `GET /v1/storefronts` | `marea.list_storefronts` |
| Read one storefront in full | `GET /v1/storefronts/:id` | `marea.get_storefront` |
| List a storefront's products | `GET /v1/storefronts/:id/products` | `marea.list_products` |
| Publish to public URL (needs user confirmation) | `POST /v1/storefronts/:id/publish` | `marea.publish_storefront` |
| Remove a product from the menu | `PATCH .../products/:productId` with `hide: true` | `marea.update_product` with `hide: true` |

**There is no delete tool for products, and no delete endpoint either.** Hiding is the way: `hide: true` takes the item off the customer-facing menu and keeps its history. Treat "delete that dish" as a request to hide it, and say so.

Prefer the MCP tool when the user has Marea connected: ids, scopes and plan gates are already resolved for that account. Fall back to REST when MCP isn't available. On an OAuth connection the tool list is already filtered by the granted scopes, so a tool that is absent means the scope was not granted, not that the call would fail.

## What NOT to do

* **Don't swallow `nextActions[]`.** The most common agent failure mode is silently retrying past a 402 (paywall) or 451 (ToS not accepted) without ever surfacing the action to the user. Render the array verbatim.
* **Don't auto-accept the ToS on the user's behalf.** A 451 → modal flow requires user input by design. The agent cannot bypass.
* **Don't bootstrap past your dev key's `rpd` cap.** Default is 50/day (configurable at issue time). Hitting it returns 429 `rate_limited` with `code: rate_limit_exceeded` and `rpd_exceeded` in `error.message`. Read `Retry-After` (seconds-until-the-day-rolls-over) and back off.
* **Don't call publish without explicit user confirmation.** Publish is destructive and user-visible. Ask the user in plain language and wait for a yes; never chain it automatically after a create or update. The MCP tool is annotated `destructiveHint: true`, so most clients also show their own confirmation, but that is the client's gate, not yours.
* **Don't cache `mk_user_*` keys across users or sessions.** Per-user keys belong in the user's own context.
* **Don't poll verification status faster than the API allows.** The user-facing rate limit is `rpm: 60` per user-key. Poll at most once every 30s during the verify window; better, create a webhook endpoint subscribed to `user.verified` — see [Webhook endpoints](https://docs.mareaalcalina.com/concepts/webhooks).
* **Don't reuse the same `Idempotency-Key` with a *different* body.** That returns `409 idempotency_conflict` and you must generate a fresh key. If you're retrying with edits, that's a new request — new key.
* **Don't branch on `error.message`.** Branch on `error.type` (stable enum) or `error.code` (stable string).

## Errors

Every error follows the same envelope: `{ type, code, message, doc, requestId, recoverable, retryAfterMs, nextActions, upgrade, requiredScopes? }`. Branch on `error.type`. The 10 stable types:

| `error.type` | Typical HTTP | Surface to user? | Retry? | What to do |
| - | - | - | - | - |
| `rate_limited` | 429 | No (transparent) | Yes after `retryAfterMs` | Sleep + retry same request + same `Idempotency-Key` |
| `invalid_request` | 400 / 410 | No (fix client-side) | No | Fix the body. Includes `invalid_idempotency_key`, `idempotency_snapshot_unavailable` |
| `auth` (401) | 401 | Maybe | No | Re-prompt for a valid key. Codes: `missing_authorization`, `invalid_authorization_format`, `key_not_found`, `key_revoked` |
| `auth` (insufficient scope) | 403 | Maybe | No | Read `requiredScopes` + `heldScopes` arrays; ask user/dev for a key with the right scope |
| `not_found` | 404 | Maybe | No | Confirm the id. May also be a silent cross-tenant denial if the calling key doesn't own the resource |
| `plan_limit` | 402 | **Yes (paywall)** | No | Surface `error.upgrade.upgradeUrl` verbatim. The user must upgrade, then retry |
| `internal` | 500 | Yes (with `requestId`) | Once with backoff | Then escalate. Surface `requestId` so support can trace |
| `conflict` | 409 | No | Yes after 1s | `idempotency_in_flight` — the same key is still processing. Retry with same key |
| `idempotency_conflict` | 409 | No | Yes with **new** `Idempotency-Key` | Same key was used earlier with a different body. Generate a fresh UUID |
| `service_unavailable` | 503 | Yes (after retries) | Yes with backoff | Exponential backoff |
| `tos_not_accepted` | 451 | **Yes (link to dashboard)** | After user accepts | Surface `nextActions[0].url` (modal). Agent cannot bypass |

Full error matrix at [/concepts/errors](https://docs.mareaalcalina.com/concepts/errors).

## Where to find docs

* **llms.txt index:** [https://docs.mareaalcalina.com/llms.txt](https://docs.mareaalcalina.com/llms.txt) — agent-curated overview of the entire surface
* **llms-full.txt:** [https://docs.mareaalcalina.com/llms-full.txt](https://docs.mareaalcalina.com/llms-full.txt) — concatenation of all pages (Mintlify auto-emits in production)
* **Per-page markdown:** `https://docs.mareaalcalina.com/<page>.md` (e.g., `/concepts/errors.md`)
* **OpenAPI 3.1 spec:** [https://api.mareaalcalina.com/v1/openapi.json](https://api.mareaalcalina.com/v1/openapi.json) — Zod-derived from the cloud-functions code; 5-min cached
* **Postman collection:** [https://docs.mareaalcalina.com/marea.postman.json](https://docs.mareaalcalina.com/marea.postman.json) — auto-generated from the OpenAPI spec
* **MCP server:** [https://mcp.mareaalcalina.com/mcp](https://mcp.mareaalcalina.com/mcp) (streamable HTTP, POST only); 10 `marea.*` tools. Merchants connect it in Claude, ChatGPT, Cursor or Claude Code over OAuth; developers and headless agents pass a static `mk_user_*` / `mk_dev_*` bearer. Verify and resend are deliberately REST-only
* **API catalog (RFC 9727):** [https://mareaalcalina.com/.well-known/api-catalog](https://mareaalcalina.com/.well-known/api-catalog) — discoverability for agent crawlers

## Conventions

* **Locale:** `Accept-Language` sets the applied defaults on `POST /v1/users` (language, currency, country). Error messages are English regardless of the header today.
* **Currency:** configurable per storefront. Pass `currency: "MXN" | "USD" | "CAD" | "BRL" | ...` on storefront create.
* **Phone format:** E.164 (`+52...`).
* **Timestamps:** ISO 8601 strings.
* **IDs:** prefixed (`usr_`, `stf_`, `prd_`, `dev_`).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.