Klarna Direct

Express checkout for Shopify Payments with a server-created Payment Request

Integrate Express checkout with Shopify Payments by creating the Payment Request on Shopify's backend, launching the Web SDK with the payment request URL, handling shipping on Shopify, and sending payment_confirmation_token plus klarna_network_session_token to Stripe.
14 min read

Overview

This guide is for Shopify. Shopify integrates Express checkout into Shopify Payments.
This variant creates the Payment Request on Shopify's backend, then launches the Klarna Purchase Journey with the paymentRequestUrl. Shipping callbacks, completion, and Stripe PaymentIntent creation stay the same as in Express checkout for Shopify Payments.
To redirect to that Payment Request without loading the Klarna Web SDK, use Express checkout without the Web SDK.
Express checkout lets a customer start a Klarna purchase from the product or cart page instead of the full checkout form. Klarna collects the shipping address and shipping selection inside the Klarna Purchase Journey and asks Shopify for rates and updated totals.
In this integration:
  • Shopify owns the storefront, Web SDK integration, server-side Payment Request creation, and shipping callbacks.
  • Stripe remains the Acquiring Partner and creates the Payment Transaction after Shopify forwards the completed tokens.
  • Shopify doesn't create a Stripe PaymentIntent before the customer completes the Klarna Purchase Journey.

Architecture and sequence

These decisions shape the integration:
  • Identity is split. The Web SDK loads with Shopify's clientId, while partnerAccountId identifies the Partner Account that represents the Store under Stripe.
  • Shopify creates the Payment Request server-side. On button click, Shopify's backend creates the Payment Request with editable shipping and returns the paymentRequestUrl to the Web SDK.
  • Shopify owns shipping. Klarna calls Shopify's backend directly. Shopify calculates shipping and tax and keeps the final snapshot approved by the customer.
  • Shopify hands off and Stripe authorizes. When the Payment Request completes, the storefront takes the customer to the confirmation page. Shopify's backend sends payment_confirmation_token and klarna_network_session_token to Stripe and creates the Stripe PaymentIntent. Stripe creates the Payment Transaction.
sequenceDiagram participant C as Customer participant SF as Shopify frontend participant SB as Shopify backend participant K as Klarna participant ST as Stripe C->>SF: Select Express checkout K->>SF: Trigger initiate callback SF->>SB: Create Payment Request SB->>K: createPaymentRequest with editable shipping K-->>SB: payment_request_url SB-->>SF: paymentRequestUrl SF->>K: Return paymentRequestUrl K-->>C: Open Klarna Purchase Journey C->>K: Enter or change shipping address K->>SB: editable-shipping-details.updated SB-->>K: HTTP 200 with rates and updated totals C->>K: Select shipping option K->>SB: editable-shipping-details.updated SB-->>K: HTTP 200 with recalculated totals C->>K: Approve Payment Request K->>SB: payment.request.state-change.completed K->>SF: complete callback SB-->>K: Acknowledge webhook SF->>C: Show confirmation page SB->>ST: Create PaymentIntent with payment_confirmation_token, klarna_network_session_token, and final snapshot ST->>K: Authorize and create Payment Transaction
Integration requirement
Don't create a Stripe PaymentIntent before the customer completes the Klarna Purchase Journey. Shopify creates the Payment Request on the backend. The initiate callback returns the paymentRequestUrl. After completion, Shopify forwards the tokens and Stripe creates the Payment Transaction.

Prerequisites

Before you start:
  1. 1.
    Enable Klarna for the Store through Klarna for Shopify Payments.
  2. 2.
    Configure Shopify's Klarna Web SDK clientId and allowlist every Shopify domain that loads the SDK.
  3. 3.
    Resolve the Stripe Partner Account for each Store.
  4. 4.
    Enable Shopify to call Klarna's Payment Request API for that Partner Account.
  5. 5.
    Configure an HTTPS endpoint on Shopify that can answer shipping callbacks synchronously.
  6. 6.
    Configure an HTTPS endpoint for Klarna webhooks and set up webhook signing keys.
  7. 7.
    Subscribe Shopify to:
    • 7.1.
      payment.request.editable-shipping-details.updated (synchronous shipping callback)
    • 7.2.
      payment.request.editable-shipping-details.updated.failed
    • 7.3.
      payment.request.state-change.completed
  8. 8.
    Prepare Shopify to send payment_confirmation_token and klarna_network_session_token to Stripe when creating the PaymentIntent after the Payment Request completes.

Initialize the Web SDK

Load the Web SDK with Shopify's clientId and the Partner Account for the Store. Resolve partnerAccountId for each Store rather than hardcoding it.
HTML
1 2 3 4 5 6 7 8 9 10
<script type="module"> const { KlarnaSDK } = await import( "https://js.klarna.com/web-sdk/v2/klarna.mjs" ); const klarna = await KlarnaSDK({ clientId: "[shopify-client-id]", partnerAccountId: "[stripe-partner-account-id-for-store]", products: ["PAYMENT"], locale: "en-US",
FieldValue
clientIdShopify's Klarna Web SDK client ID.
partnerAccountIdThe Partner Account that represents the Store under Stripe.
products["PAYMENT"].
Follow these practices when loading the Web SDK:
Do
Don't
Load the Web SDK from https://js.klarna.com/web-sdk/v2/klarna.mjs.
Bundle or self-host the Web SDK.
Keep the SDK in the top-level, first-party browsing context.
Load the Web SDK inside an iframe.
Disclose Web SDK tracking in Shopify's notices.
Use the Web SDK without disclosing tracking technologies.

Display Express checkout

Add a container and mount the Klarna payment button:
HTML
1
<div id="klarna-button-container"></div>
JAVASCRIPT
1 2 3 4 5 6 7 8 9 10
const klarnaPaymentButton = klarna.Payment.button({ id: "klarna-payment-button", shape: "rect", theme: "outlined", intent: "PAY", initiationMode: "DEVICE_BEST", initiate: startServerCreatedExpressCheckout, }); klarnaPaymentButton.mount("#klarna-button-container");
When the Payment Request completes, complete runs on the storefront. Take the customer to Shopify's confirmation page in that handler. Do not wait for Shopify's backend to create the Stripe PaymentIntent. Return false so the SDK does not also follow customerInteractionConfig.returnUrl.
The completed webhook on Shopify's backend creates the Stripe PaymentIntent in parallel.

Create the Payment Request

The Web SDK calls initiate after the customer selects the Klarna button. Create the Payment Request on Shopify's backend with shipping_config.mode set to "EDITABLE", then return the paymentRequestUrl to the Web SDK.
JAVASCRIPT
1 2 3 4 5 6 7
async function startServerCreatedExpressCheckout() { const response = await fetch("/api/shopify/klarna/payment-requests", { method: "POST", }); const data = await response.json(); return { paymentRequestUrl: data.paymentRequestUrl }; }
Shopify's backend creates a new Payment Request on every button click:
JAVASCRIPT
1 2 3 4 5 6 7 8 9 10
async function createKlarnaPaymentRequest(cart) { const klarnaResponse = await fetch( "https://api-global.klarna.com/v2/payment/requests", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer [shopify-klarna-api-token]", }, body: JSON.stringify({
createPaymentRequestAPI
FieldValue
shipping_config.mode"EDITABLE". Klarna collects the shipping address and calls Shopify for rates.
payment_request_referenceShopify's checkout identifier. Klarna returns it on shipping callbacks and the completed webhook.
customer_interaction_config.return_urlFallback return URL. The storefront complete handler still owns confirmation navigation.
Create a new Payment Request on every click
Don't reuse a Payment Request across customers or checkout attempts. Create a new one each time the customer selects Express checkout.
Treat storefront input as untrusted. Keep the authoritative cart on Shopify's backend when creating the Payment Request, and validate the final amount, line items, and shipping selection before creating the Stripe PaymentIntent.

Handle Klarna shipping callbacks

Klarna sends payment.request.editable-shipping-details.updated when the customer enters or changes an address or selects a shipping option.
EventPatternShopify response
payment.request.editable-shipping-details.updatedSynchronous callbackHTTP 200 with a JSON body in the same request.
payment.request.editable-shipping-details.updated.failedAsynchronous webhookHTTP 200, 201, 202, or 204. No response body is required.
payment.request.state-change.completedAsynchronous webhookHTTP 200, 201, 202, or 204. No response body is required.
Verify Klarna-Signature using the signing key identified by Klarna-Signing-Key-ID.
The synchronous callback has no metadata envelope and no payload wrapper. Its customer_action, payment_request_id, payment_request_reference, and shipping fields are at the root. Use payment_request_reference as Shopify's identifier for the checkout.
Don't deduplicate shipping callbacks
Don't deduplicate the synchronous callback by payment_request_id. That identifier is the same for every shipping change in one Payment Request. The callback has no per-delivery event identifier and can be repeated after a timeout. Keep rate quoting free of side effects, recompute the quote, and return it.

Handling shipping address changes

For customer_action: "SELECT_SHIPPING_ADDRESS", calculate rates and tax for the collected address. Only postal_code, city, and country are guaranteed, so treat other address fields as optional.
JSON
1 2 3 4 5 6 7 8 9 10
{ "customer_action": "SELECT_SHIPPING_ADDRESS", "payment_request_id": "krn:payment:eu1:request:552603c0-fe8b-4ab1-aacb-41d55fafbdb4", "payment_request_reference": "shopify-checkout-12345", "shipping": { "address": { "street_address": "13 Palmer Square W", "city": "Princeton", "region": "US-NJ", "postal_code": "08542",
Return ACCEPTED with the available shipping options, a preselected option, and recalculated totals. Set selected_shipping_option_reference to one of the returned shipping_option_reference values so Klarna preselects that option. Price updated_payment_request for the preselected option.
JSON
1 2 3 4 5 6 7 8 9 10
{ "result": "ACCEPTED", "shipping_selection": { "available_shipping_options": [ { "shipping_option_reference": "ups-ground-5day", "display_name": "UPS Ground", "description": "3–5 business days", "amount": 599, "shipping_carrier": "UPS",
If the Store can't ship to the address, return an address rejection:
JSON
1 2 3 4
{ "result": "SHIPPING_ADDRESS_REJECTED", "result_reason": "COUNTRY_NOT_SUPPORTED" }

Handling shipping option selection

For customer_action: "SELECT_SHIPPING_OPTION", validate the referenced option and return ACCEPTED with the final amount and line items.
JSON
1 2 3 4 5 6 7 8 9 10
{ "customer_action": "SELECT_SHIPPING_OPTION", "payment_request_id": "krn:payment:eu1:request:552603c0-fe8b-4ab1-aacb-41d55fafbdb4", "payment_request_reference": "shopify-checkout-12345", "shipping": { "selected_shipping_option": { "shipping_option_reference": "ups-2nd-day", "display_name": "UPS 2nd Day Air", "description": "2 business days", "amount": 1299,
When the option is available, return ACCEPTED with that option preselected and totals priced for it:
JSON
1 2 3 4 5 6 7 8 9 10
{ "result": "ACCEPTED", "shipping_selection": { "available_shipping_options": [ { "shipping_option_reference": "ups-ground-5day", "display_name": "UPS Ground", "description": "3–5 business days", "amount": 599, "shipping_carrier": "UPS",
If the option is no longer available but the address remains serviceable, return alternatives:
JSON
1 2 3 4 5 6 7 8 9 10
{ "result": "SHIPPING_OPTION_REJECTED", "result_reason": "SHIPPING_OPTION_NO_LONGER_AVAILABLE", "shipping_selection": { "available_shipping_options": [ { "shipping_option_reference": "store-pickup-princeton", "display_name": "Collect in store", "description": "Ready in 2 hours", "amount": 0,
Once the customer has seen a price for a shipping_option_reference, keep that price stable. If a rate changes, return it under a new reference.
Persist every accepted snapshot—selected option, shipping amount, tax, line items, and total—against the payment_request_id. Shopify forwards the last accepted snapshot to Stripe after completion.

Handle callback-validation failures

Klarna sends payment.request.editable-shipping-details.updated.failed when it can't apply Shopify's callback response. This is an asynchronous webhook with the standard metadata and payload structure.
JSON
1 2 3 4 5 6 7 8 9 10
{ "metadata": { "event_type": "payment.request.editable-shipping-details.updated.failed", "event_id": "d9f9b1a0-5b1a-4b0e-9b0a-9e9b1a0d5b1a", "event_version": "v2", "occurred_at": "2026-09-25T12:00:00Z" }, "payload": { "payment_request_id": "krn:payment:eu1:request:552603c0-fe8b-4ab1-aacb-41d55fafbdb4", "payment_request_reference": "shopify-checkout-12345",
Deduplicate this webhook using metadata.event_id. Log payload.validation_errors with the Store and payment_request_id, and include payload.error_id when contacting Klarna.

Handle completion

When the customer approves, Klarna sends payment.request.state-change.completed. Token fields are at the root of payload, not inside state_context.
paymentRequestStateChangeEventAPI
JSON
1 2 3 4 5 6 7 8 9 10
{ "metadata": { "event_type": "payment.request.state-change.completed", "event_id": "eab0c2b1-6c2b-4c1f-8c1b-afac2b1e6c2b", "event_version": "v2", "occurred_at": "2026-09-25T12:00:00Z" }, "payload": { "payment_request_id": "krn:payment:eu1:request:552603c0-fe8b-4ab1-aacb-41d55fafbdb4", "payment_request_reference": "shopify-checkout-12345",
The public webhook schema can carry:
  • payment_token — the confirmation token. Map this to payment_confirmation_token when Shopify creates the Stripe PaymentIntent. Klarna API v1 used the same name, payment_confirmation_token.
  • klarna_network_session_token — the session token Shopify must send with the Stripe PaymentIntent.
Keep both tokens opaque
Don't parse, decode, reformat, or log either token in full. Forward them promptly to Stripe and don't store them longer than required for the handoff.

Intent creation

Do not create a Stripe PaymentIntent before the customer completes the Klarna Purchase Journey. Shopify does not currently send Klarna tokens to Stripe. After the completed webhook, Shopify's backend creates the PaymentIntent and includes payment_confirmation_token and klarna_network_session_token with the last shipping snapshot Shopify accepted. The storefront complete handler must not wait for that call.
The following JSON is a conceptual internal handoff. It is not a Stripe API request and not a Klarna API payload:
JSON
1 2 3 4 5 6 7 8 9 10
{ "payment_confirmation_token": "[confirmation token from payment_token]", "klarna_network_session_token": "[opaque Klarna Network Session Token]", "amount": 48620, "currency": "USD", "payment_request_id": "krn:payment:eu1:request:552603c0-fe8b-4ab1-aacb-41d55fafbdb4", "payment_request_reference": "shopify-checkout-12345", "shipping": { "shipping_option_reference": "ups-ground-5day", "amount": 599
The amount, currency, line items, and shipping must match the last snapshot Shopify returned as ACCEPTED.
The exact Stripe call and properties that should carry both tokens haven't been confirmed for Express checkout.
Validate the Stripe PaymentIntent contract
Before implementation, validate which Stripe properties should carry payment_confirmation_token and klarna_network_session_token. Shopify does not currently send these tokens to Stripe. This guide doesn't specify a Stripe API path, method, or payload. The conceptual handoff above shows the information Stripe needs, not the call that carries it.

Test the integration

After callbacks are available in the test environment, verify that:
  • The Web SDK uses Shopify's clientId and the correct Stripe Partner Account for the Store.
  • initiate returns { paymentRequestUrl } from a newly created Payment Request.
  • The create call uses shipping_config.mode: "EDITABLE".
  • A supported address returns shipping options, a preselected option, and updated totals that match that option.
  • An unsupported address returns SHIPPING_ADDRESS_REJECTED.
  • Selecting an unavailable option returns alternatives.
  • Callback totals equal the sum of line-item totals.
  • A malformed callback response produces the failed webhook.
  • The completed webhook is deduplicated using metadata.event_id.
  • The storefront complete handler takes the customer to the confirmation page without waiting for Stripe.
  • Shopify sends payment_confirmation_token, klarna_network_session_token, and the last accepted snapshot when creating the Stripe PaymentIntent.
  • Duplicate completed webhooks don't create duplicate Stripe PaymentIntents.

Troubleshooting

SymptomLikely causeFix
The customer's second address change is ignoredCallback deliveries are deduplicated on payment_request_id.Don't deduplicate synchronous shipping callbacks.
Klarna reports a signature mismatchThe signature was checked after parsing or reserializing the body.Verify the signature against the raw request body.
Klarna sends the failed webhookThe callback response doesn't match the schema.Inspect payload.validation_errors and correlate with error_id.
Stripe declines or requests confirmationThe Stripe amount or purchase data differs from the accepted snapshot.Compare the handoff with Shopify's last ACCEPTED callback state.
Follow these practices when implementing the callback and token handoff:
DoDon't
Answer every shipping callback with HTTP 200 and a JSON body in the same request.
Acknowledge the shipping callback with an empty response and process it later.
Verify webhook signatures before processing callback or webhook data.
Act on callback or webhook data before verifying its signature.
Keep each shipping option reference tied to one stable amount.
Reprice an existing shipping option reference.
Send payment_confirmation_token and klarna_network_session_token when creating the Stripe PaymentIntent.
Parse or reshape the completed value before forwarding it.

Next steps

Related articles
Express checkout for Shopify Payments
Express checkout for Shopify Payments without the Web SDK
Klarna for Shopify Payments
Captures
SupportPartner support•Service Status
Cookies|Terms & Conditions|Copyright Klarna AB 2026
Jump to section...