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

# Bootstrap a user account

> Create a Marea account on behalf of a user with one POST. The response gives you a per-user key — use it for everything after this.

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>
  </>;

# Bootstrap a user account

<Note>
  **This is the partner / agent flow.** Use it when you're building an AI agent, vertical SaaS, or agency tool that creates Marea accounts for *other* people. If you already own a Marea storefront and just want API access to it, you don't need this — grab your `mk_user_*` from your dashboard and skip straight to [Add products](/quickstart/products).
</Note>

One call creates the user, a starter storefront, and a per-user API key. Marea also emails a 6-digit code to the user; a follow-up call verifies it and unlocks catalog writes. You never touch the user's password.

The whole flow is two HTTP calls.

```bash theme={null}
# Your own developer key — set this once.
export MAREA_DEV_KEY=mk_dev_xxxxxxxxxxxxxxxx
```

## Step 1 — Create the user

```bash theme={null}
curl -s -X POST https://api.mareaalcalina.com/v1/users \
  -H "Authorization: Bearer $MAREA_DEV_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept-Language: es-MX" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "email": "owner@taqueria.example",
    "displayName": "Marea Taqueria",
    "country": "MX",
    "currency": "MXN",
    "businessType": "restaurant",
    "sourceAgent": "claude-desktop"
  }'
```

Response — **201 Created**:

```json theme={null}
{
  "userId": "usr_4f1a2b3c4d5e6f7a8b9c0d1e",
  "storefrontId": "stf_9a8b7c6d5e4f3a2b1c0d9e8f",
  "userKey": "mk_user_01HZX9K8QW7VPYR3M2N1B4FJSA",
  "verificationStatus": "pending",
  "verificationExpiresAt": "2026-05-10T18:23:00.000Z",
  "appliedDefaults": { "language": "es", "currency": "MXN", "country": "MX", "businessType": "restaurant" }
}
```

<Warning>
  **`userKey` is shown once.** Store it now — there's no way to read it back. Treat it like a password.
</Warning>

```bash theme={null}
# Save the key returned in the response.
export MAREA_USER_KEY=mk_user_01HZX9K8QW7VPYR3M2N1B4FJSA
export USER_ID=usr_4f1a2b3c4d5e6f7a8b9c0d1e
```

From here on, **every catalog call uses `$MAREA_USER_KEY`**. You only reach back for `$MAREA_DEV_KEY` when bootstrapping another user, listing your users, or managing webhook endpoints.

### Required body fields

| Field                                                | Required | Notes                                                                                     |
| ---------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------- |
| `email`                                              | yes      | Where the 6-digit code is sent.                                                           |
| `displayName`                                        | yes      | Shown in the dashboard + on the storefront.                                               |
| `sourceAgent`                                        | yes      | Your agent identifier — e.g. `claude-desktop`. Logged on the user.                        |
| `country` / `language` / `currency` / `businessType` | no       | Inferred from `Accept-Language` if omitted; reported back in `appliedDefaults`.           |
| `initialStorefront`                                  | no       | Full [`StorefrontManifest`](/concepts/storefronts) if you want products in the same call. |

### Partial-success (207)

If `initialStorefront.products[]` exceeds the user's plan cap, Marea returns **207** with the storefront created up to the cap and an `errors[]` array describing what was skipped. Surface those verbatim and prompt for upgrade. See [Plan limits](/concepts/plan-limits).

## Step 2 — Verify the 6-digit code

The user gets a code in their inbox. Either ask them to read it aloud, or read it via the Gmail MCP — see [Verification flow](/concepts/verification-flow).

```bash theme={null}
curl -s -X POST https://api.mareaalcalina.com/v1/users/$USER_ID/verify \
  -H "Authorization: Bearer $MAREA_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "123456" }'
```

Response — **200 OK**:

```json theme={null}
{ "userId": "usr_4f1a2b3c4d5e6f7a8b9c0d1e", "verificationStatus": "verified" }
```

The **same `$MAREA_USER_KEY`** is now upgraded in place — it gains `catalog:read`, `catalog:write`, `storefront:publish` on top of the verify scopes it already had. **Do not rotate or re-issue.** You can now call any product or storefront endpoint.

### If the code is wrong / expired / never arrived

```bash theme={null}
# 3/hour, 5/day per user.
curl -s -X POST https://api.mareaalcalina.com/v1/users/$USER_ID/resendVerification \
  -H "Authorization: Bearer $MAREA_USER_KEY"
```

## Two keys, one mental model

You only see two API keys in the whole API:

* **`$MAREA_DEV_KEY`** — your agent's own key, issued from [/developers/keys](https://mareaalcalina.com/developers/keys). Used to bootstrap users and manage webhook endpoints. Never grants catalog access.
* **`$MAREA_USER_KEY`** — a single user's key, returned by `POST /v1/users`. Used for everything you do on behalf of that user (catalog reads, product writes, publish) **across every storefront the user owns**. One key per user, not one key per storefront. Tenant boundary is baked into the key — it can only see/edit that user's data.

If you hit `403 insufficient_scope`, you're sending the wrong key. The error response includes `requiredScopes[]` and `heldScopes[]` so you can tell at a glance which one was needed.

Full model + rotation rules: [/concepts/keys](/concepts/keys).

## Errors you should branch on

Every non-2xx follows the [§9.6 envelope](/concepts/errors): `{ type, code, message, doc, param, requestId, requestLogUrl, recoverable, retryAfterMs, nextActions[], upgrade }`. Branch on `error.type` and `error.code` — never on `error.message` (localized).

### Step 1 — `POST /v1/users`

| HTTP | `error.code`                                                                               | What happened                          | Agent action                                                     |
| ---- | ------------------------------------------------------------------------------------------ | -------------------------------------- | ---------------------------------------------------------------- |
| 400  | `invalid_request`                                                                          | Email malformed, missing field, etc.   | Fix the body, reissue with a NEW `Idempotency-Key`.              |
| 401  | `missing_authorization` / `invalid_authorization_format` / `key_not_found` / `key_revoked` | Bad/missing/revoked dev key            | Re-prompt for a valid `mk_dev_*`.                                |
| 403  | `insufficient_scope`                                                                       | Dev key lacks `developer:bootstrap`    | Issue a key with the right scope (see `requiredScopes` in body). |
| 409  | `email_exists`                                                                             | The email already has a Marea account  | Send the user to log in.                                         |
| 429  | `rate_limit_exceeded` (message contains `rpd_exceeded`)                                    | Dev-key daily cap hit (50/day default) | Sleep `retryAfterMs`, then retry.                                |

<Accordion title="Full error response examples (auth, conflict, rate-limit)">
  ```json theme={null}
  // 401 — missing header
  {
    "error": {
      "type": "auth",
      "code": "missing_authorization",
      "message": "Authorization header is required.",
      "doc": "https://docs.mareaalcalina.com/concepts/keys#authorization",
      "param": "Authorization",
      "requestId": "req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
      "requestLogUrl": "https://mareaalcalina.com/developers/logs/req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
      "recoverable": false,
      "retryAfterMs": null,
      "nextActions": [
        { "label": "Get a developer key.", "method": null, "url": "https://mareaalcalina.com/developers/keys" }
      ],
      "upgrade": null
    }
  }
  ```

  ```json theme={null}
  // 409 — email already exists
  {
    "error": {
      "type": "conflict",
      "code": "email_exists",
      "message": "An account with this email already exists.",
      "doc": "https://docs.mareaalcalina.com/concepts/errors#email_exists",
      "param": "email",
      "requestId": "req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
      "requestLogUrl": "https://mareaalcalina.com/developers/logs/req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
      "recoverable": false,
      "retryAfterMs": null,
      "nextActions": [
        { "label": "The user already has a Marea account — send them to log in.", "method": null, "url": "https://mareaalcalina.com/login" }
      ],
      "upgrade": null
    }
  }
  ```

  ```json theme={null}
  // 429 — dev key hit daily cap
  {
    "error": {
      "type": "rate_limited",
      "code": "rate_limit_exceeded",
      "message": "rpd_exceeded — Developer key has reached its daily quota.",
      "doc": "https://docs.mareaalcalina.com/concepts/rate-limits",
      "param": null,
      "requestId": "req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
      "requestLogUrl": "https://mareaalcalina.com/developers/logs/req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
      "recoverable": true,
      "retryAfterMs": 21600000,
      "nextActions": [{ "label": "Wait and retry after the reset.", "method": null, "url": null }],
      "upgrade": null
    }
  }
  ```
</Accordion>

### Step 2 — `POST /v1/users/:userId/verify`

| HTTP | `error.code`        | What happened                                   | Agent action                                               |
| ---- | ------------------- | ----------------------------------------------- | ---------------------------------------------------------- |
| 400  | `code_invalid`      | Wrong digits                                    | Ask the user to re-read — up to 3 attempts before lockout. |
| 404  | `code_not_found`    | No active code (already verified or never sent) | Call resend, then retry.                                   |
| 404  | `user_not_found`    | `:userId` doesn't match the calling key         | Wrong `$MAREA_USER_KEY` for this `$USER_ID`.               |
| 410  | `code_expired`      | 15-minute TTL elapsed                           | Call resend, then retry.                                   |
| 429  | `too_many_attempts` | 3 failed attempts on the same code              | Call resend (issues a new code), then retry.               |

Full error matrix at [/concepts/errors](/concepts/errors). Idempotency rules at [/concepts/safe-mutations](/concepts/safe-mutations). Rate-limit defaults at [/concepts/rate-limits](/concepts/rate-limits).

<LLMBlock action="bootstrap a Marea user account and verify their email" method="POST" path="/v1/users" keyType="mk_dev_*" bodyExample={`{ "email": "owner@taqueria.example", "displayName": "Marea Taqueria", "sourceAgent": "claude-desktop" }`} summary="Creates a Marea user, an optional starter storefront, and a per-user key. Capture `userKey` from the 201 response — that's the key you use for every subsequent catalog/publish/product call for this user. Then POST /v1/users/:userId/verify with the 6-digit code from the user's inbox to upgrade that same key to full scope. The userKey is returned ONCE — store it immediately." errors="429 rate_limited (code: rate_limit_exceeded, message contains rpd_exceeded → dev-key daily cap; sleep retryAfterMs); 409 conflict (email_exists → tell the user to log in); 400 invalid_request (fix body, new Idempotency-Key); 207 Multi-Status (initialStorefront exceeded plan cap — accepted up to cap, errors[] lists skipped). Verify: 400 code_invalid, 410 code_expired (resend), 429 too_many_attempts (resend), 404 user_not_found (wrong key)." endpoint="catalog/bootstrap-user" />

## Next steps

* [Add and edit products](/quickstart/products) — use `$MAREA_USER_KEY` from here on.
* [Publish a storefront](/quickstart/publish) — handle the 402 paywall, 422 empty, and 451 ToS gates.
* [Install MCP in Claude Desktop / Cursor / Continue.dev](/quickstart/mcp) — same flow, exposed as `marea.*` tools.
