Developers/API Reference/API Overview & Conventions
API Overview & Conventions
The conventions every Supero REST endpoint follows: one base URL, two route families, two auth schemes, and one response envelope. Read this before any individual endpoint reference.
Base URL and route families
Every Supero API call goes to a single host: https://api.supero.dev. There is no per-domain subdomain and no per-project URL prefix — your domain, project, and tenant scope come from your credentials, not the hostname or path.
Routes fall into two families that share the host but behave differently:
| Family | Path shape | Examples | What it manages |
|---|---|---|---|
| Platform/management | /api/v1/{resource} | /api/v1/auth/login, /api/v1/domains, /api/v1/schemas/upload, /api/v1/plugins | Domains, projects, schemas, users, API keys, SDKs — the control plane |
| Per-domain data | /api/v1/crud/{domain}/{type}[/{uuid}] | /api/v1/crud/acme/product, /api/v1/crud/acme/product/9c1f... | Your application records — CRUD, query, refs, aggregate |
ℹ️ Rule of thumb
If you're managing the platform (creating a project, uploading a schema, minting an API key), you're in /api/v1/{resource}. If you're reading or writing your own application data (products, orders, tickets — whatever your schemas define), you're in /api/v1/crud/{domain}/{type}.
bash
curl https://api.supero.dev/api/v1/crud/acme/product/9c1f2a3b-4d5e-6f70-8091-a2b3c4d5e6f7 \
-H "Authorization: Bearer $ACCESS_TOKEN"Authentication
Two auth schemes are accepted, and they are not interchangeable:
| Scheme | Header | Used by | Notes |
|---|---|---|---|
| JWT Bearer token | Authorization: Bearer <jwt> | Human users, browser sessions, short-lived scripts | Obtained from POST /api/v1/auth/login; access tokens last 8 hours, refresh tokens 7 days |
| API key | X-API-Key: ak_... | Services, backends, long-running integrations | Opaque secret starting with ak_; never send it as a Bearer token |
⚠️ Don't mix the two
An API key is not a JWT and must not be sent via Authorization: Bearer. It goes in the X-API-Key header. A request with neither header, or an X-API-Key that doesn't start with ak_, is rejected as unauthenticated.
Log in to get tokens:
bash
curl -X POST https://api.supero.dev/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "Secret@123",
"domain": "acme",
"project": "default-project"
}'json
{
"auth": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 28800,
"session_id": "550e8400-e29b-41d4-a716-446655440000"
},
"user": {
"user_uuid": "123e4567-e89b-12d3-a456-426614174000",
"email": "[email protected]",
"role": "tenant_admin",
"domain_name": "acme",
"project_name": "default-project",
"permissions": ["user:create", "user:read", "schema:manage"]
},
"context": { "domain": "acme", "project": "default-project", "tenant": "northwind" },
"policy": { }
}Refresh an expiring access token, and revoke a session on logout:
bash
# Refresh
curl -X POST https://api.supero.dev/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "'"$REFRESH_TOKEN"'"}'
# Logout (revokes the session)
curl -X POST https://api.supero.dev/api/v1/auth/logout \
-H "Authorization: Bearer $ACCESS_TOKEN"🚨 Scope comes from the credential, not the request
Your domain/project/tenant scope is derived from the JWT or API key you authenticate with — there are no scope headers to set, and any custom scope headers you add are ignored. To narrow scope on a request, use the project or tenant query parameter (GET) or body field (POST), and only within what your credential already has access to.
CRUD endpoints
Every schema you upload gets a full CRUD surface at /api/v1/crud/{domain}/{type}. {type} is your schema's name (e.g. product, order, ticket) — lowercase, matching what you defined.
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/crud/{domain}/{type} | Create a record |
| GET | /api/v1/crud/{domain}/{type}/{uuid} | Get one record by UUID |
| GET | /api/v1/crud/{domain}/{type} | List records (pagination only — see the gotcha below) |
| PUT | /api/v1/crud/{domain}/{type}/{uuid} | Update a record |
| DELETE | /api/v1/crud/{domain}/{type}/{uuid} | Delete a record |
| POST | /api/v1/crud/{domain}/{type}/batch | Create or upsert many records in one call. Returns 200 even when records failed — read the counts in the body, not the status |
| POST | /api/v1/crud/{domain}/query | Rich read: filters, sort, pagination — the recommended way to list with conditions |
| POST | /api/v1/crud/{domain}/list-bulk | List with an explicit type in the body plus pagination, useful for programmatic callers |
| GET | /api/v1/crud/{domain}/{type}/{uuid}/refs | Forward references from a record (optionally filtered by ref type) |
| GET | /api/v1/crud/{domain}/{type}/{uuid}/back-refs | Records that reference this record |
| PUT | /api/v1/crud/{domain}/ref-update | Add, update, or delete a single reference edge |
| PUT | /api/v1/crud/{domain}/ref-update-multi | Batch reference updates |
⚠️ A batch write returns 200 even when records failed
POST /api/v1/crud/{domain}/{type}/batch reports per-record outcomes in the body, never in the status. The response carries created, updated, failed and total_submitted, and success is exactly failed == 0 — so a call in which every record failed still comes back as 200. The errors array holds only the first 10 failures, and processing stops once 100 errors accumulate, so failed can be larger than that array and records past that point were never attempted. Reconcile on the counts. Size and error caps are on Rate Limits & Quotas; the full envelope and the other calls that fail inside a 200 are on Errors & Status Codes.
Create, read, update, delete
bash
# Create
curl -X POST https://api.supero.dev/api/v1/crud/acme/product \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "widget-01", "title": "Blue Widget", "price": 19.99, "in_stock": true}'
# Read
curl https://api.supero.dev/api/v1/crud/acme/product/9c1f2a3b-4d5e-6f70-8091-a2b3c4d5e6f7 \
-H "Authorization: Bearer $ACCESS_TOKEN"
# Update
curl -X PUT https://api.supero.dev/api/v1/crud/acme/product/9c1f2a3b-4d5e-6f70-8091-a2b3c4d5e6f7 \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"price": 17.99}'
# Delete
curl -X DELETE https://api.supero.dev/api/v1/crud/acme/product/9c1f2a3b-4d5e-6f70-8091-a2b3c4d5e6f7 \
-H "Authorization: Bearer $ACCESS_TOKEN"Python SDK equivalent
python
from supero import login
org = login(domain_name="acme", email="[email protected]", password="Secret@123", project="default-project")
# Create
product = org.crud.create("product", name="widget-01", title="Blue Widget", price=19.99, in_stock=True)
# Read
product = org.crud.get("product", product["uuid"])
# Update
org.crud.update("product", product["uuid"], price=17.99)
# Delete
org.crud.delete("product", product["uuid"])
# Query builder
in_stock = org.crud.query("product").filter(in_stock=True).order_by("-price").all()org.crud is synchronous — calls block and return plain dicts/lists, no async/await and no separate async client.
References
Relationships between records are managed as reference edges, not foreign-key columns. Add one with ref-update:
bash
curl -X PUT https://api.supero.dev/api/v1/crud/acme/ref-update \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "order",
"uuid": "9c1f2a3b-4d5e-6f70-8091-a2b3c4d5e6f7",
"ref-type": "product",
"ref-uuid": "1a2b3c4d-5e6f-7081-92a3-b4c5d6e7f809",
"operation": "ADD"
}'operation is one of ADD, UPDATE, or DELETE. Fetch the edges back out with GET .../refs (forward references from this record) or GET .../back-refs (records that reference this one).
Gotcha: GET list ignores filter query params
🚨 GET list does not filter
GET /api/v1/crud/{domain}/{type} only recognizes pagination and scoping query params (limit, offset, cursor, sort_by, sort_order, include_total, parent_id/parent_uuid, parent_type, tenant, project). Any other query param you add — ?status=active, ?category=shoes — is silently dropped. The call still returns 200 with an unfiltered page of results, which is easy to miss in testing.
To filter, use POST /api/v1/crud/{domain}/query instead. It takes the object type plus a filters object in the body and applies them server-side. Note that /query has its own parameter set — it sorts with a sort array, not the sort_by/sort_order pair the GET list endpoint uses. Querying & Pagination has the full matrix of which parameter applies to which read endpoint:
bash
curl -X POST https://api.supero.dev/api/v1/crud/acme/query \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "product",
"filters": {"in_stock": true, "category": "shoes"},
"limit": 25,
"sort": [{"field": "price", "order": "asc"}]
}'💡 SDK users are unaffected
org.crud.query(type).filter(...).all() always goes through POST /query, so this gotcha only bites callers hand-rolling GET requests against the list endpoint.
Response envelope, pagination, and errors
List endpoints share one envelope shape:
json
{
"success": true,
"obj_type": "product",
"results": [ { "uuid": "...", "fq_name": ["acme", "default-project", "northwind", "widget-01"], "name": "widget-01", "price": 19.99, "created_at": "2026-07-01T10:00:00Z", "updated_at": "2026-07-01T10:00:00Z" } ],
"result_count": 1,
"pagination": { "limit": 50, "offset": 0 }
}Pagination is controlled by limit/offset (or cursor for cursor-based paging) — there is no page parameter on the CRUD data path. Single-record reads return { "success": true, "data": { ...record } }.
Every error response, on every endpoint, follows the same two-field shape:
json
{
"error": "Forbidden: Your role does not have update access to product",
"message": "Your role does not have update access to product"
}| Status | Meaning |
|---|---|
| 200 / 201 | The request was processed — not a guarantee it did what you wanted. A batch write and a transactional service call both report failure inside a 200 |
| 400 | Bad request — missing/invalid fields |
| 401 | Unauthenticated — missing, invalid, or expired credential |
| 403 | Authenticated but not authorized for this action or scope |
| 404 | Not found (also returned instead of 403 where existence itself would leak information) |
| 429 | Rate limited — some endpoints return a Retry-After header |
| 500 | Internal server error |
Record identity: uuid, not id
Supero records key on uuid, never id. Every record — whatever your schema — carries the same system fields alongside your own fields:
| Field | Meaning |
|---|---|
| uuid | The record's unique identifier — use this in URLs and ref-update calls, not name |
| fq_name | Fully-qualified name path, e.g. [domain, project, tenant, record-name] — the hierarchical address of the record |
| parent_type / parent_uuid | The record's position in the Domain → Project → Tenant hierarchy, when applicable |
| created_at / updated_at | ISO-8601 timestamps, server-assigned |
⚠️ No id or createdAt
Client code (and especially anything ported from another platform) that reaches for record.id or record.createdAt will get undefined. Use record.uuid and record.created_at.
Gotcha: uploading a schema makes its CRUD API live immediately
🚨 No separate deploy step
There is no build/deploy/publish step between uploading a schema and its CRUD endpoints accepting traffic. POST /api/v1/schemas/upload validates and stores the schema, and /api/v1/crud/{domain}/{type} is live for that type as soon as the call returns 200/201. If you upload a schema with a mistake — a wrong field name, a missing required flag — that mistake is immediately reachable by any caller with the right role, not staged behind a review step.
Treat schema uploads to a domain your application already serves traffic from like a production deploy: validate locally first, and prefer additive changes (new optional fields) over renaming or removing fields that existing records already use.
Schema shape, for reference
Schemas define the {type} in your CRUD URLs. A schema has attributes[] describing fields, optional references[] to other types, an optional parent_type for hierarchy, and can extend a base schema with extends: "<service>:<base>".
| Field field | Meaning |
|---|---|
| name | Field name |
| type | The field's type — see Schema Reference for the full list of primitive types and the aliases accepted for each. Enums are type: string plus a values list |
| mandatory | Whether the field is required on create |
| values | Allowed values, for enum-like string fields |
| default-value | Value used when the field is omitted on create |
This page covers the API surface, not schema authoring in depth — see the Schema Reference for the full field reference and worked examples.
Next steps
- •Authentication — token lifetimes, refresh flow, and creating scoped API keys
- •OpenAPI Export & Developer Tools — export a machine-readable spec of your own schemas, and try endpoints in the playground
- •CRUD Endpoints — the full per-endpoint reference with every parameter
- •Querying & Pagination — filters, sorting, cursors, references, and aggregation
- •Schema Reference — fields, references, hierarchy, and extends in depth
- •Errors & Status Codes — the complete error catalog by endpoint
On this page