ScreenshotNeo

BlogHow-to

How to Send a POST Request to a Website with Pyppeteer

Use Pyppeteer request interception to turn a page request into a POST, set its body and headers, and avoid stalled browser traffic.

By the ScreenshotNeo team1 October 20267 min read

Short answer: enable request interception with page.setRequestInterception(True), handle the page’s request event, and call request.continue_() with method, postData, and any required headers. Every intercepted request needs a decision: continue it unchanged, modify it, respond to it, or abort it.

This technique changes a request made by the browser page. It is different from sending an independent HTTP POST with Requests, curl, or a Node.js HTTP client. Use interception when the POST must happen inside page activity or needs browser cookies, JavaScript state, or navigation behavior.

How Pyppeteer POST interception works

Pyppeteer is a Python port with an API modeled after Puppeteer. Request interception is enabled per page. Once enabled, matching requests pause until your handler resolves them. The documented override fields include:

Field Purpose
method HTTP method such as POST.
postData Request body as a string. The capitalization matters.
headers Headers sent with the continued request.
url Optional replacement URL.

See the Pyppeteer API reference for the historical 0.0.25 API documented by the project. The target website still determines the required endpoint, body encoding, authentication, cookies, CSRF token, and success response.

Complete Pyppeteer example

The following script opens a page, intercepts one specific endpoint, changes it to a form-encoded POST, records the response, and continues every other request normally.

import asyncio
from pyppeteer import launch

TARGET_ENDPOINT = "https://example.com/endpoint"
TARGET_PAGE = "https://example.com/form"

async def main():
    browser = await launch()
    page = await browser.newPage()
    await page.setRequestInterception(True)

    post_sent = False

    async def handle_request(request):
        nonlocal post_sent

        if request.url == TARGET_ENDPOINT and not post_sent:
            post_sent = True
            await request.continue_({
                "method": "POST",
                "postData": "key=value&source=pyppeteer",
                "headers": {
                    "Content-Type": "application/x-www-form-urlencoded",
                },
            })
        else:
            # Requests remain paused until one of these methods is called.
            await request.continue_()

    async def handle_response(response):
        if response.url == TARGET_ENDPOINT:
            print("status:", response.status)
            print("response body:", await response.text())

    page.on("request", lambda request: asyncio.ensure_future(handle_request(request)))
    page.on("response", lambda response: asyncio.ensure_future(handle_response(response)))

    try:
        await page.goto(TARGET_PAGE, {"waitUntil": "networkidle2"})
    finally:
        await browser.close()

asyncio.run(main())

Replace the placeholder URLs and payload with values required by the site. The postData value is a string, so encode it according to the endpoint: URL-encoded form data, JSON, multipart data, or another documented format.

Install Pyppeteer

python -m pip install pyppeteer
pyppeteer-install

The project documentation describes a first-run Chromium download; pyppeteer-install can install the browser explicitly. You can also launch an installed browser by passing an executable path.

Sending JSON instead of form data

For a JSON API, serialize the body and set the content type. Do not send a Python dictionary directly as postData.

import json

payload = {"email": "dev@example.com", "enabled": True}
await request.continue_({
    "method": "POST",
    "postData": json.dumps(payload),
    "headers": {
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
})

Triggering a POST from a page action

If the page already submits a form or calls fetch(), interception can observe or modify that browser-generated request. Attach the listener before the action that triggers it and limit the match so unrelated requests continue untouched.

async def submit_form(page):
    await page.setRequestInterception(True)

    async def handle_request(request):
        if request.url == "https://example.com/api/submit" and request.method == "POST":
            print("outgoing body:", request.postData)
            await request.continue_({
                "headers": {
                    **request.headers,
                    "X-Automation-Source": "pyppeteer",
                }
            })
        else:
            await request.continue_()

    page.on("request", lambda request: asyncio.ensure_future(handle_request(request)))
    await page.click("button[type=submit]")

When you only need to inspect the original request, call request.continue_() without overrides. A page action may issue multiple matching requests, such as a preflight request, retry, analytics call, or redirect. Match the URL, method, and—when necessary—resource type or a one-shot flag.

Headers, cookies, authentication, and CSRF

Preserve existing headers

Overriding headers can replace the browser’s existing set. Start from request.headers when you need to add one value.

headers = dict(request.headers)
headers["X-Request-ID"] = "automation-123"
await request.continue_({"headers": headers})

Cookies and login state

Browser cookies are attached to page requests automatically. Log in first, or set cookies with page.setCookie(), then intercept the request. A standalone curl or Requests call will not automatically share that browser session.

CSRF tokens

Many forms require a token from a hidden input, cookie, or meta tag. Load the page, read the token, and include it in the body or headers expected by the site. A token copied from an earlier session can expire or be bound to a particular cookie jar.

Authorization

Use the scheme documented by the endpoint, such as a bearer token or basic authentication. Keep secrets outside source code and avoid printing authorization headers or sensitive bodies in logs.

Standalone POST alternatives

Request interception is unnecessary when you simply need to call an HTTP endpoint without browser behavior.

Python Requests

import requests

response = requests.post(
    "https://example.com/endpoint",
    data={"key": "value"},
    timeout=30,
)
response.raise_for_status()
print(response.text)

For JSON, use json=payload instead of data=payload. Requests documents both forms in its Quickstart.

curl

curl --fail-with-body --request POST \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data 'key=value' \
  https://example.com/endpoint

Node.js

const response = await fetch('https://example.com/endpoint', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ key: 'value' })
});

if (!response.ok) {
  throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}

console.log(await response.text());

Playwright also provides an APIRequestContext with a Python post() method, JSON, form, and multipart options, plus optional cookie sharing with a browser context.

Or skip the browser setup

If your goal is a clean screenshot after a page action or POST-driven page state, ScreenshotNeo provides a website screenshot API. Its GET endpoint returns PNG, JPEG, WebP, or PDF, and its capture options include custom JavaScript, headers, cookies, waiting for selectors or network idle, and blocking selected requests.

One call captures a page:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo API documentation for options. Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Troubleshooting

Symptom Cause Fix
Navigation hangs An intercepted request was never resolved. Call continue_(), abort(), or respond() on every path, including nonmatching requests.
Server says method is GET The override was not applied to the matching request. Check the exact URL comparison and use the documented method key with value POST.
Empty or malformed body postData is not encoded for the endpoint. Send a string and set the matching content type; JSON requires json.dumps().
401 or 403 response Missing authentication, cookies, CSRF token, or required headers. Log status and nonsecret headers, authenticate in the same browser context, and reproduce the site’s documented request.
POST happens twice A page action retried or triggered multiple matching requests. Use a one-shot flag, inspect request method and URL, and account for redirects or retries.
requestfailed fires Transport, DNS, TLS, timeout, or browser failure. Capture the failure event, verify the URL from the browser environment, and retry only when the operation is safe to repeat.
Response status is 4xx or 5xx The server returned an HTTP error; this is distinct from a transport failure. Read the response status and body, then correct the request according to the API’s error message.
Chromium cannot launch Browser binary is missing or incompatible. Run pyppeteer-install or pass a valid installed Chromium executable path.

Performance and reliability checklist

  • Enable interception only on pages that need it; every resource request adds handler work.
  • Use a precise URL and method match so images, scripts, fonts, and analytics continue immediately.
  • Set navigation and request timeouts appropriate to the target site, and close the browser in a finally block.
  • Reuse a browser process for batches, while creating isolated pages or contexts when cookies and authentication must not leak.
  • Make POST operations idempotent where possible, or attach an idempotency key before implementing retries.
  • Record status, URL, timing, and a redacted body for diagnosis. Do not log credentials, session cookies, or personal data.
  • Respect the target website’s terms, authentication rules, rate limits, and robots or API guidance.

FAQ

Can I send a POST without opening a page?

Yes. Use Requests, curl, Node.js fetch, or Playwright’s API request context. Pyppeteer interception is for requests made by a browser page.

Why is the option called postData instead of post_data?

postData is the documented camel-case override name in Pyppeteer’s request API. Using a different key will not set the body.

Does changing the method guarantee a successful POST?

No. The server may require a particular body format, token, cookie, origin, referer, or authorization scheme.

Can interception modify every request?

It can inspect every intercepted request, but broad modifications are risky. Continue unrelated traffic unchanged and target only the request you intend to change.