sondahub / Writes that persist
A mock API where your writes are still there
POST an order and GET it back. PATCH it, list it, find it in the customer’s orders, query it over GraphQL — on a public mock API with no database, no account and no reset button, because the state travels with you.
Works on all seven APIs, over REST, GraphQL, gRPC-Web, SCIM and MCP. Free, no key.
How it works
sondahub has no database — only files and code. A write is validated, run through the same rules a real backend enforces (an order is priced from its products, a transfer checks the funds, a ticket refuses an impossible status) and answered with the status, headers and body a real server would send. The answer also carries a header:
X-Sondahub-Session: s1.rZNdb9MwFIb_SnRucSI7Tb98hcYQ… X-Sondahub-Session-Writes: 3
That token holds everything you have changed so far. Send the newest one back — as that header, or as ?_session= where a header is awkward — and the request starts from your world instead of the seed: the order you placed comes back on GET, shows up in lists and nested routes, counts in totals, and can be changed and deleted. Writes made on top of your session say X-Sondahub-Write: session; a write without one says simulated.
Try it here
curl -i -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 https://api.sondahub.com/v1/store/orders/3001?expand=items \ -H "X-Sondahub-Session: $TOKEN"
curl -X PATCH https://api.sondahub.com/v1/store/orders/3001 \
-H "X-Sondahub-Session: $TOKEN" \
-H "Content-Type: application/json" -d '{"status": "shipped"}'
curl "https://api.sondahub.com/v1/store/customers/1/orders?sort=-id&limit=3&fields=id,number,status,total" \ -H "X-Sondahub-Session: $TOKEN"
curl https://api.sondahub.com/v1/store/graphql -H "Content-Type: application/json" \
-H "X-Sondahub-Session: $TOKEN" \
-d '{"query": "{ order(id: 3001) { number status tracking_number total } }"}'
curl -X DELETE https://api.sondahub.com/v1/store/orders/3001 -H "X-Sondahub-Session: $TOKEN"
Run step 2 before step 1, or after “Drop the session”, and it answers 404 — there is no order 3001 in the seed.
In your own code
Keep the newest token and send it on every request — a few lines in any client:
let session = null
async function api(path, init = {}) {
const res = await fetch('https://api.sondahub.com' + path, {
...init,
headers: {
'Content-Type': 'application/json',
...(session ? { 'X-Sondahub-Session': session } : {}),
...init.headers,
},
})
session = res.headers.get('X-Sondahub-Session') ?? session
return res.json()
}
const created = await api('/v1/store/orders', { method: 'POST', body: JSON.stringify({"customer_id":1,"items":[{"product_id":1,"quantity":2}]}) })
const again = await api('/v1/store/orders/' + created.id) // there
await api('/v1/store/orders/' + created.id, { method: 'PATCH', body: '{"status":"shipped"}' })
From a shell:
# keep the newest token in a variable
H=$(mktemp)
curl -s -D "$H" -o /dev/null -X POST https://api.sondahub.com/v1/store/orders \
-H "Content-Type: application/json" \
-d '{"customer_id":1,"items":[{"product_id":1,"quantity":2}]}'
TOKEN=$(grep -i '^x-sondahub-session:' "$H" | cut -d' ' -f2 | tr -d '\r')
curl -H "X-Sondahub-Session: $TOKEN" https://api.sondahub.com/v1/store/orders/3001
Each test that wants a clean world starts without a token; each that wants a prepared one can start from a token saved by a setup step. A saved token is a fixture, good until the data itself is rebuilt.
The rules still apply
A session is not a scratchpad that takes anything: every write still goes through validation and the collection's rules, against your world. A second like of the same post answers 409; a bank transfer debits one account and credits the other, and both balances show it on the next GET; a booking takes a seat that the next booking cannot have. The rules, collection by collection.
The session also remembers Idempotency-Keys (the last 25), so a retried POST is answered with the first answer instead of creating a second record.
Questions
Where is my data stored?
Nowhere on our side. Everything you change — the rows you created or changed, the ids you deleted, the next id per collection, your idempotency keys — is packed into the X-Sondahub-Session token the answer carries: compressed, signed, and handed to you. The server reads it on the way in and forgets it on the way out.
Can other people see or break my test data?
No. Your writes exist only in your token, so a hundred test runs in parallel each see their own world and the seed underneath never changes. That is also why there is nothing to reset: start without a token and you are back at the seed.
How much can a session hold?
Until the token reaches 24,000 characters — dozens of new records, more if they are small. Past that, a write is still answered but not kept, and X-Sondahub-Session-Note says so; delete something or start again.
Is the token secret? Does it expire?
It is signed, not encrypted: anyone holding it can read what is in it (fake data, after all), but a changed or cut-short token is refused with a clear note instead of producing strange answers. It does not expire; a token made against an older build of the data is set aside with a note, and the request runs on the seed.
Does it work beyond REST?
Yes — the same token works on GraphQL, gRPC-Web and Connect, SCIM and the MCP server, and the record you create over one is there over the others. Webhooks fire for session writes too.