Card links
A short Layout link where a person you provisioned adds a card, with no Layout sign-in, and then goes straight back to your app.
The card page texts the person no code. Layout’s own texts are on Texts Layout sends.
Mint a link
POST /v1/users/usr_4b8e9a01c2d3/links
{
"purpose": "card",
"returnUrl": "com.acme.assistant:/layout/card",
"state": "c1f9e2"
}
→ 201
{
"url": "https://layout.link/h/3q2Xw7…",
"expiresAt": "2026-10-21T17:15:00.000Z",
"purpose": "card"
}
| Field | Notes |
|---|---|
purpose | Required. "card", the only purpose today. |
returnUrl | Optional. Where the page sends the person when they finish. It must be exactly one of the return URLs registered on your application's App details page in the console. See What comes back. |
state | Optional. 1 to 512 characters, at most 1536 once percent-encoded, with no control characters. Layout hands it back to you on the return URL, so you can match the person who comes back to the link you sent. |
It takes your secret key and works for every application. The person must have verified their number with your application first (see Verify their phone), or approved a connect link. A person you have not verified is 409 person_not_ready.
- Single use. The first open spends the link. Once open, the page keeps working for 30 minutes, reloads included.
- 15 minutes to open.
expiresAtsays when. A link that expired or was already opened shows a page that tells the person to go back to your app and try again. Mint a new one then. - Mint it when you send it. Layout keeps no copy of the url, so it cannot be read back later. Each call mints a new link.
When to send one
Send a card link when Layout asks for a card. A build fails with failure.code no_card or card_not_orderable and failure.next.step card_required, or a confirm answers 403 build_only with error.next.step card_required. For a person you provisioned neither carries a url, so mint a card link. You can also send one sooner, so the first order does not stop for a card: the verify answer and GET /v1/users/:id carry card.orderable, and false means send one. See When a card is needed.
What the person sees
A full-screen page titled Pay with Layout, with your application's logo and name, which says “{your app} partners with Layout to pay for your orders. You're only charged for orders you confirm, and {your app} never sees your card.” They enter a card, or use Apple Pay or Google Pay when their browser offers it. They do not sign in to Layout and do not make an account: verify already made it. A card link adds a card. On an account that already holds as many cards as it can, it replaces one, and the page names that card and asks before it saves:
- An account nobody signs in to Layout with (every account a verify made, until the person signs in): it replaces the default card. A card link is the only way to change the card on such an account.
- An account the person signs in to: it replaces only a card your own card links added, never one the person or another application added. If there is none, nothing is saved and going back returns
status=canceled.
When the card is saved, or the person goes back, the page sends them to your return URL by itself. Without a return URL it stays there and says “Card added. Go back to {your app}. You won't need to add a card there again.”, or “No card added. Go back to {your app}.”, with no button.
The name and logo are the ones on your application's App details page in the console, shown once Layout approves your application. Before approval, a sandbox link shows your application's name with no logo. The card is entered on Layout's page and held by Layout. It never touches your servers, and you never see it.
Return URLs
A return URL is where the card page sends the person when they finish. It is optional. A link can name only a return URL you registered for your application first, and the link must name it exactly, byte for byte. A trailing slash or a different port is a different URL.
Register one in the console
- Open your application in the console and choose App details in the sidebar.
- Under Return URLs, choose Add return URL.
- Enter the URL in Return URL and choose Add. It shows in the list at once.
An application holds up to 10, and the same list serves your sandbox and production keys. The console takes:
httpswith a host, such as a universal link (https://acme.example/layout/card).- Your app's own custom scheme, such as
com.acme.assistant:/layout/card. httponlocalhost,127.0.0.1or[::1]. The list marks these Sandbox only, and a live key cannot name one.
A return URL cannot carry a fragment, a wildcard, a user name or password, or a state or status parameter. It must be in its exact form, so https://Acme.example/cb is refused with “Register the URL in its exact form: https://acme.example/cb”. To remove one, choose the bin icon on its row, then Remove to confirm. Every link that names it stops sending people back, including links already sent. A link that names a URL you have not registered is 400 invalid_request when you mint it.
What comes back
The card page sends the person's browser to your return URL with state, when you sent one, and then status added to its query. Any query your URL already has stays as you registered it, in front. Returns from a sandbox link look like this:
http://localhost:48123/layout/card?state=abc123&status=card_added http://localhost:48123/layout/card?state=back-1&status=canceled http://localhost:48123/layout/card?status=card_added http://localhost:48123/layout/card?from=app&state=q1&status=canceled
| Parameter | Notes |
|---|---|
status | card_added when the page saved a card. canceled when the person chose Back or closed the form without saving one, or when their account already held all the cards it can and nothing was saved. In that last case the return also carries reason=account_full, and the page says “No room for another card” with “Your Layout account already holds all the cards it can, so nothing was added. Manage your cards at account.layout.link.” Only status=canceled ever carries a reason. |
state | Exactly the state you sent when you minted the link, percent-encoded. A space is %20. Absent when you sent none. |
Nothing else is added, except reason=account_full on that one canceled return: no user id, no token, no card details and no signature. Read them with your platform's URL parser, such as URLComponents on iOS or URLSearchParams on the web, rather than matching the raw string.
- It can come more than once. If the person opens the same link again in the same browser within 30 minutes, the page sends them back with
status=card_addedagain. Handle a repeat as a no-op. - It may never come. The person can close the tab, or have no app installed for your custom scheme. When the page cannot leave, it shows Back to {your app} instead. Never wait on the return to move on.
- It is not a webhook. It is the person's own browser opening your URL. It reaches your server only when your return URL is a page on your server, and Layout sends no webhook when a card is added.
Check it on your side
Anyone can open your return URL with any status, so treat it as a hint about where the person is, never as proof that a card is on file or that it will pay.
- When you mint the link, send a fresh random
stateand keep it with the person and what they were doing, such as their cart. - When a return arrives, look up its
state. Ignore one you did not send or have already handled. - On
card_added, readGET /v1/users/:idfrom your server.card.orderable: truemeans the card is there and Layout can order with it, so carry on with what they were doing.falsemeans it did not save or cannot be used: mint a new link. If the person never comes back to your return URL, the same read tells you next time they open your app. - On
canceled, say no card was added and offer to try again. The link is spent, so mint a new one. Keep the cart: a ready cart lasts 15 minutes.
Open it in your app
- iOS app. Open the link as a sheet, never in your own
WKWebView: a webview your app controls can read what the person types. UseASWebAuthenticationSessionwith a custom scheme, orSFSafariViewControllerwith a universal link. - Web app. Open it in the same tab or a new one, with an
httpsreturn URL on your site. Your page readsstateandstatusfrom its own URL. - Agent, chat or SMS app. Send the url in the conversation, with one line that says what it is. Register an
httpspage on your site as the return URL, which says the card is added and to go back to the chat. Use itsstateto tell your agent the person is back. With no return URL, Layout's page tells them to go back to your app.
iOS with a custom scheme
Register com.acme.assistant:/layout/card as a return URL and name it on the link. Pass only the scheme, with no colon, as callbackURLScheme. The session closes the sheet when the page sends the person back, and hands you the URL. It needs no URL type in your Info.plist. If the person closes the sheet, nothing comes back and the error is .canceledLogin: treat it as canceled.
let session = ASWebAuthenticationSession(url: cardLinkURL, callbackURLScheme: "com.acme.assistant") { callbackURL, error in
guard let callbackURL,
let items = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?.queryItems else {
handleCardLinkReturn(status: "canceled", state: nil)
return
}
let status = items.first { $0.name == "status" }?.value
let state = items.first { $0.name == "state" }?.value
handleCardLinkReturn(status: status, state: state)
}
session.presentationContextProvider = self
session.start()
iOS with a universal link
Register an https return URL on a domain your app claims in its associated domains, such as https://acme.example/layout/card, and open the link in SFSafariViewController. When iOS hands the return to your app, read it in onOpenURL, or in scene(_:continue:) for an NSUserActivityTypeBrowsingWeb activity, and dismiss the Safari view yourself. Serve a real page at that URL too. If iOS opens it in the browser instead, the person lands on that page, so give it a button back into your app.
Before you send the first link, tell the person about Layout in your own words. See Introduce Layout first.
In sandbox
A sandbox key mints a sandbox link. Its page collects no card and saves nothing: Add test card sends the person back with status=card_added, and Back with status=canceled. Sandbox builds and confirms never need a card, except for a person verified on a number in +1 500 555 0050 to 0099: that person has no card until they press Add test card, and then reads as a Visa ending 4242 and a confirm places. Use it to test the round trip through your return URL, including a localhost one such as http://localhost:3000/layout/card. A sandbox build never answers card_required, so it cannot tell you whether a card is there.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | purpose is not "card", state breaks its rule, returnUrl is not exactly one of your registered return URLs, a localhost returnUrl was sent with a live key, or returnUrl with state and status added would pass 4000 characters. The message says which. |
| 404 | not_found | No such user for this application in this environment, or you disconnected them. |
| 409 | person_not_ready | The person has not verified their number with your application, or cannot hold a card on Layout yet. error.next.step, when present, names the step. Verify them, then mint the link. |
| 429 | rate_limited | More than 10 links for one person in 10 minutes, or 600 for one credential in an hour. Wait Retry-After. |
| 503 | unavailable | Layout could not check the person or the return URL just now. Nothing was minted. Retry shortly. |