Klarna Payments Library

1.10.0

init

In this mandatory step, the Klarna Payments library is initialized. The method expects an option with the client_token received when creating the session.

init
Parameters
options (Object)
Name Description
options.client_token String The client token received when creating the session.
Throws
  • InvalidClientTokenError: If options.client_token is not a valid JSON Web Token.
Example
try {
  Klarna.Payments.init({
    client_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.dtxWM6MIcgoeMgH87tGvsNDY6cHWL6MGW4LeYvnm1JA'
  })
} catch (e) {
  // Handle error.
}

BOT_PATTERNS

User-agent substrings that identify definite bots, crawlers, or automated tools.

Kept focused on definitive bots to avoid blocking legitimate users — broad patterns such as 'bot', 'crawler' or 'whatsapp' would also match real shoppers (see #470, which relaxed this list after real purchase-flow events were dropped). Hoisted to module scope and frozen so it is allocated once, not on every isBot() call (isBot runs per tracked event).

BOT_PATTERNS

isBot

Detects if the current environment is a bot or crawler. Only checks definitive bot signals to avoid blocking legitimate users.

isBot(win: Window?): boolean
Parameters
win (Window?) The window object to check (defaults to global window)
Returns
boolean: True if bot is detected, false otherwise. Returns false on any error to avoid blocking legitimate users.

KLARNA_DOMAIN_SUFFIXES

Validates that a URL belongs to a Klarna-owned domain (or local dev). Hostname must exactly match or end with a Klarna suffix (e.g. "klarna.com", "js.klarna.com") to prevent bypass with domains like "klarna.net.malicious.domain". Rejects: fakeklarna.com, xss.net, localhost.fake.com, https on localhost, http on Klarna, etc. Allows: https://.klarna.com|.klarna.net|*.klarnacdn.net and http://localhost|0.0.0.0|dev-proxy for local dev.

KLARNA_DOMAIN_SUFFIXES

DEV_HOSTNAMES

Local dev / proxy hostnames; only HTTP is allowed for these.

DEV_HOSTNAMES

STORAGE_KEY

Authorization recovery — re-deliver an approved authorization after the merchant tab was torn down mid-flow.

On mobile the merchant tab is backgrounded while the customer completes the payment in the OPF popup, and the browser routinely reclaims it. When the customer comes back the tab reloads from scratch: the authorize callback, authorizationsInProgress, the OPF render() promise and the acquirer bridge all lived in that page's memory and are gone. The authorization succeeded, but nothing is left to hand the authorization_token to, so a merchant without a server-side webhook loses the order.

This is the kp-lib port of the Web SDK's completion recovery (Q3048472). The shape is the same — arm a marker before the hand-off, read the live state on the next page load, re-deliver if it completed — with one substitution: the Web SDK re-fetches a payment request, and kp-lib has no such object, so we read through OPF's acquirer bridge instead (see getAuthorizationResult below).

What is NOT persisted, deliberately:

  • The merchant's callback. Functions do not survive a reload; the callback has to be supplied fresh by the merchant after the reload. See the two call site, authorize.js.
  • The authorization_token, or anything else that can create an order. It is a bearer credential and merchant-origin localStorage is readable by every script on the page. We persist only non-secret identifiers and re-read the token from the backend when we need it.
  • The client token. The read is made with the one the merchant hands this page, and the marker only says which session to ask about, so a marker on its own entitles nobody to anything. This is also why the marker no longer carries the session's base_url, purchase_country or environment: OPF reads all three off the token, so persisting them bought nothing beyond three more strings a page script could rewrite. The cost is that a page with no client token for the marker's session cannot read at all; see startAuthorizationRecovery.

localStorage (not memory, not sessionStorage) is the one storage that survives a full tab teardown, which is the entire scenario. This is a deliberate exception to the repo rule against localStorage for session state: the marker is not session state, it is a TTL'd breadcrumb pointing at state that lives on the backend.

STORAGE_KEY

Type: string

STORAGE_KEY

Marker key in merchant-origin localStorage.

STORAGE_KEY

Type: string

RECOVERY_MAX_AGE_MS

60 minutes — the lifetime kp-lib already promises for an authorization.

authorize's own documentation states "a successful authorization guarantees that the order can be created within 60 minutes". Past that the recovered authorization_token is of no use to the merchant, so the fetch, the button busy state and the callback would all be wasted. Expiring the marker on the same clock keeps the two from drifting apart.

RECOVERY_MAX_AGE_MS

RECOVERY_REREAD_MIN_INTERVAL_MS

Shortest gap between two reads in one page life.

The customer can move between the merchant tab and the popup several times, and each return fires visibilitychange. Without a floor a customer flicking back and forth would issue a bridge read per flick, for an answer that only changes once.

RECOVERY_REREAD_MIN_INTERVAL_MS

RECOVERY_MAX_READS_PER_PAGE_LIFE

Hard cap on reads in one page life, the page-load read included.

The interval floor alone does not bound anything: a customer moving between the shop and the popup for the hour the marker lives could drive a read every few seconds, and each one is a bridge round trip to PACS plus two tracking events. The cap is what makes the worst case finite. Three is enough for the case this exists for — the page-load read, the return from the popup, and one spare — and a customer who has come back three times without the authorization completing is not going to be rescued by a fourth.

RECOVERY_MAX_READS_PER_PAGE_LIFE

Type: number

RECOVERY_MIN_AGE_MS

Markers younger than this are left alone rather than read.

Mirrors OPF's own IGNORE_PAGE_VISIBLE_WITHIN: reading the authorization result before the OPF service session exists just fails. A marker this young also means the page reloaded within seconds of the hand-off, which is not the teardown case this exists for.

RECOVERY_MIN_AGE_MS

Type: number

RECOVERY_CLAIM_WAIT_MS

How long a pay click will wait for an in-flight recovery read.

The read starts at page load, so by click time it has normally resolved and this never runs. It exists for the case where nothing resolves at all: the OPF script load has no timeout of its own, and a promise that never settles would leave authorizationsInProgress reserved for the rest of the page life, so every later click returns at the guard and the checkout is wedged shut with no error anywhere.

Set above OPF's own 10s budget for the whole read on purpose. A bridge that cannot be reached, or that answers nothing once it is up, fails through the normal path and is reported as such; only a genuinely stuck promise reaches this, and cutting a live read short would start the second authorization on an approved session that this whole feature exists to avoid.

RECOVERY_CLAIM_WAIT_MS

Type: number

recoveryForThisPageLife

Recovery runs at most once per page life.

startAuthorizationRecovery is called from init(), which merchants call freely and sometimes more than once per page. Without the latch each call would issue another bridge read for an answer that cannot change within one page life.

recoveryForThisPageLife

resetAuthorizationRecoveryForTests

Test-only: forget that recovery already ran, so the next call re-runs it.

resetAuthorizationRecoveryForTests()

sessionIdFromClientToken

The session a raw client token is for, or undefined if it cannot be read.

A token we cannot decode names no session, which refuses every comparison it takes part in. That is the right default: the comparisons decide whether this page may ask about, or be handed, somebody's authorization.

sessionIdFromClientToken(clientToken: any)
Parameters
clientToken (any)

armAuthorizationRecovery

Persist the breadcrumb, just before the customer is handed off to OPF.

Armed before the hand-off because that is the last point we are guaranteed to run: once the popup is open the tab may be backgrounded and reclaimed, and nothing scheduled after that — the render() continuation, the visibility listener, the poller — is guaranteed to execute again.

Every field is a non-secret identifier: which session was in flight, which flow on the page it belonged to, and when. The session's URLs and country are not among them — the read takes those from the client token the next page load is given.

armAuthorizationRecovery($0: Object)
Parameters
$0 (Object)
Name Description
$0.sessionId any
$0.paymentMethodCategory any
$0.instanceId any
$0.orderAmount any
$0.isOpf any

setRecoveryPromptContainer

Where load() rendered, so a recovery that resolves later can still prompt.

The prompt cannot be driven from load() alone. load() runs once, and on the page life that matters the page-load read usually answers "not finished" — the customer is still in the popup. The read that succeeds is the one the visibility listener fires when they come back, long after load() returned. Remembering the container is what lets that later answer reach the page.

setRecoveryPromptContainer(id: any, container: any)
Parameters
id (any)
container (any)

promptForRecovery

Put the modal up, if this page is entitled to speak for the marker.

The whole body is guarded. Rendering runs getTranslations, attachShadow and a good deal of DOM on a page we do not own, and none of it is worth an authorization: the modal only asks the customer to press pay, while the authorization is the thing that completes their order. Both callers are places where a throw would cost one — here it would reject the read before the answer is handed back, and from promptIfRecoveryWaiting it would escape into load().

promptForRecovery(id: any, tracker: any, entry: any)
Parameters
id (any)
tracker (any)
entry (any)

abandonAuthorizationRecovery

Give up on recovery for the rest of this page life.

Clearing the marker alone is not enough: a read the visibility listener already started keeps running, resolves after the live authorization has taken over, and repopulates the module state — where a later authorize takes it and delivers a second time. Latching consumed is safe because a teardown after this point starts a new page life, which resets all of it.

abandonAuthorizationRecovery()

clearAuthorizationRecovery

Drop the marker (flow resolved, recovered, discarded or expired).

clearAuthorizationRecovery()

readAuthorizationRecovery

Read and validate the marker, purging it if it is present but unusable.

localStorage is user-writable and can hold a marker written by an older version of this library, so a shape guard is not paranoia. Purging rather than ignoring matters: the TTL check below only runs for markers that parse, so an unparseable one would sit in storage forever, re-read and re-rejected on every page load.

readAuthorizationRecovery()

isOwnMarker

Whether this marker belongs to an authorization this page life started.

init() is called freely, including while a popup is open, and the marker armed on the way into that popup looks exactly like one left by a torn-down page. Acting on it would read the flow that is running right now and hand the merchant an authorization their own callback is about to receive from rpcCallback — the same order twice.

Recovery is for the page life that did not survive; this one is still here.

isOwnMarker(entry: any)
Parameters
entry (any)

readAuthorizationResult

Read the live authorization state through OPF's acquirer bridge.

The bridge is a hidden Klarna-origin iframe that asks PACS for a snapshot of the authorization (performGetClientStatus) rather than advancing it. OPF already uses it on visibilitychange to survive a merchant tab too throttled to receive the popup's completed postMessage; a torn-down tab is the same failure one step further along, and the same read answers it.

The call takes the client token rather than the session's claims: OPF reads the session id, the PACS base URL, the purchase country, the environment and the event base URL off it, so this page never names a session apart from the token that entitles it to ask about one. It also no longer takes an app URL — OPF derives the bridge document from its own script URL, which is what a page pinned to a branch build needs and what we used to work out here from the stored base_url.

getAuthorizationResult is not exported by the OPF library yet (it only exports preRender, render and errors). Until it is, this feature detects to a no-op and reports it, so shipping this is safe and it lights up on its own once OPF exposes the call.

readAuthorizationResult($0: Object)
Parameters
$0 (Object)
Name Description
$0.clientToken any
$0.entry any
$0.tracker any

attachVisibilityRecovery

Read again when the customer comes back to the tab.

The read is latched to once per page life, which is right for the case it was written for: a tab torn down after the customer approved, where the answer is already settled when the page loads. It is wrong for the case where the tab reloads while the customer is still in the popup. There the page-load read correctly reports the authorization is not finished, keeps the marker, and then nothing asks again — so the customer approves, returns to a checkout that never learned about it, and clicking pay opens a second popup.

visibilitychange rather than focus: returning from a popup on mobile Safari does not reliably fire focus, and OPF already standardises on visibility for this same problem. pageshow covers a restore from the back/forward cache, where no visibility change fires at all — a restore only, which is why persisted is checked: the same event fires on every ordinary load, and this listener is attached during one.

Only ever re-reads while the marker is still there, which preserves the purge-on-failure rule: every terminal outcome clears it, so the only states that can reach a second read are the two that deliberately keep it.

attachVisibilityRecovery($0: Object)
Parameters
$0 (Object)
Name Description
$0.id any
$0.sessionID any
$0.tracker any
$0.clientToken any

retryMissedVisibleReturn

Act on a return that arrived while a read was already out.

Waits out the re-read floor rather than ignoring it, so a customer flicking between tabs still cannot drive a read per flick, and everything else that bounds re-reads — the per-page cap, the marker, the consumed latch — is checked by the handler itself.

retryMissedVisibleReturn()

startAuthorizationRecovery

Start recovery for this page life, if there is anything to recover.

Kicked off from init(), a page-load entry point that runs before the customer can click anything. Doing the read here rather than on the click is what keeps the click path free of an extra network round trip that would otherwise sit between the customer's gesture and OPF opening its popup.

clientToken is the raw token the merchant handed this page life, and the read cannot happen without one. Callers that do not have it to hand omit it and it is taken from what init() stashed.

Resolves with the merchant-facing authorization result if one was recovered, or undefined. Never rejects.

startAuthorizationRecovery($0: Object)
Parameters
$0 (Object)
Name Description
$0.id any
$0.sessionID any
$0.tracker any
$0.clientToken any

matchesRequestedFlow

Whether a recovered authorization belongs to the flow asking for it.

None of this is an integrity boundary, and it is not meant to be: the marker lives in storage the merchant's page can write, so a script on that page can state whatever it likes in it. What that script cannot do is produce an authorization: the read is made with the client token the merchant handed this page, and the same script could simply call authorize() for that session itself. What stops an approval paying for the wrong basket in the end is PACS binding the authorization to its session and amount when the order is created. These checks are here to keep honest flows apart from each other.

A page can host several KP instances — different instance_ids, different payment method categories — and each has its own callback. Handing a recovery to the wrong one would give that merchant flow an authorization it did not ask for. Only an actual conflict blocks delivery: when either side did not specify a value there is nothing to disagree about, which keeps the single-instance case (the overwhelming majority, where the category is frequently absent under Utopia) working.

matchesRequestedFlow(recovered: any, match: any)
Parameters
recovered (any)
match (any)

noopTracker

For callers that take without a tracker to hand; nothing to report to.

noopTracker

takeRecoveredAuthorization

Take the recovered authorization, if one is waiting.

Delivery is at-most-once, and the marker is dropped here — before the merchant's callback runs, not after. Handing the authorization over is the job; what the merchant does with it afterwards is theirs, and a second teardown midway through their handler must not leave a marker that re-delivers the same authorization on the next page load. The trade is one lost recovery for a merchant whose handler was interrupted, against never double-delivering to a merchant whose handler is not idempotent.

takeRecoveredAuthorization(match: any, tracker: any)
Parameters
match (any)
tracker (any = noopTracker)

awaitRecoveredAuthorization

Wait for an in-flight recovery, then take it.

Used on the authorize path: the read was started at page load, so by the time the customer clicks it has almost always resolved and this returns immediately. When it has not — the customer clicked within a second or two of the page loading — waiting is still the right call, because the alternative is starting a second authorization on a session that is already authorized.

awaitRecoveredAuthorization(match: any, tracker: any)
Parameters
match (any)
tracker (any)

promptIfRecoveryWaiting

Wait for the read, then prompt if an authorization is sitting unclaimed.

load()'s half of the prompt. The read normally resolves before load() runs, and in that case the prompt attempt inside the read found no container yet and gave up; this is what catches it once there is one.

Routed through promptForRecovery rather than showing the modal directly, because the decision of whether this page may speak for the marker is not load()'s to make. Calling the renderer from here instead is what let a page tell the customer their purchase was approved while authorize() went on to correctly refuse it as belonging to a different checkout.

Deliberately does not take the authorization. Taking here would drop the marker and hand it to nobody — delivery belongs to authorize().

promptIfRecoveryWaiting(id: any, tracker: any)
Parameters
id (any)
tracker (any)

hasPendingAuthorizationRecovery

True when a marker exists that this page life may still be able to recover.

sessionId is the session the caller is about to authorize. Pass it whenever the caller knows it, because this gate decides whether the customer's pay click waits on a network read. matchesRequestedFlow refuses a marker whose session differs, so answering true for one of those buys nothing and costs the click: it reserves authorizationsInProgress and then awaits the acquirer-bridge read, which on a slow read spends the transient user activation that lets OPF open its popup. A shop that mints a fresh session on reload, and every control-arm session (allocation is by session, so a marker can only have been written by a variate session), hits exactly that case and can never claim the marker.

A caller with no session of its own omits it and gets the marker-exists-only answer, which is the best question it can ask.

hasPendingAuthorizationRecovery(sessionId: any)
Parameters
sessionId (any)

RECOVERY_EXPERIMENT

Arm from the state authorize() already has in hand.

Split out so the call site stays a single line and the mapping from kp-lib's memory shape to the marker lives next to the code that reads it back. Only the session and the flow it belongs to are persisted; the read takes the session's URLs, country and environment off the client token the next page load is given.

RECOVERY_EXPERIMENT

Type: string

RECOVERY_EXPERIMENT

The experiment this whole feature is allocated on.

The kpc in the middle is load-bearing, not a style choice. PACS mints the client token and only copies an experiment into it when the key starts with one of a fixed set of prefixes — kp-client, in-app-sdk, klarna-native-environment-shopping-browser, kpc or exp_ff_kpc. Anything else is evaluated server-side and then dropped, so the browser never sees it. New features are forced to begin exp_ff_, which rules out the bare kpc- / kp-client- forms that older keys like kpc-opf-v use, and leaves exp_ff_kpc as the only prefix we can still create under.

Renaming this constant means renaming the feature on the experimentation platform in the same change; the two are a matched pair.

RECOVERY_EXPERIMENT

Type: string

RECOVERY_VARIATE

getExperimentValue returns the variate name, so a control assignment comes back as the string 'control' — truthy. Checking the variate by name is what the rest of the library does (init.js gates the Utopia flow the same way), and it is the difference between an experiment that measures something and one that is on for everybody.

RECOVERY_VARIATE

Type: string

armFromAuthorize

Arming is the only gate the experiment needs. Without a marker there is no read, no modal and nothing to deliver, so one check here holds the whole feature shut rather than three that can drift apart.

Deliberately not gating the read or the delivery. A session armed while the experiment was on may come back after it has been turned down, and stranding that customer with an approved purchase nobody will hand over is worse than letting the allocation lapse. Markers expire within the hour, so turning the experiment off still drains cleanly.

Nothing is tracked when allocation is refused. This runs on every authorize of every session, and the control arm's size is already counted server-side where it costs no browser traffic.

armFromAuthorize($0: Object)
Parameters
$0 (Object)
Name Description
$0.decodedClientToken any
$0.paymentMethodCategory any
$0.instanceId any
$0.orderAmount any
$0.isOpf any
$0.experiments any

setRecoveryTokenContext

Remember this page's client token, decoded for armFromAuthorize and the prompt, raw for the read.

The raw one is kept because the read hands it to OPF whole: the token is what says this page may ask about the session, so re-encoding claims we decoded would defeat the point of asking with it.

setRecoveryTokenContext(id: any, decodedClientToken: any, rawClientToken: any)
Parameters
id (any)
decodedClientToken (any)
rawClientToken (any)

getRecoveryTokenContext

The decoded client token stashed by init(), if any.

getRecoveryTokenContext(id: any)
Parameters
id (any)

getRecoveryClientToken

The raw client token stashed by init(), if any.

getRecoveryClientToken(id: any)
Parameters
id (any)

isTopLevelWindow

Whether the customer is looking at the page this library is running in.

Cross-origin access to window.top throws, and a throw means there is a frame above us we cannot see into, which answers the question.

Takes the window to ask about rather than reading the global directly. window.top cannot be redefined — the HTML spec marks it unforgeable — so a seam here is the only way the framed and cross-origin answers can be exercised at all.

isTopLevelWindow
Parameters
view (any = window)

load

In this step the actual credit form is shown to the customer. First a pre-assessment is made, based on the known order details, to see if Klarna might offer the customer credit. If the pre-assessment passes, the credit form is rendered.

As a merchant, you choose when to present the form to the customer, probably when the customer selects Klarna Credit as payment method.

If you have received new or additional information about the customer (e.g. billing address) you can call the method again, which will update the current session.

load
Parameters
options (Object)
Name Description
options.container (HTMLElement | String) The container in which to render the application. Should be an HTML element or valid CSS selector.
options.payment_method_category String? The category of payment methods that should be loaded.
options.preferred_payment_method String? The payment method to be pre-selected, if possible.
data (Object?) An optional object with data to update on the session.
callback (load~callback) A function that will be called when the pre-assessment is completed.
Throws
  • ApplicationNotInitializedError: If called without options and prior to init .
  • InvalidContainerSelectorError: If options.container is neither an HTML element nor a valid CSS selector.
  • PaymentMethodCategoryNotSupportedError: If options.payment_method_category is not supported.
  • PreferredPaymentMethodNotSupportedError: If options.preferred_payment_method is not supported.
Example
try {
  Klarna.Payments.init({ client_token: '...' })
  Klarna.Payments.load({
    container: '#klarna-payments-container',
    payment_method_category: 'category'
  }, { // Data to be updated
    billing_address: {
      // ...
    }
  }, function (res) { // load~callback
    // ...
  })
} catch (e) {
  // Handle error. The load~callback will have been called
  // with "{ show_form: false }" at this point.
}

load~callback

Called with the result of the load operation.

load~callback
Parameters
res (Object) Response
Name Description
res.show_form Boolean A boolean indicating the result of the pre-assessment.
res.error Object Only available in case of solvable errors.
Example
Positive pre-assessment -> { show_form: true }
Negative pre-assessment -> { show_form: false }
Example error -> { show_form: true, error: { invalid_fields: ["billing_address.email"] } }

Handles opening the buyers protection link

openBuyersProtectionLink
Parameters
event (Event) The pointer event object
url (string) The URL to open

checkIfPrequalificationFlow

Checks if the ctaLink contains a prequal flow flag

checkIfPrequalificationFlow
Parameters
ctaLink (string) OSM CTA link
Returns
boolean:

loadPaymentReview

If your checkout offers the customer an opportunity to review the order after the payment step (e.g. an order review page) it can make sense to present the payment method the customer selected on a previous page. This gives the customer a change to review the payment method and its terms to the user.

Klarna supports this by offering a payment review widget in which all relevant data around the customers payment method is presented.

Note: This feature is currently only available in the United States.

loadPaymentReview
Parameters
options (Object)
Name Description
options.container (HTMLElement | String) The container in which to render the application. Should be an HTML element or valid CSS selector.
callback (loadPaymentReview~callback) A function that will be called when the operation is completed.
Throws
  • ApplicationNotInitializedError: If called without options and prior to init .
  • OperationNotSupportedError: If the operation is not supported for the current purchase country.
  • InvalidContainerSelectorError: If options.container is neither an HTML element nor a valid CSS selector.
Example
try {
  Klarna.Payments.init({ client_token: '...' })
  Klarna.Payments.loadPaymentReview({
    container: '#klarna-payments-container'
  }, function (res) { // loadPaymentReview~callback
    // ...
  })
} catch (e) {
  // Handle error. The loadPaymentReview~callback will have been called
  // with "{ show_form: false }" at this point.
}

loadPaymentReview~callback

Called with the result of the loadPaymentReview operation.

loadPaymentReview~callback
Parameters
res (Object) Response
Name Description
res.show_form Boolean A boolean indicating the result of the pre-assessment.

authorize

This is the final credit risk assessment. As a merchant, you will not handle the potentially sensitive information that is entered in the credit form, this information is only available to Klarna. A successful authorization guarantees that the order can be created within 60 minutes.

Upon authorizing the credit, Klarna will validate the input in the credit form. If there are any errors, the relevant fields are highlighted and corresponding error messages are shown.

The authorization is made with a client side call to authorize. The result of a successful authorization is an authorization_token that should be used when creating the order. The response object also includes a key called show_form, which indicates whether or not the Klarna payment option should remain available (i.e. success case or errors that the user can resolve) or if you should remove the option entirely (i.e. final rejection).

When new or additional information about the customer is received (e.g. billing address), authorize can be called again, which will update the current session.

Please note that the authorize call may trigger additional information requirements on the consumer. The callback can therefore be instant, take a very long time (i.e. the time it takes the customer to complete the form), or may never happen (if the consumer drops out). The integration should therefore not be relying on an immediate response, and should not implement any timeouts on the merchant side, but wait for the callback function to be called.

Also, to ensure that customers understand that some processing is going on in the background, when authorize is called, it should be visually shown that something is happening (e.g. a spinner in the button that triggered the authorize call).

authorize
Parameters
options (Object)
Name Description
options.payment_method_category String? The payment method category that was provided in the previous load call.
options.auto_finalize Boolean? An optional flag used to turn off auto-finalization for the direct bank transfer payment method
data (Object?) An optional object with data to update on the session.
callback (authorize~callback) A function that will be called when authorization is completed.
Throws
  • PaymentMethodCategoryNotSupportedError: If options.payment_method_category is not supported.
  • ApplicationNotLoadedError: If called prior to load .
Example
try {
  Klarna.Payments.authorize({
    payment_method_category: 'category'
  }, { // Data to be updated
    billing_address: {
      // ...
    }
  }, function (res) { // authorize~callback
    // ...
  })
} catch (e) {
  // Handle error. The authorize~callback will have been called
  // with "{ show_form: false, approved: false }" at this point.
}

authorize~callback

Called with the result of the authorize operation.

authorize~callback
Parameters
res (Object) Response
Name Description
res.authorization_token String If credit is approved, needed to place order.
res.show_form Boolean A boolean indicating whether to keep showing the form or to remove it.
res.approved Boolean A boolean indicating the result of the credit assessment.
res.finalize_required Boolean A boolean indicating that the finalize method should be called in order to complete the authorization.
res.error Object Only available in case of solvable errors.
Example
A successful authorization -> { authorization_token: "b4bd3423-24e3", approved: true, show_form: true }
An authorization that requires finalization -> { approved: true, show_form: true, finalize_required: true }
A rejected authorization with solvable errors -> { approved: false, show_form: true, error: { invalid_fields: ["billing_address.email"] } }
A rejected authorization (non-resolvable) -> { approved: false, show_form: false }

deliverRecoveredAuthorization

Hand a recovered authorization to the merchant.

Deliberately does not reuse rpcCallback, and deliberately does not emit authorize_completed. A recovery has no matching authorize_called from this page life, so counting it on the live-flow events would inflate the numerator of every authorize success-rate monitor and make them less likely to fire — masking real callback failures. Recovery gets its own event names instead, which is also the only way to filter it in a monitor.

deliverRecoveredAuthorization
Parameters
recovered (any)

PROMPT_ELEMENT_ID

Marks the shadow host so a second render replaces rather than duplicates.

PROMPT_ELEMENT_ID

Type: string

TOKENS

Values read off the Figma node this modal implements (Consumer-PINK-standard, 6369:75609), as design system token names so they can be traced back. Inlined because this renders inside a shadow root on the merchant's page and has no stylesheet to inherit them from.

TOKENS

MAX_Z_INDEX

The value docs/DEVELOPMENT_REFERENCE.md already calls house style for anything that must sit on top. Coequal with the legacy fullscreen iframe, which is fine: the recovered authorization is delivered and this closes before any other Klarna surface opens.

MAX_Z_INDEX

Type: string

HOST_STYLE

Written inline with !important rather than in the shadow stylesheet. :host rules are the weakest thing in the cascade, so a merchant's body > div { position: static !important } would beat them; an inline !important declaration is the strongest position available to us on a page we do not own. Longhand insets rather than inset, so a merchant top: 50px !important cannot win on a longhand we never set.

HOST_STYLE

KLARNA_BADGE

Exported straight from the Figma node rather than drawn by hand, so the glyphs are the real vector data the design uses. Inline rather than an because the modal must render with no network round trip and no CDN hostname, and built with createElementNS so this file stays free of markup parsing on a merchant page.

KLARNA_BADGE

STYLES

A shadow root blocks merchant selectors but not inheritance — font, color, letter-spacing, text-transform and friends all cross the boundary. all: initial on the outermost element cuts the whole class at once, so the rules below start from a known state.

Spacing, sizes and type follow the Figma node exactly; the space/N and radius/N token values are used as literal pixels.

STYLES

MIN_PROMPT_WIDTH

The smallest frame worth opening the modal in.

The overlay covers the viewport of the document it is appended to, which is the merchant's page when the library is loaded the usual way and the frame itself when a merchant embeds the widget in an iframe. A frame cut to the size of a pay button has no room for a dialog: the customer gets a sliver of a modal sitting on the button, which reads as a broken page rather than as a message, and the message is the entire point.

320 by 400 sits below every mainstream phone viewport and well above anything sized to hold a button or a banner.

MIN_PROMPT_WIDTH

Type: number

hasRoomForPrompt

Whether there is room to put the modal up.

Only frames are measured. A top-level window this small is a real browser window on a real device — a small phone held sideways, say — where the dialog scrolls inside the viewport and is still the best available answer. A frame this small was sized by the merchant to hold a widget, and nothing we draw inside it will look like a dialog.

hasRoomForPrompt()

lockPageScroll

Stop the page behind the modal from scrolling, and return the undo.

overflow: hidden on the body is the usual answer and iOS Safari ignores it — the checkout keeps scrolling underneath the overlay, which is the platform this feature exists for. Pinning the body with position: fixed is what actually holds, and it is what the legacy fullscreen iframe already does for the same reason. Pinning alone would jump the customer to the top of the checkout, so the body is offset by how far they had scrolled and put back afterwards.

lockPageScroll()

dismissPrompt

The teardown closure while the modal is up, and whether the customer has already dealt with it on this page. Module state rather than a closure because hideAuthorizationRecoveryPrompt is called from authorize(), which never saw the call that opened it.

dismissPrompt

showAuthorizationRecoveryPrompt

Tell the customer their purchase is approved and that the shop's own pay button will finish it.

On Standard KP there is no on_click to drive, so a recovered authorization sits waiting until the customer presses pay again — and a customer looking at an apparently unpaid checkout has no reason to. Without this they either abandon, believing the purchase failed, or press pay expecting to start over.

Shown as three steps rather than a paragraph because the thing that goes wrong here is the customer thinking they are finished. Two ticks and one empty circle say where they actually are faster than a sentence does, and the outstanding step is the only one in bold.

Attached to document.body, not to the merchant's container: a container with overflow: hidden or a transformed ancestor would clip a fixed overlay to the widget. container is still required, as the signal that a Standard KP widget exists on this page — KEC never registers one, so KEC keeps its own treatment and gets no modal.

Rendered into a shadow root and built with textContent, so merchant CSS cannot reach in and nothing here can inject into a merchant page.

showAuthorizationRecoveryPrompt($0: Object)
Parameters
$0 (Object)
Name Description
$0.container any
$0.locale any
$0.tracker any

hideAuthorizationRecoveryPrompt

Remove the modal, whether the customer dismissed it or the authorization was delivered underneath it.

Delivery is the default because authorize() calls this with no argument; it is the one caller that is not a dismissal, so it reports nothing and deliberately does not latch, since there is nothing left to re-show.

hideAuthorizationRecoveryPrompt(reason: any)
Parameters
reason (any = DISMISSED_BY_DELIVERY)

reauthorize

If your checkout offers the customer to change any details of the order (e.g. customer details, shopping cart, shipping, discount code, gift cards etc.) after the payment step (e.g. an order review page) you will need to reauthorize if the customer changes anything.

This as the authorization_token you received originally is only valid for that specific state of the order (e.g. changes to the order will be rejected, if not authorized).

On such an order review page you typically don't have a payment selector, and hence the standard way of authorizing (init + load + authorize) is not possible here. Reauthorize can be used without a payment selector, and any necessary customer communication will be handled in fullscreen modals.

We recommend that you trigger reauthorize as few times as possible, but as many times as necessary. We would suggest that you store when a change has happened, and if so is the case, you trigger reauthorize once the customer clicks on "Complete order" (i.e. only if a change has happened). As with authorize, you can provide an optional update object including all order details. It is however also possible to chain this in a way where you start with a server-side session update per REST API, followed by an empty client-side call to reauthorize.

Please note that the reauthorize call may trigger confirmation from the consumer on changed financing details. The callback can therefore be instant, take a very long time (i.e. the time it takes the customer to complete the form), or may never happen (if the consumer drops out). The integration should therefore not be relying on an immediate response, and should not implement any timeouts on the merchant side, but wait for the callback function to be called.

Also, to ensure that customers understand that some processing is going on in the background, when reauthorize is called, it should be visually shown that something is happening (e.g. a spinner in the button that triggered the reauthorize call).

If on a different page than where you originally ran init, Klarna Payments needs to be initialized.

reauthorize
Parameters
options (Object)
Name Description
options.payment_method_category String? The payment method category of the previously authorized payment method.
data (Object?) An optional object with data to update on the session.
callback (reauthorize~callback) A function that will be called when the reauthorization is completed.
Throws
  • ApplicationNotInitializedError: If called prior to init .
  • PaymentMethodCategoryNotProvidedError: If options.payment_method_category is not provided when required.
  • PaymentMethodCategoryNotSupportedError: If options.payment_method_category is not supported.
Example
try {
  Klarna.Payments.init({ client_token: '...' })
  Klarna.Payments.reauthorize({
    payment_method_category: 'category'
  }, { // Data to be updated
    billing_address: {
      // ...
    }
  }, function (res) { // reauthorize~callback
    // ...
  })
} catch (e) {
  // Handle error. The reauthorize~callback will have been called
  // with "{ show_form: false, approved: false  }" at this point.
}

reauthorize~callback

Called with the result of the reauthorize operation.

a) Successful reauthorize Response: { approved: true, authorization_token: string } Continue with the order placement.

b) Rejected reauthorize Response: { approved: false } Do not do anything. As the failed reauthorize is triggered by a customer change, the customer might want to change the details back in order to be eligible for credit again. Appropriate error messages will be handled by Klarna.

Note: Reauthorize currently also includes { show_form: true/false } in the response. This is deprecated and shall not be used. Instead only a { approved: true, authorization_token: string } response should require any action for reauthorize.

reauthorize~callback
Parameters
res (Object) Response
Name Description
res.authorization_token String If credit is approved, needed to place order.
res.show_form Boolean A boolean indicating whether to keep showing the form or to remove it.
res.approved Boolean A boolean indicating the result of the credit assessment.
res.error Object Only available in case of solvable errors.
Example
A successful reauthorization -> { authorization_token: "b4bd3423-24e3", approved: true }
A rejected reauthorization due to an invalid update -> { approved: false, error: { invalid_fields: ["billing_address.email"] } }
A rejected or aborted reauthorization -> { approved: false }

finalize

A finalization will be required for some payment methods. Whenever a finalization is required, the authorize call will return finalize_required: true. The finalize response will then contain the authorization token if the user successfully completes required steps.

The finalization should be done just before the purchase is completed, meaning the last step in a multi-step checkout.

finalize
Parameters
options (Object)
Name Description
options.payment_method_category String? The payment method category of the authorized payment method.
data (Object?) An optional object with data to update on the session.
callback (finalize~callback) A function that will be called when the finalization is completed.
Throws
  • ApplicationNotInitializedError: If called prior to init .
  • PaymentMethodCategoryNotProvidedError: If options.payment_method_category is not provided when required.
  • PaymentMethodCategoryNotSupportedError: If options.payment_method_category is not supported.
Example
try {
  Klarna.Payments.init({ client_token: '...' })
  Klarna.Payments.finalize({
    payment_method_category: 'category'
  }, { // Data to be updated
    billing_address: {
      // ...
    }
  }, function (res) { // finalize~callback
    // ...
  })
} catch (e) {
  // Handle error. The finalize~callback will have been called
  // with "{ show_form: false, approved: false }" at this point.
}

finalize~callback

Called with the result of the finalize operation.

finalize~callback
Parameters
res (Object) Response
Name Description
res.authorization_token String If credit is approved, needed to place order.
res.show_form Boolean A boolean indicating whether to keep showing the form or to remove it.
res.approved Boolean A boolean indicating the result of the credit assessment.
res.error Object Only available in case of solvable errors.
Example
A successful finalization -> { authorization_token: "b4bd3423-24e3", approved: true, show_form: true }
A rejected finalization due to an invalid update -> { approved: false, show_form: true, error: { invalid_fields: ["billing_address.email"] } }
A rejected or aborted finalization -> { approved: false, show_form: false }

on

Registers an event handler for the given eventName. The events are triggered internally in Klarna Payments. The supported events are:

  • heightChanged: Emitted when the height of the iframe changes. The registered event handler is called with the new height (in pixels) as a number (integer).
  • fullscreenOverlayShown: Emitted when the fullscreen overlay is shown.
  • fullscreenOverlayHidden: Emitted when the fullscreen overlay is hidden.
on
Parameters
eventName (String) The name of the event to which you want to subscribe.
eventHandler (on~eventHandler) The function that should be called when the event is emitted.
Throws
  • EventNotSupportedError: If trying to register an unsupported event.
Example
Klarna.Payments.on('heightChanged', function (newHeight) {
  console.log('got new iframe height', newHeight)
})

on~eventHandler

Called whenever the associated event is emitted inside Klarna Payments.

on~eventHandler
Parameters
payload (Object?) The payload may vary between events. See the list of supported events for details.

indexOf

Only send logs if external events are subscribed to. Otherwise we'll send a lot of logs that are HPP specific. https://docs.klarna.com/klarna-payments/in-depth-knowledge/klarna-payments-sdk-reference/#oneventhandler

indexOf

indexOf

Only send logs if external events are unsubscribed from. Otherwise we'll send a lot of logs that are HPP specific. https://docs.klarna.com/klarna-payments/in-depth-knowledge/klarna-payments-sdk-reference/#oneventhandler

indexOf

off

Unregisters an event handler for the given eventName.

off
Parameters
eventName (String) The name of the event from which you want to unsubscribe.
eventHandler (on~eventHandler?) The function that was previously registered for the eventName . Omit if you want to unregister all handlers for the eventName .
Example
var theEventHandler = function () { ... }
Klarna.Payments.on('heightChanged', theEventHandler)

// unregister this specific listener for heightChanged
Klarna.Payments.off('heightChanged', theEventHandler)

// unregister _all_ listeners for heightChanged
Klarna.Payments.off('heightChanged')

misreadPayload

A two-argument call is read as (data, callback) above, so an options object passed in the first position is silently taken as the order payload: a merchant asking for multi-step gets single-step, with no error and no warning. The three-argument call fails the same way with the first two arguments swapped -- whichever object lands in the payload position is read as the payload, either way. authorize-button takes fixed positional arguments and rejects a missing callback, so it cannot misread this way.

Changing how the arguments are read would break integrators relying on the current reading, so this only reports the case.

Returns the object a swallowed option key would be found on, or null when nothing usable landed in the payload position.

misreadPayload
Parameters
arg1 (any)
arg2 (any)
arg3 (any)

misreadOptionKeys

Split from misreadPayload so the reported value can be read off the same object the keys were found on. Reading it from arg1 unconditionally would report undefined for the three-argument form, where the swallowed key sits in arg2.

Returns the option keys that were swallowed, empty when there are none.

misreadOptionKeys
Parameters
payload (any)

reportableAutoFinalize

Narrows a value read off an integrator object to something safe to send. auto_finalize is documented as a boolean, but this event fires precisely because the integrator is using the API wrongly, so the value is the least safe thing here to assume a type for. It could be a large object, a circular reference, or a string carrying personal data.

Allowlists the shapes that mean anything -- absent, a boolean, or the strings the rest of this file already compares against -- and reduces everything else to its type. Deliberately stricter than truncating strings: a truncated string still carries whatever its first characters were, and this field has a legitimate domain of five values -- nothing set at all, true, false, and the two strings 'true' and 'false'. "They passed an object" is the diagnostic content; the object is not.

reportableAutoFinalize
Parameters
value (any)

readOwnDataProperty

Reads an own data property without ever running integrator code. arg1 comes straight from the merchant page, so a plain arg1.auto_finalize would invoke an accessor -- side effects and all -- or throw, and a Proxy can throw from the descriptor trap too. Telemetry must never break the flow it is observing, and must never be observable by it either.

Returns undefined for an accessor, so an accessor and a genuinely absent key are indistinguishable downstream. That is the intended trade: the field is context on a misuse we already detected, not the detection itself.

readOwnDataProperty
Parameters
target (any)
key (any)

NO_RESULT_BACKSTOP_MS

Deliberately generous backstop for the "no result" watchdogs.

The purchase flow legitimately takes minutes -- authentication inside the popup was measured at ~110s in one observed production session -- so a short delay would emit a no-result event for shoppers who are merely still authenticating. That is the failure mode of kec_popup_window_blocked_error, which fires on ~44% of instances and is unusable for alerting.

Every event carries elapsed_ms, so this threshold is meant to be tightened from the observed distribution rather than tuned up front.

NO_RESULT_BACKSTOP_MS

createNoResultWatchdog

Watches for an outcome that never arrives.

The SDK only reports an outcome when a promise settles or a callback runs. If the purchase flow never answers there is no code path that fires at all, so a total failure and a shopper who is still deciding look identical. This arms two signals for that case and guarantees onNoResult runs at most once:

  • pagehide -- the page hosting the pending outcome is going away, so the outcome cannot arrive in this page's lifetime. Fires with persisted === true when the page is only frozen into the back/forward cache, which is NOT a lost outcome: the page can be restored and the callback can still run, so that case is ignored and the timer left armed. Note a page navigated away and returned to via an integrator success URL also lands here; reason keeps the two populations separable downstream.
  • a backstop timer -- covers the case where pagehide never fires, e.g. the tab is discarded by the browser.

Note we deliberately do NOT listen for visibilitychange: the merchant page is backgrounded the moment the purchase-flow popup opens, so hidden would fire on essentially every session and the signal would be pure noise.

createNoResultWatchdog(params: Object): {settle: Function}
Parameters
params (Object = {})
Name Description
params.win Window (default window) Window to attach to; defaults to the global.
params.onNoResult Function Called at most once with { reason, elapsed_ms } when no outcome arrived.
params.backstopMs number (default NO_RESULT_BACKSTOP_MS) Backstop delay; defaults to NO_RESULT_BACKSTOP_MS .
params.enabled boolean (default true) Set false when the observed flow navigates the integrator's page away on its happy path -- nothing there is observable, so an inert handle is returned.
Returns
{settle: Function}: Call settle() as soon as the outcome arrives -- it clears the timer, detaches the listener, and makes any later firing a no-op. It returns true when a no-result had already been reported, so the caller can mark that outcome as a late arrival and the false-positive rate stays measurable.

reservePopupWindow

Reserves the KEC popup window through OPF's preRender() and reports which of the three outcomes happened.

preRender() has to run while the click's user activation is still valid, so this sits synchronously in the click handler. The window it opens shows a loader and waits for OPF's render() to pick it up.

Shared by authorize-button and load-button because the accounting these events feed -- reserved, minus collected by render(), minus deliberately abandoned, leaves orphaned -- only holds if both entry points report identically.

Note the control flow: only preRender() itself is inside the try. The catch reports a blocked popup, and that conclusion is only sound for a throw from preRender() -- it cannot tell which statement failed. With the telemetry inside the try, a throw from tracker.event() on the success path would have been recorded as a blocked popup: one pre_opened lost and one false blocked_error gained, so both sides of the ratio would move in opposite directions from a single incident, invisibly. Leaving preRenderOpened as undefined when preRender() throws also means neither branch below fires, so the catch cannot emit a duplicate event either.

reservePopupWindow($0: Object)
Parameters
$0 (Object)
Name Description
$0.win any
$0.tracker any