How to Capture GST Invoice Webpages as PDFs with Python Playwright
Choose the right route for a GST invoice: print its rendered webpage with Playwright, save a portal PDF download, or use an e-Invoice facility.
First identify what the portal provides. If the invoice is rendered as a webpage, use Playwright’s page.pdf(). If a portal control downloads a PDF attachment, capture that download and save the original file. For an e-Invoice, check the portal’s own PDF or JSON download and print options before making a browser printout. These routes produce different artifacts, and no single GST portal flow or selector applies to every service.
A PDF printed from a webpage is a copy of rendered content. It is not automatically a digitally authenticated e-Invoice. Preserve and validate the official signed data, QR code, or IRN when that is required for your workflow.
Choose the right capture route
| What the portal shows | Recommended route | What to know |
|---|---|---|
| An invoice rendered as HTML | page.pdf() |
Playwright uses print CSS by default. Configure print output or explicitly select screen media. |
| A button that returns a PDF file | page.expect_download() and save_as() |
Start waiting for the download before clicking, and save it before closing the browser context. |
| An e-Invoice with an official download or print facility | Use the portal’s documented PDF or JSON workflow | Access and controls are portal-specific. An official download may preserve data that a browser printout does not. |
Playwright documents page.pdf() as generating a PDF with print CSS media. If the invoice is already supplied as a PDF attachment, printing the page is unnecessary and may change the original document.
Install Playwright for Python
python -m pip install playwright
python -m playwright install chromium
The Python package offers synchronous and asynchronous APIs. Examples below use the synchronous API. Install the browser in the same environment where your script will run. See the Playwright Python installation guide and Page API documentation.
Print a rendered HTML invoice to PDF
This is a general pattern, not a tested recipe for a particular GST portal. Sign in through an authorized workflow and use the invoice URL and content-ready condition for your portal. Do not put passwords in source code or bypass CAPTCHA, MFA, or other portal controls.
import os
from pathlib import Path
from playwright.sync_api import sync_playwright
invoice_url = os.environ["GST_INVOICE_URL"]
output_path = Path("invoice.pdf")
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(accept_downloads=True)
page = context.new_page()
page.goto(invoice_url, wait_until="domcontentloaded", timeout=60_000)
# Replace this with a stable, portal-specific condition that means
# the invoice content is present. Do not rely on a guessed selector.
page.locator("YOUR_INVOICE_CONTENT_SELECTOR").wait_for(
state="visible", timeout=30_000
)
page.pdf(
path=str(output_path),
format="A4",
print_background=True,
margin={"top": "12mm", "right": "12mm", "bottom": "12mm", "left": "12mm"},
)
context.close()
browser.close()
print(f"Saved {output_path.resolve()}")
Set GST_INVOICE_URL in the environment before running the script. Replace YOUR_INVOICE_CONTENT_SELECTOR with a selector that identifies the invoice after it has rendered; the placeholder is deliberately not a portal-specific selector. If your authorized login flow is required, complete it according to that portal’s instructions and verify that the page is the intended invoice before saving.
Print CSS, screen CSS, and page sizing
- Print layout: The default PDF media is print. This is usually the right choice for a document and respects the page’s print styles.
- Screen appearance: If you specifically need the invoice’s screen styling, call
page.emulate_media(media="screen")beforepage.pdf(). Screen styling may paginate or scale poorly on paper. - Paper and margins:
format="A4"is a reasonable starting point. You can set margins with values such as"12mm". Choose settings that match the invoice and portal’s layout. - Backgrounds: Background graphics are omitted unless
print_background=True. - CSS page rules: If the page defines its own
@pagesize and you want that to take precedence over the requested format, considerprefer_css_page_size=True.
# Use screen CSS only when that is the desired output.
page.emulate_media(media="screen")
page.pdf(path="invoice-screen-style.pdf", format="A4", print_background=True)
Save a PDF provided as a portal download
A download is not the same as printing the current page. Use this route when a portal button produces a PDF attachment. Playwright emits a download event for attachments; put expect_download() around the action so the event is not missed.
from pathlib import Path
from playwright.sync_api import sync_playwright
invoice_url = "https://example.invalid/authorized-invoice"
output_path = Path("invoice-original.pdf")
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(accept_downloads=True)
page = context.new_page()
page.goto(invoice_url, wait_until="domcontentloaded", timeout=60_000)
# Replace with the portal's actual download control.
with page.expect_download(timeout=60_000) as download_info:
page.get_by_role("button", name="Download PDF").click()
download = download_info.value
download.save_as(output_path)
context.close()
browser.close()
print(f"Saved {output_path.resolve()}")
Both the URL and button name above are examples, not instructions for a specific portal. A browser context’s temporary downloads are deleted when that context closes, so call save_as() before teardown. If the portal opens a new tab instead of emitting a download, handle that tab as a separate page and determine whether it displays a PDF or starts a download.
Check the official e-Invoice option first
The GSTN e-Invoice manual documents downloading an e-Invoice as PDF or JSON after login with valid GST credentials; that documented facility is unavailable in pre-login mode. It also says generated downloads remain in Download History for two days, after which users must generate them again. These details apply to that documented facility, not to every GST portal. See the GST e-Invoice portal and its manual and help resources.
The IRIS IRP FAQ documents a print flow using an acknowledgement number or a 64-character IRN, and describes converting signed JSON to a PDF for sharing. That is an IRIS IRP-specific workflow, not a universal GST navigation path. Check the IRIS IRP FAQ for its portal’s current instructions.
cURL, Python, and Node.js alternatives
Playwright is useful when you need an authorized browser session, a portal-specific interaction, or exact control over page rendering. For a page that can be captured directly by URL, a screenshot API can avoid browser installation and print setup. ScreenshotNeo accepts a URL and returns an image or PDF; its options and parameter reference are in the ScreenshotNeo documentation.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
f.write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
The Node.js example uses the built-in fetch API and Bun’s file writer. With Node.js, use writeFile from node:fs/promises and convert the response to a buffer:
import { writeFile } from 'node:fs/promises';
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Or skip the browser setup
For a publicly accessible page, ScreenshotNeo can capture a URL in one request. Use your own authorized invoice URL in place of the example target, and consult the API documentation for PDF output and available options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000. Every feature is on every plan. See ScreenshotNeo for product details, then sign up for 1,000 free screenshots a month with no card.
Use a direct URL capture only if the page is accessible to the service and that use is authorized. For a private invoice behind a login, follow your portal’s supported export process or use your authorized browser session.
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| PDF is blank or missing invoice details | The app had not rendered the invoice when capture began, or the invoice content is in a frame or another page. | Wait for a stable, visible invoice element rather than only page navigation. Inspect frames with Playwright’s frame APIs. If a link or action opens a popup, capture and inspect that separate page. |
| PDF contains a loading view | Navigation completion did not mean the client-side invoice finished loading. | Wait for a portal-specific content-ready condition. Avoid assuming that networkidle is suitable for every page; some apps keep network connections open. |
| Colors or logos are absent | Print CSS hides them, or print backgrounds are disabled. | Try print_background=True. If the desired design is specifically the screen design, emulate screen media before printing. |
| Layout clips or spills across pages | The invoice’s print styles, paper size, or margins do not fit the selected format. | Check the page’s @page rules, adjust margins or format, and consider prefer_css_page_size=True when the CSS page size should win. |
| Expected download times out | The control did not initiate an attachment download, the click missed the control, or the portal returned a new page or error. | Verify the control and resulting page manually in an authorized session. Start expect_download() before the action and handle popups separately where applicable. |
| Saved file disappears after the script exits | The file was only in the browser context’s temporary download area. | Call download.save_as() to a durable path before closing the context. |
| Login or access fails | The portal requires an interactive or additional verification step, or the session lacks permission. | Use the portal’s supported sign-in and export flow. Do not bypass CAPTCHA, MFA, or access controls, and do not share credentials in scripts or logs. |
| PDF opens but its authenticity is uncertain | A browser print captures presentation, not necessarily the portal’s signed invoice data. | Use the official e-Invoice download/validation flow and preserve the relevant IRN, QR code, or signed JSON when needed. |
Playwright’s pages and popups documentation explains that popups are separate Page objects. Its downloads documentation covers download events and saving artifacts.
Performance, reliability, and cost
- Wait precisely: Waiting for the invoice element is generally more useful than adding an arbitrary long sleep. A fixed delay can still be too short under load and waste time when rendering is fast.
- Reuse a browser for batches: For multiple invoices in one controlled run, reuse a browser and create an isolated context or page per authorized session as appropriate. Close contexts to release resources, after saving downloads.
- Set timeouts deliberately: Navigation, content readiness, and download events can take different amounts of time. Give each a timeout and record which stage failed so retries target the right operation.
- Retry carefully: Retry transient navigation or rendering failures only after checking whether the portal created an export already. Avoid rapid repeated requests that could burden a portal or duplicate export work; follow its terms and rate limits.
- Protect invoice data: PDFs and signed data may contain sensitive business information. Use access-controlled storage, avoid logging credentials or full invoice contents, and remove temporary artifacts according to your retention needs.
- Cost: Playwright itself is an open-source automation library, but running a browser consumes machine time and storage. The portal may impose its own access and usage rules. ScreenshotNeo’s stated plans are free for 1,000 shots/month, then Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free.
Frequently asked questions
Does page.pdf() return PDF bytes as well as save a file?
Yes. You can provide a path to save directly, or call it without a path and use the returned PDF bytes in your own storage flow.
Can I use this with every GST portal?
The Playwright API applies broadly, but login, invoice rendering, selectors, and download controls depend on the portal. Use its official instructions and do not assume another portal’s workflow applies.
Should I save an e-Invoice as PDF or JSON?
That depends on what you need to preserve and what the portal supports. A PDF is convenient to view or share; signed JSON and associated validation data may be needed for machine processing or verification.
Can I automate an invoice that requires an account?
Only through authorized access and the portal’s permitted workflow. A direct URL capture service cannot access a page that requires a private browser session unless the service explicitly supports the necessary authenticated access mode.


