Free test API for REST, GraphQL, gRPC and WebSocket
Real endpoints you can hit right now on playground.tetiva.app. It answers with what you sent, any status you ask for, as slowly as you like. No key, nothing stored, not a proxy.
What do you want to test?
You picked
Not here? See all 22 requests below or ask us to add yours
- REST
https://playground. tetiva. app - GraphQL
https://playground. tetiva. app /graphql - WebSocket
wss://playground. tetiva. app /ws - gRPCreflection on
playground.tetiva. app:443
- Not a proxy — playground.tetiva.app never calls other hosts
- Nothing kept — bodies aren't stored or logged
- Fair use — 60 req/min, 1 MB bodies, delays up to 10 s
Every endpoint
One host, four protocols, 22 requests. Everything below works from curl, your own code, Postman or Tetiva: REST on https://, GraphQL at /graphql, WebSocket at wss:// and gRPC on playground. with server reflection.
REST · Echo
Every echo answers with the method, URL, query (as args), headers and body it received, plus a request_id. A JSON body comes back raw in data and parsed in json; forms and multipart uploads come back in form and files.
Echo a GET#
https:// playground. tetiva. app /get?source=docs&page=2Returns your request as JSON: query parameters in args, headers in headers, your IP in origin.
Query parameters · 2
| source | docs |
| page | 2 |
curl 'https://playground.tetiva.app/get?source=docs&page=2'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 314 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"method": "GET",
"url": "https://playground.tetiva.app/get?source=docs&page=2",
"args": {
"page": "2",
"source": "docs"
},
"headers": {
"Host": "playground.tetiva.app",
"User-Agent": "curl/8.7.1"
},
"origin": "203.0.113.42",
"request_id": "00000000-0000-4000-8000-000000000001"
}
Echo my POST body#
https:// playground. tetiva. app /postSend a JSON body and get it back: raw in data, parsed in json. Bodies go up to 1 MB; forms and multipart files come back in form and files.
Headers · 1
| Content-Type | application/json |
Body · JSON
{"hello":"world"}curl -X POST 'https://playground.tetiva.app/post' \
-H 'Content-Type: application/json' \
--data-raw '{"hello":"world"}'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 397 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"method": "POST",
"url": "https://playground.tetiva.app/post",
"args": {},
"headers": {
"Content-Length": "17",
"Content-Type": "application/json",
"Host": "playground.tetiva.app",
"User-Agent": "curl/8.7.1"
},
"data": "{\"hello\":\"world\"}",
"json": {
"hello": "world"
},
"origin": "203.0.113.42",
"request_id": "00000000-0000-4000-8000-000000000001"
}
Echo a PUT#
https:// playground. tetiva. app /putThe same echo for PUT: the body comes back in data and json.
Headers · 1
| Content-Type | application/json |
Body · JSON
{
"id": 42,
"name": "Ada Lovelace",
"role": "admin"
}curl -X PUT 'https://playground.tetiva.app/put' \
-H 'Content-Type: application/json' \
--data-raw '{
"id": 42,
"name": "Ada Lovelace",
"role": "admin"
}'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 488 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"method": "PUT",
"url": "https://playground.tetiva.app/put",
"args": {},
"headers": {
"Content-Length": "59",
"Content-Type": "application/json",
"Host": "playground.tetiva.app",
"User-Agent": "curl/8.7.1"
},
"data": "{\n \"id\": 42,\n \"name\": \"Ada Lovelace\",\n \"role\": \"admin\"\n}",
"json": {
"id": 42,
"name": "Ada Lovelace",
"role": "admin"
},
"origin": "203.0.113.42",
"request_id": "00000000-0000-4000-8000-000000000001"
}
Echo a PATCH#
https:// playground. tetiva. app /patchThe same echo for PATCH, handy for partial updates.
Headers · 1
| Content-Type | application/json |
Body · JSON
{
"role": "viewer"
}curl -X PATCH 'https://playground.tetiva.app/patch' \
-H 'Content-Type: application/json' \
--data-raw '{
"role": "viewer"
}'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 406 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"method": "PATCH",
"url": "https://playground.tetiva.app/patch",
"args": {},
"headers": {
"Content-Length": "22",
"Content-Type": "application/json",
"Host": "playground.tetiva.app",
"User-Agent": "curl/8.7.1"
},
"data": "{\n \"role\": \"viewer\"\n}",
"json": {
"role": "viewer"
},
"origin": "203.0.113.42",
"request_id": "00000000-0000-4000-8000-000000000001"
}
Echo a DELETE#
https:// playground. tetiva. app /delete?id=42The same echo for DELETE. A body is allowed and comes back too.
Query parameters · 1
| id | 42 |
curl -X DELETE 'https://playground.tetiva.app/delete?id=42'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 314 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"method": "DELETE",
"url": "https://playground.tetiva.app/delete?id=42",
"args": {
"id": "42"
},
"headers": {
"Host": "playground.tetiva.app",
"User-Agent": "curl/8.7.1"
},
"data": "",
"json": null,
"origin": "203.0.113.42",
"request_id": "00000000-0000-4000-8000-000000000001"
}
Any method, any path#
https:// playground. tetiva. app /anything /orders /42?dry_run=true/anything and every path under it echo any method, custom ones included, so you can stand in for a route that doesn't exist yet.
Query parameters · 1
| dry_run | true |
Headers · 1
| Content-Type | application/json |
Body · JSON
{
"status": "shipped"
}curl -X POST 'https://playground.tetiva.app/anything/orders/42?dry_run=true' \
-H 'Content-Type: application/json' \
--data-raw '{
"status": "shipped"
}'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 462 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"method": "POST",
"url": "https://playground.tetiva.app/anything/orders/42?dry_run=true",
"args": {
"dry_run": "true"
},
"headers": {
"Content-Length": "25",
"Content-Type": "application/json",
"Host": "playground.tetiva.app",
"User-Agent": "curl/8.7.1"
},
"data": "{\n \"status\": \"shipped\"\n}",
"json": {
"status": "shipped"
},
"origin": "203.0.113.42",
"request_id": "00000000-0000-4000-8000-000000000001"
}
See my headers#
https:// playground. tetiva. app /headersReturns only the headers the server received. Useful to check what your client, proxy or SDK adds.
Headers · 1
| X-Debug | 1 |
curl 'https://playground.tetiva.app/headers' \
-H 'X-Debug: 1'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 167 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"headers": {
"Host": "playground.tetiva.app",
"User-Agent": "curl/8.7.1",
"X-Debug": "1"
},
"request_id": "00000000-0000-4000-8000-000000000001"
}
REST · Status codes and delays
Make the server fail or stall on purpose: any status from 200 to 599, or a wait of up to 10 seconds.
Any status code#
https:// playground. tetiva. app /status /418Answers with the status in the path, from 200 to 599. Redirects point to /get, 401 adds WWW-Authenticate, 429 and 503 add Retry-After.
curl 'https://playground.tetiva.app/status/418'Response examples
418I'm a teapot
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 104 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"status": 418,
"reason": "I'm a teapot",
"request_id": "00000000-0000-4000-8000-000000000001"
}
Slow response#
https:// playground. tetiva. app /delay /2Waits the number of seconds in the path, up to 10, then answers; fractions work too, as in /delay/0.5. Good for testing timeouts and loading states.
curl 'https://playground.tetiva.app/delay/2'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 237 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"method": "GET",
"url": "https://playground.tetiva.app/delay/2",
"args": {},
"delay": 2,
"started_at": "2026-10-17T09:30:00Z",
"finished_at": "2026-10-17T09:30:02Z",
"request_id": "00000000-0000-4000-8000-000000000001"
}
REST · Auth
Check that your client sends credentials, and that it copes with the 401 when it doesn't.
Bearer auth#
https:// playground. tetiva. app /bearerAny non-empty token passes and comes back in token. Without Authorization: Bearer … you get 401 with WWW-Authenticate: Bearer.
Authorization
Bearer
| prefix | Bearer |
| token | test-token |
curl 'https://playground.tetiva.app/bearer' \
-H 'Authorization: Bearer test-token'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 109 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"authenticated": true,
"token": "test-token",
"request_id": "00000000-0000-4000-8000-000000000001"
}
Basic auth#
https:// playground. tetiva. app /basic-auth /tetiva /playgroundPasses when the Basic credentials match the user and password in the path, here tetiva / playground. Anything else gets 401.
Authorization
Basic
| password | playground |
| username | tetiva |
curl 'https://playground.tetiva.app/basic-auth/tetiva/playground' \
-H 'Authorization: Basic dGV0aXZhOnBsYXlncm91bmQ='Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 104 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"authenticated": true,
"user": "tetiva",
"request_id": "00000000-0000-4000-8000-000000000001"
}
REST · Fake data
Five fake users for lists, tables and pagination. The data is the same every time, and POST /users answers as if it worked but stores nothing.
Fake users#
https:// playground. tetiva. app /users?limit=3A page of fake users. limit takes 1 to 5, and next links to the following page until it is null.
Query parameters · 1
| limit | 3 |
curl 'https://playground.tetiva.app/users?limit=3'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 576 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"data": [
{
"id": 1,
"name": "Mila Novak",
"email": "mila.novak@example.com",
"role": "admin",
"created_at": "2026-03-02T09:14:00Z"
},
{
"id": 2,
"name": "Jonas Berg",
"email": "jonas.berg@example.com",
"role": "editor",
"created_at": "2026-04-18T16:40:00Z"
},
{
"id": 3,
"name": "Priya Raman",
"email": "priya.raman@example.com",
"role": "viewer",
"created_at": "2026-05-27T11:05:00Z"
}
],
"total": 5,
"limit": 3,
"next": "/users?limit=3&after=3"
}
One fake user#
https:// playground. tetiva. app /users /2One user by id, from 1 to 5. Any other id gets 404 with a hint.
curl 'https://playground.tetiva.app/users/2'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 135 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"id": 2,
"name": "Jonas Berg",
"email": "jonas.berg@example.com",
"role": "editor",
"created_at": "2026-04-18T16:40:00Z"
}
Create a user#
https:// playground. tetiva. app /usersChecks name and email and answers 201 with the new user and a Location header. Nothing is stored: stored is false.
Headers · 1
| Content-Type | application/json |
Body · JSON
{
"name": "Grace Hopper",
"email": "grace@example.com",
"role": "editor"
}curl -X POST 'https://playground.tetiva.app/users' \
-H 'Content-Type: application/json' \
--data-raw '{
"name": "Grace Hopper",
"email": "grace@example.com",
"role": "editor"
}'Response examples
201Created
Headers (7)
| content-type | application/json; charset=utf-8 |
| content-length | 151 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
| location | https://playground.tetiva.app/users/6 |
{
"id": 6,
"name": "Grace Hopper",
"email": "grace@example.com",
"role": "editor",
"created_at": "2026-10-17T09:30:00Z",
"stored": false
}
GraphQL
A small library at https://playground.tetiva.app/graphql: books, authors and one mutation. Introspection is on, so any GraphQL client can load the schema.
Run a GraphQL query#
https:// playground. tetiva. app /graphqlOperation: Books
Lists books with their authors. first takes 1 to 50; a field that doesn't exist gets an error that lists the real ones.
Query
query Books($first: Int) {
books(first: $first) {
id
title
author {
name
}
}
}Variables
{
"first": 2
}curl -X POST 'https://playground.tetiva.app/graphql' \
-H 'Content-Type: application/json' \
--data-raw '{"query":"query Books($first: Int) {\n books(first: $first) {\n id\n title\n author {\n name\n }\n }\n}","variables":{"first":2},"operationName":"Books"}'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 304 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"data": {
"books": [
{
"id": "1",
"title": "Frankenstein",
"author": {
"name": "Mary Shelley"
}
},
{
"id": "2",
"title": "Moby-Dick",
"author": {
"name": "Herman Melville"
}
}
]
}
}
One book by id#
https:// playground. tetiva. app /graphqlOperation: Book
Fetches one book. An unknown id returns "book": null, not an error.
Query
query Book($id: ID!) {
book(id: $id) {
title
year
author {
name
}
}
}Variables
{
"id": "3"
}curl -X POST 'https://playground.tetiva.app/graphql' \
-H 'Content-Type: application/json' \
--data-raw '{"query":"query Book($id: ID!) {\n book(id: $id) {\n title\n year\n author {\n name\n }\n }\n}","variables":{"id":"3"},"operationName":"Book"}'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 151 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"data": {
"book": {
"title": "The Time Machine",
"year": 1895,
"author": {
"name": "H. G. Wells"
}
}
}
}
Add a book#
https:// playground. tetiva. app /graphqlOperation: AddBook
A mutation that returns the new book with id 6. Nothing is stored: extensions.stored is false.
Query
mutation AddBook($title: String!, $authorId: ID!) {
addBook(title: $title, authorId: $authorId) {
id
title
author {
name
}
}
}Variables
{
"title": "Carmilla",
"authorId": "a5"
}curl -X POST 'https://playground.tetiva.app/graphql' \
-H 'Content-Type: application/json' \
--data-raw '{"query":"mutation AddBook($title: String!, $authorId: ID!) {\n addBook(title: $title, authorId: $authorId) {\n id\n title\n author {\n name\n }\n }\n}","variables":{"title":"Carmilla","authorId":"a5"},"operationName":"AddBook"}'Response examples
200OK
Headers (6)
| content-type | application/json; charset=utf-8 |
| content-length | 191 |
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"data": {
"addBook": {
"id": "6",
"title": "Carmilla",
"author": {
"name": "Sheridan Le Fanu"
}
}
},
"extensions": {
"stored": false
}
}
gRPC
echo.v1.EchoService on playground.tetiva.app:443 with TLS and server reflection, so clients find the methods without a .proto file.
Reflection
grpcurl playground.tetiva.app:443 list
grpcurl playground.tetiva.app:443 describe echo.v1.EchoService
Echo (unary)#
grpcs://playground.tetiva.appecho.v1.EchoService / Echo
Returns your text with seq, server_time and the request metadata as the server saw it.
Message
{
"text": "ping"
}grpcurl -d '{
"text": "ping"
}' \
'playground.tetiva.app:443' \
'echo.v1.EchoService/Echo'Response examples
0OK
Headers (4)
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
{
"text": "ping",
"seq": 1,
"serverTime": "2026-10-17T09:30:00Z",
"metadata": {
"host": "playground.tetiva.app",
"user-agent": "grpcurl/1.9.3 grpc-go/1.73.0"
}
}
Stream from gRPC#
grpcs://playground.tetiva.appecho.v1.EchoService / ServerStream
Sends count messages (1 to 100, default 5), one every interval_ms (50 to 5000, default 500). The test API page runs it over gRPC-Web.
Message
{
"text": "ping",
"count": 3,
"interval_ms": 500
}grpcurl -d '{
"text": "ping",
"count": 3,
"interval_ms": 500
}' \
'playground.tetiva.app:443' \
'echo.v1.EchoService/ServerStream'Response examples
0OK
Headers (4)
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
+0.000s CALL echo.v1.EchoService/ServerStream {"text":"ping","count":3,"interval_ms":500}
+0.000s RECV #1 {"text":"ping","seq":1,"serverTime":"2026-10-17T09:30:00Z"}
+0.500s RECV #2 {"text":"ping","seq":2,"serverTime":"2026-10-17T09:30:00.500Z"}
+1.000s RECV #3 {"text":"ping","seq":3,"serverTime":"2026-10-17T09:30:01Z"}
+1.000s END OK
Chat (bidirectional)#
grpcs://playground.tetiva.appecho.v1.EchoService / Chat
Every message you send comes straight back with the next seq, up to 100 a stream. Run it with grpcurl: the page's gRPC-Web transport can't stream both ways.
Message
{
"text": "hello"
}grpcurl -d '{
"text": "hello"
}' \
'playground.tetiva.app:443' \
'echo.v1.EchoService/Chat'Response examples
0OK
Headers (4)
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
+0.000s CALL echo.v1.EchoService/Chat {"text":"hello"}
+0.000s RECV #1 {"text":"hello","seq":1,"serverTime":"2026-10-17T09:30:00Z"}
+0.000s END OK
WebSocket
Text and binary frames up to 64 KB. /ws closes after 60 seconds without a message, with 1000 Normal Closure; every session ends after 10 minutes.
Echo over WebSocket#
wss:// playground. tetiva. app /wsEvery frame you send comes straight back, text or binary, up to 64 KB a frame.
Messages · 1
helloText
helloprintf '%s' 'hello' | websocat 'wss://playground.tetiva.app/ws'Response examples
101Switching Protocols
Headers (4)
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
+0.000s → SENT hello
+0.000s ← RECV hello
+0.000s · INFO closed 1000 Normal Closure
Ticker#
wss:// playground. tetiva. app /ws /tickerPushes {"tick": n, "at": …} every second until you disconnect. Nothing to send.
websocat 'wss://playground.tetiva.app/ws/ticker'Response examples
101Switching Protocols
Headers (4)
| x-request-id | 00000000-0000-4000-8000-000000000001 |
| access-control-allow-origin | * |
| ratelimit-limit | 60 |
| ratelimit-remaining | 59 |
+1.000s ← RECV {"tick":1,"at":"2026-10-17T09:30:01Z"}
+2.000s ← RECV {"tick":2,"at":"2026-10-17T09:30:02Z"}
+3.000s ← RECV {"tick":3,"at":"2026-10-17T09:30:03Z"}
+3.000s · INFO closed 1000 Normal Closure
Missing an endpoint or protocol? Ask us to add it
Take the requests with you
The whole Playground as a ready-made collection, with example responses. Import it into Postman or Insomnia, or open it in Tetiva, a free open-source desktop API client.
- Download Postman collectionPostman v2.1 · REST and GraphQL, 17 requests — works in Postman and Insomnia
- Download Tetiva JSONAll four protocols, 20 requests — opens in Tetiva 1.2+
- Straight into the app, with history, environments and tests. Tetiva 1.2+, gRPC needs 1.3+
- Prefer your terminal?
curl https://playground. tetiva. app /get