# API reference

Five endpoints. Base URL `https://api.layout.link/v1`, bearer auth on every call, JSON in and out.

## POST /v1/users

Create someone you can order for. You send a name and a phone number; you get back an id to use everywhere else, and the link that person uses to claim the account.

```bash
curl https://api.layout.link/v1/users \
  -H "Authorization: Bearer $LAYOUT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "firstName": "Dana", "lastName": "Whitfield", "phone": "+19165550142" }'
```

```
201 Created

{
  "id": "usr_4b8e",
  "status": "provisioned",
  "session": { "scope": "build", "expiresIn": 3600 },
  "handoffUrl": "https://account.layout.link/join"
}
```

| Field | Required | Notes |
| --- | --- | --- |
| `firstName` | Yes | Shown to the person on the consent screen. |
| `lastName` | Yes | Shown to the person on the consent screen. |
| `phone` | Yes | E.164, e.g. `+19165550142`. They verify it themselves on the link. |

Calling this twice with the same phone number gives you the same user back rather than a duplicate.

## POST /v1/orders

Start building a cart. This returns immediately with `building` — carts take a while, because Layout is driving the restaurant's own site. Wait for the `order.carted` webhook, or poll.

```bash
curl https://api.layout.link/v1/orders \
  -H "Authorization: Bearer $LAYOUT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "usr_4b8e",
    "placeId": "plc_sweetgreen_market",
    "query": "harvest bowl, no onion, and a lemonade",
    "idempotencyKey": "cart_20260912_8842"
  }'
```

```
201 Created

{
  "id": null,
  "state": "building",
  "idempotencyKey": "cart_20260912_8842"
}
```

`id` is null until there is an order to point at. Hold the `idempotencyKey`: it is your handle on this build until the id arrives.

| Field | Required | Notes |
| --- | --- | --- |
| `userId` | Yes | From `POST /v1/users`. |
| `query` | Yes | What they asked for, in their words. Keep every size and prep detail: "grande iced latte, oat milk, extra ice", not "latte". |
| `placeId` | One of three | A specific store. Otherwise send `near` or `geo`. |
| `near` | One of three | An address, city or area, as text. |
| `geo` | One of three | `{ "lat": 38.58, "lng": -121.49 }`. |
| `items` | No | The same order split one entry per item. Lets Layout tell you by name if one did not make it into the cart. |
| `modifiers` | No | Customizations as an explicit checklist, on top of `query`. |
| `idempotencyKey` | No, but send it | Retrying without one builds the order twice. See [Rate limits and idempotency](https://developer.layout.link/reference/limits). |

A retry carrying a key you already used returns `200` with `"replay": true` and the original order, instead of building anything.

## GET /v1/orders/:id

Everything Layout will tell you about one order.

```bash
curl https://api.layout.link/v1/orders/ord_7c21 \
  -H "Authorization: Bearer $LAYOUT_SECRET"
```

```
200 OK

{
  "publicId": "ord_7c21",
  "state": "placed",
  "store": "Sweetgreen, Market St",
  "items": 2,
  "totalMinor": 2140,
  "currency": "usd",
  "createdAt": "2026-09-12T17:00:41Z",
  "placedAt": "2026-09-12T17:03:02Z",
  "failureCode": null
}
```

Totals are minor units: `2140` is $21.40. An order belonging to another application is a `404`, never a `403` — you cannot tell the difference between someone else's order and one that never existed, which is the point.

### States

| State | Meaning |
| --- | --- |
| `building` | Layout is on the restaurant's site assembling the cart. |
| `carted` | Priced and ready. Show the person the total and send them to the link. |
| `placing` | They confirmed. Submission is in flight. |
| `placed` | It reached the merchant, with evidence. The only state that means the food is coming. |
| `failed` | It did not go through. `failureCode` says why. Nothing was charged. |
| `unconfirmed` | Layout cannot tell either way. Do not retry and do not say "failed". |
| `refunded` | Money went back. |

## POST /v1/users/:id/grant

Mint a build-only token for one person, for an MCP session. Ask for it deliberately, when you are about to hand an assistant the wheel.

```bash
curl -X POST https://api.layout.link/v1/users/usr_4b8e/grant \
  -H "Authorization: Bearer $LAYOUT_SECRET"
```

```
201 Created

{
  "token": "lgb_9f3a2c…",
  "expiresAt": "2026-09-12T18:00:41Z",
  "scope": "build"
}
```

The token builds carts and reads state. It cannot add a card, confirm, or spend anything — all of that happens on Layout's own page, in front of the person whose money it is.

## GET /v1/events

The same events your webhooks receive, as a cursor-paged feed. Useful when your endpoint was down, or when you would rather poll than run one.

```bash
curl "https://api.layout.link/v1/events?limit=50" \
  -H "Authorization: Bearer $LAYOUT_SECRET"
```

```
200 OK

{
  "events": [
    {
      "event": "order.placed",
      "delivery_id": "dl_9f3a2c",
      "order": { "id": "ord_7c21", "state": "placed", "store": "Sweetgreen, Market St",
                 "items": 2, "total_minor": 2140, "currency": "usd" },
      "user": { "id": "usr_4b8e" },
      "sent_at": "2026-09-12T17:03:02Z"
    }
  ],
  "nextCursor": "ev_01J8Z…"
}
```

Pass `nextCursor` back as `?cursor=` to walk backwards. `limit` defaults to 50 and caps at 100. When `nextCursor` is null you have reached the end.
