Puppeteer screenshot of a GST invoice page: wait for totals to load
Capture a GST invoice only after its calculated totals are ready. Use an application-specific wait, optional network-idle settling, and finite timeouts.
To capture a GST invoice after its totals load, wait for a page-specific condition that confirms the displayed totals are populated and any totals-loading indicator is gone. Then take the screenshot. A fixed sleep or network-idle wait alone cannot confirm that the invoice values are correct.
The selectors in the examples are placeholders. Replace them with stable selectors from your invoice page and define what “ready” means for that application.
1. Install Puppeteer and prepare the page
In a new Node.js project, install Puppeteer:
npm install puppeteer
Set INVOICE_URL to a page your browser process is allowed to access. If the invoice requires authentication, use an authorized session or provide credentials through your application’s existing secure mechanism. Do not put secrets directly in source code.
2. Wait for the invoice totals, then capture
This runnable example waits for a non-empty total and for the loading indicator to disappear. It uses a finite timeout; if the condition is not met, it throws an error instead of saving a possibly incomplete invoice.
const puppeteer = require('puppeteer');
async function main() {
const invoiceUrl = process.env.INVOICE_URL;
if (!invoiceUrl) throw new Error('Set INVOICE_URL to the invoice page URL');
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto(invoiceUrl, {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
await page.waitForFunction(() => {
const total = document.querySelector('[data-testid="invoice-total"]');
const loading = document.querySelector('[data-testid="totals-loading"]');
return total && total.textContent.trim() !== '' && !loading;
}, { timeout: 15_000 });
await page.screenshot({
path: 'gst-invoice.png',
fullPage: true,
type: 'png',
});
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error('Invoice capture failed:', error);
process.exitCode = 1;
});
Run it with INVOICE_URL set in the environment. The example assumes a CommonJS project. In an ECMAScript module project, import Puppeteer with import puppeteer from 'puppeteer'; and keep the rest of the flow.
3. Make the readiness condition match the page
An element existing in the DOM does not mean its value is final. Choose a condition that reflects the invoice application’s actual completion state.
- Check the total and loading state. Prefer stable attributes such as
data-testidif the application provides them. Check that the total has meaningful content and the spinner or loading marker is absent or hidden. - Check expected values when they are known. For a generated invoice, wait for the expected total or a known status. If amounts are formatted with currency symbols, separators, or localized decimals, normalize or match the format deliberately.
- Check multiple totals if needed. If subtotal, tax, and grand total load separately, wait for all required fields and their completion indicators.
- Wait for a response only when it is a meaningful signal. If a known request returns the calculated totals,
page.waitForResponse()can identify its completion. Still verify the rendered page before capture, since receiving data does not prove it has appeared in the UI.
For example, a page with a hidden loading indicator can use Puppeteer’s selector wait:
await page.waitForSelector('[data-testid="totals-loading"]', {
hidden: true,
timeout: 15_000,
});
await page.waitForFunction(() => {
const total = document.querySelector('[data-testid="invoice-total"]');
return total && total.textContent.trim() !== '';
}, { timeout: 15_000 });
When selecting and interacting with page elements, Puppeteer locators can wait for elements to be present and in the appropriate state. A function-based locator condition can also express page-specific readiness. See the Puppeteer interaction guide and Page API.
4. Use network idle as an optional settling step
Network idle describes a period with little or no network activity; it does not validate invoice calculations or guarantee that the displayed values have finished updating. Some pages also keep connections open or make background requests, so network idle may be delayed or never reached.
Use the invoice-specific condition as the primary gate. If the page fetches late resources that matter to the capture, network idle can be an additional settling step:
await page.waitForFunction(() => {
const total = document.querySelector('[data-testid="invoice-total"]');
const loading = document.querySelector('[data-testid="totals-loading"]');
return total && total.textContent.trim() !== '' && !loading;
}, { timeout: 15_000 });
await page.waitForNetworkIdle({
idleTime: 500,
timeout: 10_000,
});
Choose an idle timeout that fits the page, and handle a timeout explicitly if network idle is optional. Puppeteer documents Page.waitForNetworkIdle() and its idle behavior in the API reference. Its screenshot guide shows navigation with waitUntil: 'networkidle2' as another navigation option, but that also remains a transport-level signal: Puppeteer screenshots.
5. Capture the whole invoice or only its totals
Use a full-page screenshot when the rendered invoice is the artifact you need. For a focused image, wait for the same readiness condition and screenshot the totals element:
const totals = await page.waitForSelector('[data-testid="invoice-totals"]', {
visible: true,
timeout: 15_000,
});
await totals.screenshot({ path: 'gst-invoice-totals.png' });
An element screenshot captures that element’s bounds. If the selector matches a container that clips content or does not include all relevant invoice fields, choose a more suitable ancestor or capture the full page. Puppeteer documents both Page.screenshot() and ElementHandle.screenshot() in its screenshot guide.
6. Handle page-specific edge cases
- Totals update more than once: A non-empty check can pass on an intermediate amount. Wait for a final status, expected value, or application-specific settled condition.
- Loading marker remains in the DOM but is hidden: Checking that the node does not exist will fail. Check its visibility or use
waitForSelectorwithhidden: true. - Totals are inside an iframe: Find the relevant frame and evaluate the readiness condition in that frame; the main page’s
documentcannot see iframe contents. - Totals are inside a shadow root: A plain
document.querySelectordoes not cross shadow boundaries. Use a selector strategy that traverses the page’s shadow DOM or expose a stable page-level readiness signal. - Lazy content appears below the fold: A full-page screenshot may not itself trigger every site’s lazy-loading behavior. Scroll or otherwise trigger the relevant content before capture, then verify the page is ready.
- Invoice content is sensitive: Store screenshots only where authorized, restrict access, and avoid logging invoice contents or credentials.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Wait for totals times out | Placeholder selectors do not match the page, totals never loaded, or the loading marker remains. | Inspect the rendered DOM and network/application errors. Replace placeholder selectors and increase the timeout only if the page legitimately needs more time. |
| Screenshot shows an old or intermediate amount | The condition only checked that text was non-empty. | Wait for a final status or expected value, and include all relevant subtotal, tax, and total fields in the condition. |
waitForNetworkIdle never resolves |
Background polling, streaming, or other continuing requests prevent an idle period. | Make the totals condition authoritative; remove the optional idle wait or use a page-specific response/state signal. |
| Navigation times out | The page is slow, access is blocked, or the chosen navigation lifecycle event never occurs. | Check that the URL is reachable from the capture environment. Choose an appropriate navigation event and set a finite timeout based on the page. |
| Screenshot is blank or missing invoice data | Capture ran before client rendering, an access check interrupted the page, or the invoice is in another frame. | Wait on the actual rendered state, inspect the page/frame, and fail the job if readiness is not reached. |
| Element screenshot throws or captures the wrong area | The selector is absent, hidden, ambiguous, or identifies a clipped container. | Wait for a visible, unique target and select a container that covers the intended content. |
8. Performance, reliability, and cost
Browser startup and page loading are usually the main work in this flow. Reuse a browser process for batches where appropriate, while creating a separate page or context per job to avoid leaking cookies or state between invoices. Set finite navigation and readiness timeouts, close pages and browsers in cleanup paths, and record whether a capture failed at navigation or readiness.
Do not shorten waits by replacing a meaningful condition with a tiny fixed delay: it can create intermittent incomplete captures. Conversely, waiting for every possible network request can waste time or hang on pages with persistent traffic. Gate on the application state that matters, then add only settling steps required by the artifact.
Puppeteer itself is a browser automation workflow, so account for the compute and maintenance needed to run a browser in your environment. This dossier does not establish a benchmark or a Puppeteer price; actual runtime and infrastructure cost depend on your page and deployment.
9. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a screenshot or PDF. It removes cookie banners, newsletter popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000.
For a page that has dynamically rendered totals, make sure its rendered state is ready for capture; the API call does not replace the invoice-specific readiness logic described above.
See the ScreenshotNeo API documentation. This example requests a WebP screenshot:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com/invoice \
-o gst-invoice.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://example.com/invoice",
},
timeout=90,
)
r.raise_for_status()
open("gst-invoice.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/invoice',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('gst-invoice.webp', Buffer.from(await res.arrayBuffer()));
Sign up for ScreenshotNeo free: 1,000 screenshots a month, no card required.
10. FAQ
Should I wait for the total to be non-empty?
Only if non-empty means final on that page. If the app first displays an estimate or intermediate value, wait for a final state or expected amount.
Can a screenshot prove the GST calculation is correct?
No. It records the rendered page. Validate tax calculations in the application or its data layer separately.
Should I capture the whole page or just the totals?
Capture the whole page when the invoice context matters; capture the totals element when you need a compact crop and its container includes all required fields.


