Skip to main content

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

The bootstrap flow lets an agent create a Marea account on behalf of a small-business operator without ever holding the user’s password. It’s two calls separated by a 6-digit code that goes to the user’s email.

The architecture (read first)

The two-tier key model is what makes this safe (full model: Two-tier keys):
agent (holds mk_dev_*)      ──POST /v1/users──▶  Marea
                                                 │ creates user, storefront, mk_user_* (RESTRICTED)
                                                 │ emails 6-digit code to user
                            ◀──── 201 { userKey: mk_user_xxx, ... }
agent prompts user for code
user reads code aloud OR agent fetches from Gmail-MCP
                            ──POST /verify──▶
                                                 │ upgrades mk_user_* to FULL scope (same key, no rotation)
                            ◀──── 200 { verificationStatus: "verified" }

agent (now holds mk_user_*) ──any catalog op──▶  Marea
Why the split: the agent’s mk_dev_* key can bootstrap users (scope developer:bootstrap) but cannot touch individual user data. The mk_user_* key returned by bootstrap is restricted to me:verify + me:resendVerification until the user submits the code — then the same key is upgraded in place to the full per-user scope set. The agent never holds the user’s password and the user-key never has bootstrap power.

Step 1 — Create the user

Required scope on the calling key: developer:bootstrap. Hard cap: 50 bootstraps/day per dev key (rpd: 50).
curl -X POST https://api.mareaalcalina.com/v1/users \
  -H "Authorization: Bearer mk_dev_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Accept-Language: es-MX" \
  -H "Idempotency-Key: 2f1a8c4b-2e3a-4b9d-9f1a-8c4b2e3a4b9d" \
  -d '{
    "email": "owner@taqueria.example",
    "displayName": "Marea Taqueria",
    "country": "MX",
    "language": "es",
    "currency": "MXN",
    "businessType": "restaurant",
    "sourceAgent": "claude-desktop"
  }'

Request body fields

FieldTypeRequiredNotes
emailstring (RFC 5322)yesWhere the 6-digit code is sent.
displayNamestring (1–200)yesShown in the dashboard + on the storefront if no name override.
sourceAgentstring (1–64, ^[A-Za-z0-9 _.-]+$)yesYour agent identifier — e.g. claude-desktop, cursor, my-saas. Logged on the user.
countryISO 3166-1 alpha-2noDefaults from Accept-Language then MX.
languagees | en | ptnoDefaults from Accept-Language then from country.
currencyISO 4217noDefaults from country (e.g. MXMXN).
businessTypestringnoDefaults to general. Used to tune the starter storefront.
initialStorefrontStorefrontManifestnoIf present, a storefront is created in the same call.

Response — 201 Created

{
  "userId": "usr_4f1a2b3c4d5e6f7a8b9c0d1e",
  "storefrontId": "stf_9a8b7c6d5e4f3a2b1c0d9e8f",
  "userKey": "mk_user_01HZX9K8QW7VPYR3M2N1B4FJSA",
  "verificationStatus": "pending",
  "verificationExpiresAt": "2026-05-10T18:23:00.000Z",
  "verificationDeliveryHint": "email-only",
  "previewToken": "pv_8c4b2e3a4b9d9f1a8c4b2e3a",
  "appliedDefaults": {
    "language": "es",
    "currency": "MXN",
    "country": "MX",
    "businessType": "restaurant"
  },
  "idempotent": false
}
  • The userKey is shown once. Store it immediately in the user’s session — there is no way to read it back.
  • The key is restricted at this point: it can only call POST /v1/users/:userId/verify and POST /v1/users/:userId/resendVerification until the code is submitted.
  • verificationExpiresAt is the deadline for the 6-digit code. After expiry, call resend.
  • previewToken is an unguessable token: surface https://marea.pro/preview/{previewToken} to the user so they can see their starter storefront before verifying.
  • appliedDefaults reports the values Marea inferred when you omitted country / language / currency / businessType.

207 Multi-Status — partial-success with initialStorefront

If you included initialStorefront with more products than the user’s free-tier cap allows, the user + storefront are created and the over-cap products are dropped. Status is 207 and the response body adds an errors[] array describing each skipped product:
{
  "userId": "usr_...",
  "storefrontId": "stf_...",
  "userKey": "mk_user_...",
  "verificationStatus": "pending",
  "verificationExpiresAt": "2026-05-10T18:23:00.000Z",
  "verificationDeliveryHint": "email-only",
  "previewToken": "pv_...",
  "appliedDefaults": { "language": "es", "currency": "MXN", "country": "MX", "businessType": "restaurant" },
  "errors": [
    { "type": "plan_limit", "code": "products_over_limit", "message": "Skipped 'Taco al pastor': free tier allows 30 products." }
  ]
}
Surface the errors[] array to the user verbatim and offer the upgrade link from GET /v1/me. The accepted-and-stored products are already live; the over-cap ones must be added later via POST /v1/storefronts/:storefrontId/products after upgrade.

Step 2 — Verify the 6-digit code

The user receives an email with a 6-digit code (e.g. 123456). Two patterns:
  • User reads aloud / pastes — the agent prompts: “Check your inbox at owner@taqueria.example and tell me the 6-digit code.”
  • Silent variant via Gmail MCP — if the user has the Gmail MCP server installed, the agent can read the inbox directly. See Verification flow.
Required scope on the calling key: me:verify (the restricted scope automatically granted on the just-issued mk_user_*). The :userId must match the key’s owner — wrong id returns a leak-less 404 not_found, not a 403.
curl -X POST https://api.mareaalcalina.com/v1/users/usr_4f1a2b3c4d5e6f7a8b9c0d1e/verify \
  -H "Authorization: Bearer mk_user_01HZX9K8QW7VPYR3M2N1B4FJSA" \
  -H "Content-Type: application/json" \
  -d '{ "code": "123456" }'

Response — 200 OK

{
  "userId": "usr_4f1a2b3c4d5e6f7a8b9c0d1e",
  "verificationStatus": "verified"
}
The mk_user_* you already hold is now upgraded in place to the full per-user scope set (catalog:read, catalog:write, storefront:publish, me:*). Do not rotate; do not re-issue. Catalog mutations now succeed. Publish still requires the user to also accept the dashboard ToS — see Publishing.

Step 2b — Resend the code

If the user never received the email or the 10-minute TTL elapsed, call resend with the same mk_user_* key. Rate-limited per user: 3/hour, 5/day.
curl -X POST https://api.mareaalcalina.com/v1/users/usr_xxx/resendVerification \
  -H "Authorization: Bearer mk_user_xxxxxxxxxxxxxxxx"
Response — 200 OK:
{
  "verificationStatus": "pending",
  "verificationExpiresAt": "2026-05-10T18:38:00.000Z"
}
The old code is voided automatically; only the new code in the freshly-sent email is valid.

Recoverable error envelopes

Every non-2xx follows the §9.6 envelope: { 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

// 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
  }
}
// 409 — email already exists on Marea
{
  "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
  }
}
// 429 — dev key hit its daily cap (default 50/day)
{
  "error": {
    "type": "rate_limited",
    "code": "rate_limit_exceeded",
    "message": "rpd_exceeded — Developer key has reached its daily quota. Retry after the reset.",
    "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
  }
}
Quick lookup table:
HTTPerror.codeWhat happenedAgent action
400invalid_request (various)Email malformed, missing field, etc.Fix the body, reissue with a NEW Idempotency-Key.
401missing_authorization / invalid_authorization_format / key_not_found / key_revokedBad / missing / revoked dev keyRe-prompt for a valid mk_dev_* key.
403insufficient_scopeDev key lacks developer:bootstrapIssue a key with the right scope; read requiredScopes / heldScopes in the body.
409email_existsThe email already has a Marea accountSend the user to log in.
429rate_limit_exceeded (message contains rpd_exceeded / rpm_exceeded)Dev-key daily or per-minute cap hitSleep retryAfterMs, then retry.

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

// 400 — wrong digits
{
  "error": {
    "type": "invalid_request",
    "code": "code_invalid",
    "message": "Invalid verification code.",
    "doc": "https://docs.mareaalcalina.com/concepts/errors#code_invalid",
    "param": "code",
    "requestId": "req_...",
    "requestLogUrl": "https://mareaalcalina.com/developers/logs/req_...",
    "recoverable": true,
    "retryAfterMs": null,
    "nextActions": [
      { "label": "Ask the user to re-read the 6 digits from the email.", "method": null, "url": null }
    ],
    "upgrade": null
  }
}
// 410 — 10-minute TTL elapsed
{
  "error": {
    "type": "invalid_request",
    "code": "code_expired",
    "message": "Verification code has expired. Request a new one.",
    "doc": "https://docs.mareaalcalina.com/concepts/errors#code_expired",
    "param": "code",
    "requestId": "req_...",
    "requestLogUrl": "https://mareaalcalina.com/developers/logs/req_...",
    "recoverable": true,
    "retryAfterMs": null,
    "nextActions": [
      { "label": "Send a fresh code, then retry.", "method": "POST", "url": "/v1/users/usr_xxx/resendVerification" }
    ],
    "upgrade": null
  }
}
// 429 — 3 failed attempts on the same code
{
  "error": {
    "type": "rate_limited",
    "code": "too_many_attempts",
    "message": "Too many failed attempts. Request a new code.",
    "doc": "https://docs.mareaalcalina.com/concepts/errors#too_many_attempts",
    "param": "code",
    "requestId": "req_...",
    "requestLogUrl": "https://mareaalcalina.com/developers/logs/req_...",
    "recoverable": true,
    "retryAfterMs": null,
    "nextActions": [
      { "label": "Request a fresh code, then retry.", "method": "POST", "url": "/v1/users/usr_xxx/resendVerification" }
    ],
    "upgrade": null
  }
}
Quick lookup:
HTTPerror.codeWhat happenedAgent action
400code_invalidWrong digitsAsk the user to re-read the email — up to 3 attempts.
404code_not_foundNo active code (already verified, or never sent)Call resend, then retry.
404user_not_found:userId doesn’t match the calling key (leak-less)Agent is using the wrong mk_user_* for this usr_*.
410code_expired10-minute TTL elapsedCall resend, then retry.
429too_many_attempts3 failed attempts on the same codeCall resend (issues a new code), then retry.
Full error matrix at /concepts/errors. Idempotency rules at /concepts/safe-mutations. Rate-limit defaults at /concepts/rate-limits.

Next steps