S
supero.docs
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:
FamilyPath shapeExamplesWhat it manages
Platform/management/api/v1/{resource}/api/v1/auth/login, /api/v1/domains, /api/v1/schemas/upload, /api/v1/pluginsDomains, 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:
SchemeHeaderUsed byNotes
JWT Bearer tokenAuthorization: Bearer <jwt>Human users, browser sessions, short-lived scriptsObtained from POST /api/v1/auth/login; access tokens last 8 hours, refresh tokens 7 days
API keyX-API-Key: ak_...Services, backends, long-running integrationsOpaque 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.
MethodPathPurpose
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}/batchCreate 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}/queryRich read: filters, sort, pagination — the recommended way to list with conditions
POST/api/v1/crud/{domain}/list-bulkList with an explicit type in the body plus pagination, useful for programmatic callers
GET/api/v1/crud/{domain}/{type}/{uuid}/refsForward references from a record (optionally filtered by ref type)
GET/api/v1/crud/{domain}/{type}/{uuid}/back-refsRecords that reference this record
PUT/api/v1/crud/{domain}/ref-updateAdd, update, or delete a single reference edge
PUT/api/v1/crud/{domain}/ref-update-multiBatch 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"
}
StatusMeaning
200 / 201The 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
400Bad request — missing/invalid fields
401Unauthenticated — missing, invalid, or expired credential
403Authenticated but not authorized for this action or scope
404Not found (also returned instead of 403 where existence itself would leak information)
429Rate limited — some endpoints return a Retry-After header
500Internal 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:
FieldMeaning
uuidThe record's unique identifier — use this in URLs and ref-update calls, not name
fq_nameFully-qualified name path, e.g. [domain, project, tenant, record-name] — the hierarchical address of the record
parent_type / parent_uuidThe record's position in the Domain → Project → Tenant hierarchy, when applicable
created_at / updated_atISO-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 fieldMeaning
nameField name
typeThe 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
mandatoryWhether the field is required on create
valuesAllowed values, for enum-like string fields
default-valueValue 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