Download invoices from a portal behind a login with Axiom Chrome API

The Chrome API is a cloud browser you drive over Chrome DevTools Protocol with Playwright or Puppeteer, so you get full control from your own code, including file downloads. This guide signs in to a portal, finds every invoice link, and saves each PDF to disk. Because the download reuses the signed-in session, it works even when the files sit behind a login.

Before you begin


  • Node.js 18 or later.
  • Playwright installed: npm install playwright.
  • An axiom.ai API key from the Code Dashboard, set as the AXIOM_API_KEY environment variable.
  • Your portal credentials in environment variables, for example PORTAL_USER and PORTAL_PASS.
  • The CSS selectors for the login fields and the invoice download links.

Connect to the cloud browser


Connect over CDP to a fresh cloud browser. There's no Chromium to install locally.

import { chromium } from 'playwright';

const browser = await chromium.connectOverCDP(
  `wss://cdp-lb.axiom.ai/?token=${process.env.AXIOM_API_KEY}`
);

const context = browser.contexts()[0];
const page = context.pages()[0] ?? await context.newPage();

Note: Puppeteer works too: puppeteer.connect({ browserWSEndpoint: "wss://cdp-lb.axiom.ai/?token=..." }). This guide uses Playwright.

Sign in


Fill the login form and submit. Keep credentials in environment variables, never in the script.

await page.goto('https://portal.example.com/login');
await page.fill("input[name='email']", process.env.PORTAL_USER);
await page.fill("input[name='password']", process.env.PORTAL_PASS);
await page.click("button[type='submit']");
await page.waitForLoadState('networkidle');

Open the invoices page and read every download link's URL. Adjust the selector to match the portal.

await page.goto('https://portal.example.com/invoices');

const links = await page.$$eval(
  "a[href$='.pdf']", // match the portal's invoice download links
  (anchors) => anchors.map((a) => a.href)
);

console.log(`Found ${links.length} invoices`);

Download each PDF


Fetch each file with the browser context's request client. It shares the signed-in session's cookies, so authenticated downloads work without handling any download dialog. Save the bytes to disk.

import { writeFileSync, mkdirSync } from 'node:fs';
import { basename } from 'node:path';

mkdirSync('./invoices', { recursive: true });

for (const url of links) {
  const response = await context.request.get(url);
  const file = `./invoices/${basename(new URL(url).pathname)}`;
  writeFileSync(file, await response.body());
  console.log(`Saved ${file}`);
}

Note: context.request reuses the browser context's cookies, so the request is authenticated exactly like the browser. This is more reliable than intercepting browser download events over a remote connection.

Full script


import { chromium } from 'playwright';
import { writeFileSync, mkdirSync } from 'node:fs';
import { basename } from 'node:path';

async function run() {
  const browser = await chromium.connectOverCDP(
    `wss://cdp-lb.axiom.ai/?token=${process.env.AXIOM_API_KEY}`
  );

  try {
    const context = browser.contexts()[0];
    const page = context.pages()[0] ?? await context.newPage();

    // Sign in
    await page.goto('https://portal.example.com/login');
    await page.fill("input[name='email']", process.env.PORTAL_USER);
    await page.fill("input[name='password']", process.env.PORTAL_PASS);
    await page.click("button[type='submit']");
    await page.waitForLoadState('networkidle');

    // Collect invoice links
    await page.goto('https://portal.example.com/invoices');
    const links = await page.$$eval("a[href$='.pdf']", (anchors) =>
      anchors.map((a) => a.href)
    );
    console.log(`Found ${links.length} invoices`);

    // Download each PDF using the signed-in session
    mkdirSync('./invoices', { recursive: true });
    for (const url of links) {
      const response = await context.request.get(url);
      const file = `./invoices/${basename(new URL(url).pathname)}`;
      writeFileSync(file, await response.body());
      console.log(`Saved ${file}`);
    }
  } finally {
    await browser.close();
  }
}

run();

Run it with node download-invoices.js.

Next steps


  • Handle pagination: if invoices span several pages, wrap the collect step in a loop that clicks Next until it disappears, gathering links as you go.
  • Speed up navigation by blocking heavy resources: await page.route("**/*", (route) => ["image", "media", "font"].includes(route.request().resourceType()) ? route.abort() : route.continue());.
  • Read the Chrome API guide for other connection options.