Examples

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:

  1. Your server, starting a collection and polling its status
  2. 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+

test-auth-relay.js
#!/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