Practise API testing on a live REST API: hardcoded GET data, temporary objects you can create, update and delete, and four authentication types, with 141 ready-made test scenarios.
Free practice REST API · no sign-up · CORS enabled
Practise API testing on a real REST API
Pick an endpoint, press Send request and compare your response with the expected one. GET data is hardcoded, objects you create disappear on their own, and four auth types are ready to test.
https://sqatools.in/wp-json/sqa-api/v1https://sqatools.in/wp-json/sqa-api/v1/users/1{
"data": {
"id": 1,
"name": "Asha Verma",
"username": "asha",
"email": "asha@example.com",
"role": "admin",
"city": "Pune",
"age": 34,
"active": true,
"created_at": "2025-01-15T09:30:00Z"
}
}
- Pick an endpoint from the list below
- Press Send request; auth is added for you
- Compare your response with the expected one
Practice only: anything you create is visible to other learners until it expires. Never send real names, emails, passwords or other personal data.
Endpoints
Open endpoints. Hardcoded GET data never changes, so the same request always returns the same response.
Request
(none)Expected response
200 OK{
"data": [
{
"id": 1,
"name": "Asha Verma",
"username": "asha",
"email": "asha@example.com",
"role": "admin",
"city": "Pune",
"age": 34,
"active": true,
"created_at": "2025-01-15T09:30:00Z"
},
{
"id": 2,
"name": "Rahul Mehta",
"username": "rahul",
"email": "rahul@example.com",
"role": "editor",
"city": "Mumbai",
"age": 29,
"active": true,
"created_at": "2025-02-03T09:30:00Z"
},
{
"id": 3,
"name": "Priya Nair",
"username": "priya",
"email": "priya@example.com",
"role": "viewer",
"city": "Bengaluru",
"age": 26,
"active": true,
"created_at": "2025-02-20T09:30:00Z"
… (40 more lines)GET data is hardcoded, so your response should match exactly.
Request
(none)Expected response
200 OK{
"data": [
{
"id": 6,
"name": "Arjun Rao",
"username": "arjun",
"email": "arjun@example.com",
"role": "viewer",
"city": "Hyderabad",
"age": 23,
"active": true,
"created_at": "2025-04-28T09:30:00Z"
},
{
"id": 7,
"name": "Sneha Iyer",
"username": "sneha",
"email": "sneha@example.com",
"role": "admin",
"city": "Chennai",
"age": 37,
"active": true,
"created_at": "2025-05-19T09:30:00Z"
},
{
"id": 8,
"name": "Karan Malhotra",
"username": "karan",
"email": "karan@example.com",
"role": "viewer",
"city": "Delhi",
"age": 45,
"active": false,
"created_at": "2025-06-07T09:30:00Z"
… (40 more lines)GET data is hardcoded, so your response should match exactly.
Request
(none)Expected response
200 OK{
"data": [
{
"id": 7,
"name": "Sneha Iyer",
"username": "sneha",
"email": "sneha@example.com",
"role": "admin",
"city": "Chennai",
"age": 37,
"active": true,
"created_at": "2025-05-19T09:30:00Z"
},
{
"id": 1,
"name": "Asha Verma",
"username": "asha",
"email": "asha@example.com",
"role": "admin",
"city": "Pune",
"age": 34,
"active": true,
"created_at": "2025-01-15T09:30:00Z"
}
],
"meta": {
"page": 1,
"per_page": 5,
"total": 2,
"total_pages": 1,
"sort": "age",
"order": "desc"
},
"links": {
… (7 more lines)GET data is hardcoded, so your response should match exactly.
Request
(none)Expected response
200 OK{
"data": {
"id": 1,
"name": "Asha Verma",
"username": "asha",
"email": "asha@example.com",
"role": "admin",
"city": "Pune",
"age": 34,
"active": true,
"created_at": "2025-01-15T09:30:00Z"
}
}GET data is hardcoded, so your response should match exactly.
Request
(none)Expected response
404 Not Found{
"error": {
"status": 404,
"code": "not_found",
"message": "User 999 does not exist. Valid ids are 1 to 12."
}
}GET data is hardcoded, so your response should match exactly.
Request
(none)Expected response
200 OK{
"data": [
{
"id": 5001,
"user_id": 1,
"status": "delivered",
"ordered_at": "2025-09-02T11:00:00Z",
"items": [
{
"product_id": 101,
"name": "Wireless Mouse",
"quantity": 2,
"unit_price": 799,
"line_total": 1598
},
{
"product_id": 108,
"name": "Python Testing eBook",
"quantity": 1,
"unit_price": 349,
"line_total": 349
}
],
"summary": {
"item_count": 2,
"subtotal": 1947,
"tax": 350.46,
"total": 2297.46,
"currency": "INR"
},
"shipping": {
"city": "Pune",
"pincode": "411001",
"country": "IN"
… (42 more lines)GET data is hardcoded, so your response should match exactly.
Request
(none)Expected response
200 OK{
"data": [
{
"id": 108,
"name": "Python Testing eBook",
"category": "books",
"price": 349,
"currency": "INR",
"in_stock": true,
"rating": 4.8,
"tags": [
"python",
"digital"
]
},
{
"id": 109,
"name": "Selenium Cookbook",
"category": "books",
"price": 599,
"currency": "INR",
"in_stock": true,
"rating": 4.4,
"tags": [
"selenium",
"paperback"
]
},
{
"id": 110,
"name": "API Testing Handbook",
"category": "books",
"price": 449,
"currency": "INR",
… (24 more lines)GET data is hardcoded, so your response should match exactly.
Request
(none)Expected response
200 OK{
"data": {
"id": 101,
"name": "Wireless Mouse",
"category": "electronics",
"price": 799,
"currency": "INR",
"in_stock": true,
"rating": 4.3,
"tags": [
"computer",
"wireless"
]
}
}GET data is hardcoded, so your response should match exactly.
Request
(none)Expected response
200 OK{
"data": {
"id": 5001,
"user_id": 1,
"status": "delivered",
"ordered_at": "2025-09-02T11:00:00Z",
"items": [
{
"product_id": 101,
"name": "Wireless Mouse",
"quantity": 2,
"unit_price": 799,
"line_total": 1598
},
{
"product_id": 108,
"name": "Python Testing eBook",
"quantity": 1,
"unit_price": 349,
"line_total": 349
}
],
"summary": {
"item_count": 2,
"subtotal": 1947,
"tax": 350.46,
"total": 2297.46,
"currency": "INR"
},
"shipping": {
"city": "Pune",
"pincode": "411001",
"country": "IN"
}
… (2 more lines)GET data is hardcoded, so your response should match exactly.
https://sqatools.in/wp-json/sqa-api/v1/objectsRequest
Content-Type: application/jsonExpected response
201 Created{
"id": "obj_toi11wwgih",
"name": "Test laptop",
"data": {
"brand": "Dell",
"price": 55000
},
"version": 1,
"created_by": "anonymous",
"created_at": "2026-11-03T20:19:21Z",
"updated_at": "2026-11-03T20:19:21Z",
"expires_at": "2026-11-03T20:29:21Z",
"ttl_seconds": 600
}Ids, tokens and times in the expected response are examples; yours will differ.
Request
(none)Expected response
200 OK{
"data": [
{
"id": "obj_toi11wwgih",
"name": "Test laptop",
"data": {
"brand": "Dell",
"price": 55000
},
"version": 1,
"created_by": "anonymous",
"created_at": "2026-11-03T20:19:21Z",
"updated_at": "2026-11-03T20:19:21Z",
"expires_at": "2026-11-03T20:29:21Z",
"ttl_seconds": 600
},
{
"id": "demo-qy1zpu",
"name": "My phone",
"data": {
"color": "black",
"storage": "128GB"
},
"version": 2,
"created_by": "anonymous",
"created_at": "2026-11-03T20:17:57Z",
"updated_at": "2026-11-03T20:17:57Z",
"expires_at": "2026-11-03T20:27:57Z",
"ttl_seconds": 600
},
{
"id": "obj_x6lcysd5av",
"name": "Test laptop",
"data": {
… (122 more lines)Ids, tokens and times in the expected response are examples; yours will differ.
https://sqatools.in/wp-json/sqa-api/v1/objects/{id}Request
Content-Type: application/jsonExpected response
201 Created{
"id": "demo-object-1",
"name": "My phone",
"data": {
"color": "black",
"storage": "128GB"
},
"version": 1,
"created_by": "anonymous",
"created_at": "2026-11-03T20:19:21Z",
"updated_at": "2026-11-03T20:19:21Z",
"expires_at": "2026-11-03T20:29:21Z",
"ttl_seconds": 600
}{id} is your own object id for this visit. 201 the first time (created); 200 when it already exists (replaced).
https://sqatools.in/wp-json/sqa-api/v1/objects/{id}Request
(none)Expected response
200 OK{
"id": "demo-object-1",
"name": "My phone",
"data": {
"color": "black",
"storage": "128GB"
},
"version": 1,
"created_by": "anonymous",
"created_at": "2026-11-03T20:19:21Z",
"updated_at": "2026-11-03T20:19:21Z",
"expires_at": "2026-11-03T20:29:21Z",
"ttl_seconds": 600
}{id} is your own object id. Create it first with the PUT row; after DELETE or expiry this returns 404.
https://sqatools.in/wp-json/sqa-api/v1/objects/{id}Request
Content-Type: application/jsonExpected response
200 OK{
"id": "demo-object-1",
"name": "My phone",
"data": {
"color": "blue",
"storage": "128GB"
},
"version": 2,
"created_by": "anonymous",
"created_at": "2026-11-03T20:19:21Z",
"updated_at": "2026-11-03T20:19:22Z",
"expires_at": "2026-11-03T20:29:21Z",
"ttl_seconds": 600
}{id} is your own object id. Create it first with the PUT row; after DELETE or expiry this returns 404.
https://sqatools.in/wp-json/sqa-api/v1/objects/{id}Request
(none)Expected response
204 No Content(empty body){id} is your own object id. Create it first with the PUT row; after DELETE or expiry this returns 404.
https://sqatools.in/wp-json/sqa-api/v1/validate/userRequest
Content-Type: application/jsonExpected response
200 OK{
"valid": true,
"data": {
"name": "Neha",
"email": "neha@example.com",
"age": 31,
"role": "editor",
"phone": null
}
}Your response should have the same status and shape.
Username sqa_user, password sqa@123. The explorer sends Authorization: Basic c3FhX3VzZXI6c3FhQDEyMw== for you.
https://sqatools.in/wp-json/sqa-api/v1/basic/profileRequest
(none)Expected response
200 OK{
"data": {
"username": "sqa_user",
"auth": "basic",
"message": "Basic authentication succeeded."
}
}Your response should have the same status and shape.
https://sqatools.in/wp-json/sqa-api/v1/basic/objectsRequest
Content-Type: application/jsonExpected response
201 Created{
"id": "obj_xh5deqh7ae",
"name": "Basic auth object",
"data": null,
"version": 1,
"created_by": "sqa_user",
"created_at": "2026-11-03T20:19:24Z",
"updated_at": "2026-11-03T20:19:24Z",
"expires_at": "2026-11-03T20:24:24Z",
"ttl_seconds": 300
}Ids, tokens and times in the expected response are examples; yours will differ.
https://sqatools.in/wp-json/sqa-api/v1/basic/objectsRequest
(none)Expected response
200 OK{
"data": [
{
"id": "obj_xh5deqh7ae",
"name": "Basic auth object",
"data": null,
"version": 1,
"created_by": "sqa_user",
"created_at": "2026-11-03T20:19:24Z",
"updated_at": "2026-11-03T20:19:24Z",
"expires_at": "2026-11-03T20:24:24Z",
"ttl_seconds": 300
},
{
"id": "obj_azfuyqumqj",
"name": "Basic auth object",
"data": null,
"version": 1,
"created_by": "sqa_user",
"created_at": "2026-11-03T20:16:46Z",
"updated_at": "2026-11-03T20:16:46Z",
"expires_at": "2026-11-03T20:21:46Z",
"ttl_seconds": 300
},
{
"id": "obj_n4bdoj9xrk",
"name": "Basic auth object",
"data": null,
"version": 1,
"created_by": "sqa_user",
"created_at": "2026-11-03T20:16:01Z",
"updated_at": "2026-11-03T20:16:01Z",
"expires_at": "2026-11-03T20:21:01Z",
"ttl_seconds": 300
… (7 more lines)Ids, tokens and times in the expected response are examples; yours will differ.
Send the header X-API-Key: sqa-key-12345. The explorer adds it for you.
https://sqatools.in/wp-json/sqa-api/v1/apikey/profileRequest
(none)Expected response
200 OK{
"data": {
"client": "api-client",
"auth": "apikey",
"plan": "free",
"message": "API key accepted."
}
}Your response should have the same status and shape.
https://sqatools.in/wp-json/sqa-api/v1/apikey/objectsRequest
Content-Type: application/jsonExpected response
201 Created{
"id": "obj_o26doqe48d",
"name": "API key object",
"data": null,
"version": 1,
"created_by": "api-client",
"created_at": "2026-11-03T20:19:25Z",
"updated_at": "2026-11-03T20:19:25Z",
"expires_at": "2026-11-03T20:24:25Z",
"ttl_seconds": 300
}Ids, tokens and times in the expected response are examples; yours will differ.
https://sqatools.in/wp-json/sqa-api/v1/apikey/objectsRequest
(none)Expected response
200 OK{
"data": [
{
"id": "obj_o26doqe48d",
"name": "API key object",
"data": null,
"version": 1,
"created_by": "api-client",
"created_at": "2026-11-03T20:19:25Z",
"updated_at": "2026-11-03T20:19:25Z",
"expires_at": "2026-11-03T20:24:25Z",
"ttl_seconds": 300
},
{
"id": "obj_9ba03ejxnp",
"name": "API key object",
"data": null,
"version": 1,
"created_by": "api-client",
"created_at": "2026-11-03T20:16:49Z",
"updated_at": "2026-11-03T20:16:49Z",
"expires_at": "2026-11-03T20:21:49Z",
"ttl_seconds": 300
},
{
"id": "obj_hsfhfnitm5",
"name": "API key object",
"data": null,
"version": 1,
"created_by": "api-client",
"created_at": "2026-11-03T20:16:03Z",
"updated_at": "2026-11-03T20:16:03Z",
"expires_at": "2026-11-03T20:21:03Z",
"ttl_seconds": 300
… (7 more lines)Ids, tokens and times in the expected response are examples; yours will differ.
Log in with admin / admin@123 (read and write) or viewer / viewer@123 (read only), then send Authorization: Bearer <access_token>. The explorer logs in for you.
https://sqatools.in/wp-json/sqa-api/v1/auth/loginRequest
Content-Type: application/jsonExpected response
200 OK{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczpcL1wvc3FhdG9vbHMuaW5cLyIsInN1YiI6ImFkbWluIiwibmFtZSI6IkFkbWluIFVzZXIiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3OTM3MzcxNjYsImV4cCI6MTc5MzczODA2NiwianRpIjoieDJtb3BxZWkxNmJtcnl1aCJ9.4tVfwfgwLdGRbQ0E6JPi9nMYR0VNsvagqRZy0C7vIPk",
"token_type": "Bearer",
"expires_in": 900,
"expires_at": "2026-11-03T20:34:26Z",
"refresh_token": "awukaqff85r7pncacqwrsvjldpxskjblzuz4aka6",
"user": {
"username": "admin",
"name": "Admin User",
"role": "admin"
}
}Ids, tokens and times in the expected response are examples; yours will differ.
https://sqatools.in/wp-json/sqa-api/v1/auth/meRequest
(none)Expected response
200 OK{
"data": {
"username": "admin",
"name": "Admin User",
"role": "admin",
"issued_at": "2026-11-03T20:19:16Z",
"expires_at": "2026-11-03T20:34:16Z",
"can_write": true
}
}The explorer logs in as the role you picked and adds the token. Tokens and times in the example will differ.
https://sqatools.in/wp-json/sqa-api/v1/token/objectsRequest
Content-Type: application/jsonExpected response
201 Created{
"id": "obj_femppbzk2r",
"name": "Token object",
"data": null,
"version": 1,
"created_by": "admin",
"created_at": "2026-11-03T20:19:27Z",
"updated_at": "2026-11-03T20:19:27Z",
"expires_at": "2026-11-03T20:24:27Z",
"ttl_seconds": 300
}The explorer logs in as the role you picked and adds the token. Tokens and times in the example will differ.
https://sqatools.in/wp-json/sqa-api/v1/token/objectsRequest
(none)Expected response
200 OK{
"data": [
{
"id": "obj_femppbzk2r",
"name": "Token object",
"data": null,
"version": 1,
"created_by": "admin",
"created_at": "2026-11-03T20:19:27Z",
"updated_at": "2026-11-03T20:19:27Z",
"expires_at": "2026-11-03T20:24:27Z",
"ttl_seconds": 300
},
{
"id": "obj_p85cxmlbzx",
"name": "Token object",
"data": null,
"version": 1,
"created_by": "admin",
"created_at": "2026-11-03T20:16:53Z",
"updated_at": "2026-11-03T20:16:53Z",
"expires_at": "2026-11-03T20:21:53Z",
"ttl_seconds": 300
},
{
"id": "obj_dgm58wzqi7",
"name": "Token object",
"data": null,
"version": 1,
"created_by": "admin",
"created_at": "2026-11-03T20:16:05Z",
"updated_at": "2026-11-03T20:16:05Z",
"expires_at": "2026-11-03T20:21:05Z",
"ttl_seconds": 300
… (7 more lines)The explorer logs in as the role you picked and adds the token. Tokens and times in the example will differ.
https://sqatools.in/wp-json/sqa-api/v1/auth/refreshRequest
Content-Type: application/jsonExpected response
200 OK{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczpcL1wvc3FhdG9vbHMuaW5cLyIsInN1YiI6ImFkbWluIiwibmFtZSI6IkFkbWluIFVzZXIiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3OTM3MzcxNjgsImV4cCI6MTc5MzczODA2OCwianRpIjoiM2phdG5uMmtoMHdmYXV5YiJ9.MjX5y0VO2ChDa3UBK6GSc-CvhWft65bvF89IHvuyToc",
"token_type": "Bearer",
"expires_in": 900,
"expires_at": "2026-11-03T20:34:28Z",
"refresh_token": "bfgmbksqjsd5w5febkzn14yvshoxg2nkxzoxihvl",
"user": {
"username": "admin",
"name": "Admin User",
"role": "admin"
}
}Ids, tokens and times in the expected response are examples; yours will differ.
https://sqatools.in/wp-json/sqa-api/v1/auth/logoutRequest
(none)Expected response
204 No Content(empty body)The explorer logs in as the role you picked and adds the token. Tokens and times in the example will differ.
Endpoints for testing status codes, slow responses, redirects, rate limits, headers and schemas.
Request
(none)Expected response
200 OK{
"status": "ok",
"service": "SQA Tools practice API",
"version": "1.0.0",
"time": "2026-11-03T20:19:29Z"
}Ids, tokens and times in the expected response are examples; yours will differ.
Request
(none)Expected response
404 Not Found{
"status": 404,
"message": "Not Found",
"method": "GET"
}Your response should have the same status and shape.
Request
(none)Expected response
200 OK{
"requested_seconds": 2,
"actual_seconds": 2,
"time": "2026-11-03T20:19:32Z"
}Ids, tokens and times in the expected response are examples; yours will differ.
https://sqatools.in/wp-json/sqa-api/v1/echo?source=practiceRequest
Content-Type: application/jsonExpected response
200 OK{
"method": "POST",
"path": "/sqa-api/v1/echo",
"query": {
"source": "practice"
},
"headers": {
"accept": "application/json",
"content-length": "18",
"content-type": "application/json",
"host": "sqatools.in",
"user-agent": "Mozilla/5.0 (sqatools docs build)"
},
"content_type": "application/json",
"json": {
"hello": "world"
},
"form": {},
"raw_body": "{\"hello\": \"world\"}",
"body_bytes": 18,
"time": "2026-11-03T20:19:32Z"
}Ids, tokens and times in the expected response are examples; yours will differ.
Request
(none)Expected response
200 OK{
"method": "GET",
"path": "/sqa-api/v1/echo",
"query": {},
"headers": {
"accept": "application/json",
"host": "sqatools.in",
"user-agent": "Mozilla/5.0 (sqatools docs build)"
},
"content_type": null,
"json": null,
"form": {},
"raw_body": "",
"body_bytes": 0,
"time": "2026-11-03T20:19:33Z"
}Ids, tokens and times in the expected response are examples; yours will differ.
Request
(none)Expected response
200 OK{
"message": "Request accepted",
"limit": 5,
"remaining": 4,
"reset_at": "2026-11-03T20:20:33Z"
}Ids, tokens and times in the expected response are examples; yours will differ.
Request
(none)Expected response
200 OK{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://sqatools.in/wp-json/sqa-api/v1/schema/user",
"title": "user",
"type": "object",
"required": [
"id",
"name",
"username",
"email",
"role",
"city",
"age",
"active",
"created_at"
],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"name": {
"type": "string"
},
"username": {
"type": "string"
},
"email": {
"type": "string",
"format": "email"
},
"role": {
"enum": [
"admin",
… (21 more lines)Ids, tokens and times in the expected response are examples; yours will differ.
Request
(none)Expected response
200 OK{
"base_url": "https://sqatools.in/wp-json/sqa-api/v1",
"data": [
{
"methods": "GET",
"path": "/health",
"auth": "none",
"description": "Service status and server time"
},
{
"methods": "GET",
"path": "/users",
"auth": "none",
"description": "Users list: page, per_page, role, city, active, q, sort, order"
},
{
"methods": "GET",
"path": "/users/{id}",
"auth": "none",
"description": "One user (Accept: application/xml for XML)"
},
{
"methods": "GET",
"path": "/users/{id}/orders",
"auth": "none",
"description": "Orders of one user (nested resource)"
},
{
"methods": "GET",
"path": "/products",
"auth": "none",
"description": "Products: category, in_stock, min_price, max_price, sort, order"
},
{
… (127 more lines)Your response should have the same status and shape.
Full endpoint reference (26 routes) and error format
| Method | Path | Auth | What it does |
|---|---|---|---|
| GET | /health | none | Service status and server time |
| GET | /users | none | Users list: page, per_page, role, city, active, q, sort, order |
| GET | /users/{id} | none | One user (Accept: application/xml for XML) |
| GET | /users/{id}/orders | none | Orders of one user (nested resource) |
| GET | /products | none | Products: category, in_stock, min_price, max_price, sort, order |
| GET | /products/{id} | none | One product with ETag (send If-None-Match for 304) |
| GET | /orders | none | Orders: status, user_id |
| GET | /orders/{id} | none | One order with nested items, summary and shipping |
| ANY | /status/{code} | none | Responds with any status code from 200 to 599 |
| GET | /delay/{seconds} | none | Waits 0-10 seconds before responding |
| ANY | /echo | none | Echoes method, query, headers and body |
| ANY | /redirect | none | Redirects to /echo (status=301, 302, 303, 307 or 308) |
| GET | /rate-limit | none | 5 requests per minute, then 429 with Retry-After |
| POST | /validate/user | none | Validates a user payload; 422 lists every field error |
| GET | /schema/{name} | none | JSON Schema: user, product, order, object, error, token |
| GETPOST | /objects | none | Temporary objects: list, create |
| GETPUTPATCHDELETE | /objects/{id} | none | Temporary object: read, replace or create, update, delete |
| GET | /basic/profile | basic | Profile behind HTTP Basic auth |
| GETPOSTPUTPATCHDELETE | /basic/objects[/{id}] | basic | Temporary objects behind Basic auth |
| GET | /apikey/profile | apikey | Profile behind an API key |
| GETPOSTPUTPATCHDELETE | /apikey/objects[/{id}] | apikey | Temporary objects behind an API key |
| POST | /auth/login | none | Get a Bearer JWT and refresh token |
| POST | /auth/refresh | none | Swap a refresh token for new tokens |
| POST | /auth/logout | bearer | Revoke the access token |
| GET | /auth/me | bearer | Who the token belongs to |
| GETPOSTPUTPATCHDELETE | /token/objects[/{id}] | bearer | Temporary objects; viewer role may only read |
Every error has the same shape, which makes it easy to assert:
{"error": {"status": 422, "code": "validation_failed", "message": "1 field(s) failed validation.",
"details": [{"field": "age", "message": "age is required: a whole number from 18 to 100 (not a string)."}]}}
How temporary objects work
POST /objects gives a server id and 201 with a Location header. PUT /objects/your-id creates or replaces with your own id.
name (required, 1–100 characters), data (any JSON up to 5 KB), ttl (10–1800 seconds, default 600). Other fields give 422.
Each object disappears after its ttl; then GET returns 404. DELETE returns 204, a second DELETE 404.
Every change adds 1 to version. Send the ETag in If-Match (else 412) or If-None-Match (304).
141 API testing scenarios
A ready-made test plan: functional, negative, boundary, auth, roles, headers, caching, performance and rate-limit cases. Press Try to run one in the console below (setup such as creating an object or waiting for expiry runs for you), or Run all to check them all.
The response appears here.
- Read hardcoded data (GET)
- Pagination, filtering, sorting and search
- HTTP methods, status codes and redirects
- Headers, content types and caching
- Request body validation (negative testing)
- Temporary objects: create, read, update, delete
- Basic authentication
- API key authentication
- Token (Bearer JWT) authentication
- Authorization (roles and permissions)
- Performance, timeouts and rate limits
Read hardcoded data (GET) (17)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 1 | Health check returns ok Smoke test: the service is up. | GET /health | 200 | |
| 2 | List all users Default list with pagination metadata and headers. | GET /users | 200 | |
| 3 | Get a single user by id Verify exact field values. | GET /users/1 | 200 | |
| 4 | User that does not exist Negative test: resource not found. | GET /users/999 | 404 | |
| 5 | Non-numeric user id Path parameter type validation. | GET /users/abc | 400 | |
| 6 | Zero as user id Boundary value for an id. | GET /users/0 | 400 | |
| 7 | Nested resource: orders of a user Parent/child resources. | GET /users/1/orders | 200 | |
| 8 | Order with nested items and totals Check nested objects, arrays and calculated values. | GET /orders/5001 | 200 | |
| 9 | Product has an ETag header Response header assertions. | GET /products/101 | 200 | |
| 10 | Filter products by category Query parameter filtering. | GET /products?category=books | 200 | |
| 11 | Filter products by price range Combined range filters. | GET /products?min_price=1000&max_price=5000 | 200 | |
| 12 | Invalid price range (min > max) Cross-field validation of query parameters. | GET /products?min_price=5000&max_price=100 | 400 | |
| 13 | Unknown category value Enum validation. | GET /products?category=toys | 400 | |
| 14 | Filter orders by status Filter on a status field. | GET /orders?status=delivered | 200 | |
| 15 | JSON Schema for a user Fetch a schema for contract testing. | GET /schema/user | 200 | |
| 16 | Unknown schema name Negative test on a lookup. | GET /schema/invoice | 404 | |
| 17 | List every endpoint API discovery (JSON escapes / as \/). | GET /endpoints | 200 |
Pagination, filtering, sorting and search (17)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 18 | Second page of users Page 2 starts at the 6th record. | GET /users?page=2&per_page=5 | 200 | |
| 19 | Last page has the remaining records Last-page boundary. | GET /users?page=3&per_page=5 | 200 | |
| 20 | Page beyond the last page Out-of-range page returns an empty list, not an error. | GET /users?page=10 | 200 | |
| 21 | per_page above the maximum (51) Upper boundary of page size. | GET /users?per_page=51 | 400 | |
| 22 | per_page at the maximum (50) Boundary value that must pass. | GET /users?per_page=50 | 200 | |
| 23 | Page number zero Lower boundary of page. | GET /users?page=0 | 400 | |
| 24 | Link header points to the next page Hypermedia pagination headers. | GET /users?per_page=5 | 200 | |
| 25 | Filter users by role Single filter. | GET /users?role=admin | 200 | |
| 26 | City filter is case-insensitive Case handling in filters. | GET /users?city=pune | 200 | |
| 27 | Boolean filter active=false Boolean query parameters. | GET /users?active=false | 200 | |
| 28 | Invalid boolean value Boolean validation. | GET /users?active=maybe | 400 | |
| 29 | Combine two filters AND logic between filters. | GET /users?role=admin&city=Chennai | 200 | |
| 30 | Search by text Free-text search. | GET /users?q=gupta | 200 | |
| 31 | Search with no match Empty result set. | GET /users?q=zzz | 200 | |
| 32 | Sort by age descending Sorting order. | GET /users?sort=age&order=desc&per_page=1 | 200 | |
| 33 | Sort by an unknown field Whitelisted sort fields. | GET /users?sort=salary | 400 | |
| 34 | Invalid sort order Enum validation for order. | GET /users?order=up | 400 |
HTTP methods, status codes and redirects (18)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 35 | POST on a read-only resource 405 with an Allow header. | POST /users | 405 | |
| 36 | DELETE on a read-only resource Unsupported method. | DELETE /products/101 | 405 | |
| 37 | HEAD returns headers only HEAD has the same headers as GET and no body. | HEAD /users/1 | 200 | |
| 38 | OPTIONS lists allowed methods Pre-flight / discovery. | /users | 200 | |
| 39 | Status code 201 Client handles 201 Created. | GET /status/201 | 201 | |
| 40 | Status code 400 Client handles 400 Bad Request. | GET /status/400 | 400 | |
| 41 | Status code 401 Client handles 401 Unauthorized. | GET /status/401 | 401 | |
| 42 | Status code 403 Client handles 403 Forbidden. | GET /status/403 | 403 | |
| 43 | Status code 404 Client handles 404 Not Found. | GET /status/404 | 404 | |
| 44 | Status code 409 Client handles 409 Conflict. | GET /status/409 | 409 | |
| 45 | Status code 500 Client handles 500 Internal Server Error. | GET /status/500 | 500 | |
| 46 | Status 503 with Retry-After Back-off header on service unavailable. | GET /status/503 | 503 | |
| 47 | Status 204 has an empty body No Content. | GET /status/204 | 204 | |
| 48 | Invalid status code Validation of a path parameter. | GET /status/700 | 400 | |
| 49 | Redirect without following it Assert the redirect itself. | GET /redirect | 302 | |
| 50 | Follow a redirect Client follows to the final response. | GET /redirect | 200 | |
| 51 | 307 keeps the POST method 307/308 preserve method and body. | POST /redirect?status=307 | 200 | |
| 52 | 303 changes POST to GET 303 See Other always switches to GET. | POST /redirect?status=303 | 200 |
Headers, content types and caching (7)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 53 | Ask for XML with the Accept header Content negotiation. | GET /users/1 | 200 | |
| 54 | Unsupported Accept type 406 Not Acceptable. | GET /users/1 | 406 | |
| 55 | Custom request header is received Verify headers and query reach the server. | GET /echo?source=test | 200 | |
| 56 | Form-encoded body Non-JSON bodies. | POST /echo | 200 | |
| 57 | JSON body is echoed back Round-trip a JSON payload. | PUT /echo | 200 | |
| 58 | Conditional GET returns 304 ETag caching saves bandwidth. | GET /products/101 | 304 | |
| 59 | Responses are not cached Cache-Control header check. | GET /health | 200 |
Request body validation (negative testing) (18)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 60 | Valid payload passes Happy path. | POST /validate/user | 200 | |
| 61 | Empty object: every required field fails All errors are reported at once. | POST /validate/user | 422 | |
| 62 | Wrong data type (age as text) Type checking. | POST /validate/user | 422 | |
| 63 | Boundary: age 17 Boundary value analysis (18 to 100). | POST /validate/user | 422 | |
| 64 | Boundary: age 18 Boundary value analysis (18 to 100). | POST /validate/user | 200 | |
| 65 | Boundary: age 100 Boundary value analysis (18 to 100). | POST /validate/user | 200 | |
| 66 | Boundary: age 101 Boundary value analysis (18 to 100). | POST /validate/user | 422 | |
| 67 | Invalid email format Format validation. | POST /validate/user | 422 | |
| 68 | Value outside the enum Enum validation. | POST /validate/user | 422 | |
| 69 | Unknown extra field Strict schema rejects extra fields. | POST /validate/user | 422 | |
| 70 | Name too short (1 character) Minimum length. | POST /validate/user | 422 | |
| 71 | Name too long (51 characters) Maximum length. | POST /validate/user | 422 | |
| 72 | Invalid phone number Pattern validation. | POST /validate/user | 422 | |
| 73 | Malformed JSON Syntax errors give 400, not 500. | POST /validate/user | 400 | |
| 74 | Wrong Content-Type 415 Unsupported Media Type. | POST /validate/user | 415 | |
| 75 | Empty body Missing payload. | POST /validate/user | 400 | |
| 76 | JSON array instead of an object Top-level type check. | POST /validate/user | 400 | |
| 77 | Payload larger than 10 KB 413 Payload Too Large. | POST /objects | 413 |
Temporary objects: create, read, update, delete (24)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 78 | Create an object (POST) 201 Created with a Location header. | POST /objects | 201 | |
| 79 | Create without the required name Required field. | POST /objects | 422 | |
| 80 | ttl below the minimum (5 seconds) Boundary: minimum 10 seconds. | POST /objects | 422 | |
| 81 | ttl above the maximum (2000 seconds) Boundary: maximum 1800 seconds. | POST /objects | 422 | |
| 82 | Client cannot choose the id on POST Server-generated ids. | POST /objects | 422 | |
| 83 | Read the created object GET after POST. | GET /objects/{id} | 200 | |
| 84 | List objects The new object appears in the list. | GET /objects | 200 | |
| 85 | Replace with PUT Full update increments the version. | PUT /objects/{id} | 200 | |
| 86 | Create with PUT and your own id PUT to a new id creates it (upsert). | PUT /objects/my-{rand} | 201 | |
| 87 | PUT without the required name PUT needs the full representation. | PUT /objects/{id} | 422 | |
| 88 | Body id differs from URL id Consistency check. | PUT /objects/{id} | 422 | |
| 89 | Partial update with PATCH PATCH merges into existing data. | PATCH /objects/{id} | 200 | |
| 90 | PATCH removes a key set to null JSON merge patch semantics. | PATCH /objects/{id} | 200 | |
| 91 | PATCH with an empty object Nothing to update. | PATCH /objects/{id} | 422 | |
| 92 | PATCH an object that does not exist Update a missing resource. | PATCH /objects/missing-{rand} | 404 | |
| 93 | Delete an object 204 No Content. | DELETE /objects/{id} | 204 | |
| 94 | Read a deleted object The object is gone after DELETE. | GET /objects/{id} | 404 | |
| 95 | Delete the same object twice Second DELETE returns 404. | DELETE /objects/{id} | 404 | |
| 96 | Id too short Path parameter format. | GET /objects/ab | 400 | |
| 97 | Optimistic locking: stale If-Match Prevent lost updates. | PUT /objects/{id} | 412 | |
| 98 | Optimistic locking: current If-Match Matching version is accepted. | PUT /objects/{id} | 200 | |
| 99 | Conditional GET on an object 304 when unchanged. | GET /objects/{id} | 304 | |
| 100 | Object vanishes after its ttl Data expiry: created with ttl 10, read after 12 seconds. | GET /objects/{id} | 404 | |
| 101 | Text with SQL and HTML is stored as plain text Injection strings are data, never executed. | POST /objects | 201 |
Basic authentication (8)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 102 | Valid username and password Authorization: Basic base64(user:pass). | GET /basic/profile basic | 200 | |
| 103 | No credentials 401 with a WWW-Authenticate challenge. | GET /basic/profile | 401 | |
| 104 | Wrong password Invalid credentials. | GET /basic/profile basic_wrong | 401 | |
| 105 | Malformed Basic header Header that is not base64. | GET /basic/profile | 401 | |
| 106 | Create an object with Basic auth Write behind Basic auth. | POST /basic/objects basic | 201 | |
| 107 | Create without credentials Writes are protected too. | POST /basic/objects | 401 | |
| 108 | Read an object with Basic auth Read behind Basic auth. | GET /basic/objects/{id} basic | 200 | |
| 109 | Collections are separate An object created in /objects is not in /basic/objects. | GET /basic/objects/{id} basic | 404 |
API key authentication (6)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 110 | Valid key in the X-API-Key header Header-based API key. | GET /apikey/profile apikey | 200 | |
| 111 | Valid key as a query parameter Query-string key (less secure: it ends up in logs). | GET /apikey/profile?api_key=sqa-key-12345 | 200 | |
| 112 | Missing API key 401 when no key is sent. | GET /apikey/profile | 401 | |
| 113 | Wrong API key 403 when the key is not valid. | GET /apikey/profile apikey_wrong | 403 | |
| 114 | Key sent in the wrong header The key must be in X-API-Key. | GET /apikey/profile | 401 | |
| 115 | Create an object with an API key Write behind an API key. | POST /apikey/objects apikey | 201 |
Token (Bearer JWT) authentication (15)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 116 | Log in and get a token Token issue. | POST /auth/login | 200 | |
| 117 | Log in with a wrong password Failed login. | POST /auth/login | 401 | |
| 118 | Log in without fields Required fields. | POST /auth/login | 422 | |
| 119 | Token lifetime below 5 seconds Boundary on expires_in. | POST /auth/login | 422 | |
| 120 | Call a protected endpoint with the token Authorization: Bearer <token>. | GET /auth/me admin | 200 | |
| 121 | No token 401 without a token. | GET /auth/me | 401 | |
| 122 | Garbage token Not a JWT. | GET /auth/me | 401 | |
| 123 | Wrong scheme (Token instead of Bearer) Scheme must be Bearer. | GET /auth/me | 401 | |
| 124 | Tampered token signature Changing any character breaks the signature. | GET /auth/me tampered | 401 | |
| 125 | Expired token Token issued with expires_in 5, used after 7 seconds. | GET /auth/me expired | 401 | |
| 126 | Log out revokes the token Server-side logout. | POST /auth/logout admin | 204 | |
| 127 | Token used after logout Revoked tokens are rejected. | GET /auth/me revoked | 401 | |
| 128 | Refresh the token Get new tokens without the password. | POST /auth/refresh | 200 | |
| 129 | Reuse a refresh token Refresh tokens rotate: each works once. | POST /auth/refresh | 401 | |
| 130 | Refresh without a token Required field. | POST /auth/refresh | 422 |
Authorization (roles and permissions) (6)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 131 | Viewer can read objects Read permission. | GET /token/objects viewer | 200 | |
| 132 | Viewer cannot create objects 403 = authenticated but not allowed. | POST /token/objects viewer | 403 | |
| 133 | Admin can create objects Write permission. | POST /token/objects admin | 201 | |
| 134 | Viewer cannot delete Role check on DELETE. | DELETE /token/objects/{id} viewer | 403 | |
| 135 | Admin can delete Owner role succeeds. | DELETE /token/objects/{id} admin | 204 | |
| 136 | Viewer token says can_write false Claims in the response. | GET /auth/me viewer | 200 |
Performance, timeouts and rate limits (5)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 137 | Response time under 3 seconds Simple SLA assertion. | GET /users | 200 | |
| 138 | Delayed response (2 seconds) Measure latency. | GET /delay/2 | 200 | |
| 139 | Delay above the limit Validation on the delay. | GET /delay/11 | 400 | |
| 140 | Client timeout Your client should give up after 2 seconds. | GET /delay/5 | client timeout | |
| 141 | Rate limit: 6th call in a minute 429 Too Many Requests with Retry-After. | GET /rate-limit | 429 |
Code examples
Every example below runs as-is against this API.
# 1. Read hardcoded data (no auth)
curl -i https://sqatools.in/wp-json/sqa-api/v1/users/1
# 2. Create a temporary object that lives 120 seconds
curl -i -X POST https://sqatools.in/wp-json/sqa-api/v1/objects \
-H "Content-Type: application/json" \
-d '{"name": "My test object", "data": {"qty": 2}, "ttl": 120}'
# 3. Basic auth
curl -i -u sqa_user:sqa@123 https://sqatools.in/wp-json/sqa-api/v1/basic/profile
# 4. API key
curl -i -H "X-API-Key: sqa-key-12345" https://sqatools.in/wp-json/sqa-api/v1/apikey/profile
# 5. Bearer token: log in, then send the access_token
curl -s -X POST https://sqatools.in/wp-json/sqa-api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin@123"}'
curl -i -H "Authorization: Bearer <access_token>" https://sqatools.in/wp-json/sqa-api/v1/auth/meimport requests
BASE = "https://sqatools.in/wp-json/sqa-api/v1"
# No auth: hardcoded user
r = requests.get(f"{BASE}/users/1", timeout=10)
print(r.status_code, r.json()["data"]["name"]) # 200 Asha Verma
# Create a temporary object, then read it back from the Location header
r = requests.post(f"{BASE}/objects", json={"name": "My test object", "ttl": 120}, timeout=10)
print(r.status_code, r.headers["Location"]) # 201 …/objects/obj_…
print(requests.get(r.headers["Location"], timeout=10).json()["name"])
# Basic auth, API key and Bearer token
print(requests.get(f"{BASE}/basic/profile", auth=("sqa_user", "sqa@123"), timeout=10).status_code)
print(requests.get(f"{BASE}/apikey/profile", headers={"X-API-Key": "sqa-key-12345"}, timeout=10).status_code)
token = requests.post(f"{BASE}/auth/login", json={"username": "admin", "password": "admin@123"}, timeout=10).json()["access_token"]
print(requests.get(f"{BASE}/auth/me", headers={"Authorization": f"Bearer {token}"}, timeout=10).json()["data"]["role"])const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';
// No auth: list users, page 2, 5 per page
const list = await fetch(`${BASE}/users?page=2&per_page=5`);
console.log(list.status, list.headers.get('X-Total-Count')); // 200 '12'
// Create a temporary object
const created = await fetch(`${BASE}/objects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'From fetch', data: { qty: 2 }, ttl: 120 })
});
const obj = await created.json();
console.log(created.status, obj.id, obj.version); // 201 'obj_…' 1
// Bearer token
const login = await fetch(`${BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'viewer', password: 'viewer@123' })
});
const { access_token } = await login.json();
const me = await fetch(`${BASE}/auth/me`, { headers: { Authorization: `Bearer ${access_token}` } });
console.log((await me.json()).data.can_write); // falseimport pytest
import requests
BASE = "https://sqatools.in/wp-json/sqa-api/v1"
@pytest.fixture(scope="session")
def admin_headers():
r = requests.post(f"{BASE}/auth/login", json={"username": "admin", "password": "admin@123"}, timeout=10)
assert r.status_code == 200
return {"Authorization": f"Bearer {r.json()['access_token']}"}
def test_get_user():
r = requests.get(f"{BASE}/users/1", timeout=10)
assert r.status_code == 200
assert r.headers["Content-Type"].startswith("application/json")
assert r.json()["data"]["email"] == "asha@example.com"
@pytest.mark.parametrize("user_id, status", [(1, 200), (12, 200), (13, 404), ("abc", 400)])
def test_user_ids(user_id, status):
assert requests.get(f"{BASE}/users/{user_id}", timeout=10).status_code == status
def test_pagination():
body = requests.get(f"{BASE}/users", params={"page": 3, "per_page": 5}, timeout=10).json()
assert [u["id"] for u in body["data"]] == [11, 12]
assert body["meta"]["total_pages"] == 3
assert body["links"]["next"] is None
def test_object_lifecycle():
r = requests.post(f"{BASE}/objects", json={"name": "Laptop", "data": {"ram": "16GB"}, "ttl": 120}, timeout=10)
assert r.status_code == 201
url = r.headers["Location"]
r = requests.patch(url, json={"data": {"ram": "32GB"}}, timeout=10)
assert r.status_code == 200
assert r.json()["data"] == {"ram": "32GB"}
assert r.json()["version"] == 2
assert requests.delete(url, timeout=10).status_code == 204
assert requests.get(url, timeout=10).status_code == 404
def test_validation_errors():
r = requests.post(f"{BASE}/validate/user", json={"name": "A", "email": "bad", "age": "20"}, timeout=10)
assert r.status_code == 422
fields = {d["field"] for d in r.json()["error"]["details"]}
assert fields == {"name", "email", "age"}
def test_basic_auth():
assert requests.get(f"{BASE}/basic/profile", auth=("sqa_user", "sqa@123"), timeout=10).status_code == 200
r = requests.get(f"{BASE}/basic/profile", auth=("sqa_user", "wrong"), timeout=10)
assert r.status_code == 401
assert r.headers["WWW-Authenticate"].startswith("Basic")
def test_api_key():
assert requests.get(f"{BASE}/apikey/profile", headers={"X-API-Key": "sqa-key-12345"}, timeout=10).status_code == 200
assert requests.get(f"{BASE}/apikey/profile", headers={"X-API-Key": "wrong"}, timeout=10).status_code == 403
def test_token_crud(admin_headers):
r = requests.post(f"{BASE}/token/objects", json={"name": "Secured"}, headers=admin_headers, timeout=10)
assert r.status_code == 201
assert r.json()["created_by"] == "admin"
def test_timeout_is_raised():
with pytest.raises(requests.exceptions.Timeout):
requests.get(f"{BASE}/delay/5", timeout=2)Run with pip install pytest requests, then pytest.
import pytest
from playwright.sync_api import APIRequestContext, Playwright
# base_url must end with "/" and paths must not start with "/", or the /wp-json/... part is dropped
BASE = "https://sqatools.in/wp-json/sqa-api/v1/"
@pytest.fixture(scope="session")
def api(playwright: Playwright):
ctx = playwright.request.new_context(base_url=BASE)
yield ctx
ctx.dispose()
@pytest.fixture(scope="session")
def viewer_token(api: APIRequestContext):
r = api.post("auth/login", data={"username": "viewer", "password": "viewer@123"})
assert r.ok
return r.json()["access_token"]
def test_users_page_2(api: APIRequestContext):
r = api.get("users", params={"page": 2, "per_page": 5})
assert r.status == 200
assert r.headers["x-total-count"] == "12"
assert [u["id"] for u in r.json()["data"]] == [6, 7, 8, 9, 10]
def test_put_creates_then_replaces(api: APIRequestContext):
r = api.put("objects/pw-demo-object", data={"name": "First", "ttl": 60})
assert r.status in (200, 201)
r = api.put("objects/pw-demo-object", data={"name": "Second", "ttl": 60})
assert r.status == 200
assert r.json()["name"] == "Second"
def test_conditional_get(api: APIRequestContext):
etag = api.get("products/101").headers["etag"]
assert api.get("products/101", headers={"If-None-Match": etag}).status == 304
def test_viewer_is_read_only(api: APIRequestContext, viewer_token):
auth = {"Authorization": f"Bearer {viewer_token}"}
assert api.get("token/objects", headers=auth).status == 200
r = api.post("token/objects", headers=auth, data={"name": "Not allowed"})
assert r.status == 403
assert r.json()["error"]["code"] == "forbidden"
def test_basic_auth_with_http_credentials(playwright: Playwright):
ctx = playwright.request.new_context(base_url=BASE, http_credentials={"username": "sqa_user", "password": "sqa@123", "send": "always"})
assert ctx.get("basic/profile").status == 200
ctx.dispose()Run with pip install pytest-playwright, then pytest.
Limits
- 600 requests per minute per IP (then 429);
/rate-limitallows 5 per minute on purpose. - Request bodies up to 10 KB (413 above that);
dataup to 5 KB. - Up to 300 live objects per collection; the oldest are removed first.
/delay/{seconds}waits at most 10 seconds. Responses are never cached.