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

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

CommandWhat it does
layout loginSign in with a code you approve in the console.
layout logoutRevoke this CLI session and forget it locally.
layout whoamiThe developer account and CLI session.
layout apps listYour applications.
layout apps create <name>A new application. Prints its sandbox key once.
layout keys listThe 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 --yesRevoke the sandbox key and its sandbox build grants. Nothing is revoked without --yes.
layout webhooks listEndpoints 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 usageToday'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

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

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
X-Layout-Forwarded-Bylayout-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-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; 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:

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

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

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

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 canA CLI token cannot
Read the account, its applications, usage and recent sandbox deliveriesCreate, read or rotate a live key
Create applicationsRead a live signing secret
Create, read and revoke sandbox keysAdd, change or delete a webhook endpoint, or change any production setting
List webhook endpoints and read the sandbox signing secretApprove anything
Fire sandbox test events and read the sandbox event feedPlace a live order or move money
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. 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.