sondahub

APIs / Store

Store

An online shop: products, customers, orders, reviews and stock.

Two thousand products in eight categories, eight hundred customers, three thousand orders with their line items, reviews, three warehouses of stock and open carts. Orders move through a lifecycle (pending → paid → shipped → delivered) and the live stream shows orders moving. 20,730 records in all.

Connect

Base URL
https://api.sondahub.com/v1/store
OpenAPI 3
https://api.sondahub.com/v1/store/openapi.json
GraphQL
https://api.sondahub.com/v1/store/graphql
WebSocket
wss://api.sondahub.com/v1/store/ws
SSE
https://api.sondahub.com/v1/store/events
The data
categories.json, products.json, customers.json, orders.json, order_items.json, reviews.json, warehouses.json, inventory.json, carts.json

In Sonda: Import → From a URL with the OpenAPI address and the whole API lands as a project, one request per operation with example bodies. No keys, no headers to add. More on each protocol.

Writes here are simulated: POST, PUT, PATCH and DELETE are validated, run through the real logic and answered as a real server would — then forgotten. The answer carries _note and X-Sondahub-Write: simulated. A GET afterwards will not find what you wrote.
The API describes itself
curl https://api.sondahub.com/v1/store

Lists and filters

Every list answers { "data": [...], "meta": { "page", "limit", "total", "pages" } } with X-Total-Count and Link headers (next, prev, first, last). These options work on every collection and every nested route:

OptionMeaningExample
page, limitPaging, 1-based; limit 1–200, default 20. offset works too.?page=3&limit=50
sortComma list of fields, - for descending. Default id here.?sort=-price,id
field=valueEquals. Booleans as true/false, null for missing.?status=active
_ne _gt _gte _lt _lteNot equal and comparisons, on numbers, dates and strings.?price_gte=10&price_lt=100
_likeContains, case-insensitive.?sku_like=an
_inAny of a comma list.?id_in=1,2,3
_nulltrue: missing; false: present.?slug_null=true
a.b=valueInside a JSON field, dotted.?address.line1=…
qSearch across the text fields.?q=alpine
fieldsOnly these fields back.?fields=id,sku
expandEmbed related records.?expand=category,reviews

A name that is not a field answers 400 and lists the fields. Writes answer 422 with one line per problem, 404 for a missing id, 405 with an Allow header for a verb a route does not take.

categories

The eight product categories. 8 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
namerequiredstring
slugrequiredstring
descriptiontext
product_countread-onlyintSeed products in the category.

Relations: products → the products whose category_id is this category. Use ?expand=products to embed them, or the routes below.

Endpoints

GET/v1/store/categoriesA page, with every filter, sort, search, field and expand option below.
POST/v1/store/categoriesCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/store/categories/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/store/categories/{id}Change the fields you send.
PUT/v1/store/categories/{id}Replace the record; required fields must all be there.
DELETE/v1/store/categories/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/store/categories/{id}/productsIts products, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/store/categories?limit=3"
One record
curl https://api.sondahub.com/v1/store/categories/1
Its products
curl "https://api.sondahub.com/v1/store/categories/1/products?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/store/categories \
  -H "Content-Type: application/json" \
  -d '{"name":"Audio","slug":"audio"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/store/categories/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Changed name"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/store/categories/1

products

What the store sells. Prices are in USD. 2,000 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
skurequiredstringUnique stock keeping unit.
namerequiredstring
slugstring
descriptiontext
category_idrequiredint → categories
brandstring
pricerequiredfloatmin 0
compare_at_pricefloatThe struck-through price on sale, or null.
currencystring
colorstring
weight_gintmin 0
tagsjson string[]
in_stockbool
ratingread-onlyfloatAverage of the seed reviews, 1–5.
review_countread-onlyint
statusenumactive draft archived

Relations: category → one category through category_id; reviews → the reviews whose product_id is this product; inventory → the inventory whose product_id is this product. Use ?expand=category,reviews,inventory to embed them, or the routes below.

Endpoints

GET/v1/store/productsA page, with every filter, sort, search, field and expand option below.
POST/v1/store/productsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/store/products/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/store/products/{id}Change the fields you send.
PUT/v1/store/products/{id}Replace the record; required fields must all be there.
DELETE/v1/store/products/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/store/products/{id}/categoryThe category this record points at.
GET/v1/store/products/{id}/reviewsIts reviews, as a page with all the list options.
GET/v1/store/products/{id}/inventoryIts inventory, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/store/products?status=draft&price_gte=10&expand=category&limit=3"
One record
curl https://api.sondahub.com/v1/store/products/1?expand=category
Its reviews
curl "https://api.sondahub.com/v1/store/products/1/reviews?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/store/products \
  -H "Content-Type: application/json" \
  -d '{"sku":"AUD-4F7K2M","name":"Sonora Wireless Headphones","slug":"sonora-wireless-headphones","category_id":1,"brand":"Sonora","price":149.99,"currency":"USD","color":"Graphite","tags":["bluetooth","travel"],"status":"active"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/store/products/1 \
  -H "Content-Type: application/json" \
  -d '{"status":"draft"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/store/products/1

customers

People who buy. Addresses are nested objects; filter on them with a dotted name (?address.country=AR). 800 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
namerequiredstring
emailrequiredstring
phonestring
companystring
addressjson { line1, line2?, city, region, postal_code, country }
tierenumstandard silver gold platinum
marketing_opt_inbool
orders_countread-onlyint
total_spentread-onlyfloat
notestext

Relations: orders → the orders whose customer_id is this customer; reviews → the reviews whose customer_id is this customer. Use ?expand=orders,reviews to embed them, or the routes below.

Endpoints

GET/v1/store/customersA page, with every filter, sort, search, field and expand option below.
POST/v1/store/customersCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/store/customers/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/store/customers/{id}Change the fields you send.
PUT/v1/store/customers/{id}Replace the record; required fields must all be there.
DELETE/v1/store/customers/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/store/customers/{id}/ordersIts orders, as a page with all the list options.
GET/v1/store/customers/{id}/reviewsIts reviews, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/store/customers?tier=silver&limit=3"
One record
curl https://api.sondahub.com/v1/store/customers/1
Its orders
curl "https://api.sondahub.com/v1/store/customers/1/orders?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/store/customers \
  -H "Content-Type: application/json" \
  -d '{"name":"Camila Fernandez","email":"[email protected]","tier":"standard"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/store/customers/1 \
  -H "Content-Type: application/json" \
  -d '{"tier":"silver"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/store/customers/1

orders

An order and its money. Line items live in order_items (also at /orders/{id}/items and ?expand=items). 3,000 records — the file.

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.
FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
numberread-onlystringHuman order number.
customer_idrequiredint → customers
statusenumpending paid shipped delivered cancelled refunded
subtotalfloatmin 0
shippingfloatmin 0
taxfloatmin 0
discountfloatmin 0
totalfloatmin 0
currencystring
payment_methodenumcard paypal bank_transfer cash_on_delivery
shipping_addressjson { line1, line2?, city, region, postal_code, country }
shipping_methodenumstandard express overnight pickup
tracking_numberstring
placed_atdatetimeDefaults to now on POST.
paid_atdatetime
shipped_atdatetime
delivered_atdatetime
notestext

Relations: customer → one customer through customer_id; items → the order items whose order_id is this order. Use ?expand=customer,items to embed them, or the routes below.

Endpoints

GET/v1/store/ordersA page, with every filter, sort, search, field and expand option below.
POST/v1/store/ordersCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/store/orders/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/store/orders/{id}Change the fields you send.
PUT/v1/store/orders/{id}Replace the record; required fields must all be there.
DELETE/v1/store/orders/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/store/orders/{id}/customerThe customer this record points at.
GET/v1/store/orders/{id}/itemsIts order items, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/store/orders?status=paid&subtotal_gte=10&expand=customer&limit=3"
One record
curl https://api.sondahub.com/v1/store/orders/1?expand=customer
Its items
curl "https://api.sondahub.com/v1/store/orders/1/items?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/store/orders \
  -H "Content-Type: application/json" \
  -d '{"customer_id":1,"status":"paid","currency":"USD","payment_method":"card","shipping_method":"standard","items":[{"product_id":1,"quantity":2}]}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/store/orders/1 \
  -H "Content-Type: application/json" \
  -d '{"status":"paid"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/store/orders/1

order items

One product line on an order. 6,219 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
order_idrequiredint → orders
product_idrequiredint → products
skustring
namestringThe product name at the time of the order.
quantityrequiredintmin 1, max 999
unit_pricefloatmin 0
line_totalfloatmin 0

Relations: order → one order through order_id; product → one product through product_id. Use ?expand=order,product to embed them, or the routes below.

Endpoints

GET/v1/store/order_itemsA page, with every filter, sort, search, field and expand option below.
POST/v1/store/order_itemsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/store/order_items/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/store/order_items/{id}Change the fields you send.
PUT/v1/store/order_items/{id}Replace the record; required fields must all be there.
DELETE/v1/store/order_items/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/store/order_items/{id}/orderThe order this record points at.
GET/v1/store/order_items/{id}/productThe product this record points at.

Try it

List with a filter
curl "https://api.sondahub.com/v1/store/order_items?quantity_gte=1&expand=order&limit=3"
One record
curl https://api.sondahub.com/v1/store/order_items/1?expand=order
Create (simulated)
curl -X POST https://api.sondahub.com/v1/store/order_items \
  -H "Content-Type: application/json" \
  -d '{"order_id":1,"product_id":1,"quantity":1}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/store/order_items/1 \
  -H "Content-Type: application/json" \
  -d '{"quantity":2}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/store/order_items/1

reviews

Customer reviews, 1–5 stars. 2,500 records — the file.

POST recomputes the product’s rating and review_count — in the answer’s world, which ends with the answer.
FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
product_idrequiredint → products
customer_idrequiredint → customers
ratingrequiredintmin 1, max 5
titlestring
bodytext
verified_purchasebool
helpful_votesintmin 0

Relations: product → one product through product_id; customer → one customer through customer_id. Use ?expand=product,customer to embed them, or the routes below.

Endpoints

GET/v1/store/reviewsA page, with every filter, sort, search, field and expand option below.
POST/v1/store/reviewsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/store/reviews/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/store/reviews/{id}Change the fields you send.
PUT/v1/store/reviews/{id}Replace the record; required fields must all be there.
DELETE/v1/store/reviews/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/store/reviews/{id}/productThe product this record points at.
GET/v1/store/reviews/{id}/customerThe customer this record points at.

Try it

List with a filter
curl "https://api.sondahub.com/v1/store/reviews?rating_gte=1&expand=product&limit=3"
One record
curl https://api.sondahub.com/v1/store/reviews/1?expand=product
Create (simulated)
curl -X POST https://api.sondahub.com/v1/store/reviews \
  -H "Content-Type: application/json" \
  -d '{"product_id":1,"customer_id":1,"rating":1}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/store/reviews/1 \
  -H "Content-Type: application/json" \
  -d '{"rating":2}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/store/reviews/1

warehouses

Where stock sits. 3 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
coderequiredstring
namerequiredstring
addressjson { line1, city, region, postal_code, country }
timezonestring

Relations: inventory → the inventory whose warehouse_id is this warehouse. Use ?expand=inventory to embed them, or the routes below.

Endpoints

GET/v1/store/warehousesA page, with every filter, sort, search, field and expand option below.
POST/v1/store/warehousesCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/store/warehouses/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/store/warehouses/{id}Change the fields you send.
PUT/v1/store/warehouses/{id}Replace the record; required fields must all be there.
DELETE/v1/store/warehouses/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/store/warehouses/{id}/inventoryIts inventory, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/store/warehouses?limit=3"
One record
curl https://api.sondahub.com/v1/store/warehouses/1
Its inventory
curl "https://api.sondahub.com/v1/store/warehouses/1/inventory?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/store/warehouses \
  -H "Content-Type: application/json" \
  -d '{"code":"MIA","name":"A name"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/store/warehouses/1 \
  -H "Content-Type: application/json" \
  -d '{"code":"Changed code"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/store/warehouses/1

inventory

Stock of a product at a warehouse. 6,000 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
product_idrequiredint → products
warehouse_idrequiredint → warehouses
on_handrequiredintmin 0
reservedintmin 0
reorder_pointintmin 0
binstring
counted_atdatetime

Relations: product → one product through product_id; warehouse → one warehouse through warehouse_id. Use ?expand=product,warehouse to embed them, or the routes below.

Endpoints

GET/v1/store/inventoryA page, with every filter, sort, search, field and expand option below.
POST/v1/store/inventoryCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/store/inventory/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/store/inventory/{id}Change the fields you send.
PUT/v1/store/inventory/{id}Replace the record; required fields must all be there.
DELETE/v1/store/inventory/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/store/inventory/{id}/productThe product this record points at.
GET/v1/store/inventory/{id}/warehouseThe warehouse this record points at.

Try it

List with a filter
curl "https://api.sondahub.com/v1/store/inventory?on_hand_gte=1&expand=product&limit=3"
One record
curl https://api.sondahub.com/v1/store/inventory/1?expand=product
Create (simulated)
curl -X POST https://api.sondahub.com/v1/store/inventory \
  -H "Content-Type: application/json" \
  -d '{"product_id":1,"warehouse_id":1,"on_hand":1,"bin":"A-14-3"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/store/inventory/1 \
  -H "Content-Type: application/json" \
  -d '{"on_hand":2}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/store/inventory/1

carts

Open shopping carts; items are a nested array, so a cart is one document. 200 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
customer_idint → customers
session_idstringFor anonymous carts.
statusenumopen abandoned converted
itemsjson { product_id, sku, name, quantity, unit_price }[]
item_countintmin 0
subtotalfloatmin 0
couponstring
last_activity_atdatetime

Relations: customer → one customer through customer_id. Use ?expand=customer to embed them, or the routes below.

Endpoints

GET/v1/store/cartsA page, with every filter, sort, search, field and expand option below.
POST/v1/store/cartsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/store/carts/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/store/carts/{id}Change the fields you send.
PUT/v1/store/carts/{id}Replace the record; required fields must all be there.
DELETE/v1/store/carts/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/store/carts/{id}/customerThe customer this record points at.

Try it

List with a filter
curl "https://api.sondahub.com/v1/store/carts?status=abandoned&item_count_gte=1&expand=customer&limit=3"
One record
curl https://api.sondahub.com/v1/store/carts/1?expand=customer
Create (simulated)
curl -X POST https://api.sondahub.com/v1/store/carts \
  -H "Content-Type: application/json" \
  -d '{"status":"open"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/store/carts/1 \
  -H "Content-Type: application/json" \
  -d '{"status":"abandoned"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/store/carts/1

WebSocket and SSE

The same stream two ways: the world's own activity, one tick a second, generated for your connection alone. Both push JSON text messages; SSE names each one with event: and numbers it with id:. ?topics=a,b narrows either.

TopicWhat arrivesHow often
ordersAn order changing status (paid → shipped → delivered).4 s
inventoryA stock level moving at a warehouse.6 s
WebSocket
wss://api.sondahub.com/v1/store/ws?topics=orders

> {"type":"hello","api":"store","topics":[…],"subscribed":[…]}
> {"type":"event","topic":"orders","api":"store","ts":"…","data":{…}}
< {"type":"subscribe","topics":["orders"]}   # narrow to some topics
< {"type":"ping"}                            # → {"type":"pong"}
< anything else                             # → echoed back as {"type":"echo"}
Server-Sent Events
curl -N "https://api.sondahub.com/v1/store/events?topics=orders"

retry: 3000
id: 1
event: orders
data: {"type":"event","topic":"orders",…}

GraphQL

One endpoint, https://api.sondahub.com/v1/store/graphql: POST {"query", "variables"} or GET ?query=. Introspection is on, so Sonda's GraphQL mode loads the schema; the SDL is a click away. Every collection is a paged query with the same filter, sort and q options as REST (operators as suffixes: price_lt), a by-id query, relation fields both ways, and create, update, replace and delete mutations — simulated like every write, with the note in extensions.

A query
curl https://api.sondahub.com/v1/store/graphql -H "Content-Type: application/json" -d '{"query": "{ products(limit: 3, sort: \"-id\", filter: { status: active }) { total data { id sku name slug category { name } reviews(limit: 2) { id } } } }"}'
{
  products(limit: 3, sort: "-id", filter: { status: active }) {
    total
    data {
      id sku name slug
      category { name }
      reviews(limit: 2) { id }
    }
  }
}