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.
Step 1 — POST /v1/users
Required scope: developer:bootstrap. Returns 201 Created (or 207 Multi-Status if a starter manifest had over-cap 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:
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 |
Step 2 — submit the code
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):
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) |
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)
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:
| 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 athttps://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.tssrc/api/v1/users.verify.tssrc/api/v1/users.resendVerification.tssrc/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.