API reference
The Schema API is a REST API over JSON. Every route lives under /v2/, authenticates with an org-scoped API key, and returns one error envelope on failure. What you create over the API appears in the platform immediately, and both draw one usage meter.
One stateless call, the quick run (POST /v2/run), and one stateful object, the endpoint (create, serve, refresh, upgrade). Every pass emits the full output bundle; outputs are free.
curl -X POST https://api.schemalabs.ai/v2/run \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tables": [{
"id": "crm_contacts",
"columns": ["c0", "c1", "c2", "c3"],
"rows": [
["ana@example.io", "555-0142", "2025-01-10", "retained"],
["lee@example.io", "555-0199", null, "churned"]
]
}],
"target": { "mode": "auto" }
}'https://api.schemalabs.ai/v2/. A breaking change mints /v3; adding fields to the bundle is non-breaking and announced. Tolerate unknown fields. Base models (schema-{n}) and endpoint state are separate version axes.Authorization: Bearer $SCHEMA_API_KEY. Keys are org-scoped with operation scopes (read, run, serve, manage, delete). See Authentication.application/json in and out. Timestamps are ISO 8601 UTC. Large data enters through datasets and connections, not request bodies.https://api.schemalabs.ai/v2/openapi/{endpoint_id}, importable into any tool that reads OpenAPI. See Endpoint OpenAPI description.429 with Retry-After. See Rate limits.{
"error": {
"type": "validation_error",
"message": "tables[1].rows: row 3 has 5 values, expected 4 columns",
"param": "tables[1].rows",
"request_id": "req_91ab3f"
}
}Every non-2xx response returns one envelope: { "error": { "type", "message", "param", "request_id" } }. The type is one of eight machine-readable strings, param points at the offending field when there is one, and request_id is what to quote to support. A run, creation, or serve either returns the full bundle or fails as job_failed: every success is a complete bundle, and failed jobs bill nothing.
| type | HTTP | When |
|---|---|---|
validation_error | 400 | A field is malformed, or data POSTed to an endpoint does not match its pinned snapshot’s schema (the message names the divergence). |
auth_error | 401 / 403 | Missing or invalid key (401), or the key lacks the route’s scope (403). |
not_found | 404 | Unknown endpoint, dataset, report, job, or key id; or a deleted endpoint’s URL. |
conflict | 409 | An Idempotency-Key reused with a different body; a second mutating job on an endpoint that already has one running (the envelope carries running_job_id); unpinning or deleting a dataset a live endpoint depends on (the message names the endpoint). |
capability_unavailable | 422 | The request names a retired base: a run, creation, or serve on it. Omit base for the latest model, or upgrade the endpoint. |
rate_limit | 429 | Requests per minute or cells per minute exceeded, per org, per key, or per endpoint. Retry-After states when to retry. |
job_failed | 500 | An async job could not complete. The job object carries the error message. Nothing is billed. |
internal | 5xx | Something on our side. Retry with backoff and quote the request_id if it persists. |
Branch on error.type, not on the message. Retry rate_limit after Retry-After and internal with exponential backoff; retried POSTs are safe when you send an Idempotency-Key. Treat conflict on refresh or upgrade as "wait for running_job_id". Surface validation_error.param to whoever owns the request payload.
import time, requests
def schema_post(url, payload, key, attempts=5):
# one idempotency key per logical request, reused on retry
idem = "<unique key>"
headers = {"Authorization": f"Bearer {key}",
"Idempotency-Key": idem}
for i in range(attempts):
r = requests.post(url, json=payload, headers=headers)
if r.ok:
return r.json()
err = r.json().get("error", {})
kind = err.get("type")
if kind == "rate_limit":
time.sleep(int(r.headers.get("Retry-After", 2 ** i)))
continue
if kind == "internal":
time.sleep(2 ** i)
continue
raise RuntimeError(
f"{kind}: {err.get('message')} ({err.get('request_id')})"
)
raise RuntimeError("gave up after retries")POST /v2/run, POST /v2/endpoints, refresh, upgrade, and POST /v2/data/generate accept an Idempotency-Key header: any string unique per logical request (a UUID is the usual choice), reused on every retry of that request. A retried POST with the same key and body returns the original result or job; the same key with a different body is 409 conflict. Send one on every expensive submit, especially from CI and retrying clients.
{
"error": {
"type": "conflict",
"message": "endpoint churn already has a mutating job running; wait for it to finish or cancel it",
"running_job_id": "job_5c07e3b1",
"request_id": "req_c3e8d2"
}
}Endpoint creation, refresh, upgrade, synthetic generation, and large or batch quick runs return 202 with a job_id. Poll GET /v2/jobs/:id, or rely on the platform notification and email on completion. Status flow: queued → running → done | failed | cancelled | expired.
- The endpoint id is assigned at submit, so
GET /v2/endpointslists it immediately ascreating; it flips tolivewhen the job completes. - A job either completes with its full result or fails as
job_failed. Failed and cancelled jobs bill nothing; a batch job that misses its completion window expires unbilled. POST /v2/jobs/:id/cancelstops a queued or running job. A cancelled creation stops the creation, and the endpoint can be deleted or created again; a cancelled refresh or upgrade leaves the endpoint serving its previous state.
import time, requests
BASE = "https://api.schemalabs.ai/v2"
TERMINAL = ("done", "failed", "cancelled", "expired")
def wait(job_id, key, delays=(2, 4, 8, 15)):
headers = {"Authorization": f"Bearer {key}"}
for i in range(10_000):
j = requests.get(f"{BASE}/jobs/{job_id}", headers=headers).json()
if j["status"] in TERMINAL:
return j
time.sleep(delays[min(i, len(delays) - 1)])Large arrays (unified_rows.rows, prediction.results, imputation.filled) return a first page inline (sample_shown) plus a cursor; total is always the full count. Full retrieval is cursor pagination (?cursor=…&limit=…) over the endpoint or report resource, or options.out on a large quick run to write the bundle to a file or warehouse table. Lists return the resource array (endpoints, datasets, reports, jobs), next_cursor, and total; GET /v2/keys returns the full list.
Data POSTed to an endpoint is validated against the schema of its pinned snapshots; a mismatch is a 400 validation_error naming the divergence.
One canonical scheme: the endpoint ID is the one unprefixed id; every other resource carries a prefix. Immutable ids never change on rename, refresh, or upgrade; an endpoint name is a mutable alias, and routes accept either. Surfaces truncate the endpoint ID to its first 8 characters for display; the full ID is the identifier. Snapshots, reports, and ids are immutable once minted.
| Resource | Formulation | Mutable | Example |
|---|---|---|---|
| Endpoint | opaque unique ID, assigned at creation | immutable | {endpoint_id} |
| Endpoint name | lowercase slug, user-chosen | mutable (rename) | churn |
| Base | schema-{n}; abbreviated s{n} in report names | n/a | schema-2, s2 |
| Report id | r_{hex} | immutable | r_8f3a |
| Report name | {endpoint}.{base}.{op}.{YYYY-MM-DD} | reflects the name at mint | churn.s2.upgrade.2026-08-14 |
| Quick run | run_{id} | immutable | run_9f2a4c |
| Job | job_{id} | immutable | job_9a5b01d4 |
| Connection | conn_{id} plus a scheme ref | immutable | conn_3f9a, snowflake://… |
| Dataset | ds_{id} | immutable | ds_crm01 |
| Dataset snapshot | dsv_{id}, minted per ingest or sync | immutable | dsv_91f2 |
| API key id | key_{id} | immutable | key_77ab |
| API key secret | sk_live_{rand} | shown once | sk_live_… |
| System prompt fragment | sp_{id} (body versioned) | immutable id | sp_12 |
| Request | req_{id} in every error envelope and log | n/a | req_e7f04a |
| Route | Does | Scope |
|---|---|---|
POST/v2/run | Create a quick run | run |
POST/v2/data/connect | Connect a source | manage |
GET/v2/data | List data | read |
GET/v2/data/:id | Retrieve data | read |
POST/v2/data/:id/sync | Sync a connection | manage |
POST/v2/data/:id/pin | Pin a dataset | manage |
POST/v2/data/:id/unpin | Unpin a dataset | manage |
POST/v2/data/generate | Generate synthetic data | manage |
DELETE/v2/data/:id | Delete data | delete |
POST/v2/endpoints | Create an endpoint | manage |
GET/v2/endpoints | List endpoints | read |
GET/v2/endpoints/:id | Retrieve an endpoint | read |
POST/v2/serve/:id | Serve an endpoint | serve |
POST/v2/endpoints/:id/refresh | Refresh an endpoint | manage |
POST/v2/endpoints/:id/upgrade | Upgrade an endpoint | manage |
GET/v2/endpoints/:id/logs | Endpoint logs | read |
GET/v2/openapi/:id | Endpoint OpenAPI description | read |
DELETE/v2/endpoints/:id | Delete an endpoint | delete |
GET/v2/reports | List reports | read |
GET/v2/reports/:id | Retrieve a report | read |
GET/v2/jobs | List jobs | read |
GET/v2/jobs/:id | Retrieve a job | read |
POST/v2/jobs/:id/cancel | Cancel a job | manage |
GET/v2/models | List models | read |
GET/v2/usage | Retrieve usage | read |
GET/v2/keys | List keys | read |
POST/v2/keys | Create a key | manage |
POST/v2/keys/:id/rotate | Rotate a key | manage |
DELETE/v2/keys/:id | Revoke a key | delete |