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.
Verification flow
POST /v1/users (bootstrap) creates the user, returns a restricted mk_user_* key, and emails the user a 6-digit code. The agent then submits the code to POST /v1/users/:userId/verify — the same key is upgraded in place to full scope. No key rotation; store the value once.
Agent (dev key) User
│ │
│ POST /v1/users │
├─────────────────────────▶ │
│ { email, displayName, │
│ sourceAgent: "..." } │
│ │
│ 201 + restricted user key │
◀─────────────────────────┤ │
│ + verificationExpiresAt │
│ │
│ email with 6-digit code (15 min TTL)
│ │
│ │ reads code aloud OR
│ │ agent reads it from Gmail MCP
│ │
│ POST /v1/users/:userId/verify │
├─────────────────────────▶ │
│ { code: "123456" } │
│ │
│ 200 { verificationStatus: │
◀─────────────────────────┤ │
│ "verified" } │
│ │
│ same key — now has │
│ catalog:write + storefront:publish │
Step 1 — POST /v1/users
Required scope: developer:bootstrap. Returns 201 Created (or 207 Multi-Status if a starter manifest had over-cap products):
POST /v1/users HTTP/1.1
Authorization: Bearer mk_dev_...
Content-Type: application/json
Idempotency-Key: 2f1a8c4b-2e3a-4b9d-9f1a-8c4b2e3a4b9d
{
"email": "owner@taqueria.example",
"displayName": "La Taquería",
"country": "MX",
"language": "es",
"currency": "MXN",
"businessType": "restaurante",
"sourceAgent": "claude-code",
"initialStorefront": { "name": "Tacos La Marea", "products": [ ... ] }
}
sourceAgent is required (1–64 chars, [A-Za-z0-9 _.-]+ only). It is embedded in the verification email so the user can see which agent triggered the account.
Response:
{
"userId": "usr_...",
"storefrontId": "stf_...",
"userKey": "mk_user_...",
"verificationStatus": "pending",
"verificationExpiresAt": "2026-05-10T20:15:00Z",
"verificationDeliveryHint": "email-only",
"previewToken": "...",
"appliedDefaults": { "language": "es", "currency": "MXN", "country": "MX", "businessType": "restaurante" },
"idempotent": false
}
Store userKey immediately. It is shown once. It starts with the restricted scope set:
| Scope | Purpose |
|---|
catalog:read | Inspect the draft storefront |
me:verify | Submit the 6-digit code |
me:resendVerification | Request a fresh code if the email was lost |
Until verify, the restricted key cannot create/update catalog data or publish.
Step 2 — submit the code
POST /v1/users/:userId/verify HTTP/1.1
Authorization: Bearer mk_user_...
Content-Type: application/json
{ "code": "123456" }
Required scope: me:verify. The :userId path param must match the key’s owner — cross-tenant or wrong-id attempts return 404 user_not_found (leak-less, never 403).
On success (200):
{ "userId": "usr_...", "verificationStatus": "verified" }
The same key’s scope set is upgraded in place to catalog:read, catalog:write, storefront:publish. Cache propagation is bounded by the 30s positive auth cache; the planLimits cache for the user is invalidated immediately so GET /v1/me reflects verified on the next call.
Code rules
| Property | Value |
|---|
| Format | 6 decimal digits (^\d{6}$) |
| TTL | 15 minutes from issuance |
| Max attempts | 3 wrong attempts → 429 too_many_attempts (must resend) |
| Storage | Stored in plain text (intentional — see below) |
| Source of randomness | crypto.randomInt (CSPRNG) |
Plain-text storage is intentional. The user-reads-aloud flow requires the agent to be able to say the code on screen. A bcrypt-hashed code would only support submit-then-check, not the agentic read-aloud variant. Defense-in-depth lives elsewhere: 3-attempts lockout, 15-min TTL, per-user resend rate-limit, and the leak-less 404 on cross-tenant verify.
Verify error shapes
| Status | error.code | Meaning | Agent action |
|---|
400 | code_invalid | The submitted code doesn’t match | Re-prompt the user; on 3rd wrong attempt, resend |
404 | code_not_found | No active code for this user (never sent, or fully consumed) | Call POST /v1/users/:userId/resendVerification |
404 | user_not_found | :userId doesn’t match the key’s owner (silent denial) | Use the correct user key |
410 | code_expired | Code older than 15 min | Resend |
429 | too_many_attempts | 3 wrong attempts in this verification window | Resend |
Step 3 — resend (when needed)
POST /v1/users/:userId/resendVerification HTTP/1.1
Authorization: Bearer mk_user_...
Required scope: me:resendVerification. Per-user rate-limited at 3 / hour and 5 / day. Overwrites the existing code doc — the old code is invalidated on resend.
Returns:
{ "verificationStatus": "pending", "verificationExpiresAt": "2026-05-10T20:30:00Z" }
| Status | error.code | Meaning |
|---|
429 | resend_hour_limit | 3 resends used in the current hour |
429 | resend_day_limit | 5 resends used today |
Variants of step 2
A — User reads the code aloud. Default. Agent says “I sent you a 6-digit code; tell me the number.” User reads from email. Agent submits.
B — Silent verification via Gmail MCP. If the user’s mailbox is connected to the agent (e.g. Gmail MCP), the agent reads the verification email programmatically, extracts the 6-digit code, and submits — no manual step. The API contract is identical; only the source of the code differs.
Account-cancellation hatch
Every bootstrap email also embeds a single-use cancel link (24h TTL) pointing at https://api.mareaalcalina.com/public/v1/bootstrap/:previewToken. The user opens it, confirms once (GET → POST, two-step to defeat email-preview crawlers), and the account is hard-deleted. This is the LFPDPPP-required cancellation hatch for accounts created by an agent (see ARCO procedures).
Verification in code
src/api/v1/users.bootstrap.ts
src/api/v1/users.verify.ts
src/api/v1/users.resendVerification.ts
src/api/services/verification.service.ts — 3-attempt lockout + scope upgrade.
src/services/verification/sendVerificationCodeCore.ts — code generation + email delivery (15-min API TTL).
src/api/public/bootstrap.cancel.ts — emergency cancel endpoint.