Reference
REST API
Every claim on this page is checked against a live generated OpenAPI spec —
call generate_openapi_spec (MCP) or open API Reference in the dashboard
for your project's exact, current contract.
Base URL
https://api.aaly.io/<your-slug>/<project>
Both segments come from your account and your project — don't guess them.
Get the exact URL from generate_openapi_spec (MCP) or API Reference in
the dashboard; it's the servers[0].url in the spec.
Every entity gets five endpoints once its fields exist:
GET /{entity} List, with filtering, sorting, and pagination
POST /{entity} Create
GET /{entity}/{id} Get one — always immediately consistent
PATCH /{entity}/{id} Partial update
DELETE /{entity}/{id} Soft delete — cascades to nested children
Authentication
Every request needs Authorization: Bearer <token>, where the token is
either a JWT from /auth/signin or /auth/signup, or an API key
(aaly_<id>_<secret>) from POST /auth/keys. Both are accepted on every
endpoint identically. See Auth.
Filtering
GET /{entity}?{field}[operator]=<value> — a bare ?{field}=<value> means
[eq]. Multiple query parameters are AND-ed. A reference field is always
filterable, including the system audit references _createdBy / _updatedBy
/ _deletedBy; any other field type (including enum) is filterable once
created with search: true.
| Field type | Supported operators |
|---|---|
reference, enum, boolean, list | eq, ne, in, nin |
number, date, datetime | eq, gt, gte, lt, lte |
text (with search: true) | eq, starts — both prefix-match |
in/nin take a comma-separated list with OR semantics within the field.
ne/nin need another positive filter elsewhere in the request to drive the
query. Not supported: like/ends on text, in/nin/ne on
number/date/datetime, nested/dot-path filtering (customer.name=) — each
returns a clean 400 rather than a silently wrong answer.
Filter results read from an asynchronously-maintained search index — a
just-written record may not be immediately filterable (typically sub-second
lag). GET /{entity}/{id} is always immediately consistent regardless.
Sorting
?sort={field} or ?sort=-{field} — descending with the leading -. Only on
a number/date/datetime field, and only when that same field also has a
matching filter condition in the same request. Arbitrary unfiltered sort
returns a 400.
Facets
?facets=true (every faceted field) or ?facets=<field1>,<field2> (a
selected subset) on an unfiltered collection GET. A field with
search: true on an eligible type (enum/boolean/list/reference/
date/datetime) is faceted automatically:
{ "status": { "open": 42, "done": 3 } }
Facet counts are a global per-entity distribution, which is why the parameter
is unfiltered-only — combining facets= with any real filter returns a 400
(FACETS_REQUIRES_NO_FILTERS) rather than silently describing the wrong
result set.
Pagination
limit (1–200, default 50) and offset. Every list response:
{ "items": [ /* ... */ ], "count": 3, "total": 41, "limit": 50, "offset": 0 }
count is items in this page; total is every record matching the filter.
Errors
| Status | Shape | When |
|---|---|---|
400 | { "error", "field", "message" } | Validation failure, or an unsupported operator/filter combination |
401 | — | Missing or invalid bearer token |
403 | — | Cross-tenant access, or a tenant rule violation |
404 | — | Record not found |
409 | { "error", "field", "message" } | A unique field's value already exists |
500 | { "error" } | Internal error |
What's next
Auth for the identity endpoints · Concepts for field types and references · Limits for what these endpoints don't do yet (bulk operations, cursor pagination, full-text ranking).