Payment & E-Commerce

Swish Handel 2026: Integration Patterns for Node Applications

By Technspire Team
February 12, 2026
12 views

Swish is the default Swedish payment method, and Swish Handel. The merchant API. Is the backbone of countless Swedish e-commerce flows. Integrating it well in a modern Node or Next.js application means more than hitting the payment endpoint; it means certificate lifecycle, idempotent callback handling, and a reconciliation strategy that survives the occasional provider outage. This is the integration pattern that production Swish flows should use in 2026.

The Swish Handel Flow in One Picture

  1. Your server creates a payment request against the Swish API, presenting a client certificate. Swish responds with a payment reference.
  2. Your UI either shows the user a QR code (desktop) or deep-links to the Swish app (mobile).
  3. The user approves in the Swish app. Swish posts a callback to the URL you supplied, announcing the outcome.
  4. Your server marks the order paid, runs downstream effects, and responds to the user's polling request or redirects the session.

Certificates Are the Hardest Operational Part

Swish uses mutual TLS. You are issued a client certificate (PKCS#12) that identifies your merchant. Operationally, this means:

  • Certificate rotation has a finite deadline. Put the expiry on the calendar the day you receive it.
  • Never bake the cert into a container image. Store it in Azure Key Vault and mount at runtime.
  • Staging and production certificates are separate. Never share; never promote code that hard-codes an environment choice.
  • Certificate loading should happen once, at process start. Not per request.

The Payment Request

import https from 'node:https';
import fs from 'node:fs';

const agent = new https.Agent({
  pfx: fs.readFileSync(process.env.SWISH_CERT_PATH!),
  passphrase: process.env.SWISH_CERT_PASSPHRASE!,
});

async function createSwishPayment(order: Order): Promise<string> {
  // instructionUUID is our idempotency key — generate once per attempt
  const instructionUUID = randomUUID().replace(/-/g, '').toUpperCase();

  const res = await fetch(
    `${SWISH_BASE}/api/v2/paymentrequests/${instructionUUID}`,
    {
      method: 'PUT',
      agent,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        payeePaymentReference: order.id,
        callbackUrl: `${process.env.APP_URL}/api/swish/callback`,
        payerAlias: order.phoneNumber,     // omit for QR-code flows
        payeeAlias: process.env.SWISH_MERCHANT_NUMBER!,
        amount: order.total.toFixed(2),
        currency: 'SEK',
        message: `Order ${order.reference}`.slice(0, 50),
      }),
    },
  );

  if (res.status !== 201) throw new Error(`Swish error ${res.status}`);

  // Persist the mapping immediately — before any user-facing response
  await prisma.swishPayment.create({
    data: { instructionUUID, orderId: order.id, status: 'CREATED' },
  });
  return instructionUUID;
}

Callback Handling

Swish posts the outcome to your callback URL. The handler must be idempotent. Swish retries on non-200 responses, and the same callback can arrive twice.

// app/api/swish/callback/route.ts
export async function POST(req: Request) {
  const body = await req.json();
  const { id, status, amount, paymentReference } = body; // id = instructionUUID

  await prisma.$transaction(async (tx) => {
    const payment = await tx.swishPayment.findUnique({
      where: { instructionUUID: id },
    });
    if (!payment) return;              // unknown ID — acknowledge and move on
    if (payment.status === status) return; // idempotent — already processed

    await tx.swishPayment.update({
      where: { instructionUUID: id },
      data: { status, paymentReference, paidAt: status === 'PAID' ? new Date() : null },
    });

    if (status === 'PAID') {
      await tx.order.update({
        where: { id: payment.orderId },
        data: { paidAt: new Date() },
      });
      // Enqueue downstream effects via outbox — do not publish inline
      await tx.outboxEvent.create({
        data: {
          aggregate: 'Order',
          aggregateId: payment.orderId,
          type: 'OrderPaid',
          payload: JSON.stringify({ orderId: payment.orderId, amount }),
        },
      });
    }
  });

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

Reconciliation. Because Callbacks Will Fail

Network partitions, deploys, firewall changes. Eventually a callback will not reach you. Run a reconciliation job that polls payments in CREATED status older than a threshold and queries Swish for their current state. This converts callback-driven completion into a belt-and-braces system.

Design for the Failure Modes

  • User declines in the Swish app. Swish posts a DECLINED callback. The UI must handle it without leaving the user on a stuck payment page.
  • User never opens the Swish app. The payment eventually expires. Design the UI to time out gracefully and offer a retry.
  • Callback arrives before your HTTP response settles. With fast Swish callbacks and slow ORMs, the callback may land while the payment row is still being written. Create the row before initiating the request.
  • Duplicate callbacks. Always possible; design the handler to do nothing when the state is already set.

PCI and Regulatory Scope

Swish does not expose card data through this flow; the payment is bank-to-bank via the Swish infrastructure. That keeps your PCI scope narrower than a card integration would. The identity and data-residency dimensions remain: payer phone numbers and payment references are personal data under GDPR, and retention policies apply.

The Minimal Production Checklist

  • Certificate stored in Key Vault, rotated on a tracked schedule.
  • Payment request uses an idempotency key (instructionUUID) persisted before the request returns.
  • Callback handler is idempotent and wraps business effects in a transaction.
  • Reconciliation job runs at least every few minutes for payments in a non-terminal state.
  • Downstream effects go through an outbox, not inline from the callback.
  • UI handles declined, expired, and timed-out cases with clear messaging and retry.

Ready to Transform Your Business?

Let's discuss how we can help you implement these solutions and achieve your goals with AI, cloud, and modern development practices.

No commitment required • Expert guidance • Tailored solutions