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/jsonfor request/response bodies (file uploads usemultipart/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/docsdisagree,/docswins.
Authentication
Every endpoint (except a few public ones) requires an OAuth2 Bearer token in the Authorization header. There are two ways to get one.
Option A — API key (recommended for scripts & integrations)
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:
- Open
https://dev-api.modtechlabs.com/docs - Click Authorize (top-right) and provide your Bearer token (the
access_tokenfrom the exchange above). - 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
$BASEand$AUTHfrom this Quick start block.
Conventions
| Topic | Detail |
|---|---|
| Pagination | List endpoints accept ?offset= and ?limit= (default limit 100). |
| IDs | Integer primary keys unless noted. |
| Timestamps | ISO‑8601 UTC, e.g. 2026-06-23T13:25:24Z. |
| Multi-tenancy | Every request is scoped to the caller's tenant automatically. |
| Idempotency | Re-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:
| Status | Meaning |
|---|---|
400 | Bad request / invalid value |
401 | Missing or invalid credentials |
403 | Authenticated but not permitted |
404 | Not found (or not visible to your tenant) |
409 | Conflict (e.g. duplicate name — body includes a code such as name_collision) |
422 | Validation error (Pydantic field-level errors) |
500 | Server error — includes an error_id for support |
{ "detail": "You don't have permission to share this item" }
Account
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)
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
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
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
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
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
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 workflowsGET/POST /workflow-templates— reusable workflow starting pointsGET/POST /secrets/— tenant secrets (write-only values)GET /platform/analytics/...— usage analytics (admin)GET/POST /workers— worker enrollment and status
Rate limits
| Surface | Limit |
|---|---|
| Default | 200 requests / minute / IP |
| File upload | 50 requests / minute / user |
| Worker heartbeat | 30 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.