> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.paymentkit.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.paymentkit.com/_mcp/server.

# Stripe Link

Stripe Link is Stripe's accelerated-checkout network. A shopper saves their payment details with Link once, and on any later Link-enabled checkout Stripe recognizes them and lets them pay in one click.

Link appears to a shopper in one of two ways:

* **Express button** — the shopper already has an authenticated Link session in their browser. A "Pay with Link" button renders alongside your Apple Pay and Google Pay buttons.
* **Tile** — the shopper is not recognized. Link is offered as a selectable payment method, and choosing it launches Stripe's Link sign-in.

Both paths mount the same Link element and complete through the same `onLinkResult` callback — the tile is a different placement, not a different flow.

```mermaid
flowchart TD
    A[Checkout loads] --> B[initStripeLink]
    B --> C{Link available<br />on this processor?}
    C -->|No| D[Nothing renders]
    C -->|Yes| S{Stripe.js loaded?}
    S -->|No| D
    S -->|Yes| R[onLinkReady true]
    R --> E{Authenticated<br />Link session?}
    E -->|Yes| F[onLinkAuthChange true<br />mount into express slot]
    E -->|No| G[onLinkAuthChange false<br />mount into your Link tile]
    F --> H[Shopper confirms in Link]
    G --> H
    H --> I[onLinkResult]
```

# Prerequisites

Link is **Stripe-only**. It cannot be fulfilled by Airwallex, Authorize.net, or any other processor, and a saved Link payment method cannot be re-routed to another processor.

Enable Link on your Stripe processor before integrating:

1. Go to **Orchestration > Payment processors** and open your Stripe processor.
2. Under **Accept digital wallet payments**, turn on **Stripe Link**.
3. Click **Save Processor**.

The toggle appears only on Stripe processors. Processor configuration is dashboard-only — it is not part of the public API. Unlike Apple Pay and Google Pay, Link does not require registering a custom domain. See [Stripe](/guides/payment-orchestration/processors/stripe) for the full processor setup.

Add Stripe.js to your checkout page:

```html
<script src="https://js.stripe.com/v3/"></script>
```

PaymentKit collects card details through VGS, not Stripe Elements. The Link element mounts into its own container beside your card form — the two coexist on the same page.

# Setup

#### CDN

```html
<script src="https://unpkg.com/@payment-kit-js/vanilla/dist/cdn/paymentkit.min.js"></script>
<script>
  const paymentKit = PaymentKit.default({
    environment: 'production',
    secureToken: 'your_secure_token',
    paymentMethods: [PaymentKit.PaymentMethods.stripeLink]
  });
</script>
```

#### NPM/ES Modules

```typescript
import PaymentKit from '@payment-kit-js/vanilla';
import StripeLinkPaymentMethod from '@payment-kit-js/vanilla/payment-methods/stripe-link';

const paymentKit = PaymentKit({
  environment: 'production',
  secureToken: 'your_secure_token',
  paymentMethods: [StripeLinkPaymentMethod]
});
```

# Initialize Link

Unlike Apple Pay and Google Pay — where you render a button that calls `paymentKit.submit()` — **Link is the button**. Stripe renders the express button and fires its own confirm event, so the integration is callback-driven.

Register your callbacks first, then call `initStripeLink`:

```typescript
paymentKit.stripe_link.onLinkResult((result) => {
  if (result.errors) {
    // Surface the error or fall back to the card form
    return;
  }
  window.location.href = '/success';
});

paymentKit.stripe_link.onLinkAuthChange((showExpress) => {
  expressContainer.hidden = !showExpress;
  linkTile.hidden = showExpress;
});

await paymentKit.stripe_link.initStripeLink({
  processorId: 'proc_prod_a1b2c3d4e5f6g7h8',
  customerInfo: {
    first_name: 'Jane',
    last_name: 'Smith'
  }
});
```

`initStripeLink` fetches the checkout's Link configuration, sets up the Link element, and starts silent session detection. Mounting the visible button is a separate step — see [Mount the express button](#mount-the-express-button). If Link is not available for the checkout — no Stripe processor, Link disabled, or Stripe.js missing — it stays quiet and no button renders.

The ordering above matters: `onLinkReady` and `onLinkAuthChange` both fire before the returned promise resolves, and neither emission is replayed for a callback registered later.

`paymentKit.submit()` does not work for Link. Calling it with `paymentMethod: 'stripe_link'` returns a directive error pointing you back to `initStripeLink()` and `mountLinkButton()`.

# Mount the express button

Mount the button into an empty container that Link owns — the SDK appends its own child node inside the selector you pass, and only removes that node again on re-mount. Never point it at your VGS card fields.

Call this only after `initStripeLink` has resolved. Before that the express element does not exist yet and the call fails with `Express element not set up`.

```typescript
const { success, error } = paymentKit.stripe_link.mountLinkButton('#link-express-button');
if (!success) {
  console.error('[Link] mount failed:', error);
}
```

```html
<div id="link-express-button"></div>
```

`mountLinkButton` reports failure through its return value rather than throwing, so check `success`. Mounting is idempotent — calling it again replaces the button rather than stacking a second one.

# React to session state

Three callbacks cover the Link lifecycle — two drive what you render, one delivers the result. Register as many handlers per callback as you need; none are overwritten.

| Callback           | Fires with                                                                                                                               |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `onLinkReady`      | `true` once the express element is set up and ready to mount                                                                             |
| `onLinkAuthChange` | `showExpress` — `true` to render the express button, `false` to fall back to the Link tile. Only meaningful once `onLinkReady` has fired |
| `onLinkResult`     | The final result after the shopper confirms with Link                                                                                    |

`onLinkAuthChange` reflects whether the shopper has an authenticated Link session. Whenever Link is advertised for the checkout, exactly one of the express button or the tile applies: the tile is the fallback when no session is recognized. If Link is not advertised at all, neither renders — `showExpress` is `false` and both containers should stay hidden.

Link uses its own isolated Stripe element, so it does not suppress your other express buttons. A recognized Link shopper on a wallet-capable device sees Link, Apple Pay, and Google Pay together.

# Handle the result

`onLinkResult` receives either a `data` object or an `errors` object.

```typescript
paymentKit.stripe_link.onLinkResult((result) => {
  if (result.errors) {
    console.error(result.errors.stripe_link);
    return;
  }
  console.log('Checkout Session:', result.data.checkoutSessionId);
  console.log('Payment Intent:', result.data.paymentIntentId);
  console.log('Payment Method:', result.data.paymentMethodId);
});
```

## Result fields

| Field                         | Description                                                                                                                      |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `id`                          | Stripe payment intent ID for the charge. Absent in setup mode — no charge is created                                             |
| `checkoutSessionId`           | Checkout session ID. Falls back to the checkout's secure token if the backend omits it                                           |
| `checkoutAttemptId`           | Checkout attempt ID                                                                                                              |
| `state`                       | `"checkout_succeeded"` on success                                                                                                |
| `paymentIntentId`             | PaymentKit payment intent ID (e.g. `pi_dev_def456`) — not the Stripe payment intent                                              |
| `customerId`                  | Customer the payment was recorded against                                                                                        |
| `paymentMethodId`             | Saved Link payment method ID                                                                                                     |
| `processorUsed`               | Processor that fulfilled the payment                                                                                             |
| `subscriptionId`              | Subscription created, when applicable                                                                                            |
| `invoiceId` / `invoiceNumber` | Invoice created, when applicable. `invoiceNumber` is an integer                                                                  |
| `cardBrand` / `cardLast4`     | Both `null` for Link — a Link payment method carries no underlying card details. See [Save Link for later](#save-link-for-later) |

`data` also carries `errorCode`, `errorMessageForCustomer`, and `errorMessageForDebug`. These are only populated on a failure, which arrives via `result.errors` — on a success result they are `undefined`.

# Payer identity

Link returns the shopper's identity when they confirm. The SDK captures it and sends it with the confirmation — you do not pass anything or call any extra method.

| Captured        | Notes                                                                                                                                       |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Email           | Used for receipts, dunning, and deduplicating repeat purchases                                                                              |
| Name            | The single name Link returns is split on the first space — "Ada Byron King" becomes first name "Ada", last name "Byron King"                |
| Billing address | Collected by the Link element, then discarded server-side when every toggle under **Settings > Checkout > Fields > Billing address** is off |
| Phone           | Not requested, but forwarded when Stripe volunteers it anyway                                                                               |

Captured values only **fill fields that are blank**. Anything you already supplied in `customerInfo` wins, so Link never overwrites merchant-supplied data. Because `customerInfo.first_name` and `last_name` are typed as required, pass empty strings when you want Link to supply the shopper's name.

The Link element always asks for the billing address, even when you collect none. Stripe only surfaces the payer identity when it can fill both name and address, so declining the address would cost you the email too. Your field setting is honored on the server instead — the address is dropped at confirm time rather than never collected.

Phone is not requested. Any field Stripe cannot autofill from the shopper's Link account is collected in the payment interface instead — an extra step at the highest-intent moment in the funnel.

# Save Link for later

The Link payment method is always saved against the customer. What varies is whether it carries an off-session mandate — the thing that makes it chargeable later without the shopper present.

| Checkout                               | Charge          | Off-session mandate |
| -------------------------------------- | --------------- | ------------------- |
| Setup mode                             | None            | Yes                 |
| Payment mode with recurring line items | One-time charge | Yes                 |
| Payment mode, one-time items only      | One-time charge | No                  |

The SDK reads this from the checkout — there is no separate call.

Saved Link methods read back through the normal payment-method endpoints and, with the mandate, can be charged off-session for renewals and future invoices.

A saved Link method renders as a Link mark, not a card number. Link abstracts the underlying instrument, so brand and last four come back `null` by design — build your UI to omit them rather than to display a card.

# Clean up

Tear down the Link element, its observers, and all registered callbacks when unmounting or navigating away. The call is idempotent.

```typescript
paymentKit.stripe_link.teardownStripeLink();
```

In React, call this from your effect cleanup so re-mounts do not stack duplicate buttons. Teardown drops your callbacks too, so re-register them before calling `initStripeLink` again.

# Options

## `initStripeLink` options

| Option                    | Type                     | Description                                                                                                                                                                                                                                                                                   |
| ------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `processorId`             | `string`                 | **Required.** Your Stripe processor ID with Link enabled                                                                                                                                                                                                                                      |
| `customerInfo`            | `StripeLinkCustomerInfo` | **Required.** Only `first_name`, `last_name`, and `email` are accepted — other customer fields cannot be passed on the Link path                                                                                                                                                              |
| `customerInfo.first_name` | `string`                 | **Required.** Customer's first name                                                                                                                                                                                                                                                           |
| `customerInfo.last_name`  | `string`                 | **Required.** Customer's last name                                                                                                                                                                                                                                                            |
| `customerInfo.email`      | `string`                 | Customer's email. Left blank, it is filled from the payer identity Link returns (see [Payer identity](#payer-identity))                                                                                                                                                                       |
| `mockScenario`            | `StripeLinkMockScenario` | Testing only. `StripeLinkMockScenario.Success` or `.Cancelled` makes the **server** simulate the charge — the shopper still confirms through a real Link element. Import the enum from `@payment-kit-js/vanilla/payment-methods/stripe-link`; the bare string `"success"` does not type-check |

**Required** here means required by the TypeScript type. The underlying API treats each `customer_info` field as individually optional, so a plain-JavaScript caller that omits a name gets a nameless customer rather than a validation error.

# Error handling

Availability problems surface as a button that never appears, not as errors.

Treat `onLinkReady` as the availability signal: it fires `true` exactly once when Link is usable, and never fires at all when it is not — no Stripe processor, Link disabled, or Stripe.js missing. Render neither the express button nor the tile until `onLinkReady` fires.

Do not infer unavailability from `onLinkAuthChange(false)`. It means "no authenticated Link session", which is also the normal tile case, and on several initialization failure paths it never fires at all. Gating the tile on it alone renders a Link tile on checkouts where Link cannot be fulfilled.

| Error                                                                                                                                                    | Surfaces via                   | Cause                                                                                                                                                                                                                                |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Stripe Link cancelled by user`                                                                                                                          | `onLinkResult`                 | Shopper dismissed the Link dialog                                                                                                                                                                                                    |
| `Stripe Link is not available for this processor`                                                                                                        | `onLinkResult`                 | Processor is not Stripe, or does not have `stripe_link` enabled. Silently swallowed during initialization — only reaches you on confirm                                                                                              |
| `Processor not found: {id}`                                                                                                                              | `onLinkResult`                 | The `processorId` does not belong to the checkout's account. Also swallowed during initialization                                                                                                                                    |
| `Processor {id} has no credentials configured`                                                                                                           | `onLinkResult`                 | The Stripe processor has no stored credentials                                                                                                                                                                                       |
| `Stripe Link is not available: amount {n} is below the Stripe minimum of {m} for {CUR}.`                                                                 | `onLinkResult`                 | Checkout amount is under Stripe's per-currency minimum. Checkout mode only — setup mode is exempt                                                                                                                                    |
| `payment_method_id is required to confirm Stripe Link checkout`                                                                                          | `onLinkResult`                 | Confirm posted without the Link `pm_...`                                                                                                                                                                                             |
| `Stripe Link failed to create a payment method`                                                                                                          | `onLinkResult`                 | Stripe could not create the payment method and returned no message                                                                                                                                                                   |
| `Stripe Link failed`                                                                                                                                     | `onLinkResult`                 | Adapter reported a non-cancel failure with no message                                                                                                                                                                                |
| `Stripe Link payment failed`                                                                                                                             | `onLinkResult`                 | Fallback when the charge status is not `success` **and** no customer- or debug-facing message came back. Normally you receive the processor's decline message instead                                                                |
| `Failed to confirm Stripe Link payment` / `Request failed ({status})`                                                                                    | `onLinkResult`                 | The confirm request failed. In practice you receive the API's `detail` message                                                                                                                                                       |
| `Stripe Link not initialized — call setSubmitOptions first`                                                                                              | `onLinkResult`                 | Link state was cleared by `teardownStripeLink()` (or a PaymentKit-wide cleanup) before the shopper confirmed, or `initStripeLink` was never called. The `setSubmitOptions` in this message is stale — the method is `initStripeLink` |
| `Stripe Elements not ready`                                                                                                                              | `onLinkResult`                 | Internal guard — confirm fired without a live Stripe client. Should not occur in normal use                                                                                                                                          |
| `Express element not set up`                                                                                                                             | `mountLinkButton` return value | `mountLinkButton` called before `initStripeLink` resolved                                                                                                                                                                            |
| `Mount target not found: {selector}`                                                                                                                     | `mountLinkButton` return value | The selector passed to `mountLinkButton` matches no element                                                                                                                                                                          |
| `Stripe Link is confirmed via its mounted express button, not paymentKit.submit(). Use initStripeLink() + mountLinkButton() and observe onLinkResult().` | `submit` `onError`             | `paymentKit.submit()` called with `paymentMethod: 'stripe_link'`                                                                                                                                                                     |