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:

{
  "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:

StatusMeaningAction
pendingUser hasn't completed the form yetKeep polling
completedDone — token returned in this responseSave the consumerSessionToken and stop polling
expired15 min elapsedTell the user and start over
consumedToken was already retrieved (you polled twice after completion)Use the token you got the first time

When status === "completed":

{
  "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

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:

{
  "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:

{
  "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:


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

{
  "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:

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

Codes you should handle gracefully

CodeWhenWhat to tell the user
missing-consumer-session / expired-consumer-session / invalid-consumer-session / revoked-consumer-sessionSession token missing or no longer validRun onboarding again to reconnect their Layout account
merchant-not-foundNo such merchant"I couldn't find that merchant on Layout."
merchant-not-accepting-agent-ordersMerchant exists but opted out"I found this merchant, but they don't accept agent-placed orders yet."
no-locationMerchant 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-NCart is empty or malformedRe-check the menu and rebuild the cart
invalid-pickup-atPickup time is in the past"I need a future pickup time — when would you like it?"
order-velocity-hour / order-velocity-dayUser hit the per-hour or per-day order cap"You've reached today's limit on agent-placed orders. Try again later."
rate-limit-exceededPer-platform rate limitBack off. Wait the seconds in the Retry-After header.
square-errorSquare API returned an errorSurface 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


Rate limits

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


Discovery / self-description

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