# 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](#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](#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](https://developer.layout.link/console/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](https://developer.layout.link/reference/authentication#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](#button-rules) and [Brand assets](#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](https://developer.layout.link/reference/authentication#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](#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:

```bash
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](https://developer.layout.link/reference/authentication#tokens-and-refresh).

### 5. Learn who signed in

```bash
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:

```bash
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](https://developer.layout.link/reference/authentication#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) &bull;&bull;&bull;-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](https://developer.layout.link/reference/provisioning#connecting-an-existing-account)).

### 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.** `verify` with no code on someone already verified answers `verified: true` without 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](https://developer.layout.link/reference/provisioning#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](https://developer.layout.link/brand/sign-in-with-layout/continue-black-56.svg) | 269 &times; 56 |  |
| [continue-black-48](https://developer.layout.link/brand/sign-in-with-layout/continue-black-48.svg) | 237 &times; 48 |  |
| [continue-black-40](https://developer.layout.link/brand/sign-in-with-layout/continue-black-40.svg) | 215 &times; 40 |  |
| [continue-white-56](https://developer.layout.link/brand/sign-in-with-layout/continue-white-56.svg) | 269 &times; 56 |  |
| [continue-white-48](https://developer.layout.link/brand/sign-in-with-layout/continue-white-48.svg) | 237 &times; 48 |  |
| [continue-white-40](https://developer.layout.link/brand/sign-in-with-layout/continue-white-40.svg) | 215 &times; 40 |  |
| [order-black-56](https://developer.layout.link/brand/sign-in-with-layout/order-black-56.svg) | 242 &times; 56 |  |
| [order-black-48](https://developer.layout.link/brand/sign-in-with-layout/order-black-48.svg) | 213 &times; 48 |  |
| [order-black-40](https://developer.layout.link/brand/sign-in-with-layout/order-black-40.svg) | 193 &times; 40 |  |
| [order-white-56](https://developer.layout.link/brand/sign-in-with-layout/order-white-56.svg) | 242 &times; 56 |  |
| [order-white-48](https://developer.layout.link/brand/sign-in-with-layout/order-white-48.svg) | 213 &times; 48 |  |
| [order-white-40](https://developer.layout.link/brand/sign-in-with-layout/order-white-40.svg) | 193 &times; 40 |  |
| [circle-black-48](https://developer.layout.link/brand/sign-in-with-layout/circle-black-48.svg) | 48 &times; 48 |  |
| [circle-white-48](https://developer.layout.link/brand/sign-in-with-layout/circle-white-48.svg) | 48 &times; 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.
