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.

FlagWhat it does
--device-name <name>The name the console shows for this computer. The default is its host name.
--no-browserPrints the link without opening it. With CI set, login never opens a browser.
--liveAlso 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

CommandWhat 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 logoutRevoke this CLI session and forget it locally.
layout whoamiThe developer account, the CLI session, and whether it can read production.
layout apps listYour 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 --yesRevoke 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> --yesRemove 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 usageToday'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-accessPrint 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

FlagWhat it does
--jsonStable 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, --versionHelp 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>.

HeaderValue
X-Layout-Signaturet=<unix>,v1=<hex>, HMAC-SHA256 over <t>.<raw body>
X-Layout-EventThe event type
X-Layout-DeliveryThe delivery id
X-Layout-Environmentsandbox, or live with --live
X-Layout-Forwarded-Bylayout-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-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:

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

FlagWhat it does
-d name=valueA 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.
--liveProduction. 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.
{"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.

CheckPasses when
Builds a cartThe application built a sandbox cart.
Builds a cart at a real restaurantOne of them was at a real restaurant, from a sandbox places search.
Builds a ready cartOne of them reached ready.
Answers a choices questionThe application answered a choices decision.
Confirms with the cart's totalMinorThe application confirmed a cart.
Reaches placedAn order the application confirmed reached placed.
Reaches failedAn 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 unconfirmedAn order the application confirmed reached unconfirmed.
Confirms with a codeAn 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_changedAn 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 unconfirmedNo 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 succeedNo 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.

CheckPasses when your handler
Accepts a signed eventAnswers 2xx to a correctly signed event.
Refuses a bad signatureAnswers 4xx to an event signed with the wrong secret.
Refuses an old timestampAnswers 4xx to a signature 10 minutes old.
Answers 2xx to a repeated event_idAnswers 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

CodeMeaning
0Success
1Error
2Usage: 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
3Not signed in, or the session expired or was revoked
4Refused: the CLI may not do that, for example a production action
5Not found, including an account with no applications
6Rate limited
7Layout or the network is unavailable. Safe to retry.
8Conflict, for example a key already exists
130Interrupted

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 --liveNever from the CLI
listen: the live event feed, forwarded and signed like sandboxCreate, show, rotate or revoke a live key
deliveries: live webhook deliveriesShow the live signing secret
keys list: the live client id and last fourFire a test event in production
webhooks list: endpointsChange any production setting
refusals: why the live key was refused
api get, with your own sk_live_ in LAYOUT_SECRET_KEYPOST 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 canA CLI token cannot
Read the account, its applications, usage and recent sandbox deliveriesCreate, read, rotate or revoke a live key
Create applicationsRead a live signing secret
Create, read and revoke sandbox keysChange 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 approvedApprove anything
Fire sandbox test events and read the sandbox event feedPlace a live order or move money
With production access, for 12 hours: read live events, deliveries, key details and endpointsWrite anything in production
Ask for production reviewUse 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.