# Layout Bedrock — Agent API Reference

You are an AI agent. This document tells you exactly how to use the Layout Bedrock API to place real orders at Layout-connected coffee shops, cafés, and restaurants on behalf of a user.

Read this entire document before issuing your first request. The flow is short but strict — there are a few things that will fail in confusing ways if you skip them.

---

## What Bedrock does

Bedrock lets you place real food and drink orders at brick-and-mortar merchants that have integrated with Layout. The order shows up on the merchant's kitchen display system (KDS) and is fulfilled like any normal pickup order. The user picks it up at the counter.

Pickup orders are currently placed without collecting payment — you do not need to collect a card from the user. There is no card-on-file requirement. Tell the user this when confirming an order.

---

## The five things you do, in order

1. **Onboard** the user to Layout (or reconnect a returning user) via a one-time browser link.
2. **Find** the merchant the user wants to order from.
3. **Get the menu** for that merchant.
4. **Confirm** the order details with the user (item, modifiers, pickup time, total).
5. **Place** the order, then optionally **check status**.

---

## Authentication

Every API call requires this header:

```
Authorization: Bearer <platform-api-key>
```

The platform API key identifies *you* (the AI platform — Claude, ChatGPT, etc.), not the user. You get one issued to your platform by Layout.

Calls that act on a specific user's behalf additionally require:

```
X-Consumer-Session: <consumer-session-token>
```

This token is obtained through the onboarding flow described next. Treat it like a password — store it securely, send it only over HTTPS to this API, never log it, never include it in messages to the user.

If a request fails with `401 missing-consumer-session` or `401 expired-consumer-session`, run the onboarding flow again to get a fresh token.

---

## Onboarding flow

This is how a user first connects their Layout account, and also how they return later to view/edit it. **You never see the user's phone verification code.** All sensitive steps happen in the user's browser.

### 1. Start an onboarding session

```
POST /v1/onboarding/start
Authorization: Bearer <platform-api-key>
Content-Type: application/json

{
  "mode": "signup",                  // or "signin" or "dashboard"
  "requestedPhone": "+15551234567"   // optional — prefills the form
}
```

**Response:**

```json
{
  "sid": "a1b2c3...",
  "url": "https://bedrock.layoutmobile.com/v1/onboarding/page/a1b2c3?t=...",
  "expiresAt": "2026-05-12T18:30:00Z",
  "ttlSeconds": 900
}
```

### 2. Send the URL to the user

Tell the user: "Tap this link to connect your Layout account: \<url\>. It expires in 15 minutes." Don't paste the URL into a public message or share it anywhere else — it's single-use.

### 3. Poll for completion

```
GET /v1/onboarding/{sid}/status
Authorization: Bearer <platform-api-key>
```

Poll every 3–5 seconds for up to 15 minutes. Possible responses:

| Status | Meaning | Action |
|---|---|---|
| `pending` | User hasn't completed the form yet | Keep polling |
| `completed` | Done — token returned in this response | Save the `consumerSessionToken` and stop polling |
| `expired` | 15 min elapsed | Tell the user and start over |
| `consumed` | Token was already retrieved (you polled twice after completion) | Use the token you got the first time |

When `status === "completed"`:

```json
{
  "status": "completed",
  "uid": "abc123...",
  "consumerSessionToken": "bcs_..."
}
```

**The token is returned exactly once.** If you don't save it, the user has to onboard again. Persist it associated with the user in your platform's storage.

### Modes

- **`signup`** — new user. Form collects first name, last name, email, phone verification.
- **`signin`** — returning user. Just phone verification (lookup by phone).
- **`dashboard`** — signed-in user wants to view/edit their Layout account. Same flow, lands on the account view page.

Use `dashboard` when the user says things like *"let me see my Layout account"* or *"update my email on Layout."*

---

## Finding merchants

```
GET /v1/merchants?q=<search>&limit=25
Authorization: Bearer <platform-api-key>
```

Returns merchants that have opted into agent ordering. Use `q` to filter by name.

**Response:**

```json
{
  "merchants": [
    {
      "companyId": "abc123",
      "name": "Blue Dove Coffee",
      "description": "Specialty coffee in downtown Sacramento",
      "cuisine": "coffee",
      "website": "https://bluedove.example",
      "acceptsAgentOrders": true,
      "currency": "USD",
      "locations": [
        { "id": "LOC1", "name": "Downtown", "address": "...", "timezone": "America/Los_Angeles", "phone": "+1..." }
      ]
    }
  ],
  "count": 1,
  "hasMore": false
}
```

For a specific merchant:

```
GET /v1/merchants/{companyId}
```

If the merchant exists but hasn't opted in, the response will have `acceptsAgentOrders: false`. **Do not attempt to order from a merchant where this is false** — say to the user: "I found Blue Dove Coffee, but they don't accept agent-placed orders yet."

---

## Getting a menu

```
GET /v1/merchants/{companyId}/menu?locationId=<optional>
Authorization: Bearer <platform-api-key>
```

**Response shape:**

```json
{
  "companyId": "abc123",
  "categories": [
    { "id": "CAT1", "name": "Coffee", "ordinal": 0 }
  ],
  "items": [
    {
      "id": "ITEM1",
      "name": "Iced Latte",
      "description": "Espresso with cold milk over ice.",
      "priceCents": 525,
      "priceDollars": 5.25,
      "available": true,
      "soldOut": false,
      "categoryIds": ["CAT1"],
      "categoryNames": ["Coffee"],
      "variations": [
        { "id": "VAR_SM", "name": "Small", "priceCents": 425, "priceDollars": 4.25 },
        { "id": "VAR_MD", "name": "Medium", "priceCents": 525, "priceDollars": 5.25 },
        { "id": "VAR_LG", "name": "Large", "priceCents": 625, "priceDollars": 6.25 }
      ],
      "modifierLists": [
        {
          "id": "MLIST_MILK",
          "name": "Milk",
          "required": true,
          "minSelection": 1,
          "maxSelection": 1,
          "selectionType": "single",
          "modifiers": [
            { "id": "MOD_OAT",   "name": "Oat",   "priceCents": 75 },
            { "id": "MOD_WHOLE", "name": "Whole", "priceCents": 0 }
          ]
        },
        {
          "id": "MLIST_EXTRAS",
          "name": "Extras",
          "required": false,
          "minSelection": 0,
          "maxSelection": 3,
          "selectionType": "multiple",
          "modifiers": [
            { "id": "MOD_SHOT", "name": "Extra Shot", "priceCents": 100 }
          ]
        }
      ],
      "imageUrl": "..."
    }
  ]
}
```

**How to read this:**

- `variations` are size/style choices. Most items have at least one. The user must pick exactly one — use it as the `catalogVariationId` in the order.
- `modifierLists` are option groups. **Required** lists (`required: true`) MUST have a selection from the user. Confirm explicitly with the user — don't pick defaults silently.
- `selectionType: "single"` → one option. `"multiple"` → user picks `minSelection` to `maxSelection`.
- `available: false` or `soldOut: true` → don't offer that item.

---

## Confirming with the user before placing

Always present the user with a clear confirmation before calling `POST /v1/orders`. Include:

1. Merchant name
2. Each item: variation + modifiers + quantity + price
3. Estimated total (sum of `priceCents` × quantity + modifier prices)
4. Pickup time (if not specified, default is now + 15 minutes)
5. Note that no payment is collected — the user doesn't need a card

Wait for explicit user confirmation. If they want to change something, update the cart and re-confirm. Don't proceed on ambiguous answers.

---

## Placing the order

```
POST /v1/orders
Authorization: Bearer <platform-api-key>
X-Consumer-Session: <consumer-session-token>
Content-Type: application/json

{
  "companyId": "abc123",
  "locationId": "LOC1",                   // optional — merchant default if omitted
  "items": [
    {
      "catalogVariationId": "VAR_MD",
      "quantity": 1,
      "notes": "extra hot please",        // optional, max 500 chars
      "modifiers": [
        { "catalogObjectId": "MOD_OAT",  "quantity": 1 },
        { "catalogObjectId": "MOD_SHOT", "quantity": 2 }
      ]
    }
  ],
  "pickupAt": "2026-05-12T18:45:00Z",    // optional, ISO-8601, defaults to now + 15 min
  "idempotencyKey": "your-stable-key"    // optional but strongly recommended
}
```

### Idempotency

If you don't provide an `idempotencyKey`, the server generates one — but if you retry a failed request, the server can't tell it's a retry and may double-place the order. **Always send your own `idempotencyKey`** when retrying. Same key + same body = same order, no duplicates.

### Response

```json
{
  "order": {
    "orderId": "bo_abc123...",
    "squareOrderId": "sq_...",
    "squareLocationId": "LOC1",
    "status": "placed",
    "totalCents": 0,
    "subtotalCents": 625,
    "currency": "USD",
    "compApplied": true,
    "pickupAt": "2026-05-12T18:45:00Z"
  }
}
```

`compApplied: true` is the marker that no payment was collected on this order.

Tell the user:

> "Your order is in. Pick it up at \[merchant location\] at \[pickup time\]. No payment needed."

---

## Order status

```
GET /v1/orders/{orderId}
Authorization: Bearer <platform-api-key>
X-Consumer-Session: <consumer-session-token>
```

Status is `"placed"` after we hand off to the merchant. If you want to check progress beyond that, ask the user — the merchant's KDS is the source of truth for "started", "ready", etc.

---

## Listing the user's orders

```
GET /v1/consumers/me/orders?limit=20
Authorization: Bearer <platform-api-key>
X-Consumer-Session: <consumer-session-token>
```

Returns up to 50 most recent Bedrock orders by this consumer. Useful when the user asks *"what did I order last time?"*

---

## Consumer profile

```
GET /v1/consumers/me
Authorization: Bearer <platform-api-key>
X-Consumer-Session: <consumer-session-token>
```

Returns the consumer's first name, last name, email, phone, and the list of merchants they've ordered from.

To let them edit their profile or check balances, send them through the onboarding flow with `mode: "dashboard"`.

---

## Sign out (revoke session)

```
POST /v1/consumers/me/sessions/revoke
Authorization: Bearer <platform-api-key>
X-Consumer-Session: <consumer-session-token>
```

Use this when the user explicitly says *"sign me out of Layout"* or *"forget my Layout account."* The token is invalidated server-side. You should also delete it from your own storage.

---

## Errors

All errors come back as:

```json
{
  "error": {
    "code": "machine-readable-code",
    "message": "Human-readable explanation."
  }
}
```

### Codes you should handle gracefully

| Code | When | What to tell the user |
|---|---|---|
| `missing-consumer-session` / `expired-consumer-session` / `invalid-consumer-session` / `revoked-consumer-session` | Session token missing or no longer valid | Run onboarding again to reconnect their Layout account |
| `merchant-not-found` | No such merchant | "I couldn't find that merchant on Layout." |
| `merchant-not-accepting-agent-orders` | Merchant exists but opted out | "I found this merchant, but they don't accept agent-placed orders yet." |
| `no-location` | Merchant has no configured Square location | "This merchant isn't fully set up to receive orders right now." |
| `missing-items` / `invalid-line-item-at-index-N` / `missing-catalog-id-at-index-N` | Cart is empty or malformed | Re-check the menu and rebuild the cart |
| `invalid-pickup-at` | Pickup time is in the past | "I need a future pickup time — when would you like it?" |
| `order-velocity-hour` / `order-velocity-day` | User hit the per-hour or per-day order cap | "You've reached today's limit on agent-placed orders. Try again later." |
| `rate-limit-exceeded` | Per-platform rate limit | Back off. Wait the seconds in the `Retry-After` header. |
| `square-error` | Square API returned an error | Surface a friendly message — "the merchant's system returned an error, please try again." |

Never expose raw error codes or messages to the user except as friendly paraphrases — they're machine-readable for you, not user-facing copy.

---

## Things you should NOT do

- **Don't ask the user for their card info.** No payment is collected on pickup orders right now.
- **Don't ask the user for their SMS verification code** — that's collected by Firebase in the browser onboarding page. If the user types a code into the chat, ignore it and remind them it goes in the onboarding page.
- **Don't fabricate menu items or prices.** Always pull from `/v1/merchants/{companyId}/menu`. Prices in the response are authoritative; never guess.
- **Don't share the platform API key with the user.** It's yours, not theirs.
- **Don't log or transmit consumer session tokens outside HTTPS calls to this API.**
- **Don't place an order without explicit user confirmation of the full cart and total.**
- **Don't retry failed orders without a stable `idempotencyKey`** — you risk placing duplicates.

---

## Rate limits

- 60 requests/minute per platform key
- 30 requests/minute per consumer session
- 5 orders/hour per consumer
- 20 orders/day per consumer

If you hit `429 rate-limit-exceeded`, the `Retry-After` header tells you how many seconds to wait.

---

## Discovery / self-description

- **This document (Markdown):** `GET /v1/docs.md`
- **This document (HTML):** `GET /v1/docs`
- **Health check:** `GET /v1/health` (no auth, no rate limit)

When in doubt about how the API behaves, re-fetch this doc — it's the source of truth.
