ScreenshotNeo

BlogHow-to

How to Test Service APIs

Learn how to test service APIs with request assertions, integration and contract tests, end-to-end workflows, security checks, and CI automation.

By the ScreenshotNeo team4 October 202613 min read

Test a service API by checking individual requests and responses, then test the boundaries and workflows that connect them. Add consumer-provider contract tests when separately developed services depend on stable interactions, derive security cases from the API’s documented requirements, and automate repeatable suites in local development and CI. No single test type proves every aspect of correctness.

This guide shows how to plan and run those checks, with runnable examples in Python, cURL, and Node.js. The examples use a fictional service; replace its base URL, paths, credentials, and expected response fields with those from your API documentation.

1. Define what correct behavior means

Start with the current API documentation or OpenAPI specification. For each operation you plan to test, record:

  • HTTP method and path.
  • Required path and query parameters, headers, and request body.
  • Authentication and authorization requirements.
  • Expected status codes, response headers, and response fields.
  • Documented error behavior, including validation and permission failures.
  • Any state changes or dependencies on earlier operations.

Use the specification as a planning aid, then confirm it describes intended behavior. A test that simply repeats an incorrect specification can preserve a defect. OWASP recommends using API documentation and effective OpenAPI security requirements to determine what to assess: OWASP REST Assessment Cheat Sheet.

For every test, identify an observable result that matters: status, a stable response field, a relevant header, or a state change. Avoid asserting incidental values that are not part of the contract, such as a generated timestamp or an internal diagnostic message, unless that behavior is itself required.

2. Test a single request and its response

A request test exercises one concrete interaction. It should send the right method, URL, authorization, parameters, headers, and body, then assert the response that the API promises. Start with one successful case and add invalid or boundary inputs that matter to the operation.

For the examples below, suppose https://api.example.test/v1/widgets accepts a JSON body with a name field and returns a created widget with a nonempty id. The host is illustrative and will not resolve as a real service.

cURL: inspect a request manually

curl --fail-with-body --silent --show-error \
  --request POST 'https://api.example.test/v1/widgets' \
  --header 'Authorization: Bearer YOUR_TOKEN' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --data '{"name":"demo-widget"}'

cURL is useful for reproducing one request and inspecting a failure. This command does not automatically assert the response body. Add a script or use a test client when you need repeatable assertions. Do not put a real token in a shared shell history, CI log, or committed script.

Python: runnable request assertion

Install the dependency with python -m pip install requests. Save this as test_widget_api.py and run it with API_BASE_URL=https://your-api.example API_TOKEN=your-token python test_widget_api.py. Set those environment variables for your test service.

import os
import requests

base_url = os.environ["API_BASE_URL"].rstrip("/")
token = os.environ["API_TOKEN"]

response = requests.post(
    f"{base_url}/v1/widgets",
    headers={
        "Authorization": f"Bearer {token}",
        "Accept": "application/json",
    },
    json={"name": "demo-widget"},
    timeout=(5, 20),  # connect timeout, response timeout in seconds
)

assert response.status_code == 201, (
    f"expected 201, got {response.status_code}: {response.text}"
)
body = response.json()
assert isinstance(body, dict), "expected a JSON object"
assert body.get("name") == "demo-widget", body
assert isinstance(body.get("id"), str) and body["id"], body
print("create widget request passed")

The explicit timeout prevents a test process from waiting forever on an unresponsive service. Use the status and fields your API actually documents; a service may correctly return 200 or 202 for a different operation.

Node.js: runnable request assertion

This example uses the built-in fetch in current Node.js releases. Save as test-widget.mjs, then run API_BASE_URL=https://your-api.example API_TOKEN=your-token node test-widget.mjs.

const baseUrl = process.env.API_BASE_URL?.replace(/\/$/, "");
const token = process.env.API_TOKEN;
if (!baseUrl || !token) {
  throw new Error("Set API_BASE_URL and API_TOKEN");
}

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 20_000);
try {
  const response = await fetch(`${baseUrl}/v1/widgets`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: "application/json",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ name: "demo-widget" }),
    signal: controller.signal,
  });
  const text = await response.text();
  let body;
  try {
    body = JSON.parse(text);
  } catch {
    throw new Error(`expected JSON; received ${response.status}: ${text}`);
  }
  if (response.status !== 201) {
    throw new Error(`expected 201, got ${response.status}: ${text}`);
  }
  if (body.name !== "demo-widget" || typeof body.id !== "string" || !body.id) {
    throw new Error(`unexpected response body: ${text}`);
  }
  console.log("create widget request passed");
} finally {
  clearTimeout(timer);
}

Choose assertions that stay useful

  • Check the documented status code, not merely that the response is in the 2xx range.
  • Check required fields, types, and important values. Treat optional fields as optional.
  • Check headers when they affect behavior, such as content type or a documented retry hint.
  • For errors, assert the status and stable machine-readable error code where available. Avoid overfitting to human-readable wording.
  • For writes, verify the resulting state through the supported API when practical, and clean up test data.

3. Cover inputs, errors, and edge cases

Do not stop at the happy path. Select cases from the operation’s contract and likely failure modes. The right set depends on the API; this checklist is a starting point:

Case What to check
Required field missing Documented client error and stable validation details; no unintended state change.
Wrong type or malformed JSON Clear rejection rather than an unexplained server error.
Empty, minimum, maximum, and over-limit values Boundary behavior matches documented limits.
Unknown identifier Documented not-found behavior and no data leakage.
Duplicate or repeated request Expected conflict or idempotent behavior, especially for retries and create operations.
Pagination and sorting Page boundaries, stable ordering assumptions, and handling of invalid cursors or limits.
Concurrent changes Conflict or version behavior if the API documents conditional updates.
Unexpected content type or unsupported method Documented rejection and useful response headers where applicable.

Keep the test dataset controlled. Use unique names or identifiers where possible, and make cleanup safe to rerun. Be careful with destructive operations: run those against a dedicated test environment and test data, not production records.

4. Test integration boundaries and data flow

Integration tests check whether components and dependencies work together: for example, whether one endpoint’s output can be used by another, whether a service persists a change, or whether an external dependency’s response is handled correctly. They answer a different question from a single-request test.

  1. Create or identify a test resource.
  2. Capture its returned identifier or other required output.
  3. Use that value in a subsequent read, update, or related operation.
  4. Assert the resulting state and relevant side effects.
  5. Clean up the resource even when an assertion fails.

Use a mock server when a dependency is unavailable, expensive, unstable, or needs a controlled response. A mock helps isolate your service’s behavior, but it does not prove that the real dependency behaves the same way. Keep a smaller set of checks against the real dependency or a provider-controlled test environment where that boundary matters. Postman documents collections, request scripts, integration workflows, and mocks in its test scripts and mock server documentation.

5. Add consumer-provider contract tests when services evolve independently

Contract testing checks whether a provider still meets the interactions its consumers rely on. It is especially useful when teams deploy services independently and a provider change could break a known consumer expectation.

In Pact’s consumer-driven approach, a consumer test describes an expected interaction and records it in a pact; provider verification then checks that the provider meets that interaction. This can check compatibility without starting both services together for every test. Contract tests do not replace functional tests for business rules or complete workflows. See Pact’s explanation of how Pact works.

Consider contract tests when:

  • Consumers and providers are owned or released by different teams.
  • Both sides can participate in agreeing and verifying interaction expectations.
  • A breaking response or request change would otherwise be discovered late.

Do not add the tooling just to duplicate every request assertion. Pick the boundaries where compatibility has real value, and keep ordinary functional tests for behavior that the interaction contract cannot express.

6. Exercise a small number of complete workflows

An end-to-end API test chains calls across endpoints in the order a meaningful user journey requires, passing identifiers or other output data forward. For example: create an order, add an item, submit it, and verify its final state. These tests can catch failures that only appear across several operations, but they are slower and more sensitive to shared state than request-level checks.

Keep the end-to-end suite focused on important journeys. Use isolated test data, make setup and cleanup repeatable, and report which step failed. Postman describes this style as testing complete flows across multiple endpoints and APIs in its workflow documentation.

7. Derive security checks from the API’s requirements

For each operation, make a small authorization matrix from the effective security requirements in the API documentation. At minimum, consider the cases OWASP calls out:

Credential case Question
No credentials Is access rejected or allowed exactly as documented for this operation?
Valid credentials Can an authorized identity perform the operation?
Credentials missing a declared requirement Does the service reject an identity that lacks the required scope, role, or other permission?

Add cases for object-level access, tenant boundaries, and input handling where they apply to the service. A successful authentication check does not establish that a user is authorized to access every object. Run security checks only against systems and environments for which you are authorized. OWASP’s assessment guidance gives a basis for planning these checks.

The OWASP API Security Testing Framework project describes black-box endpoint discovery and test cases aligned to the OWASP API Security Top 10 2023 plus additional API checks. Treat it as a project option: review its current maturity and fit before adopting it, and do not infer detection effectiveness from the project overview alone.

8. Automate the tests in local development and CI

Make the same repeatable checks runnable locally and in automation. A practical progression is:

  1. Run fast request assertions for changed operations during development.
  2. Run relevant integration and contract checks in the build when dependencies are available.
  3. Run the focused critical workflows before release or on a schedule that fits the team.
  4. Keep test credentials in the CI secret store, and avoid printing tokens or sensitive response data.
  5. Publish enough failure context to diagnose a test: operation, status, request correlation ID when available, and a redacted response excerpt.

Choose suite scope and triggers based on feedback time and dependency stability. Postman documents manual collection runs, scheduled runs, and CI/CD execution through the Postman CLI; see its collection run documentation and Postman CLI overview. A collection is one option; code-based suites and contract tooling may fit better depending on language, collaboration needs, and maintenance cost.

9. Test a website screenshot service API

Some services return a file rather than JSON. For a website screenshot API, test the request parameters, response status and content type, and whether the returned bytes form the requested image or PDF. Include cases such as an unreachable target URL, a slow page, a page requiring authentication, and a requested output format. Keep the target pages controlled so their content does not change unexpectedly.

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. It accepts one GET request with a URL and returns a PNG, JPEG, WebP, or PDF. Its documentation covers the API parameters at ScreenshotNeo docs.

Or skip the browser setup

Use this one-call API request to capture a page as WebP. Replace the URL and API key with your target and ScreenshotNeo key.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API docs for the available parameters. Python and Node.js versions are available there as well.

  • Cookie banners are accepted and removed before capture; known consent platforms, newsletter popups, and chat widgets are removed too. Each step can be turned off.
  • Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. The response includes X-Page-Verdict and X-Billed headers.
  • An 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 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots.

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

10. Troubleshooting common API test failures

Symptom Likely cause What to do
401 Unauthorized Missing, expired, or incorrectly formatted credentials. Check the environment’s secret, authorization scheme, and token expiry. Never paste a production token into a public log.
403 Forbidden The identity authenticated but lacks an operation, scope, role, or object permission. Compare the identity to the operation’s documented security requirement and test the intended permission boundary.
404 Not Found Wrong base path, route, API version, or test identifier. Compare the full path with the current spec and confirm the resource exists in the selected environment.
400 or 422 response Input is malformed or violates validation rules. Check encoding, JSON shape, content type, required fields, and field constraints. If testing invalid input, assert the documented error response.
415 Unsupported Media Type The request body’s media type is missing or unsupported. Set the documented Content-Type and send a body in that format.
429 Too Many Requests The test suite exceeded a rate limit or shared quota. Reduce concurrency, isolate test credentials, and follow documented retry guidance. Avoid immediate unbounded retries.
5xx or intermittent failures Service or dependency instability, overloaded test environment, or a defect. Capture the request ID and timing, check dependency health, and retry only when the operation is safe to repeat. Keep a retry from hiding a persistent regression.
Connection timeout Wrong host, network access issue, slow service, or missing client timeout configuration. Confirm DNS, VPN and environment access, then set a bounded timeout suitable for the operation.
JSON parse error The response is HTML, empty, or another content type, often after a proxy or error page. Inspect status and content type before parsing; include a short redacted response excerpt in diagnostics.
Test passes alone but fails in suite Shared mutable data, order dependence, or concurrency collision. Use unique test data, remove hidden ordering assumptions, and make setup and cleanup safe to repeat.
Contract check fails after a change A consumer expectation changed or the provider no longer meets it. Determine whether the consumer contract or provider behavior should change, then update and verify the agreed interaction intentionally.

11. Performance, reliability, and cost considerations

  • Keep fast checks close to the change. Single-request tests usually need fewer dependencies than full workflows. Run broader suites at a cadence that gives useful feedback without making every edit wait on unstable external systems.
  • Bound time and concurrency. Set connection and response timeouts. Excessive parallel requests can trigger rate limits or overload a shared test environment; tune concurrency to its documented capacity.
  • Retry with care. Retries can help with transient network errors, but may hide flaky behavior and duplicate writes. Retry only when the operation is safe or has a documented idempotency mechanism, and cap attempts.
  • Control test state. Unique fixtures and reliable cleanup reduce collisions and reruns. A mock reduces dependency variability but cannot validate the real dependency’s current behavior.
  • Watch usage-based costs. CI runs, scheduled collections, external test environments, and paid API quotas can all consume resources. Estimate the number of requests per run and runs per period, then review the provider’s current plan and rate limits rather than assuming test traffic is free.
  • Separate evidence by test layer. A passing contract test demonstrates compatibility for the interactions it covers; it does not establish security, every business rule, or end-to-end reliability.

12. Choosing a testing approach

Approach Best question it answers Tradeoff
Request assertions in code or an API client Does this operation return the expected result for these inputs? Focused and direct, but does not prove multi-service workflows.
Integration tests Do components and dependencies exchange data correctly? More realistic boundaries, with more environment and dependency needs.
Consumer-provider contracts Does the provider preserve interactions a consumer depends on? Useful for independently released services; requires contract ownership and verification.
End-to-end API workflows Does a critical journey work across its sequence of endpoints? Broad coverage of a flow, but more sensitive to shared state and environment instability.
Security assessment Does access control and input handling match the stated security requirements? Needs an explicit threat and permission model; ordinary functional tests are not a substitute.

Postman documents request scripts, collections, mocks, workflow tests, and CLI automation. Pact focuses on consumer-driven contracts. These tools serve complementary roles, not a single interchangeable category. Check current product capabilities and fit against your language, collaboration needs, dependency strategy, and maintenance burden.

Frequently asked questions

Can a passing API test suite prove an API is correct?

No finite suite checks every input and environment. A suite provides evidence for the behaviors and boundaries it covers; combine layers and update tests as requirements change.

Should API tests run against production?

Use a dedicated test environment for writes, destructive cases, and security testing. Production checks, if used, should be authorized and designed to avoid changing real user data.

When should I mock a dependency?

Mock it when isolation or a controlled failure response helps answer the test’s question. Keep checks against the actual boundary where compatibility with that dependency matters.

Are contract tests the same as end-to-end tests?

No. A contract test checks a defined consumer-provider interaction. An end-to-end test follows a workflow across operations to verify the journey.