Bulk invoice download
Log into a sandbox vendor portal, navigate orders, and download invoices in bulk using browser automation.
- A Browserless API token from your account dashboard
Steps
This example uses a Browserless sandbox site that simulates a vendor portal behind a login screen. You can safely test login and download automation without affecting any real systems.
The workflow logs into the portal with demo credentials, navigates to the orders page, and extracts order data including links to each order's detail page where invoices can be downloaded.
Demo credentials:
demo@example.com/helloworld
- AI Agent
- REST API
- Frameworks
- BQL
Use the Browserless MCP server to download invoices from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).
1. Connect the MCP server
Send this prompt to your AI agent to install the Browserless MCP server:
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
2. Download invoices
Use browserless_agent. The task requires interaction: logging in, navigating orders, and clicking download buttons.
Use the browserless_agent tool to go to https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal,
log in with email demo@example.com and password helloworld,
then navigate to each order and download the invoice
Send the BQL mutation over HTTP to log in, list orders, and extract invoice data. This is a sandbox site, so no stealth mode is needed.
- cURL
- JavaScript
- Python
- Java
- C#
1. Send the request
curl -X POST \
"https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation BulkInvoiceDownload { goto(url: \"https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal\", waitUntil: networkIdle) { status } waitForForm: waitForSelector(selector: \"form\", timeout: 10000) { time } typeEmail: type(selector: \"input[name=email]\", text: \"demo@example.com\") { time } typePassword: type(selector: \"input[name=password]\", text: \"helloworld\") { time } submitLogin: click(selector: \"button[type=submit]\") { time } waitForOrders: waitForSelector(selector: \"table tbody tr\", timeout: 10000) { time } orders: mapSelector(selector: \"table tbody tr\") { orderId: mapSelector(selector: \"td:nth-child(1)\") { innerText } date: mapSelector(selector: \"td:nth-child(2)\") { innerText } items: mapSelector(selector: \"td:nth-child(3)\") { innerText } total: mapSelector(selector: \"td:nth-child(4)\") { innerText } status: mapSelector(selector: \"td:nth-child(5)\") { innerText } viewLink: mapSelector(selector: \"td a\") { href: attribute(name: \"href\") { value } } } }",
"variables": {},
"operationName": "BulkInvoiceDownload"
}'
2. Check the output
The response contains order metadata and links to each order's detail page where invoices can be downloaded:
{
"data": {
"goto": { "status": 200 },
"waitForForm": { "time": 512 },
"typeEmail": { "time": 85 },
"typePassword": { "time": 62 },
"submitLogin": { "time": 15 },
"waitForOrders": { "time": 1230 },
"orders": [
{
"orderId": [{ "innerText": "ORD-001" }],
"date": [{ "innerText": "2025-01-15" }],
"items": [{ "innerText": "3" }],
"total": [{ "innerText": "$1,250.00" }],
"status": [{ "innerText": "Delivered" }],
"viewLink": [{ "href": { "value": "/harvest-direct/vendor-portal/orders/ORD-001" } }]
}
]
}
}
1. Send the request
const query = `mutation BulkInvoiceDownload {
goto(url: "https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal", waitUntil: networkIdle) {
status
}
waitForForm: waitForSelector(selector: "form", timeout: 10000) {
time
}
typeEmail: type(selector: "input[name=email]", text: "demo@example.com") {
time
}
typePassword: type(selector: "input[name=password]", text: "helloworld") {
time
}
submitLogin: click(selector: "button[type=submit]") {
time
}
waitForOrders: waitForSelector(selector: "table tbody tr", timeout: 10000) {
time
}
orders: mapSelector(selector: "table tbody tr") {
orderId: mapSelector(selector: "td:nth-child(1)") { innerText }
date: mapSelector(selector: "td:nth-child(2)") { innerText }
items: mapSelector(selector: "td:nth-child(3)") { innerText }
total: mapSelector(selector: "td:nth-child(4)") { innerText }
status: mapSelector(selector: "td:nth-child(5)") { innerText }
viewLink: mapSelector(selector: "td a") {
href: attribute(name: "href") { value }
}
}
}`;
const response = await fetch(
'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables: {}, operationName: 'BulkInvoiceDownload' }),
}
);
const { data } = await response.json();
const orders = data.orders.map((o) => ({
orderId: o.orderId?.[0]?.innerText ?? '',
date: o.date?.[0]?.innerText ?? '',
total: o.total?.[0]?.innerText ?? '',
status: o.status?.[0]?.innerText ?? '',
viewLink: o.viewLink?.[0]?.href?.value ?? '',
}));
console.log(JSON.stringify(orders, null, 2));
2. Check the output
[
{
"orderId": "ORD-001",
"date": "2025-01-15",
"total": "$1,250.00",
"status": "Delivered",
"viewLink": "/harvest-direct/vendor-portal/orders/ORD-001"
}
]
1. Install dependencies
pip install requests
2. Send the request
import requests
query = """
mutation BulkInvoiceDownload {
goto(url: "https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal", waitUntil: networkIdle) {
status
}
waitForForm: waitForSelector(selector: "form", timeout: 10000) {
time
}
typeEmail: type(selector: "input[name=email]", text: "demo@example.com") {
time
}
typePassword: type(selector: "input[name=password]", text: "helloworld") {
time
}
submitLogin: click(selector: "button[type=submit]") {
time
}
waitForOrders: waitForSelector(selector: "table tbody tr", timeout: 10000) {
time
}
orders: mapSelector(selector: "table tbody tr") {
orderId: mapSelector(selector: "td:nth-child(1)") { innerText }
date: mapSelector(selector: "td:nth-child(2)") { innerText }
items: mapSelector(selector: "td:nth-child(3)") { innerText }
total: mapSelector(selector: "td:nth-child(4)") { innerText }
status: mapSelector(selector: "td:nth-child(5)") { innerText }
viewLink: mapSelector(selector: "td a") {
href: attribute(name: "href") { value }
}
}
}
"""
response = requests.post(
'https://production-sfo.browserless.io/chromium/bql',
params={'token': 'YOUR_API_TOKEN_HERE'},
json={'query': query, 'variables': {}, 'operationName': 'BulkInvoiceDownload'},
)
data = response.json()['data']
for order in data['orders']:
oid = order['orderId'][0]['innerText']
date = order['date'][0]['innerText']
total = order['total'][0]['innerText']
status = order['status'][0]['innerText']
print(f'{oid} | {date} | {total} | {status}')
3. Check the output
ORD-001 | 2025-01-15 | $1,250.00 | Delivered
ORD-002 | 2025-02-15 | $980.00 | Delivered
1. Send the request
import java.net.URI;
import java.net.http.*;
String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/chromium/bql?token=" + token;
String query = "mutation BulkInvoiceDownload {"
+ " goto(url: \\\"https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal\\\", waitUntil: networkIdle) { status }"
+ " waitForForm: waitForSelector(selector: \\\"form\\\", timeout: 10000) { time }"
+ " typeEmail: type(selector: \\\"input[name=email]\\\", text: \\\"demo@example.com\\\") { time }"
+ " typePassword: type(selector: \\\"input[name=password]\\\", text: \\\"helloworld\\\") { time }"
+ " submitLogin: click(selector: \\\"button[type=submit]\\\") { time }"
+ " waitForOrders: waitForSelector(selector: \\\"table tbody tr\\\", timeout: 10000) { time }"
+ " orders: mapSelector(selector: \\\"table tbody tr\\\") {"
+ " orderId: mapSelector(selector: \\\"td:nth-child(1)\\\") { innerText }"
+ " date: mapSelector(selector: \\\"td:nth-child(2)\\\") { innerText }"
+ " total: mapSelector(selector: \\\"td:nth-child(4)\\\") { innerText }"
+ " }"
+ " }";
String payload = "{\"query\": \"" + query + "\", \"variables\": {}, \"operationName\": \"BulkInvoiceDownload\"}";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoint))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
2. Check the output
{
"data": {
"goto": { "status": 200 },
"orders": [
{
"orderId": [{ "innerText": "ORD-001" }],
"date": [{ "innerText": "2025-01-15" }],
"total": [{ "innerText": "$1,250.00" }]
}
]
}
}
1. Send the request
using System.Net.Http;
using System.Text;
using System.Text.Json;
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/chromium/bql?token={token}";
var payload = new
{
query = @"mutation BulkInvoiceDownload {
goto(url: ""https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal"", waitUntil: networkIdle) { status }
waitForForm: waitForSelector(selector: ""form"", timeout: 10000) { time }
typeEmail: type(selector: ""input[name=email]"", text: ""demo@example.com"") { time }
typePassword: type(selector: ""input[name=password]"", text: ""helloworld"") { time }
submitLogin: click(selector: ""button[type=submit]"") { time }
waitForOrders: waitForSelector(selector: ""table tbody tr"", timeout: 10000) { time }
orders: mapSelector(selector: ""table tbody tr"") {
orderId: mapSelector(selector: ""td:nth-child(1)"") { innerText }
date: mapSelector(selector: ""td:nth-child(2)"") { innerText }
total: mapSelector(selector: ""td:nth-child(4)"") { innerText }
}
}",
variables = new { },
operationName = "BulkInvoiceDownload",
};
using (HttpClient httpClient = new HttpClient())
{
var content = new StringContent(
JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(endpoint, content);
string body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}
2. Check the output
{
"data": {
"goto": { "status": 200 },
"orders": [
{
"orderId": [{ "innerText": "ORD-001" }],
"date": [{ "innerText": "2025-01-15" }],
"total": [{ "innerText": "$1,250.00" }]
}
]
}
}
Connect a headless browser to the sandbox portal, log in, navigate to each order's detail page, and download the invoice.
- Puppeteer
- Playwright
1. Install dependencies
npm install puppeteer-core
2. Connect and download
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});
try {
const page = await browser.newPage();
const cdpSession = await page.createCDPSession();
await cdpSession.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: '/tmp/invoices',
});
await page.goto('https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal', {
waitUntil: 'networkidle2',
});
await page.waitForSelector('form');
await page.type('input[name="email"]', 'demo@example.com');
await page.type('input[name="password"]', 'helloworld');
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle2' });
const orderLinks = await page.$$eval('table tbody tr td a', (links) =>
links.map((a) => a.href)
);
for (const link of orderLinks) {
await page.goto(link, { waitUntil: 'networkidle2' });
const downloadBtn = await page.$('a[download]');
if (downloadBtn) {
await downloadBtn.click();
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
console.log(`Downloaded invoices from ${orderLinks.length} orders`);
} finally {
await browser.close();
}
3. Check the output
Run with node bulk-invoice-download.mjs. The script logs in, visits each order's detail page, and clicks the download button.
1. Install dependencies
npm install playwright-core
2. Connect and download
import { chromium } from 'playwright-core';
const browser = await chromium.connectOverCDP(
'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE'
);
try {
const context = browser.contexts()[0];
const page = await context.newPage();
await page.goto('https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal', {
waitUntil: 'networkidle',
});
await page.waitForSelector('form');
await page.fill('input[name="email"]', 'demo@example.com');
await page.fill('input[name="password"]', 'helloworld');
await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');
const orderLinks = await page.$$eval('table tbody tr td a', (links) =>
links.map((a) => a.href)
);
for (const link of orderLinks) {
await page.goto(link, { waitUntil: 'networkidle' });
const downloadBtn = await page.$('a[download]');
if (downloadBtn) {
const [download] = await Promise.all([
page.waitForEvent('download'),
downloadBtn.click(),
]);
const path = await download.path();
console.log(`Downloaded: ${download.suggestedFilename()} -> ${path}`);
}
}
console.log(`Downloaded invoices from ${orderLinks.length} orders`);
} finally {
await browser.close();
}
3. Check the output
Run with node bulk-invoice-download.mjs. Each download logs the filename and local path.
1. Write the mutation
Log into the sandbox vendor portal and extract order data. This is a sandbox site, so no stealth mode is needed.
mutation BulkInvoiceDownload {
goto(url: "https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal", waitUntil: networkIdle) {
status
}
waitForForm: waitForSelector(selector: "form", timeout: 10000) {
time
}
typeEmail: type(selector: "input[name=email]", text: "demo@example.com") {
time
}
typePassword: type(selector: "input[name=password]", text: "helloworld") {
time
}
submitLogin: click(selector: "button[type=submit]") {
time
}
waitForOrders: waitForSelector(selector: "table tbody tr", timeout: 10000) {
time
}
orders: mapSelector(selector: "table tbody tr") {
orderId: mapSelector(selector: "td:nth-child(1)") { innerText }
date: mapSelector(selector: "td:nth-child(2)") { innerText }
items: mapSelector(selector: "td:nth-child(3)") { innerText }
total: mapSelector(selector: "td:nth-child(4)") { innerText }
status: mapSelector(selector: "td:nth-child(5)") { innerText }
viewLink: mapSelector(selector: "td a") {
href: attribute(name: "href") { value }
}
}
}
2. Run it
Paste into the BQL IDE and click Run.
3. Check the output
{
"data": {
"goto": { "status": 200 },
"waitForForm": { "time": 512 },
"typeEmail": { "time": 85 },
"typePassword": { "time": 62 },
"submitLogin": { "time": 15 },
"waitForOrders": { "time": 1230 },
"orders": [
{
"orderId": [{ "innerText": "ORD-001" }],
"date": [{ "innerText": "2025-01-15" }],
"items": [{ "innerText": "3" }],
"total": [{ "innerText": "$1,250.00" }],
"status": [{ "innerText": "Delivered" }],
"viewLink": [{ "href": { "value": "/harvest-direct/vendor-portal/orders/ORD-001" } }]
},
{
"orderId": [{ "innerText": "ORD-002" }],
"date": [{ "innerText": "2025-02-15" }],
"items": [{ "innerText": "5" }],
"total": [{ "innerText": "$980.00" }],
"status": [{ "innerText": "Delivered" }],
"viewLink": [{ "href": { "value": "/harvest-direct/vendor-portal/orders/ORD-002" } }]
}
]
}
}
Next steps
- Fill and Submit a Form -- automate form interactions with BQL
- Take a Screenshot -- capture the portal state after downloading
- Authenticated Sessions -- persist login state for portals that require authentication