WooCommerce Stripe Link payments
Do not model Link as a card brand or a separate WooCommerce gateway. Determine the representation from the Stripe PaymentMethod object and the hydrated Woo token.
Distinguish the two Link representations
| Stripe object | Woo token | Durable fields | Meaning |
|---|---|---|---|
type = link, link.email |
WC_Payment_Token_Link |
type link, gateway stripe, token pm_..., email meta |
Native reusable Link PaymentMethod |
type = card, card.wallet.type = link |
WC_Stripe_Payment_Token_CC |
type CC, gateway stripe, pm_..., card/fingerprint fields, wallet_type=link |
A card-shaped PaymentMethod used through Link |
Both can have a pm_... ID. Never infer card shape from that prefix. The plugin deliberately does not expose Link branding for the second case: get_wallet_brand_label() returns a label only for Apple Pay and Google Pay.
Keep the identifier layers separate
- Stripe method type:
linkorcard. - Woo token type:
linkorCC. - Woo token class:
WC_Payment_Token_LinkorWC_Stripe_Payment_Token_CC. - Woo gateway ID on the token, order, and subscription:
stripe. - Saved-token form field:
wc-stripe-payment-token. - Gateway intent marker:
express_payment_type=link. - Express Checkout/WCS bookkeeping marker:
express_checkout_type=link. - Shopper-facing order title:
Link.
WC_Stripe_UPE_Payment_Method_Link::get_id() returns the Stripe method type link, not a Woo gateway ID, and is_available() deliberately returns false. The helper is not a standalone checkout gateway; there is no registered stripe_link gateway to store on an order.
Inspect tokens polymorphically
function myplugin_describe_stripe_token( WC_Payment_Token $token ): array {
if ( 'stripe' !== $token->get_gateway_id() ) {
return array();
}
$type = strtolower( $token->get_type() );
if ( 'link' === $type && method_exists( $token, 'get_email' ) ) {
return array(
'kind' => 'link',
'display' => $token->get_display_name(),
'email' => sanitize_email( $token->get_email() ),
);
}
if ( $token instanceof WC_Payment_Token_CC ) {
return array(
'kind' => 'card',
'display' => $token->get_display_name(),
'last4' => $token->get_last4(),
);
}
return array(
'kind' => $type,
'display' => $token->get_display_name(),
);
}
Use get_type() and capabilities such as method_exists() before type-specific getters. Do not call get_last4(), expiry, card brand, or fingerprint methods on a Link token.
Verified 10.9.0 quirk
WC_Payment_Token_Link::set_payment_method_type() calls set_prop( 'payment_method_type', ... ), but that property is absent from the class's extra_data. Consequently get_payment_method_type() still returns null in 10.9.0. Do not use it to classify Link; use get_type() === 'link'. Version-guard and retest if upstream adds the property.
Let the gateway create tokens
Preserve the plugin's Payment Element and intent orchestration. Add payment method and subscription change-payment use SetupIntents because they save without taking a purchase payment; normal paid checkout uses a PaymentIntent and, when future reuse is required and supported, setup_future_usage=off_session. The gateway retrieves the final Stripe PaymentMethod, selects its method handler, and creates the matching Woo token:
- native Link stores
link.emailand the PaymentMethod ID; - card stores safe card display fields and fingerprint;
- both use gateway ID
stripe.
Do not construct a partial Link token from an email or browser-submitted pm_.... Link email is display/deduplication data, not authentication or proof of ownership. If direct integration is unavoidable, retrieve the PaymentMethod server-side and verify its Stripe Customer, type, usable state, and current Woo user before invoking a version-pinned gateway service.
The plugin deduplicates native Link tokens by link.email, not PaymentMethod ID. A replacement remote pm_... for the same Link email can update the existing local token. Therefore neither Link email nor the local Woo token ID is a safe immutable business identifier.
Preserve Link's consent boundary
Link saving and WooCommerce-account tokenization are separate concepts:
- Link collects consent for the shopper's Link wallet inside Stripe's UI;
- a Woo saved method is a merchant-side local projection of a PaymentMethod attached to the Stripe Customer;
- having a Link wallet does not imply that a Woo token exists;
- deleting a Woo token does not delete the shopper's Link account.
When Link is enabled, the gateway hides the store-level save checkbox for card and Link because the Payment Element owns Link consent. Do not re-add or force that checkbox merely because custom UI expects wc-stripe-new-payment-method. Subscription and Add payment method paths have their own forced/setup logic.
Preserve the payment surface contracts
Payment Element
Link is offered inside the main Stripe/card surface rather than as a standalone Woo gateway. When card is selected and Link is enabled, the gateway requests both card and link intent types. Keep that pair; forcing only card breaks Link, SetupIntent, mandate, and some subscription paths.
Express Checkout Element
Express Link sends express_payment_type=link into the gateway intent path and express_checkout_type=link for Express Checkout/WCS bookkeeping, but the order gateway remains stripe. The final PaymentMethod may still need server-side inspection; do not treat either request marker as the provider token type.
Stripe 10.9 gives Link its own link_button_locations and link_button_size settings instead of inheriting Apple Pay/Google Pay appearance. Existing stores migrate the previous Express Checkout locations when the Link location option is absent. Read Link placement through WC_Stripe_Express_Checkout_Helper::get_button_locations( 'link' ) and height through get_link_button_height() only in version-pinned integration code; do not read the generic express_checkout_button_* options and assume they control Link. Supported locations include product, cart, checkout, and the WCS change-payment page when available.
The 10.9 Express Checkout client uses Woo Store API calls for shipping and variable-product cart mutations. Do not intercept removed legacy Stripe shipping/add-to-cart AJAX requests. Integrate custom cart data and checkout fields through Woo's Store API/classic-checkout extension surfaces, then test Link Express Checkout separately from Apple Pay/Google Pay.
Optimized Checkout
Multiple methods share the consolidated stripe gateway. Use the resolved PaymentMethod type and hydrated Woo token, not the selected container slug, as the type authority.
Validate saved-token requests
Prefer the installed gateway's normal checkout flow. In custom authenticated endpoints, treat the posted value as a local Woo token ID:
$token = WC_Payment_Tokens::get( absint( $request['token_id'] ?? 0 ) );
if (
! $token instanceof WC_Payment_Token ||
(int) $token->get_user_id() !== get_current_user_id() ||
'stripe' !== $token->get_gateway_id() ||
! in_array( strtolower( $token->get_type() ), array( 'cc', 'link' ), true )
) {
return new WP_Error( 'invalid_payment_method', __( 'Invalid payment method.', 'myplugin' ), array( 'status' => 403 ) );
}
$payment_method_id = $token->get_token( 'edit' ); // Server-side only.
Then let the gateway retrieve/use the PaymentMethod. Do not expose the pm_..., Link email, SetupIntent client secret, or Stripe Customer ID in logs or general REST output.
Account for reconciliation-on-read
The Stripe plugin filters WC_Payment_Tokens::get_customer_tokens() for logged-in requests. A token-list read can therefore:
- call Stripe for active reusable PaymentMethod types, or all reusable types under Optimized Checkout;
- create missing local Woo tokens;
- update a duplicate token's remote
pm_...; - delete local methods no longer returned for the active type set.
CLI/cron without a logged-in user does not take this synchronization path. The remote list is cached, and synchronization is skipped when the initial local token list already reaches the configured posts_per_page limit. Under Optimized Checkout, a remotely present but disabled or temporarily unavailable method is excluded from the returned list while its local token row is preserved. Outside Optimized Checkout, a disabled type can be outside the remote fetch and its local projection can still be cleaned up. Do not treat visibility as proof of remote detach, and do not assume token enumeration is pure, context-independent, complete, or cheap.
Depending on Optimized Checkout state, disabling and re-enabling Link can either preserve the hidden local row or recreate a cleaned-up projection with another Woo token ID. Store durable domain relationships against the order/subscription and remote PaymentMethod purpose, not a permanently stable local token-row ID. Load references/link-contract.md for the full sync, deletion/default, order, and subscription contracts.
Handle orders and subscriptions through stripe
For a Link payment, the gateway stores:
- Woo payment method ID
stripe; - title
Link; - Stripe Customer ID in gateway-owned metadata;
- native or card-shaped
pm_...source/payment-method ID.
Subscriptions also renew through gateway stripe; _stripe_source_id can contain a native Link pm_.... Do not switch the WCS gateway to stripe_link, infer Link from the gateway ID, or write _stripe_source_id directly.
On Express Checkout change-payment, the plugin replaces the subscription's attached Woo payment-token IDs with the local token matching the new Stripe PaymentMethod. Keep the WCS + Stripe orchestration so this, update-all consent, SetupIntent/SCA, titles, and hooks remain consistent.
Test matrix
- Native
type=linkversustype=card+wallet.type=link. - Classic Payment Element, Blocks, Optimized Checkout, and Express Checkout.
- Guest, logged-in, Add payment method, and one-time checkout save behavior.
- Existing saved Link selection through
wc-stripe-payment-token. - Duplicate Link email with the same and a replacement
pm_.... - Remote detach, Woo deletion, default change, disabled/re-enabled Link with Optimized Checkout on and off, cache refresh, and CLI versus logged-in listing.
- Subscription signup, off-session renewal, standard and Express change-payment, update-all consent, and 3DS return.
- Plugin disabled/missing custom token class; code must fail closed rather than assuming a CC token.
Cross-references
wc-stripe-add-payment-method: complete My Account form and SetupIntent contract.wc-payment-tokens: provider-neutral Woo token ownership and CRUD.wc-stripe-subscriptions: renewal, WCS change-payment, SCA, and detached-token behavior.wc-stripe-webhooks: asynchronous settlement and idempotent order transitions.
References
- Verified source paths:
wp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-upe-payment-method-link.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-upe-payment-gateway.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-express-checkout-element.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-express-checkout-helper.phpwp-content/plugins/woocommerce-gateway-stripe/includes/admin/class-wc-stripe-link-controller.phpwp-content/plugins/woocommerce-gateway-stripe/includes/admin/stripe-settings.phpwp-content/plugins/woocommerce-gateway-stripe/includes/migrations/class-wc-stripe-migrate-link-button-locations.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-tokens/class-wc-stripe-link-payment-token.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-tokens/class-wc-stripe-cc-payment-token.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-tokens/class-wc-stripe-payment-tokens.phpwp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-customer.phpwp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-intent-controller.phpwp-content/plugins/woocommerce-gateway-stripe/includes/compat/trait-wc-stripe-subscriptions.php