How to consume Sandbar over HTTP REST — the protocol shape, content negotiation, common operations, and patterns. This is a client-side guide focused on practical usage; for the mechanical endpoint catalog see
doc/api/http-rest.md; for the AI-client alternative seewriting-an-mcp-client.md.
REST is right when:
For AI clients that prefer reflection-driven discovery, use MCP instead. Both projections come from the same metamodel — no parallel state to keep in sync.
http://localhost:8080/api/
Two top-level resource groups:
/api/status — health and version/api/store/* — metamodel and instance operationsSame bearer-token scheme as MCP:
curl http://localhost:8080/api/store/classes \
-H "Authorization: Bearer $SANDBAR_TOKEN"
Endpoints that read are typically unauthenticated; endpoints that write or expose sensitive entities require a service-account token. See auth.md for issuance.
# Default — EDN
curl http://localhost:8080/api/store/classes
# Request JSON
curl http://localhost:8080/api/store/classes \
-H "Accept: application/json"
# Request Transit
curl http://localhost:8080/api/store/classes \
-H "Accept: application/transit+json"
| Accept | Response shape |
|---|---|
application/edn (default) | Clojure EDN — preserves keywords, sets, custom types |
application/json | Standard JSON |
application/transit+json | Transit-encoded JSON (Cognitect) |
Clojure keywords map directly to URL paths:
| Clojure | URL fragment |
|---|---|
:dt/Resource | dt/Resource |
:model/User | model/User |
:db.type/string | db.type/string |
Special characters in keywords (?, *) are percent-encoded: :user/active? becomes user/active%3F.
# Health
curl http://localhost:8080/api/status
# All classes
curl http://localhost:8080/api/store/classes
# Schema summary
curl http://localhost:8080/api/store/schema
# One class
curl http://localhost:8080/api/store/classes/model/User
# Slots (effective — inherited + direct)
curl http://localhost:8080/api/store/classes/model/User/slots
# Direct slots only
curl http://localhost:8080/api/store/classes/model/User/slots/direct
# Required slots
curl http://localhost:8080/api/store/classes/model/User/slots/required
# Class hierarchy
curl http://localhost:8080/api/store/classes/model/User/hierarchy
# Direct subclasses
curl http://localhost:8080/api/store/classes/dt/Ref/subclasses
Example response for /api/store/classes/model/User:
{:class :model/User
:description {:db/doc "Application user accounts" ...}
:abstract? false
:context "model"
:label "User"
:parents [:dt/Ref]
:ancestors [:dt/Ref :dt/Resource]
:slots [:db/doc :db/ident :dt/label :dt/context :dt/type
:user/login :user/secret :user/uuid]
:direct-slots [:user/login :user/secret :user/uuid]
:required-slots [:user/login :user/secret]
:direct-subclasses []
:all-subclasses []}
# All instances of a class
curl http://localhost:8080/api/store/classes/model/User/instances
# Direct instances only (no subclass instances)
curl http://localhost:8080/api/store/classes/dt/Ref/instances/direct
# One instance by ident
curl http://localhost:8080/api/store/entities/model/user.alice
# All properties
curl http://localhost:8080/api/store/properties
# One property
curl http://localhost:8080/api/store/properties/user/login
# Domain / range / cardinality
curl http://localhost:8080/api/store/properties/user/login/domain
curl http://localhost:8080/api/store/properties/user/login/range
curl http://localhost:8080/api/store/properties/user/login/cardinality
# Is Child a subclass of Parent?
curl http://localhost:8080/api/store/types/subclass-of/dt/Resource/model/User
# => {:parent :dt/Resource :child :model/User :subclass-of? true}
# Is Entity an instance of Class?
curl http://localhost:8080/api/store/types/instance-of/model/User/model/user.alice
# => {:class :model/User :entity :model/user.alice :instance-of? true}
curl -X POST http://localhost:8080/api/store/classes/event/Booking/instances \
-H "Authorization: Bearer $SANDBAR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event.booking/title": "Weekly Sync",
"event.booking/starts-at": "2026-05-14T15:00:00Z",
"event.booking/ends-at": "2026-05-14T16:00:00Z",
"event.booking/owner": "model/user.alice"
}'
Successful creation returns the entity:
{
"entity": {
"db/id": 12345,
"event.booking/title": "Weekly Sync",
"event.booking/starts-at": "2026-05-14T15:00:00Z",
...
}
}
Validation failures return 422 Unprocessable Entity with structured error data:
{
"errors": [
{"slot": "event.booking/owner", "error": "missing-required"}
]
}
curl -X PATCH http://localhost:8080/api/store/entities/event/booking.weekly \
-H "Authorization: Bearer $SANDBAR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"event.booking/location": "Conference Room A"}'
Only the provided slots are updated; others remain untouched. Validation runs after the merge.
| HTTP status | Meaning |
|---|---|
200 OK | Request succeeded |
400 Bad Request | Malformed request (parse error, missing body, etc.) |
401 Unauthorized | Missing or invalid bearer token |
403 Forbidden | Authenticated but not permitted |
404 Not Found | The requested class / property / entity doesn't exist |
409 Conflict | Constraint violation (e.g., uniqueness) |
422 Unprocessable | Validation failed — response body has structured :errors |
500 Internal | Server-side error |
Always inspect the response body for error details — error structure is consistent across endpoints.
TOKEN="<your-token>"
BASE="http://localhost:8080/api"
# Walk the metamodel
curl -s "$BASE/store/classes" | jq
# Read one entity
curl -s -H "Accept: application/json" \
"$BASE/store/entities/model/user.alice" | jq
# Create
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"event.booking/title":"Foo",...}' \
"$BASE/store/classes/event/Booking/instances" | jq
import httpx
class SandbarREST:
def __init__(self, base_url, token=None):
self.base = base_url
self.headers = {"Accept": "application/json"}
if token:
self.headers["Authorization"] = f"Bearer {token}"
def classes(self):
return httpx.get(f"{self.base}/store/classes",
headers=self.headers).json()
def class_details(self, ns, name):
return httpx.get(f"{self.base}/store/classes/{ns}/{name}",
headers=self.headers).json()
def create(self, ns, name, attributes):
return httpx.post(f"{self.base}/store/classes/{ns}/{name}/instances",
headers={**self.headers,
"Content-Type": "application/json"},
json=attributes).json()
# Usage
s = SandbarREST("http://localhost:8080/api", token="...")
print(s.classes())
class SandbarREST {
constructor(
private base: string,
private token: string,
) {}
private headers(): Record<string, string> {
return {
'Accept': 'application/json',
'Authorization': `Bearer ${this.token}`,
};
}
async classes() {
const r = await fetch(`${this.base}/store/classes`, {
headers: this.headers(),
});
return r.json();
}
async create(ns: string, name: string, attributes: object) {
const r = await fetch(`${this.base}/store/classes/${ns}/${name}/instances`, {
method: 'POST',
headers: { ...this.headers(), 'Content-Type': 'application/json' },
body: JSON.stringify(attributes),
});
if (!r.ok) {
throw new Error(`Sandbar error ${r.status}: ${await r.text()}`);
}
return r.json();
}
}
Walk /api/store/classes and /api/store/classes/<class>/slots to render forms for any class. The same pattern that works in writing-a-clojure-client.md works through REST — Sandbar's REST surface is the metamodel projected; no separate schema artifact.
Class metadata changes only when the schema evolves (rarely). Aggressive client-side caching of /api/store/classes/* is safe — listen for notifications/tools/list_changed over MCP (if you maintain both connections) or use a short-TTL refresh.
Sandbar's MCP endpoint provides resource subscriptions over SSE; the REST surface today is request/response only. If you need live updates, use the MCP transport for that subset.
| Concern | REST | MCP |
|---|---|---|
| Wire format | EDN / JSON / Transit | JSON-RPC over HTTP+SSE |
| Discovery | URL traversal | tools/list, resources/list |
| Subscriptions | None (today) | Per-URI over SSE |
| Long-running operations | Synchronous (or 202+polling) | First-class Tasks |
| Schema reflection | Per-class endpoint | tools/list returns JSON Schema |
| Ideal consumer | Browser / traditional HTTP client | AI agent / reflective consumer |
Both project from the same metamodel. Pick by consumer fit.
doc/api/http-rest.md — complete endpoint referencewriting-an-mcp-client.md — MCP alternativewriting-a-clojure-client.md — in-process Clojure accessauth.md — token issuance + managementzorp-tutorial.md — worked example using REST queriesCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |