# Layout CLI

The Layout CLI signs in to your developer account. In sandbox it does the whole build: create an application and a key, call any endpoint, forward webhook events to your machine, fire test events, and run a whole test order. In production it reads: live events, deliveries and key details. It never changes an approved application's production settings, and never moves money. Every command takes `--json`, so a coding agent can drive it.

## Install

Install it from npm. It needs Node 20 or later, and the command is `layout`.

```
npm i -g @layoutplatforms/cli
```

Or run it once without installing: `npx @layoutplatforms/cli login`.

## Sign in

```
layout login
```

It prints a code like `BCDF-GHJK` and opens the console. Check that the code matches your terminal, then choose **Approve**. The code lasts 10 minutes. The session lasts 30 days, and `layout logout` ends it. The console's [CLI](https://console.layout.link/cli) page lists every signed-in CLI, each with **Revoke**.

| Flag | What it does |
| --- | --- |
| `--device-name <name>` | The name the console shows for this computer. The default is its host name. |
| `--no-browser` | Prints the link without opening it. With `CI` set, `login` never opens a browser. |
| `--live` | Also asks to read production for 12 hours. See [Production](#production). |

The token is kept in the macOS Keychain or the Linux Secret Service (`secret-tool`). With neither, it goes in `~/.config/layout/credentials.json` with mode 0600, and the CLI warns you. `LAYOUT_CREDENTIAL_STORE` (`keychain`, `secret-tool` or `file`) picks the store.

### CI tokens

For CI, a container or a coding agent, create a token on the console's [CLI](https://console.layout.link/cli) page (**New token**) and set it as `LAYOUT_CLI_TOKEN`. Every command then runs without `layout login`.

```bash
export LAYOUT_CLI_TOKEN=lcli_...
layout init --name "My app"
```

- Creating one needs a console sign-in from the last 15 minutes. It is shown once, and Layout keeps only a hash of it.
- It lasts 30 days, 90 days (the default) or a year, and the CLI page lists it with **Revoke**. Signing out everywhere revokes it too.
- It is sandbox only. Production reads still need `layout login --live`.
- When it is set it wins over a keychain session. It is only ever sent to `https://api.layout.link`, and the CLI never writes it to disk.

`LAYOUT_API_BASE` points the CLI at another API. The default is `https://api.layout.link`. A token is only ever sent to the API that issued it.

## Commands

| Command | What it does |
| --- | --- |
| `layout init [--name <name> \| --app <id or name>] [--skip-test] [--place <placeId>]` | Set up in one step: pick or create the application, keep its sandbox key, run a test order, and print the `LAYOUT_APP` and `LAYOUT_SECRET_KEY` to save. `--place` takes a `test_place_` id, or a `placeId` from a sandbox places search. Safe to run again. |
| `layout login [--live]` | Sign in. `--live` adds 12 hours of production reads. |
| `layout logout` | Revoke this CLI session and forget it locally. |
| `layout whoami` | The developer account, the CLI session, and whether it can read production. |
| `layout apps list` | Your applications. |
| `layout apps create <name>` | A new application. Prints its sandbox key once. |
| `layout keys list [--live]` | The application's key: client id and last four. |
| `layout keys create [--replace]` | A new sandbox key, printed once. `--replace` retires the one in place: the client id stays the same and the old secret works for another 24 hours. It prints `New sandbox key, same client id. The old secret works until <YYYY-MM-DD HH:MM> UTC.` From CLI 0.4.3. |
| `layout keys revoke --yes` | Revoke the sandbox key and its sandbox build grants. |
| `layout webhooks list [--show-secret] [--live]` | Endpoints, in columns ID, STATE, RECEIVES, VERIFICATION, LAST ERROR, URL and EVENTS, and the sandbox signing secret. From CLI 0.4.2 the secret is masked unless you add `--show-secret`. |
| `layout webhooks add <https url> [--events <a,b>] [--name <name>] [--environment sandbox\|live\|both]` | Add an endpoint, for every event unless you name some. `--environment` says which environment's events it receives, and defaults to `both`. From CLI 0.4.2; `--environment` from CLI 0.4.3. |
| `layout webhooks verify <id> [--live]` | Send an endpoint the verification event now. Exit 0 when it answers with a 2xx. Otherwise exit 1 with `endpoint_not_verified` and the `subscriptionId`. Works on an approved application too. From CLI 0.4.3. |
| `layout webhooks remove <id> --yes` | Remove an endpoint. From CLI 0.4.2. |
| `layout listen --forward-to <url> [--secret <whsec>] [--events <a,b>] [--from-start] [--live]` | Forward events to a local URL. See [Forward events with listen](#forward-events-with-listen). |
| `layout trigger <event> [--order <ord_id>]` | Fire a signed test event. See [Fire a test event](#fire-a-test-event). |
| `layout refusals [--live]` | Why the application's keys were refused recently: when, the reason, how often, the client id and what it means. The same list as Refused requests in the console. From CLI 0.4.3. |
| `layout usage` | Today's sandbox use. From CLI 0.4.2 it lists every allowance `GET /v1/usage` lists, and whether each counts for the application or your developer account. |
| `layout deliveries [--limit <n>] [--live]` | Recent webhook deliveries. `--limit` is 1 to 100, and 20 by default. |
| `layout api <get\|post\|delete> <path>` | Call any REST endpoint. See [Call the API](#call-the-api). |
| `layout test-order [--place <id>] [--simulate <ending>] [--latency <realistic\|instant>]` | Run a whole sandbox order. See [Run a test order](#run-a-test-order). |
| `layout check [--forward-to <url> [--secret <whsec>] \| --no-webhooks] [--since <30m\|2h>]` | Grade your integration from its last day of sandbox, and test your webhook handler. See [Check your integration](#check-your-integration). |
| `layout request-live-access` | Print where to request production. Production is requested from the console, where a person answers a few questions about the app, and a person at Layout approves it. |
| `layout docs [topic]` | Open these docs. Topics: `cli`, `quickstart`, `sandbox`, `webhooks`, `events`, `api`. |

Destructive commands do nothing without `--yes`. `webhooks add` and `webhooks remove` work only while the application is not approved, and exit 4 after; the [console](https://console.layout.link/webhooks/subscriptions) changes endpoints at any time. An endpoint receives sandbox events, live events or both, and the default is both, so one added under review without `--environment sandbox` also receives live events once the application is approved. `webhooks add` turns the endpoint on when it answers `endpoint.verification` with a 2xx. Otherwise it is added but off, and the command exits 1 with `endpoint_not_verified` and the `subscriptionId` on stderr, or 6 if Layout was rate limited and sent no verification. The production signing secret is shown only in the console.

### Global flags

| Flag | What it does |
| --- | --- |
| `--json` | Stable machine output on stdout. Errors go to stderr as JSON. |
| `--app <id or name>` | The application to act on, by id, or from CLI 0.4.2 by a name only one application has. `LAYOUT_APP` sets it too. |
| `--help`, `--version` | Help for any command, and the CLI's version. |

`NO_COLOR`, or output that is not a terminal, turns colour off. Once a day the CLI checks npm and tells you on stderr when a newer version is out, except with `--json`. `CI` or `LAYOUT_NO_UPDATE_CHECK` turns that off.

### Choosing an application

Commands that act on one application take `--app`. If the account has exactly one application, it is used. With several, the CLI lists them and exits with code 2. With `--json`, the error carries `apps`, each with `id`, `name` and `status`. From CLI 0.4.2, a name shared by several applications also exits with code 2 and lists them, and a name no application has exits with code 5. With none, it exits with code 5, except `init`, which exits with code 2 and asks for `--name`.

## Forward events with listen

```
layout listen --forward-to http://localhost:3000/layout/webhooks
```

`listen` reads the application's sandbox event feed every 2 seconds and POSTs each event to `--forward-to`. Each request is signed like a real webhook, with a secret made for this run and printed when it starts (`whsec_cli_...`), or with `--secret <whsec>`, at least 16 characters, so your handler keeps one secret across runs. Pair it with `layout check --forward-to <url> --secret <the same secret>`.

| Header | Value |
| --- | --- |
| `X-Layout-Signature` | `t=<unix>,v1=<hex>`, HMAC-SHA256 over `<t>.<raw body>` |
| `X-Layout-Event` | The event type |
| `X-Layout-Delivery` | The delivery id |
| `X-Layout-Environment` | `sandbox`, or `live` with `--live` |
| `X-Layout-Forwarded-By` | `layout-cli` |

Verify it with the same code as a real webhook, using the printed secret. See [Webhooks](https://docs.layout.link/webhooks). Without `--secret`, the secret is new on every run and is not your application's sandbox signing secret, so give your server the one this run printed (with `--json`, the `secret` of the first line) each time you start `listen`. `listen` runs until you stop it: a coding agent should start it in the background.

- Events arrive in order. An event is retried with backoff until your server answers 2xx, so nothing is skipped.
- The position is saved after each event, separately for sandbox and production. A restart or a dropped connection resumes after the last event forwarded and never forwards one twice.
- It starts from now, not from history, unless you pass `--from-start` or a saved position exists.
- No webhook endpoint is needed in sandbox. While `listen` runs, the sandbox feed records your application's sandbox events even when it has no enabled endpoint; they are forwarded to you and sent nowhere else. Events from `layout trigger` are included.
- If Layout cannot start recording for this run, `listen` says why on stderr and asks again every 10 seconds; `recordsWithoutEndpoint` on the first line is false until it does. Ctrl-C stops it at once and exits 130.
- With `--live`, the feed holds only the production events an enabled endpoint subscribes to. The CLI warns you when the application has no enabled endpoint.
- `--events` forwards only the events you name. The ones it skips still move the saved position, so a later run without `--events` does not send them.

With `--json`, the first line is `{"event":"listening","app","environment","forwardTo","secret","resumed","recordsWithoutEndpoint"}`, then one line per forwarded event:

```json
{"cursor":"...","event":"order.placed","eventId":"evt_...","status":200,"forwardedAt":"2026-10-02T17:04:11Z"}
```

## Fire a test event

```
layout trigger order.unconfirmed --order ord_7c21
```

`trigger` does what `POST /v1/sandbox/webhooks/fire` does: any event except `order.needs_approval`, and `--order` makes it describe one of your sandbox orders. A triggered `order.updated` carries `ready_eta` and `fulfillment`: the order's own with `--order`, placeholders otherwise. It goes to every enabled endpoint subscribed to the event, and to a running `layout listen`. 1,000 a day for each developer account, and 30 a minute. See [Fire a test webhook](https://docs.layout.link/sandbox#fire-a-test-webhook).

## Call the API

```
layout api get /v1/whoami
layout api post /v1/users -d firstName=Dana -d phone=+15005550123
layout api get "/v1/places?query=coffee" --token lgb_...
layout api post /v1/carts --token lgb_... --data @cart.json
```

Places, menus, item images and carts act for one person, so they take that person's build grant or OAuth access token in `--token`. Sent with only your key, they answer `403 wrong_credential`. See [Calling as a person](https://docs.layout.link/authentication#calling-as-a-person).

`api` sends one request to the Layout API and prints the JSON answer on stdout, with the status and `X-Request-Id` on stderr. With `--json` it prints one object on stdout instead: `{"status","requestId","replayed","body"}`. The path can leave out `/v1`. It exits 0 for a 2xx and otherwise with the [exit code](#exit-codes) for the status.

| Flag | What it does |
| --- | --- |
| `-d name=value` | A body field. Repeat it. A JSON value (a number, `true`, an array) stays JSON; anything else is a string. |
| `--data <json\|@file>` | The whole body as a JSON object. |
| `--token <token>` | Send this bearer instead of your key, such as a build grant (`lgb_...`) or an OAuth access token. |
| `--idempotency-key <key>` | Each POST gets a new `Idempotency-Key`, starting `cli-`, unless you give one. |
| `--live` | Production. GET only, with your own live secret. |

Which key it uses:

- `--token`, when you give one.
- Otherwise `LAYOUT_SECRET_KEY`, when it is set. This works without `layout login`, as in CI.
- Otherwise the application's sandbox key. After `layout login`, the CLI reads it once and keeps it in the same keychain as its token, and reads it again if it was rotated. Logging out deletes it. With `LAYOUT_CLI_TOKEN`, it reads the key on every run and stores nothing. Each read counts against your developer account's 40 key reads a day, so in CI also set `LAYOUT_SECRET_KEY`.

Before a POST or DELETE, the CLI asks `GET /v1/whoami` whether that credential is sandbox, and sends nothing if Layout says production or cannot say. A live secret works only with `--live`, and only for GET.

## Run a test order

```
layout test-order --place test_place_placed
```

`test-order` runs a whole sandbox order and prints each step:

1. Uses `LAYOUT_SECRET_KEY` when it is set, so it runs in CI without signing in. A live key there is refused with exit code 2. Without it, it uses the application's sandbox key from the keychain, as `api` does: the first run reveals that key once, from your signed-in session, and keeps it in the keychain for later runs.
2. Provisions a sandbox test user with a `+1 500 555` number and verifies it with `000000`.
3. Mints a build grant and builds a cart, at `test_place_placed` unless you name another place. At a real restaurant it first reads the menu Layout holds and orders its first item.
4. Waits for the cart to be ready, confirms it (simulated), and waits for the order's outcome.

From CLI 0.5.0, `--place` takes a `placeId` from a sandbox places search, with `--simulate` for the ending (`placed` by default) and `--latency` for the wait (`instant` by default). It exits 0 when the order ends as `--simulate` says: `failed` for `failed`, `card_declined` and `closed`, `unconfirmed` for `unconfirmed`, and `placed` for the rest. A test restaurant takes neither flag. An id starting `test_place_` that no test restaurant carries exits with code 2. From CLI 0.4.2 it also runs `test_place_needs_choices` (it answers the required choice), `test_place_needs_location` (it builds at the first candidate), `test_place_pickup_later` (it waits for the pickup time) and `test_place_delivery_far`. It exits 0 only when the order reaches the outcome the test restaurant scripts:

- `placed` for `test_place_placed`, `test_place_carted`, `test_place_branch_north`, `test_place_branch_south`, `test_place_challenge`, `test_place_slow`, `test_place_price_changed`, `test_place_needs_choices`, `test_place_needs_location`, `test_place_pickup_later` and `test_place_delivery_far`. A `price_changed` refusal is shown as its own step with the old and new totals, and the confirm shows the total it confirmed.
- `failed` for `test_place_failed`, and for `test_place_closed`, whose cart fails before any confirm. From CLI 0.4.3, also `failed` for `test_place_card_declined_confirm`, whose confirm answers `402 card_declined`.
- `unconfirmed` for `test_place_unconfirmed`.
- From CLI 0.3.1, `placed` for `test_place_code`: when the confirm answers `code_required`, the CLI sends the sandbox code `000000`. Earlier versions refuse `test_place_code` with exit code 2. `layout --version` prints yours.

```json
{"app":"lyt_app_...","place":"test_place_placed","steps":[{"step":"Read the sandbox key","status":"ok","detail":"..."}],"order":{"id":"ord_7c21","status":"placed","confirmedTotalMinor":1019},"ok":true}
```

## Check your integration

```
layout check --forward-to http://localhost:3000/webhooks/layout
```

From CLI 0.5.0, `check` grades your integration from the last 24 hours of your application's sandbox, and prints each check with what to do when it does not pass. `--since` grades a shorter window instead, such as `--since 30m` or `--since 2h`, up to `24h`.

| Check | Passes when |
| --- | --- |
| Builds a cart | The application built a sandbox cart. |
| Builds a cart at a real restaurant | One of them was at a real restaurant, from a sandbox places search. |
| Builds a ready cart | One of them reached `ready`. |
| Answers a choices question | The application answered a `choices` decision. |
| Confirms with the cart's totalMinor | The application confirmed a cart. |
| Reaches placed | An order the application confirmed reached `placed`. |
| Reaches failed | An order the application confirmed reached `failed`, such as one built with `"simulate": "failed"`. A confirm refused with `402 card_declined` confirmed nothing, so it does not count. |
| Reaches unconfirmed | An order the application confirmed reached `unconfirmed`. |
| Confirms with a code | An order built with `"simulate": "code"`, or at `test_place_code`, was placed: the confirm answered `code_required`, and the application confirmed again with the person's code (`000000` in sandbox). |
| Confirms again after price_changed | An order built with `"simulate": "price_changed"`, or at `test_place_price_changed`, was placed: after the confirm answered `price_changed`, the application built a fresh cart and confirmed it with its total. |
| Does not build again after unconfirmed | No cart for the same person at the same restaurant within 10 minutes of an `unconfirmed` ending, unless that cart was built with `simulate` set to another ending. A cart built again with `"simulate": "unconfirmed"` counts. Skipped until an order has ended `unconfirmed`. |
| Webhook deliveries succeed | No delivery to your sandbox endpoints failed after the latest one delivered. This is the check that grades how your handler takes real events. Skipped with no sandbox endpoint, or when nothing was delivered. |

Only orders your application confirmed count toward an ending. Carts the CLI built are left out: every POST that `layout api` and `layout test-order` send carries an `Idempotency-Key` starting `cli-` unless you give one, and `check` leaves out every test person a cart with such a key was built for. So use a different test person for your application's own carts than for CLI runs, and never start your own idempotency keys with `cli-`.

With `--forward-to`, it also sends your handler a test `order.placed` event four ways. The URL is https, or http on `localhost`. They test how your handler checks signatures and that it answers a repeat with a 2xx. Each carries `"test": true` and its own `event_id`, except the repeat, so a handler that skips test events must still answer them as below.

| Check | Passes when your handler |
| --- | --- |
| Accepts a signed event | Answers 2xx to a correctly signed event. |
| Refuses a bad signature | Answers 4xx to an event signed with the wrong secret. |
| Refuses an old timestamp | Answers 4xx to a signature 10 minutes old. |
| Answers 2xx to a repeated event_id | Answers 2xx when the first event comes again with the same `event_id`. |

It signs with your application's sandbox signing secret. When your handler verifies with the secret `layout listen` used, pass that one as `--secret`. Without `--forward-to`, the check **Your webhook handler** fails. An integration that reads `GET /v1/orders` instead of taking webhooks passes `--no-webhooks`, which skips it.

`check` exits 0 when nothing failed and 1 otherwise, so an agent can run it until it passes.

```
$ layout check --forward-to http://localhost:3000/webhooks/layout --json
{"app":"lyt_app_...","windowHours":24,"checks":[{"name":"Builds a cart","result":"pass"},...,{"name":"Reaches unconfirmed","result":"fail","fix":"Build with \"simulate\": \"unconfirmed\", confirm it, and tell the person to check with the restaurant."},...,{"name":"Does not build again after unconfirmed","result":"skip","fix":"Runs once an order has ended unconfirmed."},...],"passed":14,"failed":1,"skipped":1,"ok":false}
```

## JSON output

With `--json`, every command prints one JSON object on stdout. Two print more than one line: `login` prints a code line and then a signed-in line, and `listen` prints a listening line and then one line per event. An error is `{"error":{"code","message"}}` on stderr. `api` is the exception: it prints the answer of any status on stdout as `{"status","requestId","replayed","body"}`, and only the CLI's own errors go to stderr. When `test-order` or `init` fails partway, stdout also gets the steps so far, with `"ok":false`.

```json
{"event":"code","userCode":"BCDF-GHJK","verificationUri":"https://console.layout.link/cli","verificationUriComplete":"https://console.layout.link/cli?code=BCDF-GHJK","expiresIn":600,"live":false}
{"event":"signed_in","account":{...},"session":{...}}
```

```
$ layout whoami --json
{"kind":"cli","environment":"sandbox","account":{"id":"...","name":"Acme","status":"active"},"session":{"id":"...","kind":"device","deviceName":"dana-mbp","createdAt":"...","expiresAt":"...","liveUntil":null},"capabilities":["whoami","listen:sandbox","apps:read","apps:create","keys:sandbox","webhooks:read","webhooks:sandbox","events:sandbox","trigger:sandbox","usage:read","deliveries:sandbox","live_access:request","refusals:read","check:sandbox"]}
```

```
$ layout apps create "Acme Ordering" --json
{"app":{"id":"lyt_app_...","name":"Acme Ordering","status":"pending","createdAt":"..."},"sandboxKey":{"clientId":"lp_test_...","secret":"sk_test_..."}}
```

```
$ layout keys create --replace --json
{"app":"lyt_app_...","environment":"sandbox","clientId":"lp_test_...","secret":"sk_test_...","replaced":true,"previousSecretExpiresAt":"2026-10-23T17:00:00.000Z"}
```

### Unknown flags and places

From CLI 0.4.3, a flag or a test place the CLI does not know ends with `This is @layoutplatforms/cli 0.4.3; newer versions may support this: npm i -g @layoutplatforms/cli`, since a newer CLI may.

### Exit codes

| Code | Meaning |
| --- | --- |
| `0` | Success |
| `1` | Error |
| `2` | Usage: a bad flag or argument, several applications and no `--app`, or a `400` or `422` from Layout, and from CLI 0.4.2 a `405` |
| `3` | Not signed in, or the session expired or was revoked |
| `4` | Refused: the CLI may not do that, for example a production action |
| `5` | Not found, including an account with no applications |
| `6` | Rate limited |
| `7` | Layout or the network is unavailable. Safe to retry. |
| `8` | Conflict, for example a key already exists |
| `130` | Interrupted |

### Retries

Network errors, `502`, `503` and `504` are retried with backoff, up to four tries, and a read also retries `429`. Every create, revoke, trigger and review request, and every POST from `api`, carries an `Idempotency-Key`, so a retry returns the first answer instead of doing it twice.

## Production

```
layout login --live
layout listen --live --forward-to http://localhost:3000/layout/webhooks
layout deliveries --live
```

`keys list --live`, `webhooks list --live` and `refusals --live` need no `layout login --live`. `listen` and `deliveries` still do. From CLI 0.4.3.

With production access, a session can read production as well as build in sandbox. The console marks the request as production and asks you to sign in again if your last sign-in was more than 15 minutes ago, the same check as showing a live secret. The access lasts 12 hours from sign-in. After that, `--live` exits 4 with `live_session_required` and sandbox keeps working for the rest of the 30 days. The application must be approved for production, or it exits 4 with `not_approved`.

| Works with `--live` | Never from the CLI |
| --- | --- |
| `listen`: the live event feed, forwarded and signed like sandbox | Create, show, rotate or revoke a live key |
| `deliveries`: live webhook deliveries | Show the live signing secret |
| `keys list`: the live client id and last four | Fire a test event in production |
| `webhooks list`: endpoints | Change any production setting |
| `refusals`: why the live key was refused |  |
| `api get`, with your own `sk_live_` in `LAYOUT_SECRET_KEY` | POST or DELETE in production, or place an order |

Layout enforces the left column on its side: a CLI token reaches only the read routes in production, and only while its access lasts.

## Security

| A CLI token can | A CLI token cannot |
| --- | --- |
| Read the account, its applications, usage and recent sandbox deliveries | Create, read, rotate or revoke a live key |
| Create applications | Read a live signing secret |
| Create, read and revoke sandbox keys | Change a webhook endpoint of an approved application, or change any production setting |
| List webhook endpoints, read the sandbox signing secret, and (from CLI 0.4.2) add and remove endpoints of an application not yet approved | Approve anything |
| Fire sandbox test events and read the sandbox event feed | Place a live order or move money |
| With production access, for 12 hours: read live events, deliveries, key details and endpoints | Write anything in production |
| Ask for production review | Use any API route outside `/v1/cli`. Every other route refuses a CLI token, and `/v1/cli` refuses a client secret or a build grant. |

- Layout stores the token hashed and records every action it takes in your account's audit trail.
- Only a signed-in console user can approve a sign-in code, and the token belongs to that account. Production access also needs a sign-in from the last 15 minutes.
- The CLI keeps only a sandbox key, never a live secret. If a computer is lost, revoke its session and replace the sandbox key.
- With `--live`, `listen` forwards only to an `https` URL or to this computer.
