Rate limits and idempotency
Every ceiling that can refuse you, when to come back after a 429, and the key that stops a retry from running twice.
Per-minute and per-hour limits
Counted per credential (your client id) over a rolling window, unless the row says per person. Your calling address does not count against any of them.
| Endpoint | Approved, in production | Sandbox |
|---|---|---|
POST /v1/users | 600 / hour | 600 / hour |
POST /v1/users/:id/grant | 1,200 / hour. A refresh does not count | 120 / hour |
POST /v1/users/:id/verify/start | 250 requests an hour per credential. Texts actually sent are capped by your account’s texts per hour, which starts at 30 | 120 / hour |
POST /v1/users/:id/verify | 1,500 / hour | 300 / hour |
POST /v1/users/:id/connect | 250 requests an hour per credential. Texts actually sent are capped by your account’s texts per hour, which starts at 30 | 120 / hour |
POST /v1/users/:id/links | 10 per 10 minutes, per person, and 600 / hour per credential | 10 per 10 minutes, per person, and 600 / hour per credential |
GET /v1/orders/:id, GET /v1/orders | 600 / minute with your secret, shared | 600 / minute with your secret, shared |
GET /v1/places, GET /v1/places/:placeId | 30 / minute, per person, shared. The first query in an area can take several seconds while it answers stillChecking | 30 / minute, per person, shared |
GET /v1/events | 120 / minute | 120 / minute |
GET /v1/whoami | 120 / minute with your secret | 120 / minute with your secret |
GET /v1/usage | 60 / minute | 60 / minute |
POST /v1/sandbox/webhooks/fire | Sandbox only | 30 / minute, and 1,000 a day per developer account |
POST /v1/menus | 20 per 10 minutes, per person, shared with that person’s own Layout app | 20 per 10 minutes, per person, shared with that person’s own Layout app |
POST /v1/images/items | 20 per 10 minutes, per person | 20 per 10 minutes, per person |
GET /v1/menus/:menuId | 120 / minute, per person | 120 / minute, per person |
GET /v1/images/items/:imageId | 120 / minute, per person | 120 / minute, per person |
POST /v1/carts/:id/cancel | 6 per 10 minutes, per person, counting only cancels that find nothing to stop | Same |
Every request, per credential
Every call also counts toward one limit per credential across every endpoint together: 3,000 a minute for an approved application in production, and 600 a minute for sandbox, or for an application not yet approved. A credential is your secret, a person's access token, or a build grant, each counted on its own.
Credentials Layout has no record of count against the calling address, at 300 a minute. A refused credential Layout issued, such as a revoked secret, counts against itself, at 600 a minute.
RateLimit headers
Every answer to a request that authenticated carries the per-credential count:
RateLimit-Limit: 3000 RateLimit-Remaining: 2987 RateLimit-Reset: 41
RateLimit-Reset is the seconds until RateLimit-Remaining next rises. A 429 from a per-endpoint limit carries that endpoint's numbers, and one from a daily ceiling carries none.
Seeing your limits and usage
GET /v1/usage with your secret answers every limit on this page as it applies to you, in that key's environment, with what your developer account has spent today against each daily one. It is free.
curl https://api.layout.link/v1/usage \
-H "Authorization: Bearer $LAYOUT_SECRET_KEY"
200 OK
{
"app": { "id": "lyt_app_3f9c2a7e1b4d8f6a0c5e9b27" },
"environment": "live",
"approvalStatus": "approved",
"resetsAt": "2026-10-02T00:00:00.000Z",
"daily": [
{ "name": "builds", "description": "Builds on the restaurant's own site. In sandbox, only engine real", "scope": "developer",
"limit": 5000, "used": 412, "remaining": 4588, "poolLimit": 20000, "custom": false },
{ "name": "users_provisioned", "description": "Users provisioned", "scope": "developer",
"limit": 10000, "used": 1250, "remaining": 8750, "poolLimit": 50000, "custom": false },
{ "name": "cardless_builds", "description": "Builds for a person with no card on file", "scope": "application",
"limit": 150, "used": 9, "remaining": 141 }
],
"perPerson": [ { "name": "builds", "period": "day", "limit": 25 } ],
"rates": [
{ "name": "requests", "scope": "credential", "period": "minute", "limit": 3000 },
{ "name": "texts", "scope": "developer", "period": "hour", "limit": 50,
"verifiedPeople": 400, "max": 1000, "next": { "verifiedPeople": 420, "limit": 51 } },
{ "name": "texts_platform", "scope": "platform", "period": "hour", "limit": 2000 }
]
}
The example is shortened: daily lists every daily ceiling on this page. poolLimit is the ceiling across every developer. custom is true when Layout set that number for your account. A pool closed to the key, such as verification texts in sandbox, has a limit of 0 and a note saying why.
Handling a 429
Wait Retry-After (also error.retryInSeconds). No Retry-After means a daily ceiling: error.limit names it and it resets at midnight UTC. A spent build ceiling on POST /v1/carts or /answer is build_limit instead of rate_limited. Carry the same idempotency key into the retry.
HTTP/1.1 429 Too Many Requests
{
"error": {
"code": "rate_limited",
"message": "Your developer account has reached its daily limit of 200 users provisioned. It resets at midnight UTC. GET /v1/usage shows every limit; request more from Usage in the console.",
"limit": { "name": "users_provisioned", "scope": "account", "max": 200, "resetsAt": "2026-10-02T00:00:00Z" }
}
}
| Field | Meaning |
|---|---|
name | What the ceiling counts: builds, users_provisioned, build_sessions, verification_texts, place_reads, menu_reads, menu_knowledge, image_searches or test_events. |
scope | Whose ceiling it is: person, application, account (your developer account, across its applications) or platform (Layout’s own, across every developer). |
max | The ceiling, per day, or per rolling hour when period is hour. |
period | Only on the hourly ceiling on texts: hour. Absent means per day. |
resetsAt | The next midnight UTC, or on an hourly ceiling when the oldest text in the hour stops counting. |
A 429 with neither Retry-After nor limit, such as an order code limit, needs the message read and a backoff of at least a minute. Over MCP, the order tool answers a refused build with no cart and a refusal object whose limit has the same shape.
Daily ceilings
Every one resets at midnight UTC, and sandbox and production are counted separately. Your developer account is every application you have created, together. When a ceiling across every developer refuses you, the message says it is Layout's limit rather than yours.
Order builds
A build is one POST /v1/carts, one POST /v1/carts/:id/answer, or one MCP order build that starts a cart. The first row it hits refuses it. A sandbox cart opens no browser unless it is sent with "engine": "real", so it counts only toward its own 10,000 a day per developer account. See Sandbox.
| Counted per | Sandbox | Sandbox, "engine": "real" | Production |
|---|---|---|---|
| Person, per credential | No ceiling | 25 | 25 |
| Application | No ceiling | 50, raised from Usage | No ceiling for a person with a card. For a person without one, see No-card builds |
| Developer account | 10,000 | 10 | 5,000 once your application is approved; 250 until then |
| Credential, every person | No ceiling | 50,000 | 50,000 |
A build with "engine": "real" counts toward both its application’s 50 and its developer account’s 10, and the first one spent refuses it. With the 10 shared by every application on the account, the 50 matters only once Layout has raised the 10. GET /v1/usage lists the sandbox 10,000 as simulated_builds, and the 10 as builds. To raise the 10, email developer@layout.link, or ask from Usage in the console (see Raising a limit).
Users, sessions and texts
Counted per developer account. Applications awaiting approval count on a separate counter, so they never use up an approved application's room.
| Ceiling | Sandbox | Production |
|---|---|---|
| Users provisioned | 200 | 10,000 |
| Build sessions minted | 500 | 50,000 |
| Verification texts | None sent; the sandbox code is 000000 | 6,000, growing to 24,000 with verified people |
| Approval texts Layout sends | None sent; you get the link | 6,000, growing to 24,000 with verified people |
Users provisioned counts POST /v1/users and the console's Provisioned users page together, and only a call that provisions a new person counts. Until an application is approved, provisioning from the console is held to 25 a day. Layout sends approval texts only for an approved application; otherwise connect.connectUrl comes back for you to deliver.
Verified people raise hourly texts
Verification and approval texts count together, per developer account, over a rolling hour. For an approved application in production the hour starts at 30 and grows by one for every 20 people you have verified, up to 1,000. GET /v1/usage shows yours under rates as texts, and the next step as next. Sandbox keeps 120 an hour per credential. Past the hour, verify/start answers 429 rate_limited with Retry-After and error.limit with "period": "hour", and connect hands you the link to deliver instead of refusing. Ask for more hourly texts in the same way, from Request a higher limit on Usage in the console.
Build grants last 15 minutes
A build grant from POST /v1/users/:id/grant lasts 15 minutes. Send the current grant back as refresh before it expires to extend the same token another 15 minutes, up to 24 hours from when it was minted. A refresh never counts toward build sessions minted or the hourly limit. A grant that expired, was revoked, or is 24 hours old answers 409 grant_expired; mint a new one without refresh.
curl -X POST https://api.layout.link/v1/users/usr_4b8e/grant \
-H "Authorization: Bearer $LAYOUT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "refresh": "lgb_9f3a2c" }'
200 OK
{ "token": "lgb_9f3a2c", "expiresAt": "2026-10-01T18:15:41Z", "scope": "build", "refreshed": true }
Order codes for build grants
Codes Layout texts for a confirm have their own limits, separate from verification and approval texts. Sandbox texts nobody but applies the per-cart and per-person limits.
| Ceiling | Limit |
|---|---|
| Codes texted for one cart | 4 |
| Codes texted per credential, per day | 3,000, or 100 until your application is approved |
| Codes texted to one number, per hour | 6 |
| Codes texted to one person, per hour | 10 |
Each refusal is 429 rate_limited on /confirm or /confirm/resend-code, and nothing was confirmed or charged. A spent cart or daily ceiling has no Retry-After: ask the person for the newest code Layout already texted, or build a new cart.
Place and menu reads
Per developer account, on top of the person's own. A live menu read or an image search counts only when Layout opens the restaurant's site; anything Layout already holds is free. An image search also counts toward the person's 30 live reads. In sandbox a menu read answers only with the menu Layout holds, in either mode, never reads one live, never gives a knowledge answer, and counts toward none of the menu rows.
| Allowance | Sandbox, or not yet approved (each environment) | Approved, in production |
|---|---|---|
| Place reads, per developer account per day | 5,000 | 50,000 |
| Live menu reads, per developer account per day | None in sandbox. 200 for an application not yet approved | 1,000 |
| Live menu reads, per person per day | None in sandbox. 30 for an application not yet approved | 30 |
| Knowledge answers, per developer account per day | None in sandbox. 100 for an application not yet approved | 1,000 |
| Image searches, per developer account per day | 300 | 2,000 |
| Place searches beyond Layout’s registry, per developer account per day | None: search covers only the places Layout already holds | 200 |
A spent allowance is a 429 rate_limited whose message names the limit. Reading the same menu again within three minutes, when Layout holds no read of it, is a 429 with Retry-After. A spent place-search allowance is not an error: GET /v1/places answers from Layout’s registry alone and says so in searchLimit.
No-card builds, per day
In production, each application has its own daily number of builds for a person with no card on file. It starts at zero and is set from the volume you gave when you applied. Spend it, and the next card-less build comes back asking for a card instead of running. Builds for someone with a card never touch it, and sandbox never asks for a card. See Building without a card.
Raising a limit
Every daily pool GET /v1/usage lists can be raised for your developer account, up to that pool’s ceiling across every developer (poolLimit), and so can your hourly texts, and your application's own sandbox and no-card build limits. Open Usage in the console, choose Request a higher limit, and say which limit and by how much. Or email developer@layout.link. A raised number shows in GET /v1/usage with custom: true within a minute. A raised hourly texts number shows the same custom: true on texts under rates, with expiresAt while Layout set it for a limited time.
Idempotency
Every POST takes an Idempotency-Key header, except the retired POST /v1/orders. Send one on every call that changes something, and carry the same key into every retry of that call. A random UUID per real intent is the simplest choice.
curl https://api.layout.link/v1/users \
-H "Authorization: Bearer $LAYOUT_SECRET_KEY" \
-H "Idempotency-Key: 4f1d2c8e-7b6a-4e5f-9c3d-2a1b0e9f8d7c" \
-H "Content-Type: application/json" \
-d '{ "firstName": "Dana", "lastName": "Whitfield", "phone": "+15005550142" }'
- Same key and body returns the stored answer for 24 hours, with
Idempotent-Replayed: true, and nothing runs again. A replay is the first answer, not the current state: read the resource for that. - Same key, different body is
409 idempotency_conflict. Nothing runs. - Still running is
409 in_progresswithRetry-After. On a confirm, readGET /v1/orders/:idinstead of sending it again. - Only successes are stored, so an error frees the key. A
5xxmay have run: read the resource before you send anything changed.
A key is 1 to 255 visible ASCII characters, scoped to your application and environment, and to the person on a person credential. Over MCP, pass idempotencyKey to order build the same way.
Retrying a confirm
If POST /v1/carts/:id/confirm times out, send the same call again with the same key. Never a new key. The retry gets the stored 202 placing, or 409 in_progress while the first is still running: read GET /v1/orders/:id then. Either way the person is charged once. A 500 internal means the outcome is not known yet, not that it failed. expectedTotalMinor is the cart’s totalMinor, fee included.
curl https://api.layout.link/v1/carts/$CART_ID/confirm \
-H "Authorization: Bearer $LAYOUT_PERSON_TOKEN" \
-H "Idempotency-Key: 7f3c2a9e-confirm-1" \
-H "Content-Type: application/json" \
-d '{ "expectedTotalMinor": 763 }'
A code_required answer is an error, so it is not stored: send the same request again with the same key and the person’s code added.
The body's idempotencyKey
POST /v1/carts, /answer and /confirm also take an idempotencyKey in the body, which names the cart or the confirm: 8 to 100 letters, digits, ., _, : or -. It is optional when you send the header, whose value then stands in for it and must meet the same rules. When you send both, the body key names the cart or the confirm and the header replays the HTTP answer. They need not match. A body key still in progress answers 409 in_progress without Retry-After: wait a couple of seconds.