Layout API — Agent Reference

You are an AI agent. This document tells you exactly how to use the Layout 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 Layout does for agents

Layout lets you place real food and drink orders at brick-and-mortar merchants that have integrated with the Layout platform. 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

{
  "orderId": "bo_abc123...",
  "squareOrderId": "sq_...",
  "squareLocationId": "LOC1",
  "status": "placed",
  "totalCents": 0,
  "subtotalCents": 625,
  "currency": "USD",
  "compApplied": true,
  "pickupAt": "2026-05-12T18:45:00Z",
  "trackingUrl": "https://layout.link/x4k9m2qp"
}

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

Always surface trackingUrl to the user — it's a public live status page that shows the order moving through placed → preparing → ready → picked up. The user can re-open it any time without coming back to the chat.

Tell the user something like:

"Your order is in — track it at \[trackingUrl\]. Pick it up at \[location\] around \[pickup time\]. No payment needed."


Order status

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

Returns the LIVE status from the merchant's KDS:

statusmeaning
placedSent to the merchant, not yet started
preparingThe kitchen has started on it
readyReady for pickup at the counter
picked_upCustomer has picked it up

Call this whenever the user asks *"is it ready?"* or *"where's my order?"* — there is no push channel, this poll is the only way to give a fresh answer. The response also includes the same trackingUrl you got back from POST /v1/orders — re-share it if the user has lost the link.


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 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 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.
pos-error / square-errorMerchant's POS / order system 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.