API testing shouldn’t require you to become a developer overnight. You just need to understand what you’re hitting, what you expect back, and how to document when things break.
This guide covers manual API testing using Postman and basic tools that work everywhere. No automation frameworks, no CI/CD pipelines, just you, your API, and testing that actually matters.
If you’re using the QA Journey Playground API for practice, this is your starting point.

Why Manual API Testing Still Matters
Let’s be real: automation is great until you need to debug a weird edge case at 3 PM on a Friday.
Manual API testing is still essential for:
- Exploratory testing finding issues automation won’t catch
- Verifying bug fixes quickly without updating test scripts
- Understanding how the API actually behaves before automating anything
- Documenting API behavior for developers who “forgot” to write docs
You can’t automate what you don’t understand. Manual testing comes first.
What You’re Actually Testing
Before smashing the “Send” button in Postman, understand what you’re validating:
1. Status Codes
Understanding HTTP status codes is critical. Here are the ones you’ll encounter most:
- 200 OK: Success—data is returned as expected
- 201 Created: Resource was created successfully
- 400 Bad Request: You sent garbage data
- 401 Unauthorized: No valid token/credentials
- 403 Forbidden: You’re authenticated but don’t have permission
- 404 Not Found: Endpoint or resource doesn’t exist
- 405 Method Not Allowed: Using POST when only GET is allowed
- 429 Too Many Requests: Rate limited—you’re hitting the API too fast
- 500 Internal Server Error: Backend is broken (not your fault)
- 502 Bad Gateway: Server acting as proxy got invalid response
- 503 Service Unavailable: Server is down or overloaded
Full reference: MDN HTTP Status Codes
QA-Proven Hack: Bookmark that MDN page. When you get a weird status code like 418 (I’m a teapot), you’ll want the official definition.
2. Response Structure Does the JSON match what the API promised? Are required fields present? Is the data type correct?
3. Response Time Is the endpoint fast enough? If it takes 10 seconds to return user data, that’s a problem.
4. Error Messages When things fail, do the error messages actually help? “Error” is not helpful. “Email already exists” is.
5. Edge Cases What happens when you send:
- Empty strings
- Null values
- Extremely long inputs
- Special characters
- Non-existent IDs
This is where bugs hide.
The QA Journey Playground API
I built a mock API for testing practice. It’s live at playground.qajourney.net/api and includes realistic endpoints testers actually encounter.
Available Endpoints
Users Module
GET /api/users → List all users
GET /api/users?id=1 → Get single user
POST /api/users/create.php → Create user
PUT /api/users/update.php?id=1 → Update user
DELETE /api/users/delete.php?id=1 → Delete user
Products Module
GET /api/products → List all products
GET /api/products?id=1 → Get single product
POST /api/products/create.php → Create product
Authentication Module
POST /api/auth/login.php → Login (returns token)
POST /api/auth/logout.php → Logout
GET /api/auth/verify.php → Verify token (requires Authorization header)
Test Scenarios Module (for practicing QA skills)
GET /api/test/slow-endpoint.php → 3-second delay
GET /api/test/random-failure.php → 50% chance of 500 error
GET /api/test/rate-limited.php → Limited to 5 requests/minute
These endpoints simulate real-world API behavior. Use them to practice before hitting production APIs.
Manual API Testing with Postman
Postman is free, works on Windows/macOS/Linux, and doesn’t require a subscription to be useful.
Download: https://www.postman.com/downloads/
Writing Test Cases with Gherkin
Before jumping into Postman, write your test cases in plain English using Gherkin syntax. This helps you think through what you’re testing before you start clicking buttons.
Gherkin Format:
Feature: [What you're testing]
Scenario: [Specific test case]
Given [Initial state]
When [Action taken]
Then [Expected result]
Example Test Case:
Feature: User Management API
Scenario: Successfully retrieve a user by ID
Given the API is available at https://playground.qajourney.net/api
And user with ID 1 exists in the database
When I send a GET request to /api/users?id=1
Then the response status code should be 200
And the response should contain user data with id, name, and email
And the response time should be less than 2000ms
Why Gherkin?
- Forces you to think through the test before executing
- Creates documentation developers can actually read
- Translates directly into automated tests later (when you’re ready)
- Stakeholders understand “Given/When/Then” without technical knowledge
QA-Proven Hack: I keep a test-cases.md file with all my Gherkin scenarios. When automating later, I just copy-paste the scenarios into my test framework.
Understanding Assertions: Chai/Mocha Basics
Postman’s test scripts use Chai assertions under the hood. Understanding Chai syntax now means you’re already halfway to automation.
Common Chai Assertions:
// Status code checks
expect(response.status).to.equal(200);
expect(response.status).to.be.oneOf([200, 201]);
// Property checks
expect(data).to.have.property('id');
expect(data).to.have.all.keys('id', 'name', 'email');
// Type checks
expect(data.id).to.be.a('number');
expect(data.name).to.be.a('string');
// Value checks
expect(data.name).to.equal('John Doe');
expect(data.email).to.include('@');
// Array checks
expect(data.users).to.be.an('array');
expect(data.users).to.have.lengthOf(5);
Postman wraps these in pm.test() blocks, but the assertion syntax is identical to Mocha/Chai test suites.
Why This Matters: When you automate with Mocha + Chai later, you’re using the exact same assertions. Learning them in Postman means zero learning curve for automation.
Setting Up Your Workspace
Step 1: Create a Collection
- Open Postman
- Click “New” → “Collection”
- Name it “QA Journey API Tests”
Step 2: Set Base URL Variable
- Click on your collection → “Variables” tab
- Add a variable:
- Variable:
base_url - Initial Value:
https://playground.qajourney.net/api
- Variable:
- Save
Now use {{base_url}}/users instead of typing the full URL every time.
QA-Proven Hack: Use variables for everything that changes URLs, tokens, user IDs. When the API moves to staging or production, you just update one variable instead of 50 requests.
Testing Real Scenarios (Happy Path)
Test 1: Get All Users
Gherkin Test Case:
Feature: User Management API
Scenario: Successfully retrieve all users
Given the API is available
When I send a GET request to /api/users
Then the response status code should be 200
And the response should contain a users array
And the response time should be less than 2000ms
Postman Setup:
Method: GET
URL: {{base_url}}/users
What to Check:
- Status code is 200
- Response contains a
usersarray - Response time is under 2 seconds
Postman Tests Tab (Chai Assertions):
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
// Chai equivalent: expect(response.status).to.equal(200);
});
pm.test("Response has users array", function () {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property('users');
// Chai: expect(jsonData).to.have.property('users');
pm.expect(jsonData.users).to.be.an('array');
// Chai: expect(jsonData.users).to.be.an('array');
});
pm.test("Response time is acceptable", function () {
pm.expect(pm.response.responseTime).to.be.below(2000);
// Chai: expect(response.responseTime).to.be.below(2000);
});
Hit “Send”. If all three tests pass, the endpoint works as expected.
QA-Proven Hack: The comments show equivalent Chai syntax. When you automate with Mocha/Chai, you’re literally copying these assertions into your test files.
Test 2: Get Single User
Gherkin Test Case:
Feature: User Management API
Scenario: Successfully retrieve a single user by ID
Given the API is available
And user with ID 1 exists
When I send a GET request to /api/users?id=1
Then the response status code should be 200
And the response should contain id, name, and email fields
And all fields should have correct data types
Postman Setup:
Method: GET
URL: {{base_url}}/users?id=1
Postman Tests Tab (Chai Assertions):
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("User has required fields", function () {
const user = pm.response.json();
// Check all required properties exist
pm.expect(user).to.have.all.keys('id', 'name', 'email');
// Chai: expect(user).to.have.all.keys('id', 'name', 'email');
});
pm.test("Data types are correct", function () {
const user = pm.response.json();
pm.expect(user.id).to.be.a('number');
pm.expect(user.name).to.be.a('string');
pm.expect(user.email).to.be.a('string');
// Additional validation
pm.expect(user.email).to.include('@');
// Chai: expect(user.email).to.include('@');
});
Test 3: Create User
Gherkin Test Case:
Feature: User Management API
Scenario: Successfully create a new user
Given the API is available
When I send a POST request to /api/users/create.php
And the request body contains valid user data
| name | email | password |
| Test User| [email protected] | SecurePass123 |
Then the response status code should be 201
And the response should contain the created user's ID
And the response should contain a success message
Postman Setup:
Method: POST
URL: {{base_url}}/users/create.php
Headers:
Content-Type: application/json
Body (raw JSON):
{
"name": "Test User",
"email": "[email protected]",
"password": "SecurePass123"
}
Postman Tests Tab (Chai Assertions):
pm.test("Status code is 201 (Created)", function () {
pm.response.to.have.status(201);
// Note: 201 means resource created, not 200
});
pm.test("User created successfully", function () {
const response = pm.response.json();
pm.expect(response).to.have.property('id');
pm.expect(response.id).to.be.a('number');
pm.expect(response.message).to.include('created');
// Chai: expect(response.message).to.include('created');
});
// Save the user ID for later tests (dependency management)
if (pm.response.code === 201) {
const userId = pm.response.json().id;
pm.environment.set("created_user_id", userId);
console.log("Created user ID:", userId);
}
QA-Proven Hack: Always save IDs from creation responses. You’ll need them for update/delete tests without manually copying values.
Test 4: Update User
Gherkin Test Case:
Feature: User Management API
Scenario: Successfully update an existing user
Given the API is available
And a user exists with ID {{created_user_id}}
When I send a PUT request to /api/users/update.php?id={{created_user_id}}
And the request body contains updated user data
Then the response status code should be 200
And the response should confirm the update
Postman Setup:
Method: PUT
URL: {{base_url}}/users/update.php?id={{created_user_id}}
Body (raw JSON):
{
"name": "Updated Name",
"email": "[email protected]"
}
Postman Tests Tab:
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Update confirmed", function () {
const response = pm.response.json();
pm.expect(response.message).to.include('updated');
});
Test 5: Delete User
Gherkin Test Case:
Feature: User Management API
Scenario: Successfully delete a user
Given the API is available
And a user exists with ID {{created_user_id}}
When I send a DELETE request to /api/users/delete.php?id={{created_user_id}}
Then the response status code should be 200
And the response should confirm deletion
Scenario: Verify user no longer exists after deletion
Given a user was deleted with ID {{created_user_id}}
When I send a GET request to /api/users?id={{created_user_id}}
Then the response status code should be 404
Postman Setup:
Method: DELETE
URL: {{base_url}}/users/delete.php?id={{created_user_id}}
Postman Tests Tab:
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Deletion confirmed", function () {
const response = pm.response.json();
pm.expect(response.message).to.include('deleted');
});
Follow-up Check (Create separate request): After deleting, try to GET the same user. You should get a 404.
Test 6: Login & Token Verification
Gherkin Test Case:
Feature: Authentication API
Scenario: Successfully login and receive authentication token
Given the API is available
When I send a POST request to /api/auth/login.php
And the request body contains valid credentials
| email | password |
| [email protected] | password123 |
Then the response status code should be 200
And the response should contain a token
And the token should be a non-empty string
Scenario: Successfully verify authentication token
Given I have a valid authentication token
When I send a GET request to /api/auth/verify.php
And the Authorization header contains the token
Then the response status code should be 200
And the response should confirm the token is valid
Step 1: Login
Method: POST
URL: {{base_url}}/auth/login.php
Body (raw JSON):
{
"email": "[email protected]",
"password": "password123"
}
Postman Tests Tab (Chai Assertions):
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Token is returned", function () {
const response = pm.response.json();
pm.expect(response).to.have.property('token');
pm.expect(response.token).to.be.a('string');
pm.expect(response.token).to.not.be.empty;
// Chai: expect(response.token).to.not.be.empty;
// Save token for authenticated requests
pm.environment.set("auth_token", response.token);
console.log("Auth token saved:", response.token.substring(0, 20) + "...");
});
Step 2: Verify Token
Method: GET
URL: {{base_url}}/auth/verify.php
Headers:
Authorization: Bearer {{auth_token}}
Postman Tests Tab:
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Token is valid", function () {
const response = pm.response.json();
pm.expect(response.valid).to.be.true;
// Chai: expect(response.valid).to.be.true;
});
This simulates how real authentication flows work. Login once, use the token for subsequent requests.
Testing Real Scenarios (Sad Path)
Happy paths are easy. Sad paths expose the real bugs.
Test 7: Get Non-Existent User
Gherkin Test Case:
Feature: User Management API
Scenario: Attempt to retrieve a non-existent user
Given the API is available
When I send a GET request to /api/users?id=999999
Then the response status code should be 404
And the response should contain an error message
And the error message should be descriptive
Postman Setup:
Method: GET
URL: {{base_url}}/users?id=999999
Postman Tests Tab (Chai Assertions):
pm.test("Status code is 404 (Not Found)", function () {
pm.response.to.have.status(404);
// NOT 200 with empty data - that's a bug
});
pm.test("Error message is helpful", function () {
const response = pm.response.json();
pm.expect(response).to.have.property('error');
// Chai: expect(response).to.have.property('error');
pm.expect(response.error).to.not.equal('Error');
// Generic messages are useless - should be specific
// Good: "User with ID 999999 not found"
// Bad: "Error"
});
Why This Matters: Returning 200 with empty data is a common mistake. The frontend will break trying to render non-existent users.
Test 8: Create User with Invalid Data
Gherkin Test Case:
Feature: User Management API
Scenario: Attempt to create user with invalid email format
Given the API is available
When I send a POST request to /api/users/create.php
And the request body contains invalid data
| name | email |
| | not-an-email |
Then the response status code should be 400
And the response should specify which fields are invalid
And no user should be created in the database
Postman Setup:
Method: POST
URL: {{base_url}}/users/create.php
Body (raw JSON):
{
"name": "",
"email": "not-an-email"
}
Postman Tests Tab (Chai Assertions):
pm.test("Status code is 400 (Bad Request)", function () {
pm.response.to.have.status(400);
});
pm.test("Validation errors are specific", function () {
const response = pm.response.json();
pm.expect(response.error).to.exist;
// Chai: expect(response.error).to.exist;
// Good error: "Email format is invalid, Name is required"
// Bad error: "Validation failed"
pm.expect(response.error).to.include('email').or.include('Email');
});
QA-Proven Hack: Test with intentionally broken data. Empty strings, wrong formats, missing required fields. If the API doesn’t reject garbage, the backend has no validation.
Test 9: Login with Wrong Credentials
Gherkin Test Case:
Feature: Authentication API
Scenario: Attempt to login with invalid credentials
Given the API is available
When I send a POST request to /api/auth/login.php
And the request body contains incorrect credentials
| email | password |
| [email protected] | wrongpass |
Then the response status code should be 401
And no authentication token should be returned
And the error message should not reveal which field is incorrect
Postman Setup:
Method: POST
URL: {{base_url}}/auth/login.php
Body (raw JSON):
{
"email": "[email protected]",
"password": "wrongpass"
}
Postman Tests Tab (Chai Assertions):
pm.test("Status code is 401 (Unauthorized)", function () {
pm.response.to.have.status(401);
});
pm.test("No token returned", function () {
const response = pm.response.json();
pm.expect(response).to.not.have.property('token');
// Chai: expect(response).to.not.have.property('token');
});
pm.test("Error message doesn't reveal details", function () {
const response = pm.response.json();
// Good: "Invalid credentials"
// Bad: "Email not found" (helps attackers enumerate accounts)
pm.expect(response.error).to.include('Invalid');
pm.expect(response.error).to.not.include('Email not found');
pm.expect(response.error).to.not.include('Wrong password');
});
Good error: “Invalid credentials”
Bad error: “Email not found” (helps attackers enumerate accounts)
Test 10: Access Protected Endpoint Without Token
Gherkin Test Case:
Feature: Authentication API
Scenario: Attempt to access protected endpoint without authentication
Given the API is available
When I send a GET request to /api/auth/verify.php
And no Authorization header is provided
Then the response status code should be 401
And the response should indicate authentication is required
Postman Setup:
Method: GET
URL: {{base_url}}/auth/verify.php
Headers: (Remove Authorization header)
Postman Tests Tab:
pm.test("Status code is 401", function () {
pm.response.to.have.status(401);
});
pm.test("Error indicates auth required", function () {
const response = pm.response.json();
pm.expect(response.error).to.include('auth').or.include('token');
});
Test 11: Rate Limiting
Gherkin Test Case:
Feature: API Rate Limiting
Scenario: Verify rate limiting is enforced
Given the API is available
When I send 6 consecutive GET requests to /api/test/rate-limited.php
Then the first 5 requests should return status code 200
And the 6th request should return status code 429
And the error message should explain the rate limit
Postman Setup:
Method: GET
URL: {{base_url}}/test/rate-limited.php
What to Do: Hit “Send” 6 times quickly (or use Postman Runner with 6 iterations).
Postman Tests Tab (Chai Assertions):
pm.test("Rate limit enforced", function () {
const statusCode = pm.response.code;
if (statusCode === 429) {
// Rate limited
pm.expect(pm.response.json().error).to.include('Rate limit');
// Chai: expect(response.error).to.include('Rate limit');
} else {
// Should be 200
pm.expect(statusCode).to.equal(200);
}
});
QA-Proven Hack: If the API doesn’t have rate limiting, mention it in your test report. It’s a security and performance issue.
Test 12: Slow Endpoint (Performance)
Gherkin Test Case:
Feature: API Performance Testing
Scenario: Verify slow endpoint handles delays properly
Given the API is available
When I send a GET request to /api/test/slow-endpoint.php
Then the response status code should be 200
And the response time should be approximately 3 seconds
And the response should contain data despite the delay
Postman Setup:
Method: GET
URL: {{base_url}}/test/slow-endpoint.php
Postman Tests Tab (Chai Assertions):
pm.test("Endpoint responds despite delay", function () {
pm.response.to.have.status(200);
});
pm.test("Response time reflects intentional delay", function () {
const responseTime = pm.response.responseTime;
pm.expect(responseTime).to.be.above(2900);
pm.expect(responseTime).to.be.below(3500);
// Chai: expect(responseTime).to.be.within(2900, 3500);
});
pm.test("Response contains data", function () {
const response = pm.response.json();
pm.expect(response).to.not.be.empty;
});
Why This Matters: Some endpoints are slow by design (reports, exports). Test that timeouts are configured properly.
Test 13: Random Failures (Flakiness)
Gherkin Test Case:
Feature: API Reliability Testing
Scenario: Verify handling of intermittent failures
Given the API is available
When I send multiple GET requests to /api/test/random-failure.php
Then some requests should return status code 200
And some requests should return status code 500
And failures should occur randomly
Postman Setup:
Method: GET
URL: {{base_url}}/test/random-failure.php
What to Do: Hit “Send” 10 times (use Postman Runner).
Postman Tests Tab:
pm.test("Endpoint behavior is documented", function () {
const statusCode = pm.response.code;
// Accept both success and failure
pm.expect(statusCode).to.be.oneOf([200, 500]);
// Chai: expect(statusCode).to.be.oneOf([200, 500]);
console.log("Status:", statusCode);
});
Why This Matters: Real APIs have intermittent failures. Test how your frontend handles unpredictable errors.
How to Document API Bugs
When an API breaks, don’t just say “it doesn’t work.” Document it properly.
Bug Report Template
**Bug:** [Describe what's broken]
**Endpoint:** [Full URL including query params]
**Method:** [GET/POST/PUT/DELETE]
**Request:**
Headers:
Content-Type: application/json
Authorization: Bearer [token]
Body:
{
"field": "value"
}
**Expected Response:**
Status Code: 200
Body:
{
"expected": "data"
}
**Actual Response:**
Status Code: 500
Body:
{
"error": "Internal Server Error"
}
**Environment:**
- API Base URL: https://playground.qajourney.net/api
- Tested on: 2025-01-15 14:30 UTC
- Postman Version: 10.x
**Steps to Reproduce:**
1. Login to get auth token
2. Send POST to /api/users/create.php with [specific payload]
3. Observe 500 error
**Attachments:**
[Screenshot of Postman request/response]
Copy this template into GitHub Issues, Jira, Taiga, or wherever your team tracks bugs.
QA-Proven Hack: I keep this template in a Google Doc. Copy-paste for every API bug. Consistent reports = faster fixes.
Common API Testing Mistakes
Mistake 1: Only Testing Happy Paths
If you’re not testing with invalid data, you’re not really testing.
Mistake 2: Ignoring Status Codes
200 doesn’t always mean success. Check the response body.
Mistake 3: Not Testing Without Authentication
Try accessing protected endpoints without tokens. If they work, that’s a security issue.
Mistake 4: Skipping Edge Cases
Test with:
- Empty strings
- Null values
- Extremely long inputs (1000+ characters)
- Special characters (&, <, >, “, ‘)
- SQL injection attempts (
' OR '1'='1) - XSS attempts (
<script>alert('test')</script>)
Mistake 5: Not Documenting API Behavior
If the API doesn’t have docs, YOU are the docs. Document expected behavior so developers know what to fix.
Quick Reference: Testing Checklist
For every endpoint, verify:
Basic Functionality:
- [ ] Correct status code (200, 201, 400, 401, 404, etc.)
- [ ] Response structure matches expectations
- [ ] Required fields are present
- [ ] Data types are correct
Error Handling:
- [ ] Invalid data returns 400
- [ ] Missing auth returns 401
- [ ] Non-existent resources return 404
- [ ] Rate limiting returns 429
- [ ] Server errors return 500
Performance:
- [ ] Response time is acceptable (< 2s for most endpoints)
- [ ] Slow endpoints have proper timeouts
- [ ] Large datasets don’t crash the API
Security:
- [ ] Protected endpoints require authentication
- [ ] Tokens expire properly
- [ ] Error messages don’t leak sensitive data
- [ ] SQL injection doesn’t work
- [ ] XSS attempts are sanitized
Edge Cases:
- [ ] Empty/null values are handled
- [ ] Special characters don’t break the API
- [ ] Extremely long inputs are rejected
- [ ] Duplicate submissions are prevented
Tools Beyond Postman
Postman is great, but sometimes you need lighter tools.
curl (Command Line)
Works on Windows (PowerShell/Git Bash), macOS, Linux—no installation on macOS/Linux.
Basic GET:
curl https://playground.qajourney.net/api/users
GET with query parameter:
curl "https://playground.qajourney.net/api/users?id=1"
POST with JSON:
curl -X POST https://playground.qajourney.net/api/users/create.php \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"[email protected]","password":"pass123"}'
With authentication:
curl https://playground.qajourney.net/api/auth/verify.php \
-H "Authorization: Bearer YOUR_TOKEN"
Get just the status code:
curl -s -o /dev/null -w "%{http_code}" https://playground.qajourney.net/api/users
QA-Proven Hack: Save common curl commands in a text file. Faster than opening Postman when you need to test one endpoint quickly.
Browser DevTools (Network Tab)
If the API is called from a web page:
- Open DevTools (F12)
- Go to Network tab
- Interact with the page
- Click on API requests to see:
- Request headers
- Request payload
- Response headers
- Response body
- Response time
Why This Matters: Sometimes the API works fine in Postman but breaks in the browser. DevTools shows you exactly what the browser is sending.
The Takeaway
Manual API testing isn’t about memorizing HTTP methods. It’s about understanding what the API promises, verifying it delivers, and documenting when it doesn’t.
Start with:
- Test happy paths (does it work as expected?)
- Test sad paths (does it fail gracefully?)
- Test edge cases (does it handle garbage data?)
- Document everything (so developers can actually fix bugs)
You don’t need automation frameworks to test APIs. You need curiosity, attention to detail, and a structured approach.
The QA Journey Playground API is live for practice: playground.qajourney.net/api
Start testing. Break things. Document what breaks.
Related guides:


