Test authentication Relay - SWEDISH_MOBILE_BANKID_SAME_DEVICE_CLIENT_SIDE_AUTHENTICATION
Full end-to-end script for testing your client authentication flow. No real bank or data is required.
Replace INSURELY_API_KEY with your API key before running.
Testing the BankID relay flow (SWEDISH_MOBILE_BANKID_SAME_DEVICE_CLIENT_SIDE_AUTHENTICATION)
against the se-demo-client-authentication company, using a mocked BankID backend so you can run
through the flow end-to-end without a real BankID app.
This script plays BOTH roles a real integration involves, so you can see the whole flow in one
place:
- Your server, starting a collection and polling its status
- Your own client (e.g. a mobile app or browser), which is who actually performs the relayed HTTP call the collection hands back while status is WAITING_FOR_AUTHENTICATION, and posts the result to
/collections/_id_/supplement-info
The mock BankID backend is an init (POST /mock-bankid/sign) + poll (GET /mock-bankid/collect) flow, mirroring how real bank BankID integrations work - so the collection hands back MULTIPLE rounds of relay instructions (one for sign, then repeated ones for collect) before it completes.
Prerequisites
Node.js 21+
#!/usr/bin/env node
// Usage:
// INSURELY_API_KEY=<your api key> node test-relay-flow.js
'use strict';
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
const BASE_URL = 'https://api.insurely.com';
const AUTH_TOKEN = process.env.INSURELY_API_KEY;
if (!AUTH_TOKEN) {
console.error('Set INSURELY_API_KEY to your Insurely API key');
process.exit(1);
}
const API_VERSION = '2026-04-01';
const COMPANY = 'se-demo-client-authentication';
const LOGIN_METHOD = 'SWEDISH_MOBILE_BANKID_SAME_DEVICE_CLIENT_SIDE_AUTHENTICATION_MOCK';
// se-demo-client-authentication is a mock company - any correctly formatted Swedish personal
// number works, no real person is looked up. Override with your own test value if you like.
const PERSONAL_NUMBER = process.env.PERSONAL_NUMBER || '194508203199';
const MAX_STATUS_POLLS = 30;
const MAX_RELAY_ROUNDS = 20;
const FALLBACK_POLL_INTERVAL_MS = 1500;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const step = (msg) => console.log(`\n\x1b[1;36m== ${msg} ==\x1b[0m`);
const info = (msg) => console.log(` ${msg}`);
const fail = (msg) => {
console.error(`\x1b[1;31mFAILED: ${msg}\x1b[0m`);
process.exit(1);
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Reads a fetch Response's body as text, trying to parse it as JSON.
// Returns { raw, json } - json is undefined if the body isn't valid JSON.
const readBody = async (response) => {
const raw = await response.text();
try {
return { raw, json: raw ? JSON.parse(raw) : undefined };
} catch {
return { raw, json: undefined };
}
};
// Performs a fetch call, fails loudly with the response body on a non-2xx status.
// Returns the parsed JSON body on success.
const httpCall = async (method, url, headers, body) => {
const response = await fetch(url, { method, headers, body });
const { raw, json } = await readBody(response);
if (!response.ok) {
console.error(`\x1b[1;31mHTTP ${response.status} from ${method} ${url}\x1b[0m`);
console.error(json !== undefined ? JSON.stringify(json, undefined, 2) : raw);
process.exit(1);
}
return json;
};
const authHeaders = () => ({
'Content-Type': 'application/json',
'Authorization-token': AUTH_TOKEN,
'Insurely-Version': API_VERSION,
});
// Plays the CLIENT's role for one relay round: performs the relayed HTTP call the collection
// asked for, then posts the result back to supplement-info.
const performRelayRound = async (collectionId, relayRequest) => {
const relayUrl = relayRequest.url;
const relayMethod = relayRequest.method || 'GET';
info(`${relayMethod} ${relayUrl}`);
// The relayed mock-BankID endpoints use the same API key as the rest of the API.
const relayResponse = await fetch(relayUrl, {
method: relayMethod,
headers: { 'Authorization-token': AUTH_TOKEN },
});
const { raw: relayBodyRaw, json: relayBodyJson } = await readBody(relayResponse);
info(`relay call returned HTTP ${relayResponse.status}`);
console.log(relayBodyJson !== undefined ? JSON.stringify(relayBodyJson, undefined, 2) : relayBodyRaw);
if (!relayResponse.ok) {
fail('relay call did not return 2xx');
}
// Build the {"Header-Name": ["value"]} map ResponseObject expects, from the raw response headers.
const relayHeadersJson = {};
for (const [name, value] of relayResponse.headers.entries()) {
relayHeadersJson[name] = [value];
}
// response body: keep it as parsed JSON if it is JSON, otherwise as a raw string
const supplementPayload = {
type: 'RESPONSE_OBJECT',
headers: relayHeadersJson,
response: relayBodyJson !== undefined ? relayBodyJson : relayBodyRaw,
};
console.log(JSON.stringify(supplementPayload, undefined, 2));
await httpCall(
'POST',
`${BASE_URL}/collections/${collectionId}/supplement-info`,
authHeaders(),
JSON.stringify(supplementPayload),
);
info('posted');
};
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
const main = async () => {
// 1. Start the collection
step(`1. Start collection (${COMPANY} / ${LOGIN_METHOD})`);
const startPayload = {
company: COMPANY,
loginMethod: LOGIN_METHOD,
parameters: [{ type: 'SWEDISH_BANKID', personalNumber: PERSONAL_NUMBER }],
};
const startResponse = await httpCall(
'POST',
`${BASE_URL}/collections`,
authHeaders(),
JSON.stringify(startPayload),
);
console.log(JSON.stringify(startResponse, undefined, 2));
const collectionId = startResponse.id;
const pollIntervalMs = startResponse.pollingInterval || FALLBACK_POLL_INTERVAL_MS;
if (!collectionId) {
fail('no collection id in response');
}
info(`collection id: ${collectionId}`);
// 2. Poll status, playing the client's role for every relay round, until terminal
step('2. Poll status, relaying every round, until the collection reaches a terminal status');
const terminalStatuses = new Set(['COMPLETED', 'COMPLETED_EMPTY', 'COMPLETED_PARTIAL', 'FAILED']);
let finalStatus = 'UNKNOWN';
let relayRounds = 0;
// The API only overwrites extraInformation.INSTRUCTIONS when it issues the NEXT relay call - it
// isn't cleared the moment you answer one via supplement-info. So a status poll landing in that
// gap still shows the request you already relayed. Track its etag (unique per relay round) so
// you don't relay - and re-POST to the API for - the same round twice.
let lastRelayedEtag = '';
let statusResponse;
for (let attempt = 1; attempt <= MAX_STATUS_POLLS; attempt++) {
statusResponse = await httpCall(
'GET',
`${BASE_URL}/collections/${collectionId}/status`,
authHeaders(),
);
finalStatus = statusResponse.status;
info(`[attempt ${attempt}] status=${finalStatus}`);
if (terminalStatuses.has(finalStatus)) {
break;
}
const relayRequest = statusResponse.extraInformation?.INSTRUCTIONS?.request;
if (relayRequest) {
const relayEtag = relayRequest.etag;
if (relayEtag && relayEtag === lastRelayedEtag) {
info(`still seeing the relay round we already handled (etag=${relayEtag}); waiting for it to advance`);
await sleep(pollIntervalMs);
continue;
}
relayRounds += 1;
if (relayRounds > MAX_RELAY_ROUNDS) {
fail(`too many relay rounds (>${MAX_RELAY_ROUNDS}) without a terminal status`);
}
step(`2.${relayRounds}. Relay round ${relayRounds} (this is what your own client does)`);
console.log(JSON.stringify(relayRequest, undefined, 2));
await performRelayRound(collectionId, relayRequest);
lastRelayedEtag = relayEtag;
continue;
}
await sleep(pollIntervalMs);
}
console.log(JSON.stringify(statusResponse, undefined, 2));
step('Result');
if (finalStatus.startsWith('COMPLETED')) {
console.log(`\x1b[1;32mPASS — collection reached ${finalStatus}\x1b[0m`);
} else {
console.error(`\x1b[1;31mFAIL — collection ended at ${finalStatus}\x1b[0m`);
process.exit(1);
}
};
main().catch((err) => fail(err instanceof Error ? err.stack : String(err)));Last updated on