payment_ confirmation_ token and klarna_ network_ session_ token to Stripe and creates the Stripe PaymentIntent. Stripe creates the Payment Transaction.payment. request. editable-shipping-details. updated (synchronous shipping callback)payment. request. editable-shipping-details. updated. failedpayment. request. state-change. completedpayment_ confirmation_ token and klarna_ network_ session_ token to Stripe when creating the PaymentIntent after the Payment Request completes.<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">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);
});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.| 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. |
shipping_ config. mode set to "EDITABLE", then redirect the customer to the payment request URL from the response.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({| Field | Value |
|---|---|
shipping_ | "EDITABLE". Klarna collects the shipping address and calls Shopify for rates. |
payment_ | Shopify's checkout identifier. Klarna returns it on shipping callbacks and the completed webhook. |
customer_ | Confirmation page Klarna redirects the customer to after a successful completion. Include {klarna. so Shopify can match the checkout. Don't put the payment token in this URL. |
state_ | One-time URL. Redirect the top-level window here to open the Klarna Purchase Journey. |
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.payment_ request_ url, for example with window. location. assign.| Do | Don'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. |
payment_ request_ url in a generic in-app WebView.prefersEphemeralWebBrowserSession to false so the journey can use Safari-shared authentication state.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.payment_ request_ url in the top-level browser context, Android Custom Tabs, or ASWebAuthenticationSession. Don't use an iframe or restricted embedded WebView.payment. request. editable-shipping-details. updated when the customer enters or changes an address or selects a shipping option.| Event | Pattern | Shopify response |
|---|---|---|
payment. | Synchronous callback | HTTP 200 with a JSON body in the same request. |
payment. | Asynchronous webhook | HTTP 200, 201, 202, or 204. No response body is required. |
payment. | Asynchronous webhook | HTTP 200, 201, 202, or 204. No response body is required. |
Klarna-Signature using the signing key identified by Klarna-Signing-Key-ID.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.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.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.{
"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",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.{
"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",{
"result": "SHIPPING_ADDRESS_REJECTED",
"result_reason": "COUNTRY_NOT_SUPPORTED"
}customer_ action: "SELECT_ SHIPPING_ OPTION", validate the referenced option and return ACCEPTED with the final amount and line items.{
"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,ACCEPTED with that option preselected and totals priced for it:{
"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",{
"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,shipping_ option_ reference, keep that price stable. If a rate changes, return it under a new reference.payment_ request_ id. Shopify forwards the last accepted snapshot to Stripe after completion.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.{
"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",metadata. event_ id. Log payload. validation_ errors with the Store and payment_ request_ id, and include payload. error_ id when contacting Klarna.payment. request. state-change. completed. Token fields are at the root of payload, not inside state_ context.{
"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",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.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.{
"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": 599ACCEPTED.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.state_ context. customer_ interaction. payment_ request_ url.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.SHIPPING_ ADDRESS_ REJECTED.metadata. event_ id.payment_ confirmation_ token, klarna_ network_ session_ token, and the last accepted snapshot when creating the Stripe PaymentIntent.| Symptom | Likely cause | Fix |
|---|---|---|
| The customer's second address change is ignored | Callback deliveries are deduplicated on payment_. | Don't deduplicate synchronous shipping callbacks. |
| Klarna reports a signature mismatch | The signature was checked after parsing or reserializing the body. | Verify the signature against the raw request body. |
| Klarna sends the failed webhook | The callback response doesn't match the schema. | Inspect payload. and correlate with error_. |
| Stripe declines or requests confirmation | The 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 opens | The storefront didn't redirect to the payment request URL. | Redirect the top-level window to state_. |
| Do | Don'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. |