Resource library

QA How-To

Playwright Python API testing (2026)

Master Playwright Python API testing with APIRequestContext, pytest fixtures, authentication, schema checks, cleanup, UI integration, and interviews today.

19 min read | 2,692 words

TL;DR

Playwright Python API testing works best when the test synchronizes with the exact application event, uses stable locators or request predicates, and asserts the resulting business state. Start with the simplest public Playwright API, then add a focused fallback only when the application requires it.

Key Takeaways

  • Start with Playwright's public, action-oriented APIs and stable user-facing locators.
  • Begin waiting before the action that triggers asynchronous work.
  • Assert the final business outcome, not only that an automation call returned.
  • Keep test data isolated and make cleanup safe for retries and parallel workers.
  • Use traces, screenshots, and targeted diagnostics to explain failures.
  • Treat fallback techniques as documented exceptions with focused coverage.

Playwright Python API testing is a practical testing workflow that verifies user-visible behavior, not merely that automation commands completed. This guide gives you runnable patterns, assertion strategies, debugging methods, and design choices suitable for a maintained 2026 test suite.

The examples emphasize deterministic synchronization, accessible locators, useful failure evidence, and tests that stay readable as the application evolves. Use the quick answer first, then work through the numbered sections for production details and interview reasoning.

Playwright Python API testing is Playwright's HTTP client for API tests, test-data setup, authentication, and backend verification without driving the browser UI. In Playwright Test, use the built-in request fixture for an isolated request context, or create one with p.request.new_context() when you need custom lifecycle and configuration.

The important design choice is context ownership. An isolated context manages its own cookies and must be disposed when you create it. A context obtained from browser_context.request shares cookie storage with that browser context, which makes API login and browser setup work together. This guide explains both models using current Python APIs for 2026.

TL;DR

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    api = p.request.new_context(base_url="http://127.0.0.1:3000")
    try:
        response = api.get("/api/me")
        assert response.ok
        assert response.json()["email"] == "qa@example.com"
    finally:
        api.dispose()
Context source Cookie relationship Lifecycle Best use
Test request fixture Isolated for the test Runner-managed API tests and data setup
p.request.new_context() Isolated You call dispose() Custom clients and non-test utilities
browser_context.request Shares browser cookies Browser context owns it API login plus UI flow

Set base_url and common headers centrally, assert both transport and payload, and remember that non-2xx responses are returned by default unless fail_on_status_code is enabled.

1. Playwright Python API testing: Core Concepts

APIRequestContext sends HTTP and HTTPS requests and returns an APIResponse. It supports convenience methods for get, post, put, patch, delete, and head, plus the general fetch method. Request options cover JSON data, HTML form data, multipart data, headers, query parameters, redirects, timeout, and selected network retries.

It is part of Playwright, but it does not need a visible browser page. That makes it suitable for API-only tests and fast setup around UI tests. A checkout test can create a cart through the API, open the browser at that cart, and verify only the workflow that matters. An API suite can exercise status codes, contracts, authorization, idempotency, and state transitions directly.

from playwright.sync_api import Playwright, APIRequestContext

def create_api(playwright: Playwright) -> APIRequestContext:
    return playwright.request.new_context(
        base_url="http://127.0.0.1:3000",
        extra_http_headers={"Accept": "application/json"},
    )

data accepts a serializable object and Playwright serializes it as JSON, adding an appropriate content type unless you override the header. A response is not automatically considered a test success just because the request completed. By default, a 400 or 500 still produces an APIResponse, so your test must assert status or use fail_on_status_code deliberately.

For a broader API quality strategy beyond the client mechanics, see the API testing interview and design guide.

2. Playwright Python API testing: Setup and First Working Test

Initialize Playwright Test and choose Python. The initializer installs the runner, browsers, configuration, and an example test.

python -m pip install pytest-playwright
playwright install
pytest -q

Configure a base URL and non-secret common headers in playwright.config.ts. Read credentials from environment variables rather than committing them.

def test_health(api_request_context):
    response = api_request_context.get("/health")
    assert response.status == 200
    assert response.headers["content-type"].startswith("application/json")
    assert response.json() == {"status": "ok"}

The built-in request fixture respects relevant test options such as base_url and extra_http_headers. A relative URL such as /api/users is resolved against the configured base URL. Keep the leading slash policy consistent because standard URL resolution rules can produce surprising paths when the base contains a pathname.

Create an API smoke test:

def test_create_project(api_request_context):
    response = api_request_context.post(
        "/api/projects",
        data={"name": "Payments regression", "owner": "qa@example.com"},
    )
    assert response.status == 201
    assert response.json()["name"] == "Payments regression"

expect(response).toBeOK() checks that the status is in the successful 2xx range. Keep an exact status assertion when 200 versus 201 or 204 is part of the contract. A health endpoint that returns HTML from a reverse proxy should not pass merely because it is 200, so validate payload and content type too.

3. Choose Between the Request Fixture and newContext()

The fixture is the simplest option inside Playwright Test. The runner creates and cleans it for the test. It provides isolation and avoids custom lifecycle code.

import pytest
from playwright.sync_api import sync_playwright

@pytest.fixture(scope="session")
def api_request_context():
    with sync_playwright() as p:
        context = p.request.new_context(
            base_url="http://127.0.0.1:3000",
            extra_http_headers={"Authorization": "Bearer test-token"},
        )
        yield context
        context.dispose()

Create a standalone context when a worker fixture, library, global setup, or specialized client needs different headers, credentials, proxy, storage state, or base URL.

def test_search_users(api_request_context):
    response = api_request_context.get(
        "/api/users", params={"role": "tester", "active": "true", "page": "1"}
    )
    assert response.ok

Responses are retained so their bodies can be read later. Disposing a context releases its resources, and methods called after disposal throw. Do not dispose the built-in fixture yourself or dispose browser_context.request independently from its owner.

Use one context per identity or isolation boundary rather than changing the Authorization header unpredictably on a shared helper. Explicit contexts make parallel tests safer and make request ownership visible during code review.

4. Send JSON, Query Parameters, Forms, and Files

Choose the request body option that matches the endpoint contract. data is commonly used for JSON. form sends application/x-www-form-urlencoded. multipart sends multipart form data and can include file-like values. params encodes query parameters separately from the path.

def test_login_and_open_dashboard(browser):
    context = browser.new_context(base_url="http://127.0.0.1:3000")
    try:
        login = context.request.post(
            "/api/login",
            data={"email": "qa@example.com", "password": "test-only-password"},
        )
        assert login.ok
        page = context.new_page()
        page.goto("/dashboard")
        assert page.get_by_role("heading", name="Dashboard").is_visible()
    finally:
        context.close()

For multipart upload, pass a file-like object with name, mimeType, and buffer. This is self-contained and does not depend on a file path.

def test_order_contract(api_request_context):
    response = api_request_context.get("/api/orders/ORD-42")
    assert response.status == 200
    assert "application/json" in response.headers["content-type"]
    order = response.json()
    assert order["id"] == "ORD-42"
    assert order["status"] in {"pending", "paid", "cancelled"}
    assert order["total"] > 0

Do not set Content-Type: multipart/form-data manually because the boundary must match the encoded body. Let Playwright generate it. Likewise, do not use JSON.stringify for a normal serializable object unless the endpoint specifically expects a raw string. Automatic serialization gives the correct content type and clearer intent.

5. Understand Authentication and Cookie Sharing

Authentication can use bearer headers, HTTP credentials, client certificates, or cookies, depending on the application. With a token API, set the Authorization header on a dedicated context or per request. Keep secrets in runtime configuration and redact them from attachments and logs.

def test_duplicate_email(api_request_context):
    payload = {"email": "duplicate@example.test", "name": "Test user"}
    first = api_request_context.post("/api/users", data=payload)
    assert first.status == 201
    duplicate = api_request_context.post("/api/users", data=payload)
    assert duplicate.status == 409
    assert duplicate.json()["code"] == "EMAIL_ALREADY_EXISTS"

A request context associated with a BrowserContext shares its cookie storage. A Set-Cookie response received through browser_context.request updates browser cookies, and browser cookies are used by later API calls from that associated context. This enables fast session setup without copying cookie values manually.

def test_redirect_contract(api_request_context):
    response = api_request_context.get(
        "/legacy-report", timeout=20_000, max_redirects=0, max_retries=2
    )
    assert response.status == 302
    assert response.headers["location"] == "/reports/current"

The try/finally ensures a failed assertion does not skip browser cleanup. For persistent reuse across tests, generate authenticated storage_state in a setup project and keep account isolation clear.

6. Inspect APIResponse Without Hiding Failures

An APIResponse exposes status(), statusText(), ok(), url(), headers(), headersArray(), body(), text(), and json(). Parse the representation that matches the endpoint. A 204 response has no JSON body, so calling json() would be a test bug.

def test_archive_project(page, api_request_context):
    created = api_request_context.post(
        "/api/projects", data={"name": "Archive candidate"}
    )
    assert created.status == 201
    project = created.json()
    try:
        page.goto(f"/projects/{project['id']}/settings")
        page.get_by_role("button", name="Archive project").click()
        page.get_by_role("button", name="Confirm archive").click()
        assert page.get_by_role("status").inner_text() == "Project archived"
    finally:
        api_request_context.delete(f"/api/test-projects/{project['id']}")

Python assertions describe expected shape to the type checker but do not validate untrusted runtime data. For a strong contract suite, use explicit assertions or a schema validator your project already depends on. Validate required fields, types, invariants, and prohibited data rather than comparing enormous payloads blindly.

Failure messages should include safe diagnostic context. Status, method, path, correlation ID, and a redacted response excerpt are useful. Tokens, session cookies, passwords, and personal data are not. If response bodies are large, call api_response.dispose() after extracting what you need, or dispose the owning context at the proper scope.

By default, request methods return responses for error statuses. This behavior is excellent for negative testing because you can assert a structured 422 or 409 response. Set fail_on_status_code: true only when non-2xx and non-3xx should abort the operation rather than become an assertion subject.

7. Model Negative Tests and Error Contracts

Negative API tests should verify more than "not 200." Assert the exact status, stable error code, safe message, field-level details, and lack of an unintended state change. Send one meaningful invalid condition at a time so the failure has a clear cause.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    api = p.request.new_context(base_url="http://127.0.0.1:3000")
    try:
        response = api.get("/api/me")
        assert response.ok
        assert response.json()["email"] == "qa@example.com"
    finally:
        api.dispose()

Keep fail_on_status_code false for this test, which is the default. A thrown transport-style error would make it harder to assert the error body. For a deeper treatment of conflict semantics, use the HTTP 409 Conflict testing guide.

Authorization tests should cover missing identity, valid identity without permission, and valid identity with permission. Distinguish authentication from authorization and assert the application's chosen 401 and 403 contract. Confirm a rejected mutation did not alter the resource by reading it afterward through a permitted account.

Avoid using production-like personal data in error tests. Unique synthetic identifiers prevent parallel collisions, and deterministic cleanup keeps reruns safe.

8. Control Timeouts, Redirects, and Network Retries

An API request has a 30-second request timeout by default. Pass a local timeout for an endpoint with a documented service-level expectation, or configure the context default. Passing 0 disables the request timeout, which is rarely suitable for CI because a dead dependency can hold a worker indefinitely.

from playwright.sync_api import Playwright, APIRequestContext

def create_api(playwright: Playwright) -> APIRequestContext:
    return playwright.request.new_context(
        base_url="http://127.0.0.1:3000",
        extra_http_headers={"Accept": "application/json"},
    )

Requests normally follow redirects, up to the supported default limit. max_redirects: 0 is useful when the redirect itself is the contract under test. Inspect location and cookie headers rather than following to the final page.

max_retries handles a narrow class of network errors. Current Playwright behavior retries ECONNRESET; it does not retry based on HTTP response status. A value of 2 does not make 503 responses automatically retry. Implement application-aware status retry only when the endpoint is safe to repeat, the policy is explicit, and the assertions can prove the attempt behavior.

def test_health(api_request_context):
    response = api_request_context.get("/health")
    assert response.status == 200
    assert response.headers["content-type"].startswith("application/json")
    assert response.json() == {"status": "ok"}

Never retry a non-idempotent payment or order creation blindly. Use idempotency keys and verify server semantics where a retry is required. The API idempotency testing guide covers duplicate delivery and safe retry cases.

Separate transport failures from HTTP failures in reports. A DNS error, TLS failure, connection reset, 503 response, and schema mismatch belong to different diagnostic categories even if all block the same test.

9. Combine API Setup with UI Verification Safely

API setup can make UI tests faster and more focused. Create only the prerequisite state through the API, then verify behavior through the browser. Cleanup through the API in a fixture so the test remains readable and repeatable.

def test_create_project(api_request_context):
    response = api_request_context.post(
        "/api/projects",
        data={"name": "Payments regression", "owner": "qa@example.com"},
    )
    assert response.status == 201
    assert response.json()["name"] == "Payments regression"

The cleanup endpoint is intentionally test-specific in this example. In real systems, use authorized cleanup that cannot affect resources outside the test namespace. Make cleanup tolerate an already deleted or partially created resource.

Do not create state through an internal implementation detail that bypasses the rules you are trying to verify. If the scenario tests public API creation, use that API. If it tests UI creation, do not replace the behavior under test with API setup. The boundary should make the test narrower without removing its purpose.

API and UI identities must also align. An isolated request fixture does not automatically share cookies with the page. Use token-based endpoints, storage state, or browser_context.request deliberately.

10. Production Checklist for Playwright Python API testing

Organize API clients around business resources, not one giant wrapper. A ProjectsApi can accept an APIRequestContext, implement request construction, and return the raw APIResponse or a deliberately validated domain result. Tests should still make the contract assertions visible.

Keep contexts isolated by actor, tenant, and parallel worker. Use synthetic unique data, explicit cleanup, and idempotent teardown. Never share a mutable account when tests change its permissions or preferences concurrently.

Apply this review checklist:

  • Base URL and common headers are configured once at an understandable scope.
  • Secrets come from the environment and never enter source, traces, or failure excerpts.
  • Exact status and meaningful payload invariants are asserted.
  • Negative responses remain inspectable instead of being converted into opaque exceptions.
  • Standalone contexts are disposed in teardown or finally.
  • Browser-shared contexts are used only when cookie sharing is intentional.
  • Request timeouts and retries reflect endpoint semantics.
  • Test data is unique, minimal, and cleaned safely.
  • API setup does not bypass the behavior the test claims to cover.

Treat client helpers as test infrastructure with code review, types, and focused tests. A wrapper that silently retries, ignores status, or catches every error can make failures harder to diagnose than direct calls. Favor small abstractions that remove repeated mechanics while preserving HTTP evidence.

Interview Questions and Answers

Q: What is APIRequestContext in Playwright?

It is an HTTP client context that sends requests and maintains its own configuration and cookie state. It supports API-only tests, data setup, authentication, and backend verification around browser tests. It returns APIResponse objects that expose status, headers, and body methods.

Q: What is the difference between the request fixture and p.request.new_context()?

The Playwright Test fixture is runner-managed and convenient for test-scoped isolated requests. A context created with newContext() is explicitly configured and owned by your code, so you must dispose it. I use the latter for worker fixtures, utilities, or separate identities.

Q: Does APIRequestContext throw for a 404 or 500 response?

Not by default. It returns an APIResponse, which lets negative tests assert status and error payload. fail_on_status_code: true changes that behavior for responses outside 2xx and 3xx.

Q: How do API calls share authentication with a browser?

browser_context.request shares cookie storage with its owning browser context. Cookies set by an API response can authenticate later page navigation, and browser cookies are available to associated API calls. A separately created request context or the isolated request fixture does not automatically share those cookies.

Q: How do you validate a JSON response safely?

I first assert status and content type, then parse JSON and validate required fields, types, and business invariants. A Python type assertion helps editor and type checker use but does not validate runtime data. Contract-critical suites should use explicit assertions or an approved runtime schema library.

Q: What does max_retries retry?

It retries selected network errors, currently ECONNRESET, up to the configured maximum. It does not retry 429, 500, or 503 responses. Status-based retry needs an explicit, idempotency-aware policy.

Q: Why must a standalone APIRequestContext be disposed?

The context retains response resources so response bodies remain available. dispose() releases those resources and ends the context lifecycle. I place it in fixture teardown or a finally block so failures do not leak resources.

Q: When should API setup be used in a UI test?

Use it for prerequisites outside the UI behavior being tested, such as creating a project before testing archive controls. It reduces duration and failure surface. Do not use it to bypass the exact creation or validation path that the scenario claims to verify.

Common Mistakes

  • Assuming every non-2xx response throws automatically.
  • Calling json() on a 204 response or a non-JSON error page.
  • Treating a Python cast as runtime schema validation.
  • Creating standalone request contexts without disposing them.
  • Manually setting a multipart content type and breaking its boundary.
  • Logging bearer tokens, cookies, credentials, or sensitive response data.
  • Sharing one mutable account across parallel tests.
  • Expecting an isolated request fixture to share page cookies.
  • Enabling retries for unsafe mutations without idempotency protection.
  • Using API setup to bypass the behavior the test is supposed to exercise.

Production Review Checklist: Playwright Python API testing

Before merging a test, confirm that it proves an observable requirement and can fail for the right reason. Keep setup explicit, use a locator that communicates intent, and place synchronization before the action that triggers the state change. Assert the final business state, not only an intermediate implementation detail. Run the case repeatedly in a clean worker and once under CI-like resource constraints. Preserve a trace, screenshot, or response detail when a failure would otherwise be difficult to reproduce.

A reviewer should also check isolation. The test must create or identify its own data, avoid depending on execution order, and clean up server-side records when appropriate. Timeouts should express a known service-level expectation rather than hide uncertainty. If the feature has several meaningful variants, use a small data table or project matrix instead of copying the entire test. Finally, keep one clear responsibility per test so that the report names the behavior that actually broke.

Conclusion

Playwright Python API testing gives QA engineers a capable HTTP client inside the same runner used for browser automation. Start with the request fixture, configure base URL and headers centrally, and create custom contexts only when identity, storage, or lifecycle requires them.

Build one focused API test that asserts status, headers, and payload, then use the same client for narrowly scoped test setup. Keep cookie ownership, cleanup, negative responses, timeouts, and retry safety explicit. Those practices produce fast API coverage without sacrificing trustworthy diagnostics.

Interview Questions and Answers

What problems does APIRequestContext solve?

It provides an HTTP client within the Playwright ecosystem for API testing, setup, cleanup, authentication, and postcondition checks. It avoids browser overhead when no UI behavior is needed. It also supports cookie sharing when obtained from a browser context.

How does the request fixture differ from browserContext.request?

The request fixture is isolated and runner-managed for the test. `browserContext.request` shares the browser context cookie jar, so API and page sessions influence each other. I choose based on whether cookie sharing is part of the scenario.

How do you test a negative API response?

I keep `failOnStatusCode` false, assert the exact status, and validate the stable error contract. I also verify that a rejected mutation did not change server state. This distinguishes a correct rejection from a misleading error response.

How do you validate JSON beyond TypeScript types?

A TypeScript cast does not validate runtime input. I assert required fields and invariants directly or use an approved runtime schema validator. I also check status and content type before parsing.

What does APIRequestContext maxRetries do?

It retries a narrow network error category, currently including `ECONNRESET`. It does not retry HTTP 429, 500, or 503 responses. Status-based retry needs explicit policy and idempotency analysis.

How do you prevent API test data collisions?

I generate unique data per test or worker and keep resources inside a test namespace. Cleanup is idempotent and runs in `finally` or fixture teardown. I avoid shared mutable accounts and fixed resource names during parallel execution.

When should API setup be used in a browser test?

I use it for prerequisites outside the UI behavior being tested. It makes the browser flow narrower and faster while preserving the user action under test. I do not bypass the exact path the scenario claims to verify.

Why is disposal important for standalone contexts?

The request context retains response resources so bodies remain readable. Disposing it releases those resources and closes the lifecycle. I guarantee disposal with fixture teardown or `finally` even when assertions fail.

Frequently Asked Questions

What is Playwright APIRequestContext?

It is Playwright's HTTP client context for sending API requests and managing request configuration and cookies. It supports API-only tests, test-data setup, authentication, and backend checks around browser tests.

Should I use the request fixture or request.newContext?

Use the built-in fixture for normal test-scoped isolated calls because the runner manages it. Use `newContext()` for custom identity, storage, or lifecycle, and dispose that context yourself.

Does APIRequestContext share cookies with the browser?

`browserContext.request` shares cookie storage with its browser context. A separately created API request context and the standard isolated request fixture do not automatically share page cookies.

Does Playwright throw on API status 400 or 500?

Not by default. It returns an `APIResponse`, which allows negative tests to assert the status and error body. Set `failOnStatusCode: true` only when those statuses should abort the operation.

How do I send JSON with APIRequestContext?

Pass a serializable object through the `data` option of `post`, `put`, `patch`, or `fetch`. Playwright serializes it as JSON and sets the content type unless you explicitly override it.

Do I need to dispose APIRequestContext?

Dispose contexts that your code creates with `request.newContext()`. The Playwright Test request fixture and contexts owned by a browser lifecycle should be cleaned up by their respective owners.

Can APIRequestContext upload a file?

Yes. Use the `multipart` option with a stream or a file-like object containing `name`, `mimeType`, and `buffer`. Let Playwright generate the multipart content type and boundary.

Related Guides