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 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 logoutrevokes the session at Layout and deletes it from this computer.- The console's 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 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.
- 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-startor 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; the CLI tells you when it has none. Events from
layout triggerare included.
With --json, the first line is {"event":"listening","app","forwardTo","secret","resumed"}, then one line per forwarded event:
{"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. 1,000 a day for each developer account, and 30 a minute. See 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:
- Reads your sandbox key into memory for the run. It is never written to disk.
- Provisions a sandbox test user with a
+1 500 555number and verifies it with000000. - Mints a build grant and builds a cart at a test restaurant,
test_place_placedunless you name another. - Waits for the cart to be ready, confirms it (simulated), and waits for the order's outcome.
Any test restaurant from Sandbox test restaurants 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.
{"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.
{"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.