Skip to main content
Business & GrowthPayment Services287 lines

Klarna

Accept payments with Klarna. Use this skill when the project needs to integrate

Quick Summary27 lines
You are a payments specialist who integrates Klarna into projects. Klarna is a
buy-now-pay-later (BNPL) provider that also offers direct payments, installment
plans, and a full hosted checkout. Klarna takes on the credit risk — merchants
get paid upfront regardless of the customer's payment schedule.

## Key Points

- Portal: `portal.playground.klarna.com`
- API: `api.playground.klarna.com`
- Test persona for US: use any name/address, Klarna auto-approves in playground
- Test with different countries to see available payment methods change
- Capture orders promptly — Klarna authorizations expire (typically 28 days)
- For digital goods, capture immediately after order placement
- Always include `order_lines` with accurate item details — Klarna uses them for risk assessment
- Use `merchant_reference1` to link Klarna orders to your internal order IDs
- Provide accurate `purchase_country` — it determines which payment methods appear
- Handle `fraud_risk.stopped` — Klarna may reject orders after initial approval
- Test with multiple countries in playground to verify localized payment methods
- Not capturing orders — Klarna will auto-release the authorization and you lose the sale

## Quick Example

```typescript
await klarnaRequest('POST', `/ordermanagement/v1/orders/${klarnaOrderId}/cancel`);
```
skilldb get payment-services-skills/KlarnaFull skill: 287 lines
Paste into your CLAUDE.md or agent config

Klarna Payment Integration

You are a payments specialist who integrates Klarna into projects. Klarna is a buy-now-pay-later (BNPL) provider that also offers direct payments, installment plans, and a full hosted checkout. Klarna takes on the credit risk — merchants get paid upfront regardless of the customer's payment schedule.

Core Philosophy

Klarna pays you upfront

When a customer chooses "Pay in 3" or "Pay later", Klarna pays the merchant immediately. Klarna collects from the customer over time. This eliminates credit risk for the merchant.

Two integration paths

Klarna Payments embeds Klarna as a payment option alongside other methods. Klarna Checkout is a full hosted checkout that handles the entire purchase flow. Choose Payments for flexibility, Checkout for speed.

Orders must be acknowledged and captured

Klarna separates authorization from capture. You must capture (acknowledge the order) when you ship goods. For digital goods, capture immediately.

Setup

API credentials

Klarna uses HTTP Basic Auth with your API credentials (username/password) from the Klarna Merchant Portal. There is no official Node.js SDK — use HTTP directly or a wrapper library.

const KLARNA_API_BASE = process.env.KLARNA_ENV === 'production'
  ? 'https://api.klarna.com'
  : 'https://api.playground.klarna.com';

const KLARNA_AUTH = Buffer.from(
  `${process.env.KLARNA_USERNAME}:${process.env.KLARNA_PASSWORD}`
).toString('base64');

async function klarnaRequest(method: string, path: string, body?: object) {
  const res = await fetch(`${KLARNA_API_BASE}${path}`, {
    method,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Basic ${KLARNA_AUTH}`,
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) throw new Error(`Klarna ${res.status}: ${await res.text()}`);
  return res.status === 204 ? null : res.json();
}

Key Techniques

Klarna Payments — Create a session

const session = await klarnaRequest('POST', '/payments/v1/sessions', {
  purchase_country: 'US',
  purchase_currency: 'USD',
  locale: 'en-US',
  order_amount: 5000, // $50.00 in minor units
  order_tax_amount: 0,
  order_lines: [
    {
      name: 'Pro Plan — Monthly',
      quantity: 1,
      unit_price: 5000,
      total_amount: 5000,
      tax_rate: 0,
      total_tax_amount: 0,
    },
  ],
});

// Return session.client_token and session.payment_method_categories to frontend

Frontend — Klarna Payments widget

<script src="https://x.klarnacdn.net/kp/lib/v1/api.js"></script>

<div id="klarna-container"></div>

<script>
  Klarna.Payments.init({ client_token: clientToken });

  // Load the widget for a specific payment method
  Klarna.Payments.load(
    {
      container: '#klarna-container',
      payment_method_category: 'pay_later', // or 'pay_over_time', 'direct_debit'
    },
    (res) => {
      if (res.show_form) {
        // Widget loaded successfully
      }
    }
  );

  // When user clicks "Place Order"
  async function authorize() {
    Klarna.Payments.authorize(
      { payment_method_category: 'pay_later' },
      {
        purchase_country: 'US',
        purchase_currency: 'USD',
        locale: 'en-US',
        order_amount: 5000,
        order_tax_amount: 0,
        order_lines: [
          {
            name: 'Pro Plan — Monthly',
            quantity: 1,
            unit_price: 5000,
            total_amount: 5000,
            tax_rate: 0,
            total_tax_amount: 0,
          },
        ],
      },
      async (res) => {
        if (res.approved) {
          // Send res.authorization_token to your server to place the order
          await fetch('/api/klarna/place-order', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ authorizationToken: res.authorization_token }),
          });
        }
      }
    );
  }
</script>

Place an order (server-side)

const order = await klarnaRequest(
  'POST',
  `/payments/v1/authorizations/${authorizationToken}/order`,
  {
    purchase_country: 'US',
    purchase_currency: 'USD',
    locale: 'en-US',
    order_amount: 5000,
    order_tax_amount: 0,
    order_lines: [
      {
        name: 'Pro Plan — Monthly',
        quantity: 1,
        unit_price: 5000,
        total_amount: 5000,
        tax_rate: 0,
        total_tax_amount: 0,
      },
    ],
    merchant_reference1: orderId,
    merchant_urls: {
      confirmation: 'https://yourdomain.com/confirmation',
      notification: 'https://yourdomain.com/api/klarna/webhook',
    },
  }
);

// Store order.order_id for capture/refund

Capture an order (on fulfillment)

await klarnaRequest('POST', `/ordermanagement/v1/orders/${klarnaOrderId}/captures`, {
  captured_amount: 5000,
  description: 'Shipped order',
  order_lines: [
    {
      name: 'Pro Plan — Monthly',
      quantity: 1,
      unit_price: 5000,
      total_amount: 5000,
      tax_rate: 0,
      total_tax_amount: 0,
    },
  ],
});

Refund a capture

await klarnaRequest('POST', `/ordermanagement/v1/orders/${klarnaOrderId}/refunds`, {
  refunded_amount: 5000,
  description: 'Customer requested refund',
  order_lines: [
    {
      name: 'Pro Plan — Monthly',
      quantity: 1,
      unit_price: 5000,
      total_amount: 5000,
      tax_rate: 0,
      total_tax_amount: 0,
    },
  ],
});

Cancel an uncaptured order

await klarnaRequest('POST', `/ordermanagement/v1/orders/${klarnaOrderId}/cancel`);

Webhook Processing

EventAction
order.approvedOrder authorized, ready to capture
order.capturedFunds captured, confirm fulfillment
order.cancelledOrder cancelled
order.refundedRefund processed
fraud_risk.stoppedKlarna rejected the order
dispute.createdChargeback initiated
export async function POST(req: Request) {
  const body = await req.json();

  // Klarna webhooks are verified by registering a webhook URL in the portal
  // and optionally checking the Klarna-Idempotency-Key header

  switch (body.event_type) {
    case 'order.approved':
      // For digital goods, capture immediately
      await captureOrder(body.event_data.order_id);
      break;

    case 'order.captured':
      await markOrderFulfilled(body.event_data.order_id);
      break;

    case 'fraud_risk.stopped':
      await cancelInternalOrder(body.event_data.order_id);
      break;

    case 'dispute.created':
      await handleDispute(body.event_data.order_id);
      break;
  }

  return new Response('OK', { status: 200 });
}

Testing

Klarna playground environment:

  • Portal: portal.playground.klarna.com
  • API: api.playground.klarna.com
  • Test persona for US: use any name/address, Klarna auto-approves in playground
  • Test with different countries to see available payment methods change

Best Practices

  • Capture orders promptly — Klarna authorizations expire (typically 28 days)
  • For digital goods, capture immediately after order placement
  • Always include order_lines with accurate item details — Klarna uses them for risk assessment
  • Use merchant_reference1 to link Klarna orders to your internal order IDs
  • Provide accurate purchase_country — it determines which payment methods appear
  • Handle fraud_risk.stopped — Klarna may reject orders after initial approval
  • Test with multiple countries in playground to verify localized payment methods

Anti-Patterns

  • Not capturing orders — Klarna will auto-release the authorization and you lose the sale
  • Omitting order_lines or using placeholder data — increases decline rates
  • Assuming all payment methods are available everywhere — availability varies by country
  • Ignoring Klarna's fraud rejection webhooks — you ship goods that Klarna won't pay for
  • Using Klarna for subscription billing without separate authorization per cycle
  • Capturing more than the authorized amount — this will fail
  • Not testing with the playground environment — live credentials have real financial impact

Install this skill directly: skilldb add payment-services-skills

Get CLI access →