Tre API:er, två certifikattyper
Det som kallas Swish-integration är oftast ett av tre flöden:
- Betalningsförfrågningar (Swish Commerce API) – kunden betalar dig, i appen eller på webben.
- Återbetalningar (samma API) – du betalar tillbaka hela eller delar av en tidigare betalning.
- Utbetalningar (Swish Payout API) – du betalar ut pengar till en privatperson, till exempel vinster, ersättningar eller uttag.
Alla anrop går från din server med ömsesidig TLS (mTLS): du presenterar ett klientcertifikat som är kopplat till ditt Swish-nummer, och du litar på Swish rot-CA. Certifikaten hanteras i Swish Certificate Management, där den som banken har registrerat för företaget loggar in med BankID. För utbetalningar behövs dessutom ett separat signeringscertifikat som används för att signera varje utbetalning.
Avtalet tecknas med er bank. Utan avtal och Swish-nummer kan du ändå bygga och testa allt mot simulatorn MSS, som beskrivs längre ner.
M-handel eller e-handel?
Båda använder samma endpoint. Skillnaden är om du skickar med betalarens telefonnummer (payerAlias) eller inte.
- M-handel (utan
payerAlias): du får tillbaka en token som öppnar Swish-appen direkt på samma enhet, eller som kan visas i en QR-kod. Användaren skickas tillbaka till din app efteråt. Det här är Swish rekommendation för appar och för mobilwebb. - E-handel (med
payerAlias): användaren skriver in sitt nummer, och betalningen dyker upp när hen själv öppnar Swish. Ingen automatisk återgång till din app. Passar desktop, där alternativet är att visa en QR-kod.
I en React Native-app är svaret nästan alltid m-handel med appbyte.
Skapa en betalningsförfrågan
I version 2 av API:et skapar du själv id:t för betalningen och gör en PUT mot /swish-cpcapi/api/v2/paymentrequests/{instructionUUID}. Det gör anropet idempotent: tappar du svaret kan du göra om samma PUT med samma id i stället för att riskera en dubbel betalning. Spara därför id:t i databasen innan du anropar Swish.
import { Agent, request } from "undici";
import { randomUUID } from "node:crypto";
import { readFileSync } from "node:fs";
const SWISH_URL = process.env.SWISH_URL!; // MSS i test, cpc.getswish.net i produktion
const swish = new Agent({
connect: {
pfx: readFileSync(process.env.SWISH_PFX_PATH!),
passphrase: process.env.SWISH_PFX_PASSPHRASE!,
ca: readFileSync(process.env.SWISH_CA_PATH!),
},
});
export async function createPaymentRequest(order: { id: string; amountSek: number; message: string }) {
// 32 hex-tecken med versaler, samma format som i Swish egna exempel
const instructionUUID = randomUUID().replace(/-/g, "").toUpperCase();
const callbackIdentifier = randomUUID().replace(/-/g, "").toUpperCase();
await savePayment({ orderId: order.id, instructionUUID, callbackIdentifier }); // först!
const res = await request(
SWISH_URL + "/swish-cpcapi/api/v2/paymentrequests/" + instructionUUID,
{
method: "PUT",
dispatcher: swish,
headers: { "content-type": "application/json" },
body: JSON.stringify({
payeeAlias: process.env.SWISH_NUMBER, // ert Swish-nummer, 123...
payeePaymentReference: order.id,
amount: order.amountSek, // räknas alltid fram på servern
currency: "SEK",
message: order.message,
callbackUrl: "https://api.exempel.se/swish/callback",
callbackIdentifier,
}),
}
);
if (res.statusCode !== 201) {
throw new Error("Swish svarade " + res.statusCode + ": " + (await res.body.text()));
}
await res.body.dump();
// Bara m-handel: token för att öppna Swish-appen
const token = res.headers["paymentrequesttoken"] as string | undefined;
return { instructionUUID, token };
}Svaret 201 Created innehåller headern PaymentRequestToken när du inte skickade payerAlias. Beloppet ska alltid räknas ut på servern – ta aldrig emot ett belopp från klienten.
Öppna Swish-appen från React Native
Appbytet sker med URL-schemat swish://paymentrequest?token=...&callbackurl=.... callbackurl är dit Swish (eller BankID) skickar användaren efteråt, och den måste URL-kodas. Med Expo:
import * as Linking from "expo-linking";
export async function openSwish(token: string, instructionUUID: string) {
// Djuplänk tillbaka till appen, med betalningens id så att vi kan återuppta kassan
const back = Linking.createURL("betalning", { queryParams: { id: instructionUUID } });
const url =
"swish://paymentrequest?token=" + encodeURIComponent(token) +
"&callbackurl=" + encodeURIComponent(back);
try {
await Linking.openURL(url);
} catch {
// Swish saknas på enheten: visa QR-kod eller be om telefonnummer (e-handel)
throw new Error("SWISH_NOT_INSTALLED");
}
}- Vill du fråga om Swish finns installerat med
Linking.canOpenURLmåsteswishfinnas iLSApplicationQueriesSchemespå iOS (i Expo viaios.infoPlist). På Android 11 och senare krävs en deklaration av paketsynlighet i manifestet. Enklare är ofta att försöka öppna och fånga felet, som ovan. - Appen behöver ett eget URL-schema (
schemeiapp.json) för att kunna ta emot återhoppet. - Återhoppet är ingen bekräftelse. Swish skriver uttryckligen att operativsystemet styr vart callback-URL:en öppnas. Visa en vänteskärm och fråga din egen backend om status.
På desktop visar du i stället token som QR-kod som användaren skannar i Swish-appen, eller använder e-handelsflödet med telefonnummer. Swish har ett separat API för att generera QR-koder, se Generate QR codes.
Callbacken är sanningen
När betalningen når ett nytt läge gör Swish en POST till din callbackUrl med hela betalningsobjektet. Slutlägena är PAID, DECLINED, ERROR och CANCELLED.
// Hono/Elysia/Express spelar ingen roll – principen är densamma
app.post("/swish/callback", async (c) => {
const body = await c.req.json();
const payment = await findPaymentByInstructionUUID(body.id);
if (!payment) return c.text("ok"); // okänd: svara 200 så att Swish slutar försöka
// Jämför headern callbackIdentifier med värdet vi skickade
if (c.req.header("callbackidentifier") !== payment.callbackIdentifier) {
return c.text("forbidden", 403);
}
// Idempotent uppdatering: callbacks kan komma flera gånger
if (payment.status !== "PAID" && body.status === "PAID") {
await markPaid(payment.id, { paymentReference: body.paymentReference, amount: body.amount });
await fulfilOrder(payment.orderId);
} else if (["DECLINED", "ERROR", "CANCELLED"].includes(body.status)) {
await markFailed(payment.id, body.status, body.errorCode);
}
return c.text("ok");
});- Callback-URL:en måste vara HTTPS på port 443 med ett certifikat från en allmänt betrodd CA. Swish rekommenderar IP-filtrering, och produktionen kan vitlistas på domännivå – se Environments.
- Misslyckas callbacken försöker Swish igen upp till tio gånger med ökande intervall. Svara 200 snabbt och gör tungt arbete i bakgrunden.
- Spara
paymentReferencefrån callbacken. Det är den du behöver för en återbetalning. - Kommer ingen callback alls kan du hämta status med
GET /swish-cpcapi/api/v1/paymentrequests/{id}. Bygg ett schemalagt jobb som gör det för betalningar som har legat i väntläge för länge.
Användaren har tre minuter på sig att godkänna i appen. Om appens timeout inte triggas skickar Swish backend ERROR efter fem minuter för e-handel och fem och en halv för m-handel. Ett obetalt köp ska alltså aldrig ligga och vänta längre än så.
Återbetalningar
En återbetalning skapas med PUT /swish-cpcapi/api/v2/refunds/{instructionUUID} och refererar till originalbetalningens paymentReference via fältet originalPaymentReference. Här är du själv betalare, så payerAlias är ditt Swish-nummer. Du kan göra flera delåterbetalningar tills summan når originalbeloppet, och bara på betalningar från de senaste 13 månaderna.
Callbacken kommer normalt två gånger: först DEBITED (pengarna har dragits från ert konto, inom tio sekunder annars blir det ERROR) och sedan PAID. Ett fel kan komma även efter DEBITED, till exempel om mottagarens bank vägrar – då går pengarna tillbaka till er. Bestäm medvetet om ni visar återbetalningen som klar redan vid DEBITED.
Säkerhetsfällor vi ser ofta
- Beloppet kommer från klienten. Klienten skickar bara vad som ska köpas; priset slås upp på servern och sparas tillsammans med
instructionUUID. - Callbacken litas på blint. Kontrollera
callbackIdentifier, att id:t finns hos er och att belopp och valuta i callbacken stämmer med det ni begärde. För höga belopp kan ni dessutom hämta status från Swish via mTLS innan ni levererar. - Ordern levereras två gånger. Callbacks kan komma flera gånger och samtidigt som ert statusjobb. Gör övergången till betald som en villkorad uppdatering i databasen så att bara en process vinner.
- Certifikat i repot. Klient- och signeringsnycklar hör hemma i en hemlighetshanterare eller som krypterade miljövariabler, aldrig i git eller i appen.
Utbetalningar med signerad payload
Swish Payout API skiljer sig på några viktiga punkter. Du gör en POST till /swish-cpcapi/api/v1/payouts med ett objekt som innehåller payload, signature och valfri callbackUrl. Payloaden innehåller bland annat payoutInstructionUUID, mottagarens nummer (payeeAlias), mottagarens personnummer (payeeSSN), belopp, instructionDate och serienumret på ditt signeringscertifikat.
import { createHash, createSign, X509Certificate } from "node:crypto";
// Serienumret i hex utan kolon, så som Swish vill ha det
const serial = new X509Certificate(signingCertPem).serialNumber.replace(/:/g, "").toUpperCase();
function signPayload(payloadJson: string, signingKeyPem: string) {
// Enligt Swish exempel: SHA-512 av payloaden, sedan SHA512withRSA på hashen
const hash = createHash("sha512").update(payloadJson, "utf8").digest();
return createSign("RSA-SHA512").update(hash).sign(signingKeyPem, "base64");
}
const payloadJson = JSON.stringify(payload); // signera exakt den sträng du skickar
const body = JSON.stringify({ payload, signature: signPayload(payloadJson, signingKeyPem), callbackUrl });Det här är ett av de ställen där vi har lagt mest tid i FolkPanelen, där användare tar ut intjänade pengar till Swish. Några lärdomar:
- Spara `payoutInstructionUUID` innan anropet. Då kan både callback och statuskontroll hitta rätt rad, oavsett vad som händer med själva HTTP-anropet.
- En timeout betyder att utfallet är okänt. Om anropet till Swish tar för lång tid eller får ett 5xx-svar kan utbetalningen ändå ha gått igenom. Återför inte saldot automatiskt – låt ett schemalagt jobb fråga Swish om status (
GET /swish-cpcapi/api/v1/payouts/{payoutReference}) och avgöra. Bara ett tydligt 4xx-avslag är ett säkert nej. - Validera telefonnummer och personnummer före anropet. Swish vill ha numret i formatet
467...och personnumret med tolv siffror. Fel format ger avslag som är onödiga att visa för användaren. - Payloaden måste signeras exakt som den skickas. Serialisera en gång och använd samma sträng för hash och request-body.
Testa mot MSS och sandlådan
Swish har två testmiljöer. Merchant Swish Simulator (MSS) på https://mss.cpc.getswish.net validerar dina anrop och skickar simulerade callbacks. Den kräver Swish egna testcertifikat (dina produktionscertifikat avvisas) och fungerar för betalningar, återbetalningar och utbetalningar. Callbacks från MSS kommer från en specifik IP som du behöver släppa igenom i brandväggen.
Det mest användbara i MSS är felsimulering: sätt en felkod som message i begäran, till exempel RF07, så får du tillbaka motsvarande fel. Gå igenom alla felkoder du hanterar och kontrollera att användaren får rätt besked.
En skillnad som brukar ställa till det: MSS bryr sig inte om att alias i begäran matchar certifikatet. Det gör produktionen, där begäran avvisas om payeeAlias respektive payerAlias inte stämmer med Swish-numret i certifikatet. Testa därför första produktionsanropet med ett litet belopp.
Vill du testa hela flödet med riktiga appar finns Swish Sandbox, med särskilda testversioner av Swish- och BankID-apparna. Där krävs registrering via Swish support.
App Store, Google Play och Swish
Den vanligaste överraskningen handlar inte om tekniken. Apple och Google kräver att digitalt innehåll som låses upp i appen – prenumerationer, premiumfunktioner, virtuell valuta – säljs via deras egna köpsystem. Swish går bra för fysiska varor och för tjänster som konsumeras utanför appen, som biljetter, bokningar och mat. Kontrollera vad ni säljer mot respektive butiks regler innan ni bygger kassan, inte efter ett avslag.
Checklista innan produktion
- Produktionscertifikat från Swish Certificate Management, Swish rot-CA i trust store och påminnelse innan certifikaten går ut.
- Belopp räknas fram på servern;
instructionUUIDsparas före anropet. - Callback på HTTPS/443,
callbackIdentifierkontrolleras, uppdateringar är idempotenta. - Schemalagt jobb hämtar status för betalningar och utbetalningar som fastnat.
- Appen hanterar att Swish saknas och att återhoppet hamnar fel.
- Alla felkoder ni bryr er om är testade via MSS.
- Butikernas köpregler är kontrollerade för det ni säljer.
Behöver du också identifiera användaren? Läs vår guide om BankID i app, eller se hur vi arbetar med betalningar och BankID.