Vad du behöver innan du skriver en rad kod
BankID:s API för identifiering och signering heter RP API (Relying Party) och är i dag i version 6. Det är ett JSON-API över HTTPS där din server identifierar sig med ett klientcertifikat (mTLS). Det finns ingen nyckel att lägga i appen – all kommunikation med BankID sker från din backend.
- Ett BankID-avtal via din bank. Produktionscertifikatet beställs från banken du har avtalet med, inte från BankID direkt.
- Ett testcertifikat. Testmiljön är fri och öppen; certifikatet laddas ner från BankID:s testsida.
- En backend. Appen pratar med din server, servern pratar med BankID. Klientcertifikatet får aldrig ligga i appen.
- Ett test-BankID i BankID-appen, konfigurerad för testmiljön. Det skapar du själv på BankID:s testportal.
Ett alternativ till eget avtal är att gå via en förmedlingstjänst som har avtalet och certifikatet åt dig. Det sänker tröskeln för att komma igång, men du får ett extra led med egna API-format och egen driftstatus. I FolkPanelen började vi via en sådan tjänst; principerna i den här guiden gäller i båda fallen eftersom flödet under huven är detsamma.
Miljöer
- Test:
https://appapi2.test.bankid.com/rp/v6.0/ - Produktion:
https://appapi2.bankid.com/rp/v6.0/
Olika certifikat gäller för test och produktion, och din klient ska lita på BankID:s rot-CA för respektive miljö – inte på själva servercertifikatet, eftersom det byts ut. Se Environments för aktuella rotcertifikat.
Flödet i fyra anrop
- Appen ber din backend starta en inloggning. Backend anropar
POST /authoch får tillbakaorderRef,autoStartToken,qrStartTokenochqrStartSecret. - Backend sparar ordern (inklusive
qrStartSecretoch tidpunkten för svaret) och skickarorderRefplus antingenautoStartTokeneller QR-data till klienten.qrStartSecretlämnar aldrig servern. - Klienten öppnar BankID-appen (samma enhet) eller visar en animerad QR-kod (annan enhet). Under tiden anropar backend
POST /collectungefär varannan sekund. - När
/collectsvararcompleteskapar backend en session. Svarar denfailedvisar du rätt meddelande utifrånhintCode. Avbryter användaren i din app anropar duPOST /cancel.
/sign fungerar likadant som /auth men kräver userVisibleData – texten användaren godkänner. Signering används när användaren faktiskt ska skriva under något, till exempel ett avtal eller ett uttag.
Backend: starta en order
Här är en avskalad version i TypeScript för Node eller Bun. Vi använder undici för att kunna skicka klientcertifikatet; samma sak går att göra med https.request.
import { Agent, request } from "undici";
import { readFileSync } from "node:fs";
const BANKID_URL = process.env.BANKID_URL!; // test- eller produktions-URL
const agent = new Agent({
connect: {
pfx: readFileSync(process.env.BANKID_PFX_PATH!),
passphrase: process.env.BANKID_PFX_PASSPHRASE!,
ca: readFileSync(process.env.BANKID_CA_PATH!), // BankID:s rot-CA
},
});
async function rp<T>(path: string, body: unknown): Promise<T> {
const res = await request(BANKID_URL + path, {
method: "POST",
dispatcher: agent,
// Ingen charset-parameter: BankID svarar 415 om den finns med
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
const json = (await res.body.json()) as any;
if (res.statusCode !== 200) {
throw new BankIdError(json?.errorCode ?? "unknown", res.statusCode);
}
return json as T;
}
export async function startAuth(opts: { endUserIp: string; nonce: string }) {
const order = await rp<{
orderRef: string;
autoStartToken: string;
qrStartToken: string;
qrStartSecret: string;
}>("/auth", {
endUserIp: opts.endUserIp,
returnUrl: "minapp://bankid#nonce=" + opts.nonce,
userVisibleData: Buffer.from("Logga in i Minapp").toString("base64"),
app: { appIdentifier: "se.exempel.minapp" },
});
await saveOrder({ ...order, startedAt: Date.now() }); // din databas
return { orderRef: order.orderRef, autoStartToken: order.autoStartToken };
}Några parametrar förtjänar en kommentar:
- `endUserIp` ska vara användarens IP som din tjänst ser den – inte adressen till din egen proxy eller lastbalanserare. Läs den från rätt header bakom proxyn, och bara om du litar på proxyn som satte den.
- `returnUrl` skickas från servern sedan v6, och det är det BankID rekommenderar. Lägg en nonce i den och kontrollera vid återkomsten att den hör till samma session. Att skicka
redirecti startlänken är numera markerat som föråldrat. - `userVisibleData` är UTF-8-text som base64-kodas. Ge användaren kontext (vilken tjänst, vad som sker) så att den kan upptäcka om någon försöker lura den.
- `app.appIdentifier` är bundle-id på iOS och paketnamn på Android. BankID skriver uttryckligen att fel värde kan leda till att legitima ordrar blockeras. Skicka
appellerweb, aldrig båda.
Polla /collect – och hantera hintCodes
/collect returnerar status som är pending, failed eller complete. Vid pending och failed får du en hintCode som styr vad användaren ska se. BankID kan lägga till nya koder utan förvarning, så din kod måste ha en standardgren för okända värden.
type Collect =
| { status: "pending"; hintCode: string }
| { status: "failed"; hintCode: string }
| { status: "complete"; completionData: { user: { personalNumber: string; name: string } } };
const pendingText: Record<string, string> = {
outstandingTransaction: "Starta BankID-appen.",
noClient: "Starta BankID-appen.",
started: "Söker efter BankID, det kan ta en liten stund …",
userSign: "Skriv in din säkerhetskod eller använd biometri i BankID-appen.",
};
const failedText: Record<string, string> = {
expiredTransaction: "BankID-appen svarar inte. Försök igen.",
certificateErr: "Ditt BankID är spärrat eller för gammalt.",
userCancel: "Åtgärden avbröts.",
cancelled: "Åtgärden avbröts. Försök igen.",
startFailed: "BankID-appen verkar inte finnas på enheten.",
};
export async function poll(orderRef: string) {
const r = await rp<Collect>("/collect", { orderRef });
if (r.status === "pending") {
return { state: "pending", message: pendingText[r.hintCode] ?? "Identifiering pågår." };
}
if (r.status === "failed") {
return { state: "failed", message: failedText[r.hintCode] ?? "Okänt fel. Försök igen." };
}
return { state: "complete", user: r.completionData.user };
}Texterna ovan är förkortade. BankID har färdiga, rekommenderade användarmeddelanden (RFA1, RFA9 och så vidare) kopplade till varje kod på sidan User messages. Använd dem – användarna känner igen formuleringarna.
Ett avslutat orderRef går inte att hämta två gånger: ett andra /collect efter complete eller failed ger invalidParameters. Om klienten pollar via din backend och två anrop korsar varandra (dubbelklick, nätverksretry, appen som vaknar ur bakgrund) får du fel trots att inloggningen lyckades. I FolkPanelen löste vi det genom att cacha slutresultatet per orderRef en kort stund och låta samtidiga anrop vänta på samma pågående begäran i stället för att skicka två till BankID.
Fel på HTTP-nivå
alreadyInProgress– det finns redan en pågående order för personen. Visa att en inloggning redan pågår.invalidParameters– fel i din begäran. Försök inte igen automatiskt och visa det inte som ett BankID-fel.requestTimeoutochinternalError– försök inte automatiskt; be användaren försöka igen.maintenance(503) – får försökas igen tyst; upprepas det, informera användaren.
Samma enhet: öppna BankID-appen från React Native
När användaren har BankID på samma telefon som din app startar du BankID-appen med autoStartToken. BankID rekommenderar universallänken https://app.bankid.com/. I en Expo-app räcker Linking:
import * as Linking from "expo-linking";
import { Alert } from "react-native";
export async function openBankId(autoStartToken: string) {
// returnUrl har redan skickats från servern i /auth, därför ingen redirect här
const url = "https://app.bankid.com/?autostarttoken=" + autoStartToken;
try {
await Linking.openURL(url);
} catch {
Alert.alert("BankID", "Kunde inte öppna BankID-appen. Använd BankID på annan enhet.");
}
}För att BankID ska kunna skicka tillbaka användaren behöver appen ett registrerat URL-schema eller en universallänk (i Expo: scheme i app.json). Behandla återkomsten som en signal att polla klart, inte som ett resultat. BankID skriver själva att Android inte garanterar att ett resultat kommer tillbaka till din aktivitet – sanningen finns bara i /collect.
Försök inte heller gissa om BankID-appen är installerad. Ge användaren två tydliga val: BankID på den här enheten och BankID på annan enhet. Det är så vi gör i FolkPanelen, och det har gett färre supportärenden än automatiska gissningar.
Annan enhet: animerad QR-kod
Om användaren sitter vid en dator (eller har BankID på en annan telefon) visar du en QR-kod. Statiska QR-koder fungerar inte längre med v6 – koden måste vara animerad och räknas fram på servern varje sekund:
import { createHmac } from "node:crypto";
// startedAt = tidpunkten (ms) då svaret från /auth kom tillbaka
export function qrData(order: { qrStartToken: string; qrStartSecret: string; startedAt: number }) {
const time = Math.floor((Date.now() - order.startedAt) / 1000).toString();
const qrAuthCode = createHmac("sha256", order.qrStartSecret).update(time).digest("hex");
return ["bankid", order.qrStartToken, time, qrAuthCode].join(".");
}Formatet är bankid.qrStartToken.time.qrAuthCode, där qrAuthCode är HMAC-SHA256 av antalet sekunder med qrStartSecret som nyckel. Låt klienten hämta en ny sträng varje sekund (eller skicka den i samma svar som collect-statusen) och rendera QR-koden lokalt. Generera inte koder i förväg och skicka aldrig qrStartSecret till klienten. Se BankID:s QR-guide.
- En order är giltig för skanning i 30 sekunder. Vill du visa QR-koden längre skapar du nya ordrar, och ger användaren en knapp för att förlänga.
- Svart på vitt, låg felkorrigeringsnivå, ingen logga i mitten och aldrig inverterade färger – tänk på det i mörkt läge.
- Sedan tillgänglighetskraven 2025 ska QR-koden vara en knapp som kan visas i helskärm, och nedräkningen ska fungera med skärmläsare.
Efter complete: vad ska du spara?
completionData innehåller personnummer, namn, enhetens IP och signaturen. Personnumret är en känslig uppgift, och mycket sällan behöver du lagra det i klartext. I FolkPanelen sparar vi en HMAC av personnumret med en serverhemlighet som nyckel. Det räcker för att känna igen en återkommande person (till exempel för att förhindra att samma person tar en välkomstbonus två gånger) utan att databasen innehåller läsbara personnummer. En vanlig SHA-256 utan nyckel räcker inte – personnummer är så få att de går att räkna fram.
- Jämför gärna
completionData.device.ipAddressmed IP:t du såg när ordern startades i samma-enhet-flödet. - Sätt
returnRisk: trueom du vill få en riskindikation (low,medium,high) – den förutsätter korrektendUserIp. - Skapa din egen session först när du själv har fått
completefrån/collect. Lita aldrig på ett klartecken från klienten.
Testmiljön i praktiken
För test behöver du testcertifikatet, test-URL:en och en BankID-app som är konfigurerad för test. Ett test-BankID kan skapas för valfritt personnummer, så använd Skatteverkets testpersonnummer och aldrig riktiga. Vanliga personnummer används av många samtidigt och kan ge konstiga resultat. Spärrat BankID testar du genom att mata in fel säkerhetskod fem gånger.
App Store och Google Play
Granskarna hos Apple har inget svenskt BankID. BankID påpekar själva att App Store-granskningen kräver inloggningsuppgifter till ett demokonto. Bygg därför in en av två saker från början:
- Ett granskningskonto som loggar in utan BankID, låst till en sandlådad användare utan riktiga pengar eller data.
- Ett sätt att peka appen mot BankID:s testmiljö, så att granskaren kan använda ett test-BankID.
Google Play frågar efter samma sak under appåtkomst i Play Console. Skriv instruktionerna på engelska och testa själv att de fungerar på en nyinstallerad app. Ett avslag för att granskaren inte kom in kostar ofta flera dagar.
Checklista innan produktion
- Produktionscertifikat från banken, rätt rot-CA i trust store och ett datum i kalendern för när certifikatet går ut.
endUserIpär verklig klient-IP, inte proxyns.returnUrlmed nonce skickas från servern och kontrolleras vid återkomsten.- Okända
hintCodeocherrorCodehar en standardtext. - Polling slutar vid
failedochcomplete, och dubbla/collectär hanterade. /cancelanropas när användaren avbryter i din app.- Personnummer lagras inte i klartext om det inte krävs.
- Granskningskonto eller testläge är klart för App Store och Google Play.
Ska BankID kombineras med betalningar? Läs vår guide om Swish-integration, eller se hur vi löser betalningar och BankID i kunduppdrag.