Klarna Direct

Express checkout for Shopify Payments without the Web SDK

Integrate Express checkout with Shopify Payments by creating the Payment Request on Shopify's backend, redirecting the customer to the payment request URL, handling shipping on Shopify, and sending payment_confirmation_token plus klarna_network_session_token to Stripe.
15 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 and redirects the customer to the payment request URL. It doesn't load the Klarna Web SDK. Shipping callbacks, completion, and Stripe PaymentIntent creation stay the same as in Express checkout for Shopify Payments and Express checkout with a server-created Payment Request.
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, the payment button, 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:
  • Shopify authenticates the Payment Request. Shopify creates the Payment Request with the API credential for the Store's Stripe Partner Account.
  • Shopify opens the Klarna Purchase Journey with a redirect. On button click, Shopify's backend creates the Payment Request with editable shipping and the storefront redirects the customer to the payment request URL.
  • 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, Klarna redirects the customer to Shopify's 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 SF->>SB: Create Payment Request SB->>K: createPaymentRequest with editable shipping K-->>SB: payment_request_url SB-->>SF: paymentRequestUrl SF->>C: Redirect to payment_request_url 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->>C: Redirect to return_url SB-->>K: Acknowledge webhook C->>SF: Open 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 and redirects the customer to the payment request URL. 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.
    Resolve the API credential for the Stripe Partner Account that represents each Store.
  3. 3.
    Enable Shopify to call Klarna's Payment Request API for that Partner Account.
  4. 4.
    Configure an HTTPS endpoint on Shopify that can answer shipping callbacks synchronously.
  5. 5.
    Configure an HTTPS endpoint for Klarna webhooks and set up webhook signing keys.
  6. 6.
    Subscribe Shopify to:
    • 6.1.
      payment.request.editable-shipping-details.updated (synchronous shipping callback)
    • 6.2.
      payment.request.editable-shipping-details.updated.failed
    • 6.3.
      payment.request.state-change.completed
  7. 7.
    Prepare Shopify to send payment_confirmation_token and klarna_network_session_token to Stripe when creating the PaymentIntent after the Payment Request completes.

Display Express checkout

Show a Klarna payment button with Klarna's payment button stylesheet. The stylesheet draws the button. It doesn't load the Klarna Web SDK.
HTML
1 2 3 4 5 6 7 8 9 10
<link rel="stylesheet" href="https://js.klarna.com/web-sdk/buttons/payment-button.css" /> <button style="width: 100%" class="klarna-sdk-button theme-outlined shape-rect" aria-label="Express checkout with Klarna" id="klarna-express-checkout-button" > <div class="klarna-sdk-button__outline" aria-hidden="true"></div> <div class="klarna-sdk-button__inner-container">
On click, create the Payment Request and redirect the top-level window to the payment request URL:
JAVASCRIPT
1 2 3 4 5 6 7 8
document.getElementById("klarna-express-checkout-button") .addEventListener("click", async () => { const response = await fetch("/api/shopify/klarna/payment-requests", { method: "POST", }); const data = await response.json(); window.location.assign(data.paymentRequestUrl); });
Klarna redirects the customer to customer_interaction_config.return_url after a successful completion. Don't wait for Shopify's backend to create the Stripe PaymentIntent before showing the confirmation page.
The completed webhook on Shopify's backend creates the Stripe PaymentIntent in parallel.
Follow these practices when displaying Express checkout:
Do
Don't
Load the payment button stylesheet from https://js.klarna.com/web-sdk/buttons/payment-button.css.
Load the Klarna Web SDK to open the Klarna Purchase Journey.
Redirect the top-level window to the payment request URL.
Open the payment request URL inside an iframe.
Create a new Payment Request on every button click.
Reuse a Payment Request across checkout attempts.
Show the confirmation page without waiting for the Stripe PaymentIntent.
Wait for the Stripe PaymentIntent before showing the confirmation page.

Create the Payment Request

Create the Payment Request on Shopify's backend with shipping_config.mode set to "EDITABLE", then redirect the customer to the payment request URL from the response.
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_urlConfirmation page Klarna redirects the customer to after a successful completion. Include {klarna.payment_request.payment_request_reference} so Shopify can match the checkout. Don't put the payment token in this URL.
state_context.customer_interaction.payment_request_urlOne-time URL. Redirect the top-level window here to open the Klarna Purchase Journey.
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.

Open the Klarna Purchase Journey

After createPaymentRequest returns state_context.customer_interaction.payment_request_url, open that one-time URL in a browser surface that supports the full redirect flow. The journey can redirect to Klarna, bank, and identity-provider domains during authentication.

Web storefront

Start the navigation from the top-level, first-party browsing context where the customer selected Express checkout. Navigate the top-level window to payment_request_url, for example with window.location.assign.
DoDon't
Navigate the top-level window to payment_request_url.
Open payment_request_url inside an iframe or nested browsing context.
Allow the browser to follow the complete HTTP redirect chain.
Open payment_request_url in a restricted embedded WebView.
Use an HTTPS return_url for the confirmation page.
Intercept or cancel redirects in the Klarna Purchase Journey.

Native apps

When Express checkout starts in a native app, use the redirect flow in a system browser surface. Don't load payment_request_url in a generic in-app WebView.
Allow every redirect in the journey, including HTTP 302 and 303 responses. Configure the HTTPS customer_interaction_config.return_url so the customer can return to the confirmation experience. If the native app handles that URL through an Android App Link or iOS Universal Link, test the return behavior on every supported OS version. Treat payment.request.state-change.completed as the authoritative completion signal for backend processing.
Use a redirect-capable browser surface
Open payment_request_url in the top-level browser context, Android Custom Tabs, or ASWebAuthenticationSession. Don't use an iframe or restricted embedded WebView.

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 confirmation page must not wait for that call. Klarna redirects the customer there through return_url.
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 storefront doesn't load the Klarna Web SDK.
  • Button click creates a new Payment Request and redirects the top-level window to state_context.customer_interaction.payment_request_url.
  • Web integrations don't open the payment request URL in an iframe or nested browsing context.
  • Native integrations use Android Custom Tabs or ASWebAuthenticationSession instead of a generic in-app WebView.
  • The browser surface follows the complete redirect chain.
  • The create call uses shipping_config.mode: "EDITABLE".
  • return_url is the confirmation page and includes {klarna.payment_request.payment_request_reference}.
  • return_url doesn't include the payment token.
  • 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.
  • Klarna redirects 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.
The Klarna Purchase Journey never opensThe storefront didn't redirect to the payment request URL.Redirect the top-level window to state_context.customer_interaction.payment_request_url.
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 with a server-created Payment Request
Klarna for Shopify Payments
Captures
SupportPartner support•Service Status
Cookies|Terms & Conditions|Copyright Klarna AB 2026
Jump to section...