ScreenshotNeo

BlogGuides

How to Test APIs with Cypress: Part 1

Use Cypress cy.request() to test real API endpoints, verify response contracts, handle errors, and understand when to use cy.intercept().

By the ScreenshotNeo team4 October 20268 min read

Cypress API tests call a running endpoint directly with cy.request() and assert on its status, response body, headers, or duration. They run as Cypress end-to-end specs and do not need to visit an application page first. Use cy.intercept() when you need to observe or control a request initiated by the app in the browser; it does not intercept the direct HTTP call made by cy.request().

1. Set up and run an API spec

Start the API you want to test, then configure its origin as e2e.baseUrl. Relative paths in cy.request() resolve against this value. You can also pass an absolute URL directly.

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3001',
  },
})
// cypress/e2e/api/users.cy.js
describe('GET /users', () => {
  it('returns a list of users', () => {
    cy.request('GET', '/users').then((response) => {
      expect(response.status).to.eq(200)
      expect(response.body.results).to.have.length.greaterThan(1)
    })
  })
})

Replace the sample origin, route, and assertions with a stable endpoint and the contract your service promises. Run just this spec with:

npx cypress run --spec 'cypress/e2e/api/users.cy.js'

API-only specs still use Cypress’s end-to-end testing type. Cypress starts a browser per spec file, so group related API checks thoughtfully to avoid paying that startup cost for many tiny specs. Group by resource, such as users or orders, rather than creating a separate spec for every HTTP verb.

2. Assert the API contract

cy.request() yields a response object. Check only behavior that matters to the endpoint’s contract: status, required fields, response shape, headers, or a domain outcome. JSON response bodies are automatically parsed into JavaScript objects when the content type indicates JSON.

cy.request('GET', '/users/42').then((response) => {
  expect(response.status).to.eq(200)
  expect(response.headers).to.have.property('content-type').that.includes('application/json')
  expect(response.body).to.include.keys('id', 'email')
  expect(response.body.id).to.eq(42)
})

You may also assert on response.duration. Treat it as an observation unless the test environment and measurement goal justify a threshold: Cypress examples do not establish a universal API latency target. A strict limit in a noisy shared CI environment can create flaky tests.

3. Use the right request options

The common call forms are cy.request(url), cy.request(method, url, body), and cy.request(options). The options form is useful when a request needs headers, authentication, a body, or special failure handling.

cy.request({
  method: 'POST',
  url: '/api/users',
  body: { name: 'Ada Lovelace', email: 'ada@example.test' },
  headers: { 'content-type': 'application/json' },
}).then((response) => {
  expect(response.status).to.eq(201)
  expect(response.body).to.have.property('id')
})
Option or behavior When it matters
method, url, body Describe the HTTP operation and payload. A relative URL uses configured baseUrl.
headers Pass authentication or other request metadata required by the service.
failOnStatusCode Defaults to true; set to false when an error status is the expected result and you intend to assert on it.
timeout Set a request timeout appropriate to the endpoint and environment. A timeout is not an automatic retry of an assertion.

Consult the cy.request() reference for the full option list and details matching your installed Cypress version.

4. Test expected errors explicitly

By default, a response outside the 2xx/3xx range fails the command. Turn that behavior off only when the test is specifically checking an error response:

it('rejects an unauthenticated request', () => {
  cy.request({
    method: 'GET',
    url: '/api/private',
    failOnStatusCode: false,
  }).then((response) => {
    expect(response.status).to.eq(401)
    expect(response.body).to.have.property('error')
  })
})

This keeps unexpected server errors visible in ordinary success-path tests while making intentional 4xx/5xx behavior testable.

5. Cover a create, read, update, delete lifecycle

Capture identifiers from one response and use them in subsequent requests. Keep each assertion tied to a meaningful state transition. Clean up test-created records when the service and test environment allow it.

describe('widgets API lifecycle', () => {
  let widgetId

  it('creates, reads, updates, and deletes a widget', () => {
    cy.request('POST', '/api/widgets', { name: 'sample-widget' }).then((created) => {
      expect(created.status).to.eq(201)
      widgetId = created.body.id
      expect(widgetId).to.exist
    })

    cy.then(() => cy.request('GET', `/api/widgets/${widgetId}`)).then((read) => {
      expect(read.status).to.eq(200)
      expect(read.body.name).to.eq('sample-widget')
    })

    cy.then(() => cy.request('PATCH', `/api/widgets/${widgetId}`, { name: 'updated-widget' })).then((updated) => {
      expect(updated.status).to.eq(200)
      expect(updated.body.name).to.eq('updated-widget')
    })

    cy.then(() => cy.request('DELETE', `/api/widgets/${widgetId}`)).then((deleted) => {
      expect([200, 204]).to.include(deleted.status)
    })
  })
})

Adapt accepted status codes and payloads to your API. For suites that can be interrupted before cleanup, use isolated test data or a reliable environment reset so leftover records do not affect later runs.

6. Add authentication without duplicating setup

When many requests share a token, a custom command can centralize the header setup. Keep credentials out of committed specs and retrieve secrets through the environment configuration supported by your Cypress version. The Cypress API guide demonstrates a custom cy.api() command and cy.env() for a token; verify that API against the version installed in your project.

// Example custom command pattern; supply token through your project's secure environment setup.
Cypress.Commands.add('apiGet', (path, token) => {
  return cy.request({
    method: 'GET',
    url: path,
    headers: { Authorization: `Bearer ${token}` },
  })
})

// In a spec, obtain the token using your configured secret mechanism:
cy.apiGet('/api/profile', token).its('status').should('eq', 200)

The token retrieval variable above is intentionally supplied by the project: do not put a real secret in the spec or source control.

7. Choose between cy.request(), cy.intercept(), and cy.task()

Need Use What it proves or does
Call the real endpoint and assert on its response cy.request() Makes a direct HTTP request from Cypress’s Node process; no page navigation is needed.
Observe or wait for a request made by the application cy.intercept() Matches application traffic passing through Cypress’s proxy.
Give the app a controlled fake response cy.intercept() with a static response or handler Tests UI behavior against the chosen response without requiring the live backend response.
Run Node-side work such as database access or file I/O cy.task() Delegates work to the Cypress Node process.

cy.request() bypasses cy.intercept(), does not appear as browser-originated Network traffic, and is not subject to browser CORS enforcement. Cypress also documents cookie handling between cy.request() and the browser’s cookie jar. For browser requests, use cy.intercept() and wait on an alias:

cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('/users')
cy.wait('@getUsers').its('response.statusCode').should('eq', 200)

Use a real request when a test must verify the actual endpoint. Use a stub when the purpose is to exercise the UI’s response to a controlled server condition. Label the intent clearly so a passing stubbed UI test is not mistaken for proof of the live API contract. See Cypress’s guides for cy.intercept() and network requests.

8. Keep the suite fast and reliable

  • Group related endpoint tests by resource so Cypress’s per-spec browser startup is amortized.
  • Use direct API calls to seed state when that setup is faster and clearer than walking through screens.
  • Assert on stable contract behavior. Avoid relying on incidental fields or timing thresholds that vary across environments.
  • Use isolated test records and clean them up where practical to reduce order dependence.
  • Separate tests of the real backend from tests using stubbed browser traffic.
  • Use a request timeout appropriate for the environment, but remember that a timeout waiting for a response and a failed assertion are distinct problems.

API checks can isolate backend contract failures from UI selector problems, while UI checks still verify the behavior a user sees. The two layers complement each other.

9. Troubleshoot common failures

Symptom Likely cause Fix
Relative URL is sent to the wrong host or rejected e2e.baseUrl is missing or points to a different environment. Set the API origin in Cypress configuration or pass a correct absolute URL.
A test fails immediately on an expected 401, 404, or 500 failOnStatusCode defaults to true. Set failOnStatusCode: false for that case, then assert the exact expected status and error shape.
cy.intercept() never sees a request The request was issued by cy.request(), which is direct Node-side traffic. Assert on the cy.request() response itself, or intercept the browser app’s request instead.
Browser CORS issue is confused with API spec behavior cy.request() does not enforce browser CORS rules. Test browser CORS behavior through an actual browser-originated request; use direct API tests for endpoint behavior.
Response body has an unexpected shape The service may return a different content type or the endpoint contract changed. Inspect status, headers, and body; make the server’s response content type and the spec’s expected schema explicit.
Request times out intermittently The server is unavailable, slow, or not ready when the spec runs. Confirm the service is running and reachable, check its logs, and choose an appropriate timeout. Do not mask a readiness problem with repeated assertion retries.
Tests pass alone but fail in a suite Shared or leftover backend state makes tests order-dependent. Use unique records, reset test data, and clean up created records when possible.

10. ScreenshotNeo: capture API results as images

API tests verify responses; when the useful artifact is a screenshot of a rendered API documentation page, dashboard, or other web page, ScreenshotNeo provides a website screenshot API and MCP server. It is separate from Cypress and does not replace endpoint assertions. Its API documentation describes the request options.

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}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

ScreenshotNeo accepts cookie banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. It does not bill bot checks or CAPTCHAs, blank pages, timeouts, failed loads, or cache hits, and response headers report the page verdict and billing status. AI agents can use its MCP server tools take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan.

Sign up for 1,000 free screenshots a month, with no card required.

11. Frequently asked questions

Do API specs need to visit the app first?

No. cy.request() calls the endpoint directly, so an API spec can run without loading a page.

Does cy.request() use the browser’s cookies?

Cypress documents cookie handling between the command and browser cookie jar. Account for that behavior when designing authenticated tests.

Can I use Cypress for GraphQL or file uploads?

The request command supports HTTP calls used by such workflows; consult the request reference and API guide for the details applicable to your payload and installed version.

Can a cy.request() assertion retry until it passes?

No. Chained assertions on the response run once; the request waits for the server response up to its timeout.

Where should I check if an option changed?

Use the official documentation matching your installed Cypress version. The request reference records that QUERY method support was added in version 15.20.0, so do not assume it works in older installations.

References