> ## 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 returns a restricted key + emails a 6-digit code; POST /v1/users/:userId/verify upgrades the key to full scope. Plain code by design.

# 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):

```http theme={null}
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:

```json theme={null}
{
  "userId": "usr_...",
  "storefrontId": "stf_...",
  "userKey": "mk_user_...",
  "verificationStatus": "pending",
  "verificationExpiresAt": "2026-05-10T20:15:00Z",
  "verificationDeliveryHint": "email-only",
  "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

```http theme={null}
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`):

```json theme={null}
{ "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)

```http theme={null}
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:

```json theme={null}
{ "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](/concepts/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.
