> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clinkbill.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Vault SDK

> Let an Agent pay through the merchant’s own PSP while the real card stays inside Clink’s PCI boundary.

The Agent Vault SDK lets an Agent pay on a merchant's checkout page without ever handling the user's real card. The Agent fills a public placeholder card. The SDK intercepts the page's request to the merchant's payment service provider (PSP), and Clink Vault swaps in the real card inside Clink's PCI boundary before calling the PSP.

Handing a real card number to an LLM or a script creates serious compliance and leakage risks. With the SDK, the Agent keeps control of the page, while card data and authorization checks stay with Clink. For every method and option, see the [SDK reference](https://docs.clinkbill.com/guides/agent/vault_sdk/api_reference.md).

## How it works

```mermaid actions={false} theme={null}
%%{init: {"theme":"base","themeVariables":{"fontFamily":"inherit","actorBkg":"#F7F7F7","actorBorder":"#E7E7E7","actorTextColor":"#161616","actorLineColor":"#E7E7E7","signalColor":"#71717A","signalTextColor":"#3F3F46","noteBkgColor":"#F8E1CC","noteBorderColor":"#F8E1CC","noteTextColor":"#161616"},"sequence":{"width":104,"height":44,"actorMargin":40,"diagramMarginX":12,"diagramMarginY":12,"messageMargin":28,"noteMargin":14,"boxMargin":12,"wrap":false,"mirrorActors":false,"actorFontSize":14,"messageFontSize":14,"noteFontSize":14}}}%%
sequenceDiagram
  participant A as Agent
  participant P as Checkout page
  participant S as Vault SDK
  participant V as Clink Vault
  participant M as Merchant PSP
  A->>P: Fill placeholder
  P->>S: Tokenize request
  Note over S: Match rule, pause
  S->>V: Request template
  Note over V: Swap in real card
  V->>M: Call PSP
  M-->>V: PSP response
  V-->>S: Relay response
  S-->>P: Replay to page
```

1. **Intercept.** The SDK attaches to the browser your Agent drives (Playwright or raw CDP) and intercepts requests from the page and every cross-origin iframe.
2. **Placeholder card.** The SDK returns a public test card of the **same brand** as the user's real card, such as Visa `4111 1111 1111 1111` with the real card's expiry. The Agent fills it into the checkout form.
3. **Match.** When the page sends a tokenization request to the PSP, the SDK matches it against reviewed PSP rules (host, path, and fields). It pauses the request only if the card fields equal the placeholder card issued for this checkout.
4. **Relay.** The SDK sends only the request template, a placeholder marker, the `card_token`, and allowlisted request headers to Clink Vault.
5. **Substitute.** Clink Vault checks the 30-minute payment window and the PSP rule, replaces the placeholder with the real card inside the PCI boundary, and calls the PSP directly.
6. **Replay.** The SDK replays the PSP's original status code, headers, and body to the page, which continues the order as with any normal payment.

> **Visible to the Agent and browser**
>
> * Card brand and last four digits
> * A placeholder test card of the same brand
> * Card setup links and payment status
> * The PSP's native response to the page

> **Only inside Clink's PCI boundary**
>
> * The real card number (PAN) and CVV, stored with KMS encryption
> * Checks on the payment window and PSP rules
> * The request sent to the PSP with the real card
> * Card ciphertext for client-side encryption PSPs such as Adyen

> [!WARNING]
>
> Payments go through the merchant's own PSP; Clink sees only the PSP's HTTP response. A 2xx response from the tokenization endpoint means the card was tokenized, not that the order was charged. Use the order result on the merchant page as the final outcome and report it with `reconcile()`.

## Get started

Run the SDK on your application server or in a trusted checkout worker, never in the user's browser.

| Requirement | Details |
| :- | :- |
| Runtime | Node.js `>=20.19`, ESM only |
| Browser driver | Playwright `>=1.40` (peer dependency). Not needed if you use raw CDP only. |
| Credentials | `client_id` and `client_secret` issued by Clink (OAuth2 `client_credentials`). Keep them on the server. |
| Environment | `sandbox` (default) → `uat-api.clinkbill.com`; `production` → `api.clinkbill.com` |

**1. Install the SDK**

```bash theme={null}
npm install @clink-ai/agent-vault playwright
```

**2. Attach to the checkout page**

Create a client, then attach it to the page your Agent drives. Pass `email` so the SDK can start card setup when the user has no usable card.

```typescript theme={null}
import { ClinkVault } from '@clink-ai/agent-vault';

const vault = new ClinkVault({
  clientId: process.env.CLINK_CLIENT_ID!,
  clientSecret: process.env.CLINK_CLIENT_SECRET!,
  environment: 'sandbox',
});

const checkout = await vault.attach(page, {
  // Returning user: pass the saved IDs. First use: pass email only.
  customerId,
  cardToken,
  email,
  // Deliver links and QR codes yourself, not through the model
  onUserAction: (action) => {
    if (action.url) notifyUser(action.type, action.url);
    if (action.qr_image_base64) showQrCode(action.qr_image_base64);
  },
  // Save customerId and cardToken once card setup completes
  onCardBound: (card) => saveCard(card),
  // Read the order result from the merchant page
  resolveMerchantResult: () => readOrderResult(page),
});
```

`vault.attach(page)` works with Playwright, locally or on a cloud browser through `connectOverCDP`, and covers cross-origin iframes. Without Playwright, such as over a cloud browser WebSocket, use `vault.attachToCdp(cdp, sessionId, options)`. Both return the same `CheckoutController`.

**3. Call pay() until it returns a final status**

Each `pay()` call returns the next thing the Agent should do in `next_action`. Perform it, then call `pay()` again. `fillCard` and `clickPayButton` are your own page functions; see [Code examples](#code-examples).

```typescript theme={null}
let step = await checkout.pay();
while (step.status === 'requires_action') {
  const action = step.next_action!;
  if (action.type === 'fill_card') {
    // Placeholder card only, never the real card
    await fillCard(page, action.card);
  } else if (action.type === 'submit') {
    await clickPayButton(page);
  } else if (action.type === 'select_method') {
    await selectPaymentMethod(page, action.method);
  } else if (action.type === 'wait_user') {
    // pay_qr: onUserAction already sent the QR code to the user.
    // 3ds or redirect: the user completes it in the browser.
    // In either case, stop here and reconcile in step 4.
    if (action.reason !== 'bind_card') break;
    // Card setup: wait for the user to finish the link
    await waitForUser(action.expires_at);
  }
  step = await checkout.pay();
}
```

| `next_action.type` | What the Agent does |
| :- | :- |
| `fill_card` | Fill `card` into the checkout form. It is the placeholder card, issued while the payment window is open. |
| `submit` | Click the page's pay button. The SDK waits 30 seconds by default for the PSP request. |
| `select_method` | Select the payment method named in `method`. Used for QR code payments, and for falling back to a card after a QR payment was confirmed unpaid. |
| `wait_user` | The user needs to act. See [When the user needs to act](#when-the-user-needs-to-act). |

**4. Reconcile and detach**

```typescript theme={null}
// declined, failed, unsupported: no charge, nothing to report
if (!['declined', 'failed', 'unsupported'].includes(step.status)) {
  // Wait for the user to scan or verify and the page to show a result
  await waitForOrderResult(page, 10 * 60_000);
  const state = await checkout.reconcile();
}
await checkout.detach();
```

`waitForOrderResult` is your own page function: it polls `readOrderResult` until the page shows a result or the timeout passes. See `card-form.ts` in [Code examples](#code-examples). This wait covers the user scanning a QR code or completing 3DS or a redirect after the loop stops at `wait_user`, and the order page loading after `succeeded`.

`reconcile()` reads the merchant's order result through `resolveMerchantResult` and reports it to Clink Vault. Always call `detach()` when checkout ends.

| `resolveMerchantResult` returns | `state.status` |
| :- | :- |
| `completed` | `completed` |
| `failed` | `failed` |
| `pending` | Unchanged; the checkout stays open. For a card payment, this is `awaiting_merchant`. |
| `requires_user_action` | `requires_user_action` |
| `unknown`, or the resolver throws | `outcome_unknown`. Also returned if the result cannot be reported to Clink Vault. |

If no card request reached the PSP, or `resolveMerchantResult` is not set, `reconcile()` returns the current state unchanged. See [Handle the result](#handle-the-result).

Before paying, you can call `checkout.getPaymentMethods()` to see which methods the page supports. A card's status is `ready`, `needs_bind`, or `unsupported`. QR wallets and PayPal on the page are `needs_user`: ask the user first, then start them with `pay({ method })`. See [PSP coverage](#psp-coverage).

## When the user needs to act

Vault does not use passkeys, SMS codes, or per-payment confirmation links. The user opens a **card setup link** once: they add a new card, or choose an existing card and re-enter its CVV. This opens a 30-minute payment window, during which the Agent completes `fill_card` → `submit` without the user. Clink hosts the card setup page; it requires no sign-in and can be used once.

```mermaid actions={false} theme={null}
%%{init: {"theme":"base","themeVariables":{"fontFamily":"inherit","actorBkg":"#F7F7F7","actorBorder":"#E7E7E7","actorTextColor":"#161616","actorLineColor":"#E7E7E7","signalColor":"#71717A","signalTextColor":"#3F3F46","noteBkgColor":"#F8E1CC","noteBorderColor":"#F8E1CC","noteTextColor":"#161616"},"sequence":{"width":116,"height":44,"actorMargin":64,"diagramMarginX":12,"diagramMarginY":12,"messageMargin":28,"noteMargin":14,"boxMargin":12,"wrap":false,"mirrorActors":false,"actorFontSize":14,"messageFontSize":14,"noteFontSize":14}}}%%
sequenceDiagram
  participant U as User
  participant A as Agent + SDK
  participant V as Clink Vault
  A->>V: pay()
  V-->>A: wait_user (bind_card)
  A-->>U: Card setup link
  U->>V: Add card or re-enter CVV
  Note over V: 30-minute window opens
  A->>V: pay()
  V-->>A: Setup completed
  Note over A: onCardBound, then fill_card
```

When `next_action.type` is `wait_user`, its `reason` tells you what the user needs to do. The same action is also passed to `onUserAction`.

| `reason` | When | What you do |
| :- | :- | :- |
| `bind_card` | No usable card: none is set up, or the payment window has expired. Requires `email` in `attach`. | Send the link to the user, then keep calling `pay()`. Once setup completes, the SDK calls `onCardBound` and the same `pay()` returns `fill_card`. Save `customerId` and `cardToken` from `onCardBound` and pass them next time. |
| `pay_qr` | The page shows a QR code. | Show the PNG in `qr_image_base64` to the user directly from your host, not through the model. Then call `reconcile()`. |
| `3ds` | The PSP requires cardholder verification. | The PSP's own script starts verification on the page. Make the browser visible to the user, through a local window or a cloud browser live view, then call `reconcile()`. The SDK does not complete verification. |
| `redirect` | Card payment: the PSP sends the user to another page to verify. PayPal: `url` is the paypal.com approval link. | Card payment: handle it like `3ds`. There is no `url`; the user completes it in the browser, then call `reconcile()`. PayPal: give the link to the user; the SDK's part ends here. |

> [!NOTE]
>
> **Notifications.** Clink does not send the user SMS, email, or payment notifications. Your code delivers the card setup link through your own UI, messages, or notification channels. Do not let an LLM compose or rewrite the link.

You can also start card setup without waiting for `pay()`: call `checkout.bind()` during checkout, or `vault.sessions.create({ email })` outside checkout. New cards and CVV re-entry use the same link. Call `vault.cards.list({ customerId })` to see a user's cards and when their windows expire.

## Handle the result

| Status | Meaning | How to handle it |
| :- | :- | :- |
| `succeeded` | The PSP authorized the payment. | Wait for the order result on the page, then call `reconcile()`. The payment is complete only when the order confirmation is visible. |
| `declined` | The PSP explicitly declined, and the decline response was replayed to the page. | Tell the user. They can switch cards or try again. |
| `outcome_unknown` | Whether the card was charged cannot be determined, for example after a timeout or network error. | **Do not retry automatically.** Call `reconcile()` or verify manually first. |
| `unsupported` | The page's PSP, payment method, or card brand is not covered. | Use another payment method, or hand the payment to the user. |
| `failed` | A definite failure. No charge occurred. | Handle it according to `reason`, listed below. |

Common `reason` values with `failed`:

* `card_not_bound`: No usable card exists, and no `email` was passed, so the SDK cannot start card setup.
* `window_expired`: The 30-minute payment window has expired, and no `email` was passed. With `email`, `pay()` returns `wait_user` (`bind_card`) instead, and the user selects this card and re-enters its CVV.
* `card_frozen` or `token_revoked`: The card is frozen, or its card token has been revoked. Ask the user to set up another card with `checkout.bind()`.
* `placeholder_mismatch`: The page request contained a card number or CVV other than the placeholder card. The SDK refused to let it through.
* `checkout_request_not_seen`: No PSP request was intercepted within 30 seconds of `submit`. Check that the pay button was actually clicked.
* `session_expired`: The card setup session failed or was locked. Call `retry()`, then `pay()` to generate a new link. A link that has only expired does not cause this error; `pay()` generates a new link automatically.
* `card_token_missing` or `card_token_not_found`: Card setup completed, but the session carried no card token, or the token is not in the card list. The SDK does not start card setup again; investigate the server-side data.
* `merchant_declined`: The PSP authorized the payment, but `resolveMerchantResult` reported `failed`, and `reconcile()` recorded it. Call `retry()` to try again.
* `paypal_redirect_url_not_found`: No PayPal approval link was found within 30 seconds (`submitTimeoutMs`) of clicking pay with PayPal.
* `cancelled`: `cancel()` or `detach()` was called before the payment had a result.
* `api_error` or `network_error`: A call to Clink Vault was rejected or could not reach the API, for example because the client credentials are invalid. Check the credentials and network, then try again.

With `unsupported`:

* `rule_not_matched` or `psp_unsupported`: The page's PSP request matched no enabled rule. See [PSP coverage](#psp-coverage).
* `unsupported_card_brand`: The user's card brand has no placeholder card.
* `card_encrypted`: The page encrypted the card fields and sent them to an endpoint no rule covers, such as the merchant's own server.
* `cse_request_unsupported`: An Adyen request the SDK does not relay, such as saving the card or paying with a stored card.

With `outcome_unknown`:

* `pay_qr_unavailable`: No usable QR code was found after submission. The merchant may have accepted the order, so do not switch to a card automatically.
* `psp_timeout`, `missing_response`, or `merchant_result_unknown`: The PSP did not answer in time, Clink Vault returned no PSP response, or `resolveMerchantResult` returned `unknown` or threw. Verify the order before taking any other action.

## PSP coverage

The SDK syncs matching rules from the server during `attach`, so supporting a new standard PSP requires a rule update rather than a new SDK release. If rule sync fails because of a network error, timeout, 429, or 5xx, or returns a malformed response, the SDK falls back to its built-in rules. Any other error from Clink, such as 401 or 403, does not fall back: `attach` still succeeds but loads no rules, so a card request on the page returns `unsupported` (`rule_not_matched`). With invalid client credentials, `pay()` fails earlier with `failed` (`api_error`).

The SDK covers one-time payments with the placeholder card typed into the page. Leave any option on the merchant page to save the card turned off. If the page pays with a card the merchant already stores, that request carries no placeholder and does not go through Clink Vault.

* **Token mode.** The page sends the placeholder card in plain text, and Clink Vault substitutes the real card. Covers 18 PSPs with 80 built-in rules: Stripe, Checkout.com, Airwallex, Rapyd, Global Payments, Square, Authorize.net, Nuvei, EBANX, Mercado Pago, PayU, Razorpay, Mollie, Paysafe, Recurly, Bambora, Spreedly, and Shopify.
* **CSE mode.** For Adyen Sessions payments (`checkoutshopper-*.adyen.com`), enabled. The SDK recognizes adyen-web JWE ciphertext and legacy securedFields 3.x `adyenjs_0_1_*` ciphertext. The server encrypts the real card with the Adyen public key and sends the request, so neither the page nor the Agent sees the ciphertext.
* **Not yet supported.** Braintree, Worldpay, dLocal, Stripe Checkout Session confirm, Hosted Form, and merchant self-hosted Adyen. The SDK returns `unsupported`, and the encrypted placeholder card is not sent.

Placeholder cards are available for Visa, Mastercard, American Express, Discover, JCB, and UnionPay. For other card brands, `pay()` returns `unsupported` (`unsupported_card_brand`).

Besides cards, the SDK supports two payment methods that are completed **entirely in the browser**. They do not use Clink Vault authorization and send no card token. The page must already offer the method; the SDK does not create orders or generate QR codes.

* **QR code payment** (`paynow`, `wechat_pay`, `upi_qr`, `pix`, `bcmc_mobile`). The SDK extracts that wallet's own QR code from the page or an iframe, skipping logos and other wallets' codes, and returns it as a PNG. For PayNow, the expiry is read from the QR code; other methods default to 10 minutes. Use `reconcile()` for the payment result.
* **PayPal link** (`paypal`). After the Agent selects PayPal and clicks pay, call `pay({ method: 'paypal' })` once to get the paypal.com approval link. What the user does on PayPal afterwards is outside the SDK.

`fallback: 'card'` applies only to QR code payments. The SDK falls back to a card only when `resolveMerchantResult` explicitly returns `{ status: 'failed', payment_confirmed_unpaid: true }`. An unknown or pending result, or a QR code that expired locally, never triggers an automatic fallback.

## Code examples

These examples run on your server. Merchant checkout pages differ, so adapt the selectors and page checks to the checkout your Agent automates.

### Complete checkout

`checkout.ts` runs one card payment from attach to reconcile. `card-form.ts` fills the placeholder card, clicks pay, and reads the merchant's order result. `loadVaultCard`, `saveVaultCard`, `notifyUser`, and `showQrCode` are your application's own functions.

```typescript checkout.ts theme={null}
import { chromium } from 'playwright';
import { ClinkVault, type CheckoutState } from '@clink-ai/agent-vault';
import {
  clickPayButton,
  fillCard,
  readOrderResult,
  waitForOrderResult,
} from './card-form.js';
import {
  loadVaultCard,
  notifyUser,
  saveVaultCard,
  showQrCode,
} from './app.js';

const vault = new ClinkVault({
  clientId: process.env.CLINK_CLIENT_ID!,
  clientSecret: process.env.CLINK_CLIENT_SECRET!,
  environment: 'sandbox',
});

export async function payOnCheckout(
  user: { id: string; email: string },
  checkoutUrl: string,
): Promise<CheckoutState> {
  // A visible browser lets the user complete 3DS if the PSP asks for it
  const browser = await chromium.launch({ headless: false });
  const page = await browser.newPage();
  const saved = await loadVaultCard(user.id);

  const checkout = await vault.attach(page, {
    customerId: saved?.customerId,
    cardToken: saved?.cardToken,
    email: user.email,
    // Deliver links and QR codes yourself, not through the model
    onUserAction: async (action) => {
      if (action.url) await notifyUser(user.id, action.type, action.url);
      if (action.qr_image_base64) {
        await showQrCode(user.id, action.qr_image_base64);
      }
    },
    onCardBound: (card) => saveVaultCard(user.id, card),
    resolveMerchantResult: () => readOrderResult(page),
  });

  try {
    await page.goto(checkoutUrl);
    // Your Agent fills contact and shipping details here

    const setupDeadline = Date.now() + 15 * 60_000;
    let step = await checkout.pay();
    while (step.status === 'requires_action') {
      const action = step.next_action!;
      if (action.type === 'fill_card') {
        await fillCard(page, action.card);
      } else if (action.type === 'submit') {
        await clickPayButton(page);
      } else if (action.type === 'wait_user') {
        // QR, 3DS, or a redirect: the user completes it, then reconcile
        if (action.reason !== 'bind_card') break;
        // Card setup: poll until the user finishes the link
        if (Date.now() > setupDeadline) {
          await checkout.cancel();
          return checkout.getState();
        }
        await page.waitForTimeout(2_000);
      }
      step = await checkout.pay();
    }

    // No charge occurred
    if (['declined', 'failed', 'unsupported'].includes(step.status)) {
      return checkout.getState();
    }

    // succeeded, outcome_unknown, or the user is verifying
    await waitForOrderResult(page, 10 * 60_000);
    return await checkout.reconcile();
  } finally {
    await checkout.detach();
    await browser.close();
  }
}
```

```typescript card-form.ts theme={null}
import type { Page } from 'playwright';
import type { FillCard, MerchantResult } from '@clink-ai/agent-vault';

// Example selectors. Match them to the merchant's checkout page.
const FIELDS = {
  number: ['input[autocomplete="cc-number"]', 'input[name="cardnumber"]'],
  expiry: ['input[autocomplete="cc-exp"]', 'input[name="exp-date"]'],
  cvc: ['input[autocomplete="cc-csc"]', 'input[name="cvc"]'],
};

// Card fields usually sit inside PSP iframes, so search every frame
export async function fillCard(page: Page, card: FillCard): Promise<void> {
  await typeInto(page, FIELDS.number, card.number);
  const expiry = `${card.exp_month}/${card.exp_year.slice(-2)}`;
  await typeInto(page, FIELDS.expiry, expiry);
  await typeInto(page, FIELDS.cvc, card.cvc);
}

async function typeInto(
  page: Page,
  selectors: string[],
  value: string,
): Promise<void> {
  for (const frame of page.frames()) {
    for (const selector of selectors) {
      const input = frame.locator(selector).first();
      if (await input.isVisible().catch(() => false)) {
        await input.fill('');
        // Type key by key; PSP fields often ignore a direct fill()
        await input.pressSequentially(value, { delay: 30 });
        return;
      }
    }
  }
  throw new Error(`Card field not found: ${selectors[0]}`);
}

export async function clickPayButton(page: Page): Promise<void> {
  const name = /pay now|place order|complete order/i;
  await page.getByRole('button', { name }).first().click();
}

// Called by reconcile(). Report only what the merchant page shows.
export async function readOrderResult(page: Page): Promise<MerchantResult> {
  const confirmed = page.getByText(/thank you for your order/i);
  if (await confirmed.first().isVisible()) {
    const orderId = await page.locator('.order-number').first()
      .textContent({ timeout: 1_000 })
      .catch(() => null);
    return orderId
      ? { status: 'completed', order_id: orderId.trim() }
      : { status: 'completed' };
  }
  const declined = page.getByText(/payment (declined|failed)/i);
  if (await declined.first().isVisible()) {
    return { status: 'failed' };
  }
  return { status: 'unknown' };
}

export async function waitForOrderResult(
  page: Page,
  timeoutMs: number,
): Promise<void> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if ((await readOrderResult(page)).status !== 'unknown') return;
    await page.waitForTimeout(2_000);
  }
}
```

### Expose payment steps as Agent tools

When an LLM decides each step, give it tools that advance the payment rather than card values. The model sees only the status and the next step. Your code fills the card, and `resolveMerchantResult` reads the order result from the page, so the model cannot report a success that the merchant did not show.

```typescript agent-tools.ts theme={null}
import type { Page } from 'playwright';
import type { CheckoutController, PayStep } from '@clink-ai/agent-vault';
import { clickPayButton, fillCard } from './card-form.js';

// Register these handlers as tools in your Agent framework
export function createPaymentTools(
  page: Page,
  checkout: CheckoutController,
) {
  let step: PayStep | undefined;

  return {
    async start_payment() {
      step = await checkout.pay();
      return forModel(step);
    },
    async fill_placeholder_card() {
      if (step?.next_action?.type !== 'fill_card') {
        return { error: 'Call start_payment first' };
      }
      await fillCard(page, step.next_action.card);
      step = await checkout.pay();
      return forModel(step);
    },
    async submit_payment() {
      if (step?.next_action?.type !== 'submit') {
        return { error: 'Fill the card first' };
      }
      await clickPayButton(page);
      step = await checkout.pay();
      return forModel(step);
    },
    // resolveMerchantResult reads the page; the model cannot claim success
    async reconcile_checkout() {
      return checkout.reconcile();
    },
  };
}

// Remove card values and QR images before a step reaches the model
function forModel(step: PayStep) {
  const { qr_image_base64: _image, qr_data: _data, ...rest } = step;
  const action = rest.next_action;
  if (action?.type === 'fill_card') {
    return { ...rest, next_action: { type: 'fill_card' } };
  }
  if (action?.type === 'wait_user') {
    const { qr_image_base64: _img, qr_data: _qr, ...next } = action;
    return { ...rest, next_action: next };
  }
  return rest;
}
```

### Use a cloud browser

The SDK does not launch or manage a browser. For a cloud browser, attach to the page your Agent already drives, then run the same `pay()` loop.

```typescript Playwright (connectOverCDP) theme={null}
import { chromium } from 'playwright';

// The cloud browser's CDP endpoint, such as a Browserbase connectUrl
const browser = await chromium.connectOverCDP(connectUrl);
const context = browser.contexts()[0] ?? (await browser.newContext());
const page = context.pages()[0] ?? (await context.newPage());

const checkout = await vault.attach(page, { customerId, cardToken, email });
await page.goto(checkoutUrl);
```

```typescript Raw CDP theme={null}
// cdp: a flatten-mode CDP connection that implements send() and on/off
const { targetId } = (await cdp.send('Target.createTarget', {
  url: checkoutUrl,
})) as { targetId: string };
const { sessionId } = (await cdp.send('Target.attachToTarget', {
  targetId,
  flatten: true,
})) as { sessionId: string };

const checkout = await vault.attachToCdp(cdp, sessionId, {
  customerId,
  cardToken,
  email,
});
```

Keep the cloud browser's API key and your Clink client secret on the server; do not pass them to the Agent. For 3DS, show the user the cloud browser's live view so they can complete verification.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.