Skip to main content

API Reference

MOD Platform exposes a RESTful JSON API. Everything the Dashboard and Workflow Builder do is built on these same endpoints, so anything you can do in the UI you can automate.

  • Base URL: https://api.<your-domain> — on this environment: https://dev-api.modtechlabs.com
  • Content type: application/json for request/response bodies (file uploads use multipart/form-data)
  • Interactive docs: the full, always-current OpenAPI/Swagger spec lives at /docs (e.g. https://dev-api.modtechlabs.com/docs) and the raw schema at /openapi.json. When this page and /docs disagree, /docs wins.

Authentication

Every endpoint (except a few public ones) requires an OAuth2 Bearer token in the Authorization header. There are two ways to get one.

Create a key in Dashboard → Settings → API Keys. The full key (prefix sk_) is shown once at creation — store it securely. A key inherits the permissions of the user who created it.

An API key is not sent on every request. Instead you exchange it once for a short-lived Bearer token at the public POST /api-keys/token endpoint, then send that token:

BASE=https://dev-api.modtechlabs.com

# Exchange your sk_ key for a ~1h Bearer token (no auth header needed on this call)
TOKEN=$(curl -s -X POST "$BASE/api-keys/token" \
-H "Content-Type: application/json" \
-d '{"api_key": "sk_xxxxxxxxxxxxxxxxxxxxxxxx"}' | jq -r .access_token)

# Use it on any endpoint
curl -s "$BASE/me" -H "Authorization: Bearer $TOKEN"

The exchange returns { "access_token", "token_type": "Bearer", "expires_in", "scope" }. When the token expires (expires_in seconds, ~1 hour), just call /api-keys/token again.

Option B — Bearer token (OAuth2 / Keycloak)

Tokens are issued by Keycloak and expire after ~1 hour. This is what the Dashboard uses. Obtain one with the OAuth2 authorization-code grant against the realm, then send it the same way: -H "Authorization: Bearer <access_token>".

Refresh an expired token (Dashboard BFF flow):

curl -s -X POST "$BASE/auth/refresh-token" \
-H "Content-Type: application/json" \
-d '{"refresh_token": "<refresh_token>"}'

If the header is missing or invalid you get 401:

{ "detail": "Authentication required. Provide either Bearer token or X-API-Key header." }

:::tip Try it live in the API explorer The API is FastAPI, so the interactive Swagger UI at /docs lets you run any endpoint from your browser:

  1. Open https://dev-api.modtechlabs.com/docs
  2. Click Authorize (top-right) and provide your Bearer token (the access_token from the exchange above).
  3. Expand any endpoint → Try it out → fill the params → Execute. You'll see the exact curl, request URL, and live response.

The deep-links below (▶ Open in explorer) jump straight to each section. :::

Quick start

BASE=https://dev-api.modtechlabs.com

# 0. Exchange your API key for a Bearer token (see Authentication above)
TOKEN=$(curl -s -X POST "$BASE/api-keys/token" \
-H "Content-Type: application/json" \
-d '{"api_key": "sk_xxxxxxxxxxxxxxxxxxxxxxxx"}' | jq -r .access_token)
AUTH="Authorization: Bearer $TOKEN"

# 1. Who am I?
curl -s "$BASE/me" -H "$AUTH"

# 2. Upload a file
curl -s -X POST "$BASE/cogs/upload-file" -H "$AUTH" \
-F "file=@./input.png"

# 3. List your files
curl -s "$BASE/cogs/" -H "$AUTH"

The examples below reuse $BASE and $AUTH from this Quick start block.

Conventions

TopicDetail
PaginationList endpoints accept ?offset= and ?limit= (default limit 100).
IDsInteger primary keys unless noted.
TimestampsISO‑8601 UTC, e.g. 2026-06-23T13:25:24Z.
Multi-tenancyEvery request is scoped to the caller's tenant automatically.
IdempotencyRe-sharing or re-granting a permission upserts rather than erroring.

Errors

Errors return a JSON body with a detail field and the appropriate status code:

StatusMeaning
400Bad request / invalid value
401Missing or invalid credentials
403Authenticated but not permitted
404Not found (or not visible to your tenant)
409Conflict (e.g. duplicate name — body includes a code such as name_collision)
422Validation error (Pydantic field-level errors)
500Server error — includes an error_id for support
{ "detail": "You don't have permission to share this item" }

Account

Open in explorer

GET /me # current user profile
curl -s "$BASE/me" -H "$AUTH"
{
"id": 3,
"email": "you@example.com",
"first_name": "Ada",
"last_name": "Lovelace",
"tenant_id": 1,
"is_platform_admin": false,
"onboarding_complete": true
}

Files (Cogs)

Open in explorer

In MOD, files and folders are cogs. A cog is one of master_folder (the tenant root, "My Files"), asset_folder, or file.

GET /cogs/?parent_id={id} # list children of a folder (omit parent_id for the root)
GET /cogs/root # get-or-create the tenant root folder
POST /cogs/ # create a folder {name, cog_type, parent_id?}
POST /cogs/upload-file # upload a file (multipart)
GET /cogs/{id} # cog details
GET /cogs/{id}/download # download the file bytes
PATCH /cogs/{id} # rename / move {name?, parent_id?}
DELETE /cogs/{id} # soft-delete (also removes its permissions)

Create a folder:

curl -s -X POST "$BASE/cogs/" -H "$AUTH" \
-H "Content-Type: application/json" \
-d '{"name": "Project A", "cog_type": "asset_folder", "parent_id": 19}'

Upload a file (simple, under ~100 MB):

curl -s -X POST "$BASE/cogs/upload-file" -H "$AUTH" \
-F "parent_id=19" \
-F "file=@./render.exr"
# 201 Created -> the new cog

Upload a large file (multipart): big files use a chunked, presigned multipart upload — initiate, PUT each part to its presigned URL, then confirm with the part ETags:

POST /cogs/uploads/initiate # -> upload_id + presigned URL per part (64 MB default chunk)
PUT <presigned part URL> # upload each chunk directly to object storage (no auth header)
POST /cogs/confirm-upload # parent_id, upload_id, s3_upload_id, parts_json (the ETags), file_size

The Dashboard's uploader does this for you; if you're scripting it, see /docs for the exact initiate/confirm-upload parameters (part size, parts_json shape). For most automation the simple POST /cogs/upload-file above is all you need.

Download:

curl -s "$BASE/cogs/123/download" -H "$AUTH" -o out.exr

Permissions & sharing

Open in explorer

Grant another user access to a file or folder. Folder grants cascade to everything inside; a direct grant on a file overrides what it inherited.

GET /permissions/cogs/{id}?include_inherited=true # who has access
POST /permissions/cogs/{id} # grant
PATCH /permissions/{permission_id} # change level
DELETE /permissions/{permission_id} # revoke
curl -s -X POST "$BASE/permissions/cogs/123" -H "$AUTH" \
-H "Content-Type: application/json" \
-d '{"target_type": "cog", "target_id": 123, "user_id": 5, "level": "editor"}'

level is viewer, editor, or owner. Optional expires_at (ISO‑8601) sets an expiry; optional capability overrides (can_upload, can_share, can_delete, …) tune a level.

Workflows

Open in explorer

A workflow is a versioned DAG of nodes. You run a specific version; each run is tracked as a workflow run.

All workflow routes live under the /workflows prefix:

GET /workflows/ # list workflows
POST /workflows/ # create a workflow
GET /workflows/workflow-versions/{version_id}/variables # which extra_variables this version accepts
POST /workflows/workflow-versions/{version_id}/run # execute a version -> 202 Accepted
GET /workflows/workflow-runs/{run_id} # run status + results
GET /workflows/workflow-runs/{run_id}/progress # live progress detail
POST /workflows/workflow-runs/{run_id}/cancel # cancel a run
POST /workflows/workflow-runs/{run_id}/restart # restart a run

Run a workflow version and poll for the result. The body's cog_ids are the input files; extra_variables overrides node values at runtime. The variable names are workflow-specific — call the variables endpoint to see exactly which keys this version accepts:

# Discover the accepted variables (each item has a "variable_name" to use in extra_variables)
curl -s "$BASE/workflows/workflow-versions/42/variables" -H "$AUTH"

# Kick off the run (202 Accepted -> { "workflow_run_id": ..., "cog_ids": [...] })
RUN=$(curl -s -X POST "$BASE/workflows/workflow-versions/42/run" -H "$AUTH" \
-H "Content-Type: application/json" \
-d '{"cog_ids": [123], "extra_variables": {"text_content": "a red sports car"}}' \
| jq -r .workflow_run_id)

# Poll until the status is terminal
curl -s "$BASE/workflows/workflow-runs/$RUN" -H "$AUTH" | jq '{status}'

A run's status moves through queued/running and ends at success, partial_success, failure, or cancelled. The run object carries status, run_files, node_runs, and error_message (there is no progress field on it — use the dedicated /progress endpoint, which returns progress_percentage, completed_nodes/total_nodes, and current_node, for live detail).

Running a workflow needs container-registry (Harbor) credentials configured for your tenant. If a run is rejected for that reason, set them in Settings → Secrets first.

Node definitions

Open in explorer

Nodes are the building blocks of workflows; each references a containerized capability.

GET /node_definitions/ # list available nodes
GET /node_definitions/{id} # node schema (inputs/outputs/params)
POST /node_definitions/ # register a custom node

Note the underscore in /node_definitions/ (not a hyphen).

API keys

Open in explorer

POST /api-keys/token # PUBLIC: exchange an sk_ key for a Bearer token
GET /api-keys/ # list your keys (prefix only, never the secret)
POST /api-keys/ # create {description} -> returns the full key ONCE
DELETE /api-keys/{id} # revoke

Create a key (the secret is returned once, in the access_key field):

curl -s -X POST "$BASE/api-keys/" -H "$AUTH" \
-H "Content-Type: application/json" \
-d '{"description": "CI pipeline"}'
# -> { "id": 7, "key_prefix": "sk_a1b2", "access_key": "sk_xxxx...", ... } (access_key shown once)

Then exchange that access_key for a Bearer token whenever you need to call the API — see Authentication. The first key must be created in the Dashboard (you need a token to call this endpoint, and the token comes from a key).

Billing

Open in explorer

GET /subscriptions # current plan, credits, and usage
curl -s "$BASE/subscriptions" -H "$AUTH" \
| jq '{plan: .plan.name, credits_remaining, credits_used, storage_used_bytes, seats_used, seats_limit}'

Other resources

These follow the same patterns; see /docs for full schemas:

  • GET/POST /workflows/triggers/ — schedule or webhook triggers that launch workflows
  • GET/POST /workflow-templates — reusable workflow starting points
  • GET/POST /secrets/ — tenant secrets (write-only values)
  • GET /platform/analytics/... — usage analytics (admin)
  • GET/POST /workers — worker enrollment and status

Rate limits

SurfaceLimit
Default200 requests / minute / IP
File upload50 requests / minute / user
Worker heartbeat30 requests / minute / worker

Exceeding a limit returns 429 Too Many Requests. Back off and retry with jitter.


Tip: the Dashboard is the fastest way to discover request/response shapes — open your browser's Network tab while clicking around, and you'll see the exact calls. For the authoritative, generated schema, always check /docs.