Sign in with Layout
One button that signs a person in with their Layout account and lets your app place orders for them, paid with the card they keep in Layout.
Two ways in
| Way | What the person sees | Use it when |
|---|---|---|
| Hosted | Layout's own sign-in and approval page, in the system browser sheet. | You want the shortest build. Layout handles sign-in, the code and consent. |
| Headless | Your own screens. Layout texts the person a code and they type it into your app. | You already collect the person's phone number and want to stay on your screens. |
Either way, Layout writes the sentence the person agrees to, and a code only ever links their number to your app. It is never a sign-in to their Layout account, and your app never gets one.
Hosted
1. Create an OAuth client
On the console's OAuth clients page, add your redirect URIs. You get a client_id. It is a public client: authorization code with PKCE (S256), no client secret. See OAuth clients.
2. Add the button
Use the button as Layout ships it. Link it to your authorize URL.
<a href="/login/layout">
<img src="https://developer.layout.link/brand/sign-in-with-layout/continue-black-48.svg"
width="237" height="48" alt="Continue with Layout" />
</a>
On iOS and Android, use the PNGs at @2x and @3x as an image button. See Button rules and Brand assets.
3. Send them to Layout
https://api.layout.link/v1/oauth/authorize ?response_type=code &client_id=$CLIENT_ID &redirect_uri=https%3A%2F%2Fyourapp.example%2Fcallback &code_challenge=$CODE_CHALLENGE &code_challenge_method=S256 &scope=order &resource=https%3A%2F%2Fapi.layout.link &state=$STATE &login_hint=%2B19165550142
| Parameter | Notes |
|---|---|
redirect_uri | Must exactly match one you registered on the client. |
code_challenge | Required. S256 only. |
scope | order. |
resource | https://api.layout.link for the REST API. Add https://mcp.layout.link as a second resource to cover both in one approval (see One authorization for both). |
state | Comes back unchanged. Check it on your callback. |
prompt | Optional. Leave it out and a person who already approved your app is sent straight back (see Returning people). consent always shows the approval page. none never shows a page: you get a code, or error=login_required or error=consent_required on your redirect URI. |
login_hint | Optional. The person's phone number in E.164 (+19165550142), URL-encoded. Layout fills it in on its sign-in step. It never sends a code by itself; the person still taps to get one. Anything that is not an E.164 number is ignored. |
Open it in the system browser sheet, never an embedded web view. On iOS that is ASWebAuthenticationSession, on Android a Custom Tab, on the web a full-page redirect. An embedded view cannot share the person's Layout sign-in, and the person cannot tell the page is really Layout's.
The person signs in with their phone and a code from Layout, and approves your app on Layout's page. It says, in Layout's words: “{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.” Signing in does not ask for a card. Layout asks for one when the person confirms their first order, and the confirm shows the card that will be billed.
4. Exchange the code
Layout redirects to your redirect_uri with code and state, or with error when the person cancels (access_denied) or the request is refused. Exchange the code from your server:
curl https://api.layout.link/v1/oauth/token \ -d grant_type=authorization_code \ -d code=$CODE \ -d redirect_uri=https://yourapp.example/callback \ -d code_verifier=$CODE_VERIFIER \ -d client_id=$CLIENT_ID \ -d resource=https://api.layout.link
You get an access token (ten minutes) and a rotating refresh token (thirty days, renewed on every refresh). See Tokens and refresh.
5. Learn who signed in
curl https://api.layout.link/v1/whoami \
-H "Authorization: Bearer $ACCESS_TOKEN"
200 OK
{
"kind": "user",
"app": { "id": "lyt_app_3f9c2a7e1b4d8f6a0c5e9b27" },
"environment": "live",
"user": { "id": "usr_4b8e9a01c2d3" },
"scopes": ["order"]
}
user.id is stable: the same person gets the same id in your app every time, on every device. Key your own account on it. It is yours alone; another app knows the same person by a different id.
Returning people
Send a returning person through the same authorize URL. If they are signed in to Layout in that browser and already approved your app for the same scope and resources, Layout sends them straight back with a code and shows nothing. Exchange it as before; whoami answers the same user.id. If they signed out of Layout everywhere, disconnected your app, or your request asks for more than they approved, they see the page again.
To check silently, for instance when your app starts, send prompt=none. You get a code, or one of these on your redirect URI, and the person sees no page:
error | What to do |
|---|---|
login_required | They are not signed in to Layout in this browser. Show the button. |
consent_required | They are signed in but have not approved your app, or not for this. Show the button. |
temporarily_unavailable | Layout could not check. Show the button, or retry. |
prompt=none with any other prompt value is invalid_request. A redirect URI you did not register is never redirected to, with or without prompt. Only an https redirect URI is answered silently: a localhost redirect always shows the approval page.
Sign out
When the person signs out of your app, revoke their connection with either token:
curl https://api.layout.link/v1/oauth/revoke \ -d token=$REFRESH_TOKEN \ -d client_id=$CLIENT_ID 200 OK
This disconnects your app, the same as the person doing it in Layout, so the next sign-in shows the approval page again. If you only want to forget the session in your app and keep the connection, drop your tokens and do not revoke. See Revoking a connection.
Headless
Your screens collect the phone number; Layout sends the code. Provision the person, text the code, and check what they type. The id you get back is the person's id in your app.
POST /v1/users
{ "firstName": "Dana", "lastName": "Whitfield", "phone": "+19165550142" }
→ 201 { "id": "usr_4b8e9a01c2d3", "next": "verify", … }
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": "provisioned", "claimed": false }
The text is Layout's: Layout: 246810 links this number to {your app}, which can then place orders for you. It expires in 3 minutes. If you didn't ask for this, ignore this text. Nothing you send appears in it. A number that already has its own Layout account answers authorized: false with next: "connect"; send the person to the connect link (see Someone who already has Layout).
Returning people
A person who connected before and opens your app on a new phone is recognised the same way. POST /v1/users with their number returns the same id, verify/start texts them a fresh code, and verify with the right code answers the same id and changes nothing. The same limits apply as for a first verification.
- Only an answer to a request that carried a code proves the person holds the phone.
verifywith no code on someone already verified answersverified: truewithout a check, so a lost response can be read again. - 409
phone_changed: the person's Layout account no longer holds the number you provisioned. No code is sent. Provision their current number. - 409
already_claimed: the person signed up to Layout on their own and has not connected your app. Send them to the connect link.
In sandbox nothing is texted and the code is always 000000, for returning people too. See Verify in sandbox.
Button rules
The button is Layout's, so a person recognises it in every app. Use it exactly as shipped.
- Black or white. Black on light backgrounds, white on dark or photographic ones. No other colour, including Layout blue.
- Two labels. Continue with Layout to sign in. Order with Layout where the tap starts an order. No other wording, and no translation of the word Layout.
- Four sizes. Pills 56, 48 and 40 pixels tall, and a 48-pixel circle with the symbol alone where space is tight. Scale nothing.
- Nothing changed. Not the colour, font, shape, corner radius or wording. No border, shadow or extra icon.
Brand assets
SVG and PNG at @1x, @2x and @3x, at https://developer.layout.link/brand/sign-in-with-layout/. The label is drawn as shapes, so it looks the same with no font installed.
| File | Size | Button |
|---|---|---|
continue-black-56 | 269 × 56 | |
continue-black-48 | 237 × 48 | |
continue-black-40 | 215 × 40 | |
continue-white-56 | 269 × 56 | |
continue-white-48 | 237 × 48 | |
continue-white-40 | 215 × 40 | |
order-black-56 | 242 × 56 | |
order-black-48 | 213 × 48 | |
order-black-40 | 193 × 40 | |
order-white-56 | 242 × 56 | |
order-white-48 | 213 × 48 | |
order-white-40 | 193 × 40 | |
circle-black-48 | 48 × 48 | |
circle-white-48 | 48 × 48 |
Each name has a .svg, a .png, an @2x.png and an @3x.png, for example continue-black-48@3x.png. Give the image the size in the table and an alt of its label.