Provisioning users
Someone you can order for. You vouch for a name and a phone number; they prove the phone by reading back the code Layout texts them, and that makes their Layout account.
The path: provision, verify, card link, build, confirm, disconnect.
Create a user
POST /v1/users
{
"firstName": "Dana",
"lastName": "Whitfield",
"phone": "+15005550142",
"email": "dana@example.com"
}
→ 201
{
"id": "usr_4b8e9a01c2d3",
"status": "provisioned",
"next": "verify",
"session": { "scope": "build", "expiresIn": 900 }
}
Provisioning returns no credential. session carries no token and authenticates nothing, so you can ignore it. handoffUrl is kept for older integrations. Do not send anyone to it. To build, mint a build grant with POST /v1/users/:id/grant. A grant builds, reads and releases carts, and confirms a live order with the person's code when a code is due. It cannot add a card.
The next step
next says how to connect this person:
next | What it means |
|---|---|
verify | A new number, or one whose only Layout account another application's verify made. Verify their phone. A good code makes or joins the person's Layout account. |
awaiting_user_approval | The person signs in to Layout on this number, and Layout texted them the approval link. Wait for the connection. awaitingApproval.phoneHint is the masked number. |
connect | The person signs in to Layout on this number. Send them to connect.connectUrl. You get the link instead of a text when you asked for it (connectDelivery: "developer"), in sandbox, before approval, or when Layout could not text it. If connect is missing, the link could not be made: call POST /v1/users/:id/connect with connectDelivery: "developer". |
connected | Already connected. Start building. |
A Layout account on the number that is scheduled for deletion counts as one the person signs in to, until it is purged.
Fields
| Field | Notes |
|---|---|
firstName | Required. A string, 1 to 100 characters. |
lastName | Required. A string, 1 to 100 characters. |
phone | Required. E.164, such as +19165550142. Layout keeps only the digits and reads any ten as a US number, so a number whose E.164 form is ten digits long cannot be provisioned. Spaces, parentheses, dots and hyphens are dropped; any other character is 400 invalid_request. Sandbox takes only +1 500 555 test numbers, such as +15005550142. Production refuses them. |
email | Optional and unverified. Kept with the provisioning link, never written to the person's Layout account and never shown to another partner. A malformed address is 400 invalid_request. |
label | Optional. Up to 120 characters, for your own reference in the console. |
connectDelivery | Optional. "developer" returns the connect link to you instead of Layout texting it (see Someone who signs in to Layout). Any other value is 400 invalid_request. |
A body that is not a JSON object, or a field above that is not a string, is 400 invalid_request and counts toward no limit.
Provisioning is idempotent by number: the same number in the same environment returns the same id, with 201, and mints no second account. A number you disconnected with DELETE /v1/users/:id, or whose owner disconnected you, comes back under the same id as a fresh invitation that needs connecting again. A number already provisioned by another application in your developer account, in the same environment, is 409 provisioned_to_another_app.
Introduce Layout first
Layout texts the person from its own number, and the card link opens a Layout page. Someone who has not heard of Layout from you can take either for spam and stop there. Before the first code or link reaches them, say in your own words that your app uses Layout to place their orders, that Layout will text them a code, and that they add a card once, on Layout's page, which you never see. For example:
Acme Assistant uses Layout to place your orders. Layout will text you a 6-digit code to link your number: read it back to me here. Before your first order you'll add a card once, on Layout's page. Acme never sees your card.
Keep it short and in your own voice. When you send the card link, say again what it is.
Verify their phone
The person never types their number or opens a sign-in screen. Layout texts them a code, they read it back to you, and you send it to Layout.
POST /v1/users/usr_4b8e9a01c2d3/verify/start
→ 200
{ "ok": true, "id": "usr_4b8e9a01c2d3", "phoneHint": "+1 (916) •••-0142", "expiresInSec": 180 }
POST /v1/users/usr_4b8e9a01c2d3/verify
{ "code": "246810" }
→ 200
{ "id": "usr_4b8e9a01c2d3", "verified": true, "authorized": true, "status": "active", "claimed": true }
start texts a six-digit code, good for three minutes and five tries. The text names your application as it appears in the console: Layout: 123456 is your code to link this number to Acme Assistant, which can then place orders for you. It expires in 3 minutes. Didn't ask? Ignore this text. Every Layout text is on Texts Layout sends. Reading the code back is the person's agreement that your application places orders for them. If Layout has no name for your application, the text says Layout: 123456 is your code to verify this number for an app you are connecting to Layout. It expires in 3 minutes and does not sign you in. Didn't ask? Ignore this text. It only verifies the number, and the person's first order then needs a code. A new start replaces the last code. A wrong code is 401 bad_code. After five wrong tries or three minutes, verify answers 400 code_expired: call start again.
start:429rate_limitedwhen a code went to this number too recently, withRetry-After,error.retryInSecondsandRateLimit-*headers, or a texting limit is reached (see Users and texts),502send_failedwhen the text could not be sent, and422invalid_requestwhen the user has no usable number.verify:400invalid_requestwhen the code is not six digits, or is missing for someone not yet verified, and429rate_limitedafter too many wrong codes for the number, withRetry-Afteranderror.retryInSeconds.
authorized: true means you can build now. A good production code makes the person's Layout account (status: "active", claimed: true), or joins the one another application's verify made, if the text named your application. A card already on that account stays, so your first build may not ask for one. If the person signs in to Layout on this number themselves, authorized is false with next: "connect": send a connect link. Verify never returns a session or anything about an existing account.
A returning person. A person you are already connected to can verify again on a new device. They get the same id. If their account now holds a different number, start answers 409 phone_changed and texts nobody. You stay connected and can keep building. Do not provision their new number: that makes a second id that cannot connect. Sent with no code, verify on someone already verified answers verified: true without a check, so only a request that carried a code proves they hold the phone.
If someone you provisioned but never verified signs up to Layout on their own, start answers 409 already_claimed: send them a connect link.
If the person once replied STOP to Layout, no code can reach them and start answers 409 texts_blocked. Ask them to reply START to the number Layout texts from, then call start again.
Reading the code on Android
An Android app can read the code from the text with the SMS User Consent API, which asks the person once to share that one message with your app. It needs no app hash and no READ_SMS permission, and the text carries no hash. Pass the six digits to your verify call as you would a typed code.
Verify in sandbox
A sandbox number reaches nobody, so sandbox sends nothing. With an sk_test_ key, start answers the same shape plus "sandbox": true, and verify accepts the fixed code 000000. The same per-number spacing applies as in production: a second start too soon, or a sixth in an hour, is 429 rate_limited, and Retry-After is the exact wait.
POST /v1/users/usr_4b8e9a01c2d3/verify/start
→ 200
{ "ok": true, "id": "usr_4b8e9a01c2d3", "phoneHint": "+1 (500) •••-0142", "expiresInSec": 180, "sandbox": true }
POST /v1/users/usr_4b8e9a01c2d3/verify
{ "code": "000000" }
→ 200
{ "id": "usr_4b8e9a01c2d3", "verified": true, "authorized": true, "status": "active", "claimed": true }
Any other six-digit code is 401 bad_code. The code works only for a sandbox user on a sandbox key; in production 000000 is just a wrong code. A verified sandbox user stays a test account, so its orders confirm as a simulation (see Finishing an order in sandbox). Sandbox does not simulate a number another application verified. Sandbox numbers belong to your developer account, not to one application, so give each application its own +1 500 555 numbers.
Two ranges behave differently. A number in +1 500 555 0050 to 0099 is a person with no card: once verified, card reads { "onFile": false, "orderable": false, "brand": null, "last4": null }, and a confirm answers 403 build_only with error.next.step card_required. Open a sandbox card link and press Add test card, and the person reads as a Visa ending 4242 and a confirm places. Another call, card-setup/confirm, does not add the test card.
A number in +1 500 555 2000 to 9999 that a console sign-in test account holds is a person who signs in to Layout. Provisioning it, or calling POST /v1/users/:id/connect for it, answers next: "connect" with a connectUrl and "sandbox": true, and sends no text.
Someone who signs in to Layout
When the person signs in to Layout on this number, only they can connect you, on Layout's consent screen. Headless verify will not do it. By default Layout texts them the approval link and provisioning returns next: "awaiting_user_approval": wait for the connection. Layout texts only for an approved application in production, and never to a number that replied STOP. In sandbox and before approval you get the link instead. To send the link yourself, pass connectDelivery: "developer" when you provision, or call:
POST /v1/users/usr_4b8e9a01c2d3/connect
{ "returnUrl": "https://your.app/connected" }
→ 200
{ "next": "awaiting_user_approval", "awaitingApproval": { "phoneHint": "+1 (916) •••-0142", "expiresInSec": 600 } }
POST /v1/users/usr_4b8e9a01c2d3/connect
{ "returnUrl": "https://your.app/connected", "connectDelivery": "developer" }
→ 200
{ "next": "connect", "connectUrl": "https://account.layout.link/authorize?request_id=…", "expiresInSec": 600 }
If the text cannot be sent, you get the connect form instead. A person already connected answers connected: true. Open connectUrl in the person's browser. They sign in to Layout and see “{your app} will be able to place orders for you, paid with the card on file in your Layout account. You can disconnect {your app} anytime in Layout.” Until Layout approves your application the screen says “This app”. Approving is their agreement that your application places orders for them. They land back at your returnUrl with connected=1, or with error=access_denied if they decline. returnUrl is optional, https, and at most 2,000 characters. Without one they see a short confirmation on Layout's page. The link is good for ten minutes.
This is the only way an existing account is attached to you. You get nothing about their account back.
The connection lasts
Once connected, the connection lasts. You keep building for them and confirming what they say yes to, with a code only when a code is due. It holds when they open the Layout app, add a card or sign in elsewhere. They can disconnect you at any time from their Layout account, and your next build for them then stops.
Disconnect a person
DELETE /v1/users/usr_4b8e9a01c2d3
→ 200
{ "id": "usr_4b8e9a01c2d3", "status": "revoked" }
Ends your application's link to this person. It takes your secret key. Every build grant for them stops working at once, and their standing agreement that your application places orders for them ends. Their Layout account, their cards and their orders stay theirs and are not deleted. Repeating it answers the same. An id that is not your application's is 404 not_found. To reconnect later, provision the same number again: you get the same id back as a new invitation, and the person verifies again (or approves a connect link, if they sign in to Layout themselves).
After a reconnect, the person’s standing agreement is gone, so the first order asks for a code again: a confirm answers code_required with reason first_order_consent. Their card survives, and the cards your application’s card links added still count as the application’s own.
Building without a card
A person with no card can still have carts built. Layout drives the restaurant and returns a priced cart, with nothing charged. The card is needed only to place the order.
Layout sets a daily ceiling on no-card builds per application, seeded from the volume you gave when you applied. Ask us to raise it as you grow. Once it is spent, the next build for a person with no card asks for a card instead. GET /v1/usage lists it as cardless_builds, with what you have used and what remains.
When a card is needed
Before the first live confirm the person needs their Layout account and a card. A good verify makes the account. A person who signs in to Layout on this number themselves is attached only when they approve a connect link (POST /v1/users/:id/connect); signing in does not attach them. Until then a confirm answers 403 build_only, names the missing step in error.next.step, and texts nothing: verify_required, verify them with the two calls above; connect_required, send them a connect link; card_required, send them a card link.
A card is needed when the person is ready to order, or when the day's no-card builds are used up. A build then fails with failure.code no_card (or card_not_orderable when Layout cannot use their card) and failure.next.step card_required. A confirm answers build_only with error.next.step card_required. For a person you provisioned, neither carries a url. Mint a card link with POST /v1/users/:id/links and send them to it. The page takes the card and sends them back with status=card_added or status=canceled, to a return URL you registered on the console's App details page (see Return URLs). You never see the card number. Until the person signs in to Layout themselves, a card link is the only way to change their card.
To know before a build whether to send a card link, read card on the verify answer or on GET /v1/users/:id. card.orderable: false means send one. After a card link returns status=card_added, read GET /v1/users/:id again to confirm it, since anyone can open your return URL. Both carry only the brand and last four of the card Layout would charge, and only once the person has authorized your application.
Sign-up protections
Layout limits how fast a code can be sent and tried, so that your sign-up cannot be used to text a stranger over and over. These limits stand in front of yours, not in place of them.
- Per number: a second code to the same number waits 30 seconds, and a number gets 5 codes an hour. A code lasts 3 minutes and takes 5 tries, then it is spent.
- Per credential: starts per hour, counted for each credential. See Per-minute and per-hour limits.
- Per developer account: an hourly pool of texts that grows with the people you verify, a daily pool, and one pool across every developer, all on Users, sessions and texts.
Put your own gate in front of the endpoint that calls verify/start. Require a signed-in session, a CAPTCHA or a rate limit per visitor, so an anonymous page cannot use up your texts. Layout’s limits protect Layout and the person’s phone; yours protect your allowance.
Server apps and grants
An application that places orders from its own server, rather than from the person’s device, mints a build grant for each order session with POST /v1/users/:id/grant. Cache the grant per user, refresh it lazily by sending it back as refresh just before it expires, and when any call answers 409 grant_expired, mint a new one and retry. Do not mint one grant for every request: minted grants are a limit, and refreshes are not.
Reconciliation
A person you verified who signs in to Layout later lands on the same account. If someone you provisioned but never verified signs up on their own within 30 days, the orders you built for them move to their new account. You never see a person's own orders, their orders through another app, or their card number.