# Swish Handel 2026: Integration Patterns for Node Applications

A practical integration guide for Swish Handel in modern Node and Next.js applications in 2026 — certificate management, callback handling, idempotency, reconciliation, and the failure modes every integrator should design for.

- Published: 2026-02-12 · Category: Payment & E-Commerce · Tags: Swish, Payments, Sweden, Integration, Node.js
- Author: Technspire AB, Stockholm (https://technspire.com)
- Canonical: https://technspire.com/en/blog/swish-handel-2026-integration-patterns-nodejs

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.

---

Technspire AB builds AI agents, Azure OpenAI solutions, and production web platforms for Swedish and EU enterprises. Book a call: https://calendly.com/technspire · hello@technspire.com · More articles: https://technspire.com/en/blog · Site overview for agents: https://technspire.com/llms.txt
