sondahub / Fake REST API
A free fake REST API that behaves like a real one
78,076 records in 37 collections across seven APIs — e-commerce, IoT, banking, social, helpdesk, flights and a company directory — with ids that never change and the rules a real backend enforces. Point a frontend, a test suite, a tutorial or an API client at it.
POST, PUT, PATCH and DELETE are validated and answered as a real server would, and they stick: the answer hands you a session token, and every request that carries it sees your writes. No database anywhere — the state travels with you.
Quick start
Base URL https://api.sondahub.com/v1. JSON in, JSON out, HTTPS, CORS open to every origin. No key, no header to add.
curl "https://api.sondahub.com/v1/store/products?price_lt=20&sort=price&limit=3"
curl "https://api.sondahub.com/v1/store/orders/1?expand=customer,items"
curl -X POST https://api.sondahub.com/v1/store/orders \
-H "Content-Type: application/json" \
-d '{"customer_id": 1, "items": [{"product_id": 1, "quantity": 2}]}'
curl -X POST https://api.sondahub.com/v1/store/products -H "Content-Type: application/json" -d '{"price": "free"}'
Every collection
37 collections, 78,076 records, related to each other through ids that never change. Each has a page with its fields, sample records and ready-made queries.
| Collection | API | Records | Path |
|---|---|---|---|
| categories | Store | 8 | /v1/store/categories |
| products | Store | 2,000 | /v1/store/products |
| customers | Store | 800 | /v1/store/customers |
| orders | Store | 3,000 | /v1/store/orders |
| order items | Store | 6,219 | /v1/store/order_items |
| reviews | Store | 2,500 | /v1/store/reviews |
| warehouses | Store | 3 | /v1/store/warehouses |
| inventory | Store | 6,000 | /v1/store/inventory |
| carts | Store | 200 | /v1/store/carts |
| sites | Fleet | 40 | /v1/fleet/sites |
| devices | Fleet | 500 | /v1/fleet/devices |
| readings | Fleet | 4,908 | /v1/fleet/readings |
| alerts | Fleet | 500 | /v1/fleet/alerts |
| commands | Fleet | 300 | /v1/fleet/commands |
| customers | Bank | 400 | /v1/bank/customers |
| accounts | Bank | 640 | /v1/bank/accounts |
| cards | Bank | 505 | /v1/bank/cards |
| transactions | Bank | 7,674 | /v1/bank/transactions |
| transfers | Bank | 800 | /v1/bank/transfers |
| fx rates | Bank | 28 | /v1/bank/fx_rates |
| users | Social | 800 | /v1/social/users |
| posts | Social | 4,000 | /v1/social/posts |
| comments | Social | 8,000 | /v1/social/comments |
| likes | Social | 8,000 | /v1/social/likes |
| follows | Social | 5,000 | /v1/social/follows |
| teams | Helpdesk | 4 | /v1/helpdesk/teams |
| agents | Helpdesk | 25 | /v1/helpdesk/agents |
| customers | Helpdesk | 300 | /v1/helpdesk/customers |
| tickets | Helpdesk | 2,000 | /v1/helpdesk/tickets |
| messages | Helpdesk | 5,069 | /v1/helpdesk/messages |
| airports | Flights | 50 | /v1/flights/airports |
| airlines | Flights | 6 | /v1/flights/airlines |
| flights | Flights | 2,500 | /v1/flights/flights |
| bookings | Flights | 3,500 | /v1/flights/bookings |
| users | Identity | 300 | /v1/identity/users |
| groups | Identity | 40 | /v1/identity/groups |
| memberships | Identity | 1,457 | /v1/identity/memberships |
What a list can do
| Option | Example |
|---|---|
| Paging | ?page=3&limit=50 · ?offset=100 |
| Sorting, several keys | ?sort=-total,id |
| Equals | ?status=shipped · ?in_stock=true |
| Comparisons | ?price_gte=10&price_lt=50 · ?placed_at_gt=2026-08-01 |
| Not equal, contains, any of, missing | ?status_ne=cancelled · ?name_like=lamp · ?id_in=1,2,3 · ?notes_null=true |
| Inside a JSON field | ?address.country=AR |
| Full-text search | ?q=alpine |
| Sparse fields | ?fields=id,name,price |
| Embed relations | ?expand=customer,items |
| Nested routes | /v1/store/customers/1/orders |
Single records carry an ETag; send it back in If-None-Match and get a 304.
Rules beyond CRUD
A fake API that accepts anything teaches a client nothing about the errors it will meet. These collections behave:
| Collection | What a write does |
|---|---|
bank/transfers | POST checks both accounts exist, are active, hold the same currency and that the amount is available, then debits, credits and writes a transaction on each side; the answer carries debit_transaction_id and credit_transaction_id. Anything wrong answers 422 with the reason. |
store/orders | POST may carry items: [{ product_id, quantity }]: the hub prices them from the products, computes subtotal, shipping, tax and total, creates the order items and returns them inline. PATCH to shipped stamps shipped_at and a tracking number; delivered stamps delivered_at. |
helpdesk/tickets | PATCH checks the status move (open → pending | resolved; pending → open | resolved; resolved → closed | open; closed → open) and answers 422 invalid_transition otherwise; resolving and closing stamp their timestamps, assigning stamps first_response_at. Counters on the customer and agent follow. |
flights/bookings | POST needs passenger.first_name and last_name, refuses cancelled or departed flights and sold-out ones (409), picks a free seat in the cabin when you give none, prices the fare from the flight, and mints a six-character locator. Cancelling gives the seat back. |
social/likes | POST refuses a second like of the same post by the same user (409 already_liked) and bumps the post’s likes_count; DELETE lowers it. |
identity/users | POST and PATCH keep user_name and email unique (409 already_exists, naming the user who has it); display_name defaults to the two names and active to true. The same people sign in through SAML and OIDC and are provisioned over SCIM. |
OpenAPI 3 for every API
Import one into Sonda, or into any client or code generator that reads OpenAPI, and every operation arrives with its schema and an example body.
| Store | https://api.sondahub.com/v1/store/openapi.json |
| Fleet | https://api.sondahub.com/v1/fleet/openapi.json |
| Bank | https://api.sondahub.com/v1/bank/openapi.json |
| Social | https://api.sondahub.com/v1/social/openapi.json |
| Helpdesk | https://api.sondahub.com/v1/helpdesk/openapi.json |
| Flights | https://api.sondahub.com/v1/flights/openapi.json |
| Identity | https://api.sondahub.com/v1/identity/openapi.json |
Questions
Does the data change after a POST?
For you, yes. A write is validated and answered exactly as a real server would answer it, and the answer carries an X-Sondahub-Session token with the change. Send that token back and the next GET finds your record, lists include it and totals count it; leave it off and you get the seed data, marked X-Sondahub-Write: simulated. The server stores nothing, so nobody else sees your writes. How sessions work.
What status codes and errors can I expect?
200 and 201 (with a Location header) for success, 304 for a matching ETag, 400 for a query naming a field that does not exist, 404 for a missing id, 405 with an Allow header for a verb a route does not take, 409 for conflicts such as a duplicate like or a sold-out flight, and 422 for a body that fails validation — with one line per problem. Every error is {"error": {"code", "message", "details"}}. Any other status is one call away at /v1/utils/status/{code}.
How does pagination work?
Lists answer {"data": [...], "meta": {"page", "limit", "total", "pages"}}, with X-Total-Count and an RFC 8288 Link header carrying next, prev, first and last. page and limit (up to 200) or offset.
Can I use it in automated tests and CI?
Yes — the seed is fixed, so record 1 is the same record on every run and assertions stay valid. It is a free public service on a shared daily budget, so for heavy suites, run your own copy: the source is MIT, and npm run build && npm run dev serves the same hub on localhost.