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
| Field | Type | Required | Notes |
|---|
email | string (RFC 5322) | yes | Where the 6-digit code is sent. |
displayName | string (1–200) | yes | Shown in the dashboard + on the storefront if no name override. |
sourceAgent | string (1–64, ^[A-Za-z0-9 _.-]+$) | yes | Your agent identifier — e.g. claude-desktop, cursor, my-saas. Logged on the user. |
country | ISO 3166-1 alpha-2 | no | Defaults from Accept-Language then MX. |
language | es | en | pt | no | Defaults from Accept-Language then from country. |
currency | ISO 4217 | no | Defaults from country (e.g. MX → MXN). |
businessType | string | no | Defaults to general. Used to tune the starter storefront. |
initialStorefront | StorefrontManifest | no | If 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:
| HTTP | error.code | What happened | Agent action |
|---|
| 400 | invalid_request (various) | 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_* key. |
| 403 | insufficient_scope | Dev key lacks developer:bootstrap | Issue a key with the right scope; read requiredScopes / heldScopes in the 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 / rpm_exceeded) | Dev-key daily or per-minute cap hit | Sleep 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:
| HTTP | error.code | What happened | Agent action |
|---|
| 400 | code_invalid | Wrong digits | Ask the user to re-read the email — up to 3 attempts. |
| 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 (leak-less) | Agent is using the wrong mk_user_* for this usr_*. |
| 410 | code_expired | 10-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. Idempotency rules at /concepts/safe-mutations. Rate-limit defaults at /concepts/rate-limits.
Next steps