Most Selenium suites get slower and flakier than they need to be because they use the browser for everything. Logging in through the UI, creating test data through forms, waiting for background jobs, then finally checking one assertion in the browser is expensive and brittle. A cleaner pattern is to let the API do the setup and the browser do what only the browser can do, which is validate the user experience.

That is the practical meaning of how to combine API calls with Selenium tests. You use requests with Selenium to prepare state, seed data, authenticate faster, or validate backend side effects, then you let Selenium verify the page behavior in a real browser. Done well, this shortens tests, removes unnecessary UI steps, and gives you clearer failures. Done poorly, it creates hidden coupling and makes test ownership harder. The trick is to be deliberate about what belongs in the API layer and what belongs in the browser layer.

For teams that want this pattern without wiring two frameworks together, a platform like Endtest, an agentic AI [Test automation](https://en.wikipedia.org/wiki/Test_automation) platform, can be a simpler alternative because it lets you chain API requests and browser steps in one place. I still think it is worth understanding the custom implementation first, because the tradeoffs become much easier to judge once you have built it by hand.

Why combine API and UI tests at all?

The browser is a poor place to set up state. If the scenario under test is “an admin user can see an approved order,” you do not need Selenium to click through a dozen screens just to create that order. An API call can usually do the job more reliably and with fewer moving parts.

The main benefits are straightforward:

  • Faster setup, because API calls are usually faster than browser interactions.
  • Less flake, because you avoid UI waits during test data creation.
  • Better scope, because the browser focuses on rendering, navigation, and user-visible behavior.
  • More realistic end-to-end coverage, because you can verify that backend state and frontend state agree.

A good test should spend browser time on browser problems. Everything else should be reduced, mocked, or moved earlier in the pipeline when that is safe.

This is especially useful in CI/CD, where every extra second compounds across the suite. If your regression suite needs to run on every merge, shaving even a little unnecessary UI work matters operationally. Continuous integration is not just about speed, it is about getting fast, trustworthy feedback before broken changes spread downstream, see the concept of continuous integration.

What this pattern is, and what it is not

Combining API calls with Selenium tests does not mean turning UI tests into backend integration tests. The browser still has to validate important user-visible behavior. If you overuse API setup, you can accidentally bypass the very flows you wanted to protect.

Use this pattern for:

  • seeding test data
  • creating users, orders, projects, or other prerequisites
  • authenticating and storing session state
  • verifying side effects after a UI action
  • checking business rules at the API layer before asserting the UI outcome

Avoid using it for:

  • replacing every UI flow with API setup
  • asserting internal implementation details through the browser test
  • testing API logic only through Selenium when direct API tests would be clearer
  • creating tests that only work because of a fragile set of hard-coded backend assumptions

The best split is usually this: API for setup and precise validation, UI for rendering, accessibility, navigation, and the actual user interaction path that matters.

A simple mental model for test design

When I decide whether to use API and UI tests Selenium together, I ask three questions:

  1. What is the user-visible behavior? That belongs in Selenium.
  2. What state is required before the page can be meaningfully tested? That is usually API territory.
  3. What assertion is cheapest and clearest at each layer? Use the cheapest reliable assertion that still validates the risk.

This keeps the test from degenerating into a long end-to-end script that tries to prove everything and ends up proving nothing well.

Example scenario: create data by API, verify it in the browser

Suppose you are testing a dashboard that lists a newly created project. The UI path to create that project takes several screens and includes a background job. That is not the thing you want Selenium to pay for on every run.

Instead, use the API to create the project, then open the dashboard in Selenium and verify that the project appears.

Python example with requests and Selenium

import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

BASE_URL = “https://app.example.com” API_URL = “https://api.example.com” TOKEN = “your-api-token”

session = requests.Session() session.headers.update({“Authorization”: f”Bearer {TOKEN}”})

API setup

response = session.post( f”{API_URL}/projects”, json={“name”: “Selenium API Project”, “status”: “active”}, ) response.raise_for_status() project_id = response.json()[“id”]

Browser verification

driver = webdriver.Chrome() wait = WebDriverWait(driver, 10)

driver.get(f”{BASE_URL}/dashboard”) wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, “[data-testid=’project-list’]”)))

project_row = driver.find_element(By.CSS_SELECTOR, f”[data-project-id=’{project_id}’]”) assert “Selenium API Project” in project_row.text

driver.quit()

This example shows the core pattern. The API creates durable test data, the browser confirms that the page renders it correctly. The test is shorter, and if it fails, the failure is easier to interpret.

Authentication is often the biggest win

A lot of teams first combine API calls with Selenium tests just to avoid logging in through the UI. That is usually a good tradeoff.

Browser login flows are often slow because they involve redirects, third-party identity providers, MFA, or consent screens. For test purposes, you can often authenticate with the API, then transfer the session into the browser.

There are several common strategies:

1. Use an API to obtain a token, then inject it into the browser session

If your app uses token-based auth, the API can authenticate once and return a token. Then Selenium can load a page with that token in place, or set it in local storage or cookies before navigation.

This works well, but be careful:

  • tokens should be scoped to test environments
  • session injection must match how the app actually reads auth state
  • avoid creating a test that passes only because the browser was preloaded in a way users never experience

2. Use a backend endpoint to create a logged-in session

Some systems expose a test-only or internal endpoint that returns a session cookie. This can be very effective, but it needs strict environment isolation and access control.

3. Reuse browser storage after an API login

In some architectures, logging in via API and copying the resulting cookie or local storage value into Selenium is the least disruptive option.

If you are evaluating this path, the question is not “Can I do it?” but “Will this reflect the real authentication model enough to be trustworthy?”

Verifying API side effects from the UI

Another useful pattern is UI action first, API assertion second.

For example, a user clicks “Submit” in Selenium, and then the test calls the API to confirm the object was created with the expected fields. This is especially useful when the UI itself does not expose every field or when the backend applies transformations you want to verify precisely.

import requests
from selenium import webdriver
from selenium.webdriver.common.by import By

API_URL = “https://api.example.com” TOKEN = “your-api-token”

browser = webdriver.Chrome() api = requests.Session() api.headers.update({“Authorization”: f”Bearer {TOKEN}”})

browser.get(“https://app.example.com/new-order”) browser.find_element(By.ID, “customer”).send_keys(“Ada”) browser.find_element(By.ID, “submit”).click()

order_id = browser.find_element(By.CSS_SELECTOR, “[data-testid=’order-id’]”).text api_response = api.get(f”{API_URL}/orders/{order_id}”) api_response.raise_for_status()

assert api_response.json()[“customer_name”] == “Ada” browser.quit()

This pattern is valuable when you want to confirm the browser initiated the workflow and the backend persisted the correct result.

Keep API calls narrow and explicit

A common failure mode is letting API setup grow into a separate hidden test framework inside the UI test. That usually leads to problems:

  • setup logic gets copied into many tests
  • response parsing becomes inconsistent
  • failures are harder to diagnose because the setup has its own branches
  • the test becomes dependent on too many backend internals

Instead, keep API calls narrow and explicit.

Good signs:

  • one or two setup requests per test, not ten
  • response fields are used for a real UI assertion
  • the test uses stable IDs or handles from the API response
  • cleanup is deliberate, not implied

If you need a shared helper, that is fine. For example, a create_project() helper can centralize setup logic. But the helper should remain small and readable, and it should not hide important assumptions.

Practical rules for stable API plus Selenium tests

Prefer stable identifiers over visible text when possible

Visible text changes often, especially in product teams that iterate quickly. If the UI provides a data-testid or a stable attribute, use it. When asserting against items created by API, keep a returned ID in memory and use that to locate the right row or card.

Wait on conditions, not on time

The API may create data immediately, but the UI may still need time to fetch and render it. Use explicit waits for the expected condition, not sleep calls.

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

wait = WebDriverWait(driver, 15) wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, “[data-testid=’status’]”), “Active”))

This reduces flake and makes your failures more meaningful.

Clean up test data

API setup makes cleanup easier, so use it. If a test creates state through the API, it can often delete or reset that state through the API as well. That matters in CI because leftover data can create false positives or pollute later runs.

Validate only what matters in each layer

If the API already proves the object exists and has the right attributes, the browser should not repeat every field. It should verify that the page displays the important user-facing information and that the action path is intact.

When to keep pure API tests separate from Selenium

Even though this article is about combining API calls with Selenium tests, not every API should be embedded in a browser test.

Keep separate API tests when:

  • the endpoint has important business logic independent of the UI
  • you need fast coverage for many edge cases
  • failures should be isolated from browser instability
  • the API contract is a shared dependency across teams

This is where OpenAPI can help. A documented schema gives you a contract surface to test against, and it helps you avoid treating the UI test as the only source of truth.

A good test stack usually looks like this:

  • API tests for endpoint correctness, edge cases, and contract checks
  • Selenium tests for user journeys, rendering, and workflow validation
  • Combined API plus UI tests for a few high-value end-to-end paths where setup friction is otherwise high

That division is healthier than trying to make one framework do all three jobs.

Common failure modes and how to avoid them

Race conditions between API setup and UI rendering

You create a record through the API, then the browser loads a list page before the frontend cache or search index has caught up. The test fails intermittently.

Fixes include:

  • wait for the backend state to become queryable before opening the UI
  • poll the API until the resource is visible in the expected read model
  • in test environments, reduce async lag where appropriate

Tests that depend on implementation details

If your Selenium test expects a specific database-generated ID format or internal workflow status, it is probably too coupled.

Keep the assertion focused on what users observe, unless the internal state is part of the contract you are explicitly testing.

Shared setup data causing cross-test contamination

If multiple tests reuse the same API-created account, state changes in one test can break another.

Prefer isolated fixtures. If shared fixtures are unavoidable, reset them aggressively.

Too much logic in test helpers

A helper that hides retries, branching, conditional cleanup, and response transformations may seem convenient, but it becomes hard to debug when the test fails in CI.

Helpers should reduce repetition, not hide behavior.

Where this fits in CI/CD

Combined API and Selenium tests are a good fit for regression suites that run on pull requests or in scheduled pipelines. They should not be used as a blanket replacement for all browser coverage, but they can significantly reduce the cost of common workflows.

A practical pipeline often looks like this:

  1. run fast unit and API checks first
  2. run a smaller set of combined API plus UI smoke tests
  3. run broader browser regression later, or in parallel

This sequencing matters because it helps you fail earlier on issues that are cheap to diagnose.

name: ci

on: [push, pull_request]

jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: “3.11” - run: pip install -r requirements.txt - run: pytest tests/api tests/e2e

This is intentionally simple, but the principle is important. Separate concerns in the pipeline so you can see where a failure belongs. If the API setup fails, you want to know that before you start blaming the browser.

A note on Selenium versus more integrated tooling

Selenium is flexible, mature, and widely supported. The official documentation is still the best place to check current APIs and browser support details, see the Selenium documentation.

That said, flexibility comes with maintenance cost. If your team is spending a lot of time on fixture code, helper wrappers, and retry logic just to combine API requests with browser steps, a platform that handles both together may be a better fit.

This is one reason teams sometimes evaluate Endtest vs Selenium. Endtest includes API testing as part of the same end-to-end suite, so you can send API requests, assert on responses, and chain them with browser steps in one workflow. Its migration docs from Selenium also show that there is a path for teams that want to move existing coverage without rebuilding everything manually.

My practical view is simple: if your team wants maximum control and already has strong framework ownership, Selenium plus a good HTTP client is a reasonable choice. If your team wants a simpler operational model, human-readable steps, and one platform for UI plus API flows, Endtest is worth a look.

Decision checklist

Before you combine API calls with Selenium tests, check the following:

  • Does the API step remove unnecessary UI setup, or does it bypass meaningful user behavior?
  • Can the API give you a stable identifier for later UI assertions?
  • Is the test still readable if someone new opens it in six months?
  • Will cleanup be reliable in CI?
  • Are you mixing a few high-value integration paths, or are you trying to turn every UI test into an orchestration script?
  • Would a separate API test be clearer for this assertion?

If you cannot answer these clearly, the test is probably too complex.

Final take

The best use of API calls in Selenium tests is not to make every test more powerful, it is to make each test more focused. Use the API to create or verify state quickly, then use the browser to confirm that the application presents and handles that state correctly. That gives you faster tests, fewer flaky setup steps, and clearer failures.

The cost is that you now own two layers in one test. That is fine when the boundaries are clear and the test is narrow. It is a problem when the test turns into a mini orchestration system.

For most SDET and QA automation teams, the rule of thumb is this: use API setup to remove friction, not to hide complexity. Keep the browser on the part of the workflow the browser is good at, and keep the API work explicit enough that the next engineer can understand why the test exists.

If your team wants the same API plus browser flow without stitching together multiple tools, Endtest is a reasonable simpler alternative because it can combine API requests and browser steps in one platform, with editable, platform-native test steps rather than framework glue. For teams already invested in Selenium, though, the patterns in this article are enough to build a maintainable approach without overengineering it.