One API.
Token-gated.
Idempotent.
Every core object in Space Place has a stable HTTP API. Push Events from your POS, post signed Inventory Movements from a WMS, Receive Purchase Orders from a dock scanner. Authentication is a DataStream ID plus a Token — gated by scoped, time-bounded Provisioning entries.
Three nouns. One request.
Before your first API call, you need three things — all created in the Connect section of the app.
-
1
A DataStream — the logical channel events will land in. Every Organization starts with an "All Events" stream; create more for each system you'll push from.
-
2
A Token — any string you want. Tokens authenticate the call.
-
3
A Token Provision — gates that Token for a list of endpoints (or
*) inside a start/end time window. Without an active Provision, the Token is inert.
Once those exist, copy the DataStream's UUID and the Token value and paste them into the request below.
curl -X POST https://www.spaceplace.app/api/events \ -H "Content-Type: application/json" \ -H "X-DataStream-ID: cb71-4a…" \ -H "X-Token: tk_live_…" \ -d '{ "number": 42, "source": "pos-04", "key": "sale", "value": "BEV-SPARK-16" }'
Every call carries a stream and a token.
There are two ways to send credentials. Headers are the recommended path; body fields exist for clients that can't easily set custom headers.
HTTP Headers recommended
X-DataStream-ID: your-datastream-uuid X-Token: your-token-value
Request body
{
"data_stream_id": "your-datastream-uuid",
"token": "your-token-value"
}
401 or 403.
All endpoints, one table.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/events | List and search events with filtering, sorting, pagination. |
| GET | /api/events/since | Get events since a given datetime — ideal for poll loops. |
| GET | /api/events/{id} | Retrieve a single event by ID. |
| POST | /api/events | Create a new event. All event fields are optional. |
| PUT | /api/events/{id} | Update an existing event. |
| DELETE | /api/events/{id} | Soft-delete an event. |
| GET | /api/inventory | List on-hand inventory rows. Filter by location, product, UPC, or low / out. |
| POST | /api/inventory/movements | Post a signed-quantity movement to the ledger. Requires an idempotency_key. |
| POST | /api/purchase-orders/{po}/receive | Receive lines against a PO in a single batch. Idempotent on batch_key. |
Five fields, all optional.
Every Event has a tiny, fixed shape. Defaults fill in anything you don't send, so the smallest valid POST is an empty body.
Event fields
| Field | Type | Default |
|---|---|---|
| timestamp | string | now (server) |
| number | integer | 0 |
| source | string | "" |
| key | string | "" |
| value | string | "" |
timestamp accepts ISO 8601 or Y-m-d H:i:s. The server also records remote_ip automatically.
List & search params
| Parameter | Description |
|---|---|
| source | Exact match on source |
| key | Exact match on key |
| value | Partial match (case-insensitive) |
| number_min / number_max | Numeric range |
| from / to | Datetime range |
| search | Across source, key, value |
| sort / dir | timestamp · number · source · key · value · created_at — asc / desc |
| per_page / page | 1–200 (default 50) |
Post movements to the ledger.
On-hand is a cache. The truth lives in the inventory_movements append-only ledger. Every write requires an idempotency_key — replaying the same key in the same Organization returns the prior movement without re-applying.
{
"sku": "BEV-SPARK-16",
"location_slug": "warehouse-01",
"movement_type": "adjustment",
"quantity": -3,
"idempotency_key": "count-2026-05-25-row-7",
"notes": "Spillage during cycle count"
}
Resolve the product by product_id, sku, or upc — in that precedence; the location by location_id or location_slug. GET /api/inventory accepts location_id, product_id, sku, upc, and a filter=low|out shortcut, and returns both identifiers on every product.
Addressing a product. sku is the reliable identifier: every product has one, and it reaches goods that were never listed externally (raw components, sub-assemblies) and therefore have no barcode. upc is the optional marketplace barcode and remains fully supported, so integrations written before SKUs existed keep working unchanged.
Receive a PO in one call.
Match lines by line_id, product_id, sku, or upc. The batch_key (e.g. the vendor's packing-slip number) makes the call idempotent — replay it and you get the same Vendor Receipt back, not a second one.
Tracked products: lot-tracked lines require lot_number (optional expiration_date, vendor_lot_no); serial-tracked lines require a serials array with exactly qty entries.
{
"batch_key": "slip-2026-05-25-A",
"notes": "Two cartons dented; flagged for QC",
"lines": [
{ "sku": "BEV-SPARK-16", "qty": 144 },
{ "sku": "SNK-GRANOLA-6", "qty": 36 }
]
}
Working samples in PHP and Bash.
Fifteen end-to-end recipes — single posts, batches, headers vs. body auth, error handling, reusable clients, URL monitors, long-running daemons, listing, polling, updating, deleting. Each example exists as both a PHP and Bash script. Click any tile to read it with syntax highlighting and one-click copy.
Or jump to the full examples index.
What every status code means.
Successful read, update, or delete.
A new event, movement, or receipt was recorded.
Missing or unknown DataStream ID or Token value.
Token has no currently active Provision for this endpoint.
Event, product, location, or PO not in this Organization.
Validation failed — body returns the offending fields.
Start pushing
in five minutes.
Create a DataStream, mint a Token, provision it. Then any system that can speak HTTP can write into Space Place.