Skip to content

Instantly share code, notes, and snippets.

@kacper-serewis
Created August 24, 2025 11:06
Show Gist options
  • Select an option

  • Save kacper-serewis/bcfb4775c5ee4fb8547830d8dd867f36 to your computer and use it in GitHub Desktop.

Select an option

Save kacper-serewis/bcfb4775c5ee4fb8547830d8dd867f36 to your computer and use it in GitHub Desktop.
Extract icscards.nl card statements to JSON using TamperMonkey
// ==UserScript==
// @name ICS Dashboard Button
// @namespace http://tampermonkey.net/
// @version 2025-08-24
// @description Export as JSON
// @author You
// @match https://www.icscards.nl/web/consumer/dashboard
// @icon https://www.google.com/s2/favicons?sz=64&domain=icscards.nl
// @grant none
// ==/UserScript==
(function () {
'use strict';
let btnEl = null;
// --- shared helpers -------------------------------------------------------
async function getXsrfToken() {
if (window.cookieStore && cookieStore.get) {
const c = await cookieStore.get('XSRF-TOKEN');
return c?.value ?? '';
}
const m = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);
return m ? decodeURIComponent(m[1]) : '';
}
function setStatus(text, { busy = false } = {}) {
if (!btnEl) return;
btnEl.textContent = text;
btnEl.disabled = !!busy;
btnEl.setAttribute('aria-busy', busy ? 'true' : 'false');
}
function downloadJSON(data, filename) {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
/**
* Unified request helper.
*/
async function makeRequest(url, opts = {}) {
const {
method = 'GET',
headers = {},
body = undefined,
json = true,
raw = false,
credentials = 'include',
mode = 'cors',
referrer = location.href,
} = opts;
const xsrf = await getXsrfToken();
const defaultHeaders = {
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.5',
'X-XSRF-TOKEN': xsrf,
};
const finalHeaders = { ...defaultHeaders, ...headers };
let finalBody = body;
const isPlainObject =
body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof Blob);
if (isPlainObject && json) {
finalHeaders['Content-Type'] = 'application/json;charset=UTF-8';
finalBody = JSON.stringify(body);
}
const resp = await fetch(url, {
method,
headers: finalHeaders,
body: finalBody,
credentials,
mode,
referrer,
});
if (!resp.ok) {
const text = await resp.text().catch(() => '');
throw new Error(`Request failed ${resp.status} ${resp.statusText}: ${text.slice(0, 500)}`);
}
if (raw) return resp;
if (json) return resp.json();
return resp.text();
}
// --- API wrappers ---------------------------------------------------------
async function getAccounts() {
setStatus('Loading accounts…', { busy: true });
const accounts = await makeRequest(
'https://www.icscards.nl/api/nl/sec/frontendservices/allaccountsv2'
);
return accounts; // return full list so we can use accountNumber later
}
async function getAvailableStatements(accountNumber) {
setStatus('Loading periods…', { busy: true });
const json = await makeRequest(
`https://www.icscards.nl/api/nl/sec/frontendservices/periods?accountNumber=${encodeURIComponent(
accountNumber
)}`
);
const first = json[0];
const last = json[json.length - 1];
return [first.period, last.period]; // [start, end]
}
async function fetchAllStatements() {
try {
const accounts = await getAccounts();
const accountNumber = accounts[0].accountNumber;
const [start, end] = await getAvailableStatements(accountNumber);
setStatus('Fetching transactions…', { busy: true });
const data = await makeRequest(
`https://www.icscards.nl/api/nl/sec/frontendservices/transactionsv3?accountNumber=${encodeURIComponent(
accountNumber
)}&flushCache=true&fromPeriod=${encodeURIComponent(end)}&untilPeriod=${encodeURIComponent(
start
)}`
);
// Build safe filename: {accountNumber}-{start}-{end}.json
const safeAccount = String(accountNumber).replace(/[^\w-]+/g, '');
const safeStart = String(start).replace(/[^\w-]+/g, '');
const safeEnd = String(end).replace(/[^\w-]+/g, '');
const filename = `${safeAccount}-${safeStart}-${safeEnd}.json`;
downloadJSON(data, filename);
// Optional: show count if structure has .transactions
const count = Array.isArray(data?.transactions) ? data.transactions.length : undefined;
setStatus(
count != null ? `Downloaded (${count} items)` : 'Downloaded ✓',
{ busy: false }
);
} catch (err) {
console.error(err);
setStatus('Failed — see console', { busy: false });
} finally {
// Re-enable button but keep status text
btnEl && (btnEl.disabled = false);
}
}
// --- UI button ------------------------------------------------------------
function addButton() {
const container = document.querySelector('.add-withdraw');
if (!container) return; // not yet available
if (container.querySelector('.tm-custom-button')) return; // avoid duplicates
btnEl = document.createElement('button');
btnEl.textContent = 'Extract statements';
btnEl.className = 'tm-custom-button';
btnEl.type = 'button';
btnEl.addEventListener('click', () => {
setStatus('Starting…', { busy: true });
fetchAllStatements().catch(() => {});
});
container.appendChild(btnEl);
}
// Try immediately
addButton();
// In case the page loads dynamically
const observer = new MutationObserver(() => addButton());
observer.observe(document.body, { childList: true, subtree: true });
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment