Developers/API Reference/CRUD Endpoints
CRUD Endpoints
The auto-generated Create, Read, Update, and Delete endpoints every schema type gets on api.supero.dev, plus bulk reads, rich queries, and batch writes.
Overview
Every object type in your project's schema — whether it's a built-in type like project or tenant, or a custom type you define like product or booking — automatically gets a full set of REST endpoints under /api/v1/crud. There's no code to write: upload a schema through the Schema API and the endpoints exist.
All CRUD endpoints live under a single base path scoped by your domain name:
text
https://api.supero.dev/api/v1/crud/{domain}/{type}
https://api.supero.dev/api/v1/crud/{domain}/{type}/{uuid}{domain} is your Supero domain name (not a hostname), and {type} is the schema type name — either bare (product) or namespace-qualified (billing:customer) if the schema was uploaded into a namespace. Records are identified by uuid, never id.
| Operation | Method | Endpoint |
|---|---|---|
| Create | POST | /api/v1/crud/{domain}/{type} |
| Batch create / upsert | POST | /api/v1/crud/{domain}/{type}/batch |
| Get by UUID | GET | /api/v1/crud/{domain}/{type}/{uuid} |
| List | GET | /api/v1/crud/{domain}/{type} |
| Update (partial) | PUT | /api/v1/crud/{domain}/{type}/{uuid} |
| Delete | DELETE | /api/v1/crud/{domain}/{type}/{uuid} |
| Bulk list with filters | POST | /api/v1/crud/{domain}/list-bulk |
| Rich query | POST | /api/v1/crud/{domain}/query |
bash
curl -X POST "https://api.supero.dev/api/v1/crud/acme-corp/product" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "wireless-keyboard",
"title": "Wireless Keyboard",
"price": 79.99,
"in_stock": true
}'ℹ️ name vs. title
Every record needs a unique name field — a short, URL-safe identifier used internally to build the record's fq_name. It's separate from any human-readable title/display_name field your schema defines. Omit fq_name and Supero derives it from your session context (domain/project/tenant) plus name.
Authentication and Scope
CRUD endpoints accept either a user JWT or an API key:
- •Authorization: Bearer <jwt> — from POST /api/v1/auth/login, for user-session calls.
- •X-API-Key: ak_... — an opaque API key, for server-to-server and integration calls. Never send an API key as a Bearer token.
Read/write scope (which domain, project, and tenant a request can see) comes entirely from the JWT or API key you authenticate with — there are no separate required scoping headers to set. If your key or session spans more than one project or tenant, narrow a request with the optional project or tenant query parameter; you cannot use them to widen scope beyond what your token already grants.
⚠️ Heads up
Requesting a UUID that exists but belongs to a tenant or owner you can't see returns 404, not 403 — this avoids leaking the existence of records outside your scope.
Creating Records
POST a JSON body with your schema's fields. name is required; fq_name, parent_type, and parent_uuid are optional — omit them for a top-level record and Supero derives fq_name from your session. For a child record (e.g. a booking under a specific customer), pass parent_uuid and parent_type explicitly.
http
POST /api/v1/crud/acme-corp/product HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "wireless-keyboard",
"title": "Wireless Keyboard",
"price": 79.99,
"in_stock": true
}json
{
"success": true,
"data": {
"product": {
"uuid": "9f2b1c3a-8e6d-4b21-9a1f-0c7e2d4b5f10",
"fq_name": ["acme-corp", "default-project", "northwind", "wireless-keyboard"],
"name": "wireless-keyboard",
"created_at": "2026-07-18T14:30:00Z",
"updated_at": "2026-07-18T14:30:00Z"
}
},
"uuid": "9f2b1c3a-8e6d-4b21-9a1f-0c7e2d4b5f10",
"fq_name": ["acme-corp", "default-project", "northwind", "wireless-keyboard"]
}ℹ️ Follow up with a GET
The create response mirrors uuid and fq_name at the top level for convenience, but data.<type> on a create response isn't guaranteed to carry back every field you sent. Treat the response as confirmation that you have a uuid; call GET /api/v1/crud/{domain}/{type}/{uuid} right after if you need the full record, including any server-computed or default values.
On failure, the response includes a structured error_code so clients don't have to string-match the message: ALREADY_EXISTS (409), PARENT_NOT_FOUND (400), VALIDATION_FAILED (422), or PERMISSION_DENIED (403).
Batch Create and Upsert
POST /api/v1/crud/{domain}/{type}/batch creates many records in one call, and can upsert (insert-or-update) against a matching key instead of always inserting.
json
{
"records": [
{"name": "record-1", "title": "First"},
{"name": "record-2", "title": "Second"}
],
"upsert": false,
"match_fields": ["external_id"]
}json
{
"success": true,
"created": 10,
"updated": 5,
"failed": 0,
"total_submitted": 15,
"errors": []
}- •Recommended maximum batch size: 1000 records per call.
- •Records are processed concurrently; processing stops after 100 accumulated errors.
- •If a record omits name, Supero auto-generates one from external_id or its position in the batch.
- •Set "upsert": true with match_fields to update existing rows that match instead of failing on a duplicate.
Reading Records
Get a single record
http
GET /api/v1/crud/acme-corp/product/9f2b1c3a-8e6d-4b21-9a1f-0c7e2d4b5f10 HTTP/1.1
Authorization: Bearer <token>json
{
"success": true,
"data": {
"product": {
"uuid": "9f2b1c3a-8e6d-4b21-9a1f-0c7e2d4b5f10",
"fq_name": ["acme-corp", "default-project", "northwind", "wireless-keyboard"],
"name": "wireless-keyboard",
"title": "Wireless Keyboard",
"price": 79.99,
"in_stock": true,
"created_at": "2026-07-18T14:30:00Z",
"updated_at": "2026-07-18T14:30:00Z"
}
}
}List records
http
GET /api/v1/crud/acme-corp/product?limit=20&sort_by=created_at&sort_order=desc HTTP/1.1
Authorization: Bearer <token>json
{
"success": true,
"obj_type": "product",
"results": [
{"uuid": "abc-001", "name": "widget-a", "created_at": "2026-07-15T10:30:00Z"},
{"uuid": "abc-002", "name": "widget-b", "created_at": "2026-07-14T09:00:00Z"}
],
"result_count": 20,
"pagination": {
"limit": 20,
"has_more": true,
"next_cursor": "eyJ1dWlkIjoiYWJjLTAyMCJ9"
}
}| Parameter | Type | Description |
|---|---|---|
| limit | integer | Page size. Default 50, maximum 1000. |
| offset | integer | Skip N records (offset pagination). Mutually exclusive with cursor — if both are sent, cursor wins. |
| cursor | string | Opaque cursor from a previous response’s pagination.next_cursor (cursor pagination). Fast at any page depth; prefer it for iteration and infinite scroll. |
| sort_by | string | Field to sort on. Safe on any record: created_at, updated_at, uuid, name. |
| sort_order | string | "asc" or "desc". |
| include_total | boolean | Include pagination.total (a full collection count). Costs a full scan — request it on the first page only, then omit it on later pages. |
| parent_id | string | Restrict to children of a given parent UUID (also accepted as parent_uuid). |
Send no pagination parameters at all and list behaves like a plain list: it returns every matching record and the response has no pagination key.
⚠️ GET does not filter by field
The GET list endpoint accepts pagination, sorting, and parent_id — it does not accept arbitrary field filters in the query string. To filter by field value, use POST /list-bulk or POST /query (next section).
⚠️ A bare list is scoped to your token’s project
GET /api/v1/crud/{domain}/{type} on one of your own schema types narrows the read to the single project your token was issued for — including when you are a domain_admin. Records of that type in your other projects are simply absent from the response. There is no error and no warning: the call returns 200 with a shorter results array, which is the most common reason a list looks unexpectedly empty. To read a type across the whole domain, use POST /api/v1/crud/{domain}/list-bulk with an fq_name_prefix of just the domain name.
bash
curl -X POST "https://api.supero.dev/api/v1/crud/acme-corp/list-bulk" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "product",
"fq_name_prefix": ["acme-corp"],
"detail": true,
"limit": 200
}'json
{
"success": true,
"results": [
{"uuid": "abc-001", "name": "widget-a", "fq_name": ["acme-corp", "storefront", "northwind", "widget-a"]},
{"uuid": "abc-042", "name": "widget-z", "fq_name": ["acme-corp", "warehouse", "northwind", "widget-z"]}
],
"result_count": 2
}A one-segment fq_name_prefix constrains only the domain, so records from every project and tenant under it come back. Widening this way requires an admin role — for other roles fq_name_prefix can still narrow scope, but a prefix broader than the token's own scope is clamped back down rather than honoured. Built-in domain-level types (project, audit_log, and similar) are already domain-wide on a bare list; the project clamp applies to the schema types you define yourself.
Filtering, Bulk Reads, and Rich Queries
Two POST endpoints cover what GET can't: filtering by field value, fetching a specific set of UUIDs, and reading children by parent or reference. Querying & Pagination documents both endpoints in full, along with the filter operators and cursor paging.
POST /list-bulk — pagination plus simple equality filters
filters can be a JSON object of exact-match field:value pairs, or the equivalent comma-separated string field==value,field2==value2.
bash
curl -X POST "https://api.supero.dev/api/v1/crud/acme-corp/list-bulk" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "product",
"filters": "status==active,category==electronics",
"limit": 20,
"sort_by": "name",
"sort_order": "asc"
}'json
{
"success": true,
"results": [
{"uuid": "abc-010", "name": "usb-c-hub", "status": "active", "category": "electronics"}
],
"result_count": 1,
"pagination": {"limit": 20, "has_more": false}
}list-bulk also accepts obj_uuids (fetch a specific known set of records, still paginated) and parent_id / back_ref_id (comma-separated UUIDs) to scope by hierarchy or reference.
POST /query — the same filtering, framed as a read query
json
{
"type": "product",
"filters": {"status": "active", "category": "electronics"},
"limit": 20,
"offset": 0,
"sort": [{"field": "price", "order": "asc"}]
}json
{
"success": true,
"results": [
{"uuid": "abc-010", "name": "usb-c-hub", "status": "active", "category": "electronics", "price": 49.99}
],
"result_count": 1
}ℹ️ Query uses offset, not cursor
POST /query doesn't support cursor pagination — page it with limit/offset, the same as pre-pagination clients. Cursor pagination is only available on GET list and POST /list-bulk.
Updating and Deleting Records
PUT is a partial update: send only the fields you want to change, and everything else is left as-is.
http
PUT /api/v1/crud/acme-corp/product/9f2b1c3a-8e6d-4b21-9a1f-0c7e2d4b5f10 HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json
{
"price": 69.99,
"in_stock": false
}json
{
"success": true,
"data": {
"product": {
"uuid": "9f2b1c3a-8e6d-4b21-9a1f-0c7e2d4b5f10",
"price": 69.99,
"in_stock": false,
"updated_at": "2026-07-18T15:05:00Z"
}
}
}The system fields uuid, fq_name, created_at, updated_at, parent_type, and parent_uuid are immutable on update — include them in the body and they're silently dropped, keeping the platform's own values.
http
DELETE /api/v1/crud/acme-corp/product/9f2b1c3a-8e6d-4b21-9a1f-0c7e2d4b5f10 HTTP/1.1
Authorization: Bearer <token>json
{
"success": true,
"message": "product 9f2b1c3a-8e6d-4b21-9a1f-0c7e2d4b5f10 deleted"
}Error Handling
Errors return a non-2xx status with a JSON body containing at least error and message. Create failures also include a structured error_code (see Creating Records above). Errors & Status Codes carries the full catalog, and Rate Limits covers throttling responses.
json
{
"error": "Forbidden: Domain access denied",
"message": "Domain access denied"
}| Status | Meaning | When you’ll see it |
|---|---|---|
| 400 | Bad Request | Malformed body, missing required name, or an invalid fq_name shape. |
| 401 | Unauthorized | Missing or invalid token/API key. |
| 403 | Forbidden | Authenticated, but not permitted for this domain/project/tenant or operation. |
| 404 | Not Found | No record with that UUID — or a record you don’t have access to (returned as 404, not 403). |
| 409 | Conflict | A record with that name/fq_name already exists. |
| 422 | Unprocessable Entity | Body failed schema validation (missing required field, wrong type, etc.). |
| 500 | Internal Server Error | Unexpected server-side error. |
Using the Python SDK
The Python SDK wraps these same endpoints behind a synchronous org.crud.* API, so you don't have to hand-build requests.
bash
pip install superopython
from supero import login
org = login(domain_name="acme-corp", email="[email protected]", password="...")
# Create
product = org.crud.create("product", name="wireless-keyboard", title="Wireless Keyboard", price=79.99)
# Get / List / Update / Delete
product = org.crud.get("product", product["uuid"])
products = org.crud.list("product", limit=50)
org.crud.update("product", product["uuid"], price=69.99)
org.crud.delete("product", product["uuid"])
# Query builder — filter, order, limit
active = (
org.crud.query("product")
.filter(status="active")
.order_by("-price")
.limit(10)
.all()
)You can also connect with an API key instead of a user login:
python
from supero import connect
org = connect(domain_name="acme-corp", api_key="ak_...")💡 Handling errors
Catch AuthenticationError, AuthorizationError, and RateLimitError from supero for auth and rate-limit failures. The client is synchronous — there is no async client.
Next Steps
Related reading:
- •API Overview & Conventions — base URL and the request/response conventions shared across every endpoint. For throttling, see Rate Limits & Quotas.
- •Authentication — logging in, refreshing tokens, and creating API keys.
- •Schema Reference — how fields, references, and extends define what fields a type accepts.
- •Python SDK — the full org.crud, org.aggregate, and query-builder surface beyond basic CRUD.
On this page