Linque Implementation
Let a guest shopper pay with a card they already hold, and receive it as a token in your tenant.A checkout verifies the shopper's phone, shows the cards discovered for them, and turns the one they select into a token. With Elements, three calls do it. bt.linque.start sends the code. bt.linque.verify submits it and fills the wallet. bt.linque.getCard verifies the selected card and returns the token Linque created in your tenant, with element references for the Proxy. A backend holding a private key calls the same four endpoints.
With Elements
Elements renders the shopper-facing flow and keeps cardholder data inside Basis Theory-controlled iframes. The card number and security code never reach your page.
linque: true enables the Linque module on your Basis Theory instance. It adds bt.linque.* and the Linque element types, and is required to use either. To use the sandbox, point the SDK at it with apiBaseUrl.
- Web Elements
- React Elements
import { BasisTheory } from '@basis-theory/web-elements';
const bt = await BasisTheory('<PUBLIC_API_KEY>', {
linque: true,
apiBaseUrl: 'https://api.btsandbox.com',
});
import { BasisTheoryProvider } from '@basis-theory/react-elements';
function App() {
return (
<BasisTheoryProvider
apiKey="<PUBLIC_API_KEY>"
options={{ linque: true, apiBaseUrl: 'https://api.btsandbox.com' }}
>
<Checkout />
</BasisTheoryProvider>
);
}
Mount Both Elements
Mount both elements before the flow begins. Neither takes any options. The wallet renders manual card entry until the shopper verifies their phone, then fills itself with the discovered cards. Creating it after verification instead means its markup appears mid-checkout and shifts the layout under the shopper.
- Web Elements
- React Elements
const verification = bt.createElement('verification');
const wallet = bt.createElement('wallet');
await Promise.all([
verification.mount('#verification'),
wallet.mount('#wallet'),
]);
verification.on('change', ({ detail }) => {
continueButton.disabled = !detail.complete;
});
verification.on('resend', () => bt.linque.resend());
wallet.on('change', ({ detail }) => {
payButton.disabled = !detail.complete;
});
import { useRef, useState } from 'react';
import { VerificationElement, WalletElement, useBasisTheory } from '@basis-theory/react-elements';
function Checkout() {
const { bt } = useBasisTheory();
const verificationRef = useRef(null);
const walletRef = useRef(null);
const [canContinue, setCanContinue] = useState(false);
const [canPay, setCanPay] = useState(false);
return (
<>
<VerificationElement
ref={verificationRef}
onChange={(event) => setCanContinue(event.detail.complete)}
onResend={() => bt.linque.resend()}
/>
<WalletElement
ref={walletRef}
onChange={(event) => setCanPay(event.detail.complete)}
/>
</>
);
}
Send the Code
Send the shopper's name and mobile number once the checkout has collected them. Linque sends a six-digit code by SMS.
const { sms_sent } = await bt.linque.start({
name: { first: 'Ada', last: 'Morgan' },
phone: '+14155551212',
});
sms_sent reports whether an SMS was actually sent. The reserved test numbers return false. If sms_sent is false, do not tell the shopper that a message was sent.
Requests are limited to three sends per five minutes per mobile number. Resends count against that limit. Call bt.linque.resend() from the verification element's resend event; the element does not call the resend API itself.
Verify the Phone
Submit the code on your Continue button, or directly from change once complete is true. Pass the verification element. verify reads the code internally, so your page never handles it. The call returns the shopper and their discovered cards, and the wallet fills itself.
const individual = await bt.linque.verify(verification);
verify returns the cards on individual.cards if you want to show a count, log them, or render your own list.
An incorrect code rejects with VERIFICATION_CODE_INVALID; another attempt may succeed. An expired code rejects with VERIFICATION_CODE_EXPIRED; send another. Handle the two separately. Telling a shopper that an expired code is merely incorrect encourages them to keep retrying a code that cannot succeed. The element clears the field and shows the reason before the promise rejects.
An empty cards array is a valid empty wallet, not an error. The wallet keeps rendering manual card entry.
Verify the Card and Pay
On your Pay button, take the card from the wallet.
const linqueCard = await bt.linque.getCard(wallet);
// A discovered card is already a token in your tenant.
// A card entered in the New card row is tokenized here.
const token = linqueCard.token ?? await bt.tokens.create({
type: 'card',
data: linqueCard,
});
// Send token.id to your backend and charge it as you charge any card token.
For a discovered card, getCard returns token, the Basis Theory token Linque created in your tenant when the card was verified. The card is already tokenized. Use that token and do not tokenize it again. The card itself never enters your page.
A card entered in the wallet's New card row has no token. Tokenize it with tokens.create as you would any card element. The same getCard(wallet) call serves both cases, so your checkout needs only one payment path.
getCard also returns element references for number, expiration_month, expiration_year and cvc. Use them anywhere you would use another element. For a discovered card, proxy.post sends detokenization expressions for the token, so a pre-configured proxy needs nothing new and a session apiKey needs token:use on that token. In a Proxy body, place the expiry references under expiration_month and expiration_year. The SDK uses those keys to tell month from year.
An incorrect security code rejects with SECURITY_CODE_INVALID. The rejection carries the attempts remaining, and the wallet clears the field and shows the count. Attempts are counted per card. Exhausting them on one card rejects with SECURITY_CODE_ATTEMPTS_EXHAUSTED and does not lock the shopper out of the others. A card that cannot currently be verified rejects with CREDENTIAL_UNAVAILABLE. In both cases the other cards and the New card row stay available. The full mapping is in the Elements reference.
Start a New Shopper
bt.linque.start begins a new shopper. It discards the previous shopper's cards, pending verification, session key, and everything the wallet held, so a second checkout on the same page starts clean.
With the API
The API can be called directly without the SDK. Verifying a card returns a Basis Theory token in your tenant, created by your application, so an API-only integration never needs to handle a card number unless it reveals the token with a private key that holds token:read with a reveal rule.
A security code collected in your own input is cardholder data, and that input is in PCI scope. The wallet element exists to keep it out of your page.
A private key does not need a session key. After the verification, name the shopper with the id the verification returned.
- Send a verification code with the shopper's name and mobile number. Keep
verification_id. - Submit the code the shopper received. Keep
id, the shopper, and showcredentials. - Verify the card the shopper selected, with its security code when
requires_security_codeistrue. Name the shopper with?id=. Keeptoken.id.
curl 'https://api.btsandbox.com/linque/credentials/<CREDENTIAL_ID>/verifications?id=<INDIVIDUAL_ID>' \
-X 'POST' \
-H 'BT-API-KEY: <PRIVATE_API_KEY>' \
-H 'BT-IDEMPOTENCY-KEY: <IDEMPOTENCY_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"security_code": "<SECURITY_CODE>"
}'
{
"token": {
"id": "19e2ca48-8ae4-4276-9fdc-d81f8ce5f2d7",
"type": "card",
"tenant_id": "<YOUR_TENANT_ID>",
"created_by": "<YOUR_APPLICATION_ID>",
"created_at": "2026-10-20T23:20:42Z",
"containers": ["/pci/high/"],
"card": {
"bin": "424242",
"last4": "1580",
"brand": "visa",
"expiration_month": 4,
"expiration_year": 2027
},
"data": {
"number": "XXXXXXXXXXXX1580",
"expiration_month": 4,
"expiration_year": 2027
}
},
"card": {
"brand": "visa",
"last4": "1580",
"expiration_month": 4,
"expiration_year": 2027
},
"data": {
"type": "card",
"number": "4242424242421580",
"expiration_month": 4,
"expiration_year": 2027,
"cvc": "123"
}
}
The top-level data is the card itself and arrives only for a private key. It is deprecated and will be removed; do not build on it. token.data is the masked value every card token carries.
Use an idempotency key when sending a verification code. Without one, retrying after a dropped connection can send another SMS. Use one on the card verification too: a retry under the same key returns the token already created instead of creating a second one.
FAQ
Which cards can Linque find?
Linque discovers eligible cards associated with a shopper using their name and verified United States mobile number. Card availability varies by issuer and other eligibility factors, so some shoppers may have no discoverable cards or may have cards that cannot be verified.
An empty wallet is a supported checkout outcome, and shoppers can always continue with manual card entry.
Does the shopper have to enroll first?
No. Unlike a merchant-stored-card wallet, Linque can discover eligible cards for a shopper on their first visit to your checkout. Every checkout verifies the phone number, including for shoppers Linque has seen previously. existing reports whether the shopper was already known.
Why does the security code come before the token is created?
The security code is part of proving possession of the card. Until that check succeeds, the card is not verified and no token exists. It is therefore part of selecting the card rather than a confirmation step performed afterward.
Where does the token end up?
In your Basis Theory tenant. Linque creates it when the card is verified, as your application, so it carries your application as created_by. From there, charging it uses the same Basis Theory integration you already have.
Can my page see the card number?
Not through Linque. A publishable key receives the token and the display card. Server side, reveal the token with a private key under the vault's access rules.
Does Linque charge the card?
No. Linque returns a card token. Charge it through the Proxy or from your backend, as you charge any card token.
How long do the code and the session key last?
A verification code is valid for 10 minutes. A session key is valid for 30 minutes after the phone is verified. The SDK holds the session key inside the iframe. A backend keeps the shopper's id instead, which names the shopper to the API for 90 days without a session key.
What happens if Linque or its provider is unavailable?
Linque distinguishes service availability failures from shopper-level errors such as an incorrect security code. An availability failure answers 503 with type: "upstream_unavailable", which the SDK reports as API_ERROR. It does not consume security-code attempts, so the shopper can retry. If Linque is unavailable or a discovered card cannot be verified, shoppers can continue with manual card entry without interrupting checkout.
Can we use Linque from a native app?
Native mobile applications can call the Linque API. Secure in-app collection of the security code requires a native capture implementation, which is not available yet. Contact Basis Theory to discuss native mobile requirements and the recommended integration approach.