SchemaLabsDocs
API reference · Overview

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.

First callbash
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" }
  }'
Base URLhttps://api.schemalabs.ai
VersionAll routes under /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.
AuthAuthorization: Bearer $SCHEMA_API_KEY. Keys are org-scoped with operation scopes (read, run, serve, manage, delete). See Authentication.
Content typeapplication/json in and out. Timestamps are ISO 8601 UTC. Large data enters through datasets and connections, not request bodies.
OpenAPIEvery live endpoint publishes its own description at https://api.schemalabs.ai/v2/openapi/{endpoint_id}, importable into any tool that reads OpenAPI. See Endpoint OpenAPI description.
Rate limitsRequests per minute and cells per minute, per org and per key. Exceeding returns 429 with Retry-After. See Rate limits.
MeteringCells (rows x columns), summed per table. Inputs metered, outputs free. See Usage and billing.
Error envelopejson
{
  "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.

typeHTTPWhen
validation_error400A field is malformed, or data POSTed to an endpoint does not match its pinned snapshot’s schema (the message names the divergence).
auth_error401 / 403Missing or invalid key (401), or the key lacks the route’s scope (403).
not_found404Unknown endpoint, dataset, report, job, or key id; or a deleted endpoint’s URL.
conflict409An 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_unavailable422The request names a retired base: a run, creation, or serve on it. Omit base for the latest model, or upgrade the endpoint.
rate_limit429Requests per minute or cells per minute exceeded, per org, per key, or per endpoint. Retry-After states when to retry.
job_failed500An async job could not complete. The job object carries the error message. Nothing is billed.
internal5xxSomething 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.

Retry patternpython
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.

409 conflict, mutating job runningjson
{
  "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: queuedrunningdone | failed | cancelled | expired.

  • The endpoint id is assigned at submit, so GET /v2/endpoints lists it immediately as creating; it flips to live when 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/cancel stops 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.
Poll until terminalpython
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.

ResourceFormulationMutableExample
Endpointopaque unique ID, assigned at creationimmutable{endpoint_id}
Endpoint namelowercase slug, user-chosenmutable (rename)churn
Baseschema-{n}; abbreviated s{n} in report namesn/aschema-2, s2
Report idr_{hex}immutabler_8f3a
Report name{endpoint}.{base}.{op}.{YYYY-MM-DD}reflects the name at mintchurn.s2.upgrade.2026-08-14
Quick runrun_{id}immutablerun_9f2a4c
Jobjob_{id}immutablejob_9a5b01d4
Connectionconn_{id} plus a scheme refimmutableconn_3f9a, snowflake://…
Datasetds_{id}immutableds_crm01
Dataset snapshotdsv_{id}, minted per ingest or syncimmutabledsv_91f2
API key idkey_{id}immutablekey_77ab
API key secretsk_live_{rand}shown oncesk_live_…
System prompt fragmentsp_{id} (body versioned)immutable idsp_12
Requestreq_{id} in every error envelope and logn/areq_e7f04a
RouteDoesScope
POST/v2/runCreate a quick runrun
POST/v2/data/connectConnect a sourcemanage
GET/v2/dataList dataread
GET/v2/data/:idRetrieve dataread
POST/v2/data/:id/syncSync a connectionmanage
POST/v2/data/:id/pinPin a datasetmanage
POST/v2/data/:id/unpinUnpin a datasetmanage
POST/v2/data/generateGenerate synthetic datamanage
DELETE/v2/data/:idDelete datadelete
POST/v2/endpointsCreate an endpointmanage
GET/v2/endpointsList endpointsread
GET/v2/endpoints/:idRetrieve an endpointread
POST/v2/serve/:idServe an endpointserve
POST/v2/endpoints/:id/refreshRefresh an endpointmanage
POST/v2/endpoints/:id/upgradeUpgrade an endpointmanage
GET/v2/endpoints/:id/logsEndpoint logsread
GET/v2/openapi/:idEndpoint OpenAPI descriptionread
DELETE/v2/endpoints/:idDelete an endpointdelete
GET/v2/reportsList reportsread
GET/v2/reports/:idRetrieve a reportread
GET/v2/jobsList jobsread
GET/v2/jobs/:idRetrieve a jobread
POST/v2/jobs/:id/cancelCancel a jobmanage
GET/v2/modelsList modelsread
GET/v2/usageRetrieve usageread
GET/v2/keysList keysread
POST/v2/keysCreate a keymanage
POST/v2/keys/:id/rotateRotate a keymanage
DELETE/v2/keys/:idRevoke a keydelete
Type to search.
    navigate open