# Layout CLI

The Layout CLI signs in to your developer account and works in sandbox: create an application and a sandbox key, forward webhook events to your machine, fire test events, and run a whole test order. Every command takes `--json`, so a coding agent can drive it. It never touches production.

## Install

The CLI is in the Layout monorepo at `packages/cli` and is not published yet. Ask [developer@layout.link](mailto:developer@layout.link) for access.

It needs Node 20 or later. The command is `layout`.

## Sign in

```
layout login
```

It prints a code like `BCDF-GHJK` and opens `https://developer.layout.link/console/cli?code=BCDF-GHJK`. Sign in to the console, check that the code matches your terminal and that the computer named is yours, then choose **Approve**.

- The CLI asks every 5 seconds, and slows down if Layout tells it to.
- The code lasts 10 minutes and can be approved once.
- The session lasts 30 days.
- `layout logout` revokes the session at Layout and deletes it from this computer.
- The console's [CLI](https://developer.layout.link/console/cli) page lists every signed-in CLI, each with **Revoke**. Signing out everywhere also ends every CLI session.

| 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. |

The token is stored 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. The token is never printed.

`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.

### Endpoints

Sign-in is the OAuth 2.0 device authorization grant (RFC 8628).

```
POST /v1/cli/device/code
{ "device_name": "dana-mbp" }

200 OK
{ "device_code": "ldc_...", "user_code": "BCDF-GHJK",
  "verification_uri": "https://developer.layout.link/console/cli",
  "verification_uri_complete": "https://developer.layout.link/console/cli?code=BCDF-GHJK",
  "expires_in": 600, "interval": 5 }
```

```
POST /v1/cli/device/token
{ "device_code": "ldc_...", "grant_type": "urn:ietf:params:oauth:grant-type:device_code" }

200 OK
{ "access_token": "lcli_...", "token_type": "Bearer", "expires_in": 2592000, "session_id": "..." }

400 Bad Request
{ "error": "authorization_pending", "error_description": "...", "interval": 5 }
```

`device_name` is optional. A 400 from the token endpoint carries one of `authorization_pending`, `slow_down`, `expired_token`, `access_denied` or `invalid_grant`. These two endpoints use the OAuth error shape. Every other `/v1/cli` route uses the API's `{ "error": { "code", "message" } }` envelope.

## Commands

| Command | What it does |
| --- | --- |
| `layout login` | Sign in with a code you approve in the console. |
| `layout logout` | Revoke this CLI session and forget it locally. |
| `layout whoami` | The developer account and CLI session. |
| `layout apps list` | Your applications. |
| `layout apps create <name>` | A new application. Prints its sandbox key once. |
| `layout keys list` | The application's sandbox key: client id and last four. |
| `layout keys create [--replace]` | A new sandbox key, printed once. With a key already in place it asks for `--replace`, which retires the old one. |
| `layout keys revoke --yes` | Revoke the sandbox key and its sandbox build grants. Nothing is revoked without `--yes`. |
| `layout webhooks list` | Endpoints and the sandbox signing secret. Endpoints are added and deleted in the [console](https://developer.layout.link/console/webhooks/subscriptions) only: an endpoint has no environment, so one added to an application under review also receives its live events once it is approved. |
| `layout listen --forward-to <url> [--events <a,b>] [--from-start]` | Forward sandbox events to a local URL. |
| `layout trigger <event> [--order <ord_id>]` | Fire a signed test event at your endpoints. |
| `layout usage` | Today's allowances and counts. |
| `layout deliveries [--limit <n>]` | Recent sandbox webhook deliveries. |
| `layout test-order [--place <test_place_id>]` | Run a whole sandbox order and print each step. |
| `layout request-live-access [--note <text>]` | Ask Layout to review production access. A person approves it. |
| `layout docs [topic]` | Open these docs. Topics: `cli`, `quickstart`, `sandbox`, `webhooks`, `events`, `api`. |

### Global flags

| Flag | What it does |
| --- | --- |
| `--json` | Stable machine output on stdout. Errors go to stderr as JSON. |
| `--app <lyt_app_id>` | The application to act on. `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.

### Choosing an application

Commands that act on one application take `--app`. If the account has exactly one application, it is used. Otherwise the CLI lists them and exits with code 2.

## 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_...`).

| 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` |
| `X-Layout-Forwarded-By` | `layout-cli` |

Verify it with the same code as a real webhook, using the printed secret. See [Webhooks](https://developer.layout.link/reference/webhooks).

- 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. 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.
- The feed holds an application's events only while it has at least one enabled webhook endpoint. If it has none, add one once in the [console](https://developer.layout.link/console/webhooks/subscriptions); the CLI tells you when it has none. Events from `layout trigger` are included.

With `--json`, the first line is `{"event":"listening","app","forwardTo","secret","resumed"}`, 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. It needs an enabled endpoint subscribed to the event; add one once in the [console](https://developer.layout.link/console/webhooks/subscriptions). 1,000 a day for each developer account, and 30 a minute. See [Fire a test webhook](https://developer.layout.link/reference/sandbox#fire-a-test-webhook).

## Run a test order

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

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

1. Reads your sandbox key into memory for the run. It is never written to disk.
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 a test restaurant, `test_place_placed` unless you name another.
4. Waits for the cart to be ready, confirms it (simulated), and waits for the order's outcome.

Any test restaurant from [Sandbox test restaurants](https://developer.layout.link/reference/sandbox) that builds a cart without a question works; `test_place_needs_choices` and `test_place_needs_location` do not. A `price_changed` refusal is read and confirmed again once, and `test_place_closed` passes when its cart fails with `STORE_CLOSED`. It exits 0 only when the order reaches the outcome the test restaurant scripts: `placed` for `test_place_placed` and `test_place_carted`, `failed` for `test_place_failed`, and `unconfirmed` for `test_place_unconfirmed`.

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

## 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.

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

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

```
$ 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}
```

### Exit codes

| Code | Meaning |
| --- | --- |
| `0` | Success |
| `1` | Error |
| `2` | Usage: a bad flag or argument, or several applications and no `--app` |
| `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 |
| `6` | Rate limited |
| `7` | Layout or the network is unavailable. Safe to retry. |
| `8` | Conflict, for example a key already exists |
| `130` | Interrupted |

### Retries

Reads and sign-in polling retry network errors and 503 with backoff. Every create, revoke, trigger and review request carries an `Idempotency-Key`, so a retried call returns the first answer instead of doing it twice.

## Security

| A CLI token can | A CLI token cannot |
| --- | --- |
| Read the account, its applications, usage and recent sandbox deliveries | Create, read or rotate a live key |
| Create applications | Read a live signing secret |
| Create, read and revoke sandbox keys | Add, change or delete a webhook endpoint, or change any production setting |
| List webhook endpoints and read the sandbox signing secret | Approve anything |
| Fire sandbox test events and read the sandbox event feed | Place a live order or move money |
| 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. It lasts 30 days.
- Every action it takes is recorded in your account's audit trail, like a console action.
- Only a signed-in console user can approve a sign-in code, once, within 10 minutes. The token belongs to the account of whoever approved it.
- Wrong codes are rate limited.
