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 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. |
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 page (New token) and set it as LAYOUT_CLI_TOKEN. Every command then runs without layout login.
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. |
layout trigger <event> [--order <ord_id>] | Fire a signed test event. See 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. |
layout test-order [--place <id>] [--simulate <ending>] [--latency <realistic|instant>] | Run a whole sandbox order. See 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. |
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 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. 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-startor a saved position exists. - No webhook endpoint is needed in sandbox. While
listenruns, 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 fromlayout triggerare included. - If Layout cannot start recording for this run,
listensays why on stderr and asks again every 10 seconds;recordsWithoutEndpointon 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. --eventsforwards only the events you name. The ones it skips still move the saved position, so a later run without--eventsdoes not send them.
With --json, the first line is {"event":"listening","app","environment","forwardTo","secret","resumed","recordsWithoutEndpoint"}, 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. 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.
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.
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 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 withoutlayout 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. WithLAYOUT_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 setLAYOUT_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:
- Uses
LAYOUT_SECRET_KEYwhen 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, asapidoes: the first run reveals that key once, from your signed-in session, and keeps it in the keychain for later runs. - Provisions a sandbox test user with a
+1 500 555number and verifies it with000000. - Mints a build grant and builds a cart, at
test_place_placedunless you name another place. At a real restaurant it first reads the menu Layout holds and orders its first item. - 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:
placedfortest_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_laterandtest_place_delivery_far. Aprice_changedrefusal is shown as its own step with the old and new totals, and the confirm shows the total it confirmed.failedfortest_place_failed, and fortest_place_closed, whose cart fails before any confirm. From CLI 0.4.3, alsofailedfortest_place_card_declined_confirm, whose confirm answers402 card_declined.unconfirmedfortest_place_unconfirmed.- From CLI 0.3.1,
placedfortest_place_code: when the confirm answerscode_required, the CLI sends the sandbox code000000. Earlier versions refusetest_place_codewith exit code 2.layout --versionprints yours.
{"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.
{"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,listenforwards only to anhttpsURL or to this computer.