Linque
Linque finds cards a shopper already holds using their name and mobile number, and returns the one they select as a token in your tenant. In Elements it is the linque option, the bt.linque service, and two element types, verification and wallet. The verification code, the session key, the security code and the verified card stay inside Basis Theory-controlled iframes. The guide covers the end-to-end flow.
The linque option
linque: true on BasisTheory(), or in the options prop of BasisTheoryProvider, enables the Linque module. It adds bt.linque.* and registers the two element types, and is required to use either. The module is loaded only when the option is set. Without it, bt.linque.* rejects with a message naming the option and createElement('wallet') throws. To use the sandbox, set apiBaseUrl to it.
const bt = await BasisTheory('<PUBLIC_API_KEY>', {
linque: true,
apiBaseUrl: 'https://api.btsandbox.com',
});
The publishable application needs linque:verification:create and linque:credential:read. Add token:create if manually entered cards are tokenized with the same key. See Setup.
bt.linque
| Method | Does | Resolves with |
|---|---|---|
start({ name, phone }) | Sends a verification code to the shopper by SMS. name is { first, last } and phone is E.164. Begins a new shopper and discards the previous one | { sms_sent } |
resend() | Sends the same shopper another code | { sms_sent } |
verify(verification) | Submits the code. With the element, the code is read internally. A string works for a page that collects the code itself. The session key stays in the iframe | { id, existing, cards } |
getCard(wallet) | Verifies the selected card and returns its token, or reads the manually entered one | { number, expiration_month, expiration_year, cvc, token? } |
start and resend
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. Requests are limited to three sends per five minutes per mobile number, resends included. The SDK attaches an idempotency key, so a retried request does not spend a send. The SDK never retries a Linque call on its own.
verify
const individual = await bt.linque.verify(verification);
Returns the shopper's id, whether Linque already knew the shopper (existing), and cards:
| Field | Type | Description |
|---|---|---|
id | string | Identifier for the discovered card |
name | string | Card-product name when known, otherwise the issuer-provided name |
network, issuer, art_url, last4 | string | null | For display. Fall back to a brand icon when art_url is null |
requires_security_code | boolean | Whether the card requires a security code to be verified |
security_code_length | number | null | Number of digits to collect, or null when the card needs no code |
The wallet fills itself from the same instance, so you never pass the discovered cards back in. cards is there if you want to show a count, log them, or render your own list. An empty array is a valid empty wallet, not an error.
getCard
const linqueCard = await bt.linque.getCard(wallet);
const token = linqueCard.token ?? await bt.tokens.create({ type: 'card', data: linqueCard });
Returns references to the card the shopper selected: number, expiration_month, expiration_year and cvc. For a discovered card it also returns token, the card token created in your tenant when the card was verified. The card is already tokenized; use token and do not tokenize it again. A manually entered card has no token, so tokenize its references with tokens.create. The references work anywhere you would use another element. For a discovered card, what leaves the browser depends on the service:
| Call | What leaves the browser | Needs |
|---|---|---|
tokens.create, tokenize | Nothing. The token created on verification is returned instead of a second token. Creation-only options are rejected; set them with token:update | |
proxy.* | Detokenization expressions for the token, resolved by the Proxy | A pre-configured proxy with require_auth off, or a session apiKey with token:use on that token |
A manually entered card behaves as any card element does. Place the expiry references under expiration_month and expiration_year. The SDK uses those keys to tell month from year and rejects any other.
getCard rejects with a validation error while the wallet is incomplete. It rejects with an ElementError if the wallet is not mounted, or is unmounted before the card comes back.
verification
The verification code is six digits because that is what Linque sends. The element renders six boxes with inputmode="numeric" and autocomplete="one-time-code", so supported devices can offer the SMS code automatically. It takes no options.
- Web Elements
- React Elements
const verification = bt.createElement('verification');
await verification.mount('#verification');
verification.on('change', ({ detail }) => (continueButton.disabled = !detail.complete));
verification.on('resend', () => bt.linque.resend());
verification.on('error', ({ detail }) => console.warn(detail.code));
import { VerificationElement } from '@basis-theory/react-elements';
<VerificationElement
ref={verificationRef}
onChange={(event) => setCanContinue(event.detail.complete)}
onResend={() => bt.linque.resend()}
onError={(event) => console.warn(event.detail.code)}
/>
| Event | event.detail |
|---|---|
change | { complete }. complete becomes true once a full code is present. The code itself is never emitted |
resend | {}. The shopper requested another code |
error | { code, message } with the error code, before verify rejects |
ready, focus, blur | Standard |
The element does not call the resend API itself. Call bt.linque.resend() from the resend event. The code is never emitted from the element. verify(verification) reads it internally, so your page does not need to handle it.
The element tells the shopper what happened: that a code was sent, that none was sent to this number, how long to wait after too many sends, and why a submission failed. An incorrect or expired code clears the field. An accepted code is cleared too, so a shopper who returns to the field does not resubmit a used code. clear() empties the field. focus() places the cursor inside it.
wallet
Renders the discovered cards, the security code field for the selected card, and a New card row. Each card shows its artwork or a brand icon, its name, and its last four digits. It takes no options.
Before verification, and whenever discovery returns nothing, the wallet renders card entry only. That covers a first-time shopper, a shopper with no discovered cards, and a shopper who skips or does not complete discovery. You do not need a separate manual-entry form.
Selecting a card opens its security code field inside that card's row. The field is sized from the card's security_code_length, and is absent when that is null. Do not derive the required number of digits yourself. Linque already knows what the selected card requires.
The rows form a radio group. Tab enters and leaves the group, and the arrow keys move within it.
- Web Elements
- React Elements
const wallet = bt.createElement('wallet');
await wallet.mount('#wallet');
wallet.on('change', ({ detail }) => {
payButton.disabled = !detail.complete;
summary.textContent = detail.card ? `${detail.card.name} •••• ${detail.card.last4}` : 'New card';
});
import { WalletElement } from '@basis-theory/react-elements';
<WalletElement
ref={walletRef}
onChange={(event) => setCanPay(event.detail.complete)}
onError={(event) => console.warn(event.detail.code)}
/>
| Event | event.detail |
|---|---|
change | { card, complete }. card is { id, name, network, last4, issuer }, or null for a manually entered card. complete becomes true once getCard has everything it needs |
error | { code, message } with the error code, before getCard rejects |
ready, focus, blur | Standard |
Use complete to enable your Pay button. Calling getCard before the wallet is complete returns a validation error.
While a card is being verified, the selected row shows a spinner. On failure the wallet recovers in that row. An incorrect code clears the field and shows the attempts remaining. A card whose attempts are exhausted, or that cannot be verified, is disabled. The other rows and the New card row stay available.
wallet.clear() discards the selection, anything entered, and any verified card with its token. References the page still holds then fail validation on send. bt.linque.start() does the same for a new shopper.
Height
A wallet's height changes as cards arrive and rows open. Both Linque elements report their content height, and the SDK sizes the iframe to it on mount and on every change. To keep a fixed box instead, give the container a height and overflow: auto. The iframe still grows and the container scrolls it.
Design tokens
Both Linque elements render inside Basis Theory-controlled iframes and inherit the standard Elements theme you pass to BasisTheory(). The inputs inside the wallet use the same primitives as cardNumber, expiry, and cvv, so they inherit your existing theme.
The card list can be customized through theme.linque, injected into the iframe as --bt-linque-* custom properties.
const bt = await BasisTheory('<PUBLIC_API_KEY>', {
linque: true,
theme: {
linque: {
row: {
gap: '10px',
padding: '12px 16px',
radius: '12px',
background: { default: 'transparent', hover: '#f7f7fa', selected: '#f2f0ff' },
border: { default: '#e6e8ee', selected: '#4f39e6' },
},
art: { width: '42px', radius: '4px' },
},
},
});
| Token | Controls | Default |
|---|---|---|
linque.row.gap | Space between cards | spacing.sm |
linque.row.padding | Padding inside a card row | spacing.md |
linque.row.radius | Card-row corner radius | borders.radius.md |
linque.row.background.default | Default row background | colors.background.default |
linque.row.background.hover | Row background on hover | Same as default |
linque.row.background.selected | Selected-card background | Same as default |
linque.row.border.default | Default row border | colors.border.default |
linque.row.border.selected | Selected-card border | colors.primary |
linque.art.width | Card-art width. The height follows the card ratio | 42px |
linque.art.radius | Card-art corner radius | borders.radius.sm |
Every value falls back to the base theme, so you do not need to configure theme.linque for the wallet to match the rest of your checkout. Use these tokens only when your checkout needs more specific styling for saved-card rows.
The selected card is indicated through its background and border rather than an additional badge or checkmark. The New card row uses the same row styling. The element's copy is English only.
Errors
Every Linque failure is a Problem Details body whose type is a stable slug. Each slug maps to one ErrorCode. The element's error event and the rejected promise both carry it. The rejection also carries status and the problem body as body, including errors.attempts_remaining and errors.retry_after where the API sent them.
API type | Status | ErrorCode |
|---|---|---|
verification_code_invalid | 403 | VERIFICATION_CODE_INVALID |
verification_code_expired | 410 | VERIFICATION_CODE_EXPIRED |
verification_rate_limited | 429 | VERIFICATION_RATE_LIMITED |
security_code_invalid | 403 | SECURITY_CODE_INVALID |
security_code_attempts_exhausted | 429 | SECURITY_CODE_ATTEMPTS_EXHAUSTED |
credential_unavailable | 409 | CREDENTIAL_UNAVAILABLE |
feature_unavailable | 403 | FEATURE_UNAVAILABLE |
idempotency_key_in_use | 409 | IDEMPOTENCY_KEY_IN_USE |
invalid_request, invalid_phone_number | 400 | VALIDATION_ERROR |
unauthorized on a card verification | 401 | SESSION_NOT_FOUND. The session key expired; verify the phone again |
unauthorized, forbidden, not_found, upstream_unavailable | 401, 403, 404, 503 | API_ERROR |
| No response | NETWORK_ERROR |
The SDK never retries a call that sends a code, submits one, or verifies a card. A retried send is another refusal the shopper waits through. A retried submit can spend an attempt twice.