Skip to content

REST API

The REST API is for apps and custom integrations. Most users should prefer MCP or the CLI, which wrap this.

Base URL

https://api.framehood.ai

api.framehood.ai is the primary REST host. worker.framehood.ai is an equivalent legacy alias kept during the api.* transition — it serves the exact same backend and handlers, so every path below works identically on either host. Prefer api.framehood.ai for new integrations.

Authentication

Send a bearer token on every request:

Authorization: Bearer <token>

A token is either:

  • a session token (what the web console uses), or
  • an API key — create one in the console (Settings → API keys) or via POST /api-keys.

Job lifecycle

Generation is asynchronous. Submit a job, then poll it until it reaches a terminal status (succeeded / failed / cancelled).

Submit a job

POST /v1/jobs/{kind}

{kind} is a model kind (list them with GET /v1/models). The body carries the model inputs:

sh
curl -X POST https://api.framehood.ai/v1/jobs/flux_pro \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"inputs":{"prompt":"a red fox in the snow","out":"fox.jpg"}}'

Add ?wait=25 to block up to 25s for fast jobs and return the finished result inline instead of a job_id.

A queued response:

json
{ "job_id": "job_…", "kind": "flux_pro", "status": "queued",
  "status_url": "/v1/jobs/job_…",
  "next_step": { "tool": "get_status", "args": { "job_id": "job_…" },
    "why": "Poll this job with get_status until done=true; the result URL will then be in outputs." } }

REST vs MCP: next_step

Job responses on both REST and MCP now carry a machine-readable next_step object (#223) — the REST API previously returned a prose string here. REST emits {tool, args, why} (the poll step is always tool: "get_status"); the MCP NextStep type additionally allows an optional action? for other worker-side steps, which REST job responses don't use. In-flight (queued/running) responses include next_step; terminal responses omit it.

Poll a job

GET /v1/jobs/{job_id}

When status is succeeded, the result URLs are in outputs (image_url / video_url / audio_url). When failed, see error.

Poll many jobs at once

GET /v1/jobs/batch?ids=job_a,job_b,…

Up to 50 comma-separated ids per call — the parallel-submit pattern: fire all your submits, collect the ids, then batch-poll every 30–60s until summary.running + summary.queued is 0.

json
{
  "summary": { "total": 3, "queued": 0, "running": 1, "succeeded": 1,
               "failed": 0, "cancelled": 0, "not_found": 1 },
  "jobs": [
    { "job_id": "job_a", "status": "succeeded", "done": true,
      "outputs": { "image_url": "…" }, "credits": 12, "…": "…" },
    { "job_id": "job_b", "status": "running", "done": false, "…": "…" },
    { "job_id": "job_c", "status": "not_found" }
  ]
}

Each entry mirrors the single-job compact shape (done is true once the job is terminal; failed entries carry the structured error {code, message}). An id that doesn't exist — or that belongs to another account — comes back as status: "not_found"; it never fails the whole request. More than 50 ids is a 400.

Other job endpoints

Method & pathPurpose
GET /v1/jobslist your recent jobs (filters: kind, status, since — RFC3339 or unix seconds, limit, cursor)
POST /v1/jobs/{job_id}/cancelcancel a job — a queued job cancels immediately, no charge (200). A running job's cancel is a best-effort request (202): if the model doesn't support cancellation at all it stays running (409); otherwise the request is accepted but the job may still complete and be charged if the model doesn't honor a mid-run stop — poll status for the outcome

Models & guidance

Method & pathPurpose
GET /v1/modelslist available model kinds
GET /v1/models/{kind}input schema for a kind
GET /v1/models/{kind}/prompt-guideprompting guide for a kind
GET /v1/models/{kind}/skillagent skill (usage guidance) for a kind
GET /v1/skillentry-point skill (how to drive the API)
GET /v1/workflowslist available multi-step workflows
GET /v1/workflows/{name}/skillskill for a named workflow
GET /v1/healthliveness ({ "ok": true }) — no auth

Files

Method & pathPurpose
GET /fileslist your stored outputs
GET /files/{key}download a file
DELETE /files/{key}delete a file
POST /files/{key}/publishmake a file public
POST /files/{key}/unpublishmake a published file private again
POST /files/{key}/import-to-libraryadd an uploaded file to your library so it can be assigned to a project (optional JSON body { "name": "…", "project_id": "…" }; returns { ok, asset_id, url, type, size, project_id })
GET /files/public/{userId}/{key}public file (no auth)

import-to-library mints a library asset for a file you already uploaded (PUT /upload, POST /upload-from-url, or the files MCP tool) — it references the existing file (no copy) and is free. Image/video/audio files only. It resolves your OWN private uploads only: a published (public/) key returns 404, and a teammate's file is not reachable (unlike publish/unpublish/delete). Re-importing the same file mints a new library asset each time.

Uploads

Bring your own input (a reference image, audio, etc.) into your private storage, then reference the returned url in a job's inputs.

Method & pathPurpose
PUT /upload?key={key}stream a file body directly to storage
POST /upload-from-urlfetch a remote file into storage ({ "source_url": "…", "key": "…" })

Breaking change: /upload-from-url error responses and accepted sources

POST /upload-from-url now enforces the same rules as the files(import_remote) MCP action above:

  • Only https:// source URLs are accepted (http:// is rejected).
  • Only image, video, and audio files are accepted — a PDF, SVG, or other non-media file that previously stored successfully is now rejected.
  • Error responses changed shape. They used to be a free-text { "error": "<sentence>" }. They are now a structured code: { "error": "<code>", "message": "<detail>" }, where <code> is one of url_not_allowed (400), unsupported_media_type (415), too_large (413), external_file_expired (502), fetch_failed (502). If your integration matched on the old error text, update it to match on error.

The success response shape ({ ok, key, r2_key, url, content_type, size }) and the request body are unchanged.

Billing

Read endpoints are available to any org member. The endpoints that change a subscription, card, or plan are owner-only (enforced server-side; a member gets 403 forbidden).

Method & pathPurpose
GET /billing/balancecurrent credit balance
GET /billing/transactionscredit ledger
GET /billing/subscriptioncurrent subscription (status, allowance, balance, role)
GET /billing/plansavailable credit packages
GET /billing/manageowner: card on file, cancel state, recent invoices (in-app billing)
POST /billing/checkoutowner: start a Stripe Checkout for a package ({ "package": "…" }) → { url }
POST /billing/changeowner: switch the active subscription to another package, prorated ({ "package": "…" })
POST /billing/previewowner: preview the prorated cost + credits of a switch ({ "package": "…" })
POST /billing/cancelowner: cancel at period end, or { "reactivate": true } to resume
POST /billing/topupowner: buy a one-off batch of extra credits now ({ "amount_eur": 20 }, min €20; credits at the extra-usage rate) → a hosted invoice { url } (an owner with a saved card is charged automatically)
GET / PUT /billing/extra-usageowner: view / configure automatic overflow top-ups (Extra usage)
GET /billing/extra-usage/chargesExtra-usage charge history (date, amount, credits, receipt link)
POST /billing/cardowner: open the Stripe card-entry page → { url }
POST /billing/card-finalizeowner: set the just-added card as default ({ "session": "…" })
POST /billing/portalowner: open the Stripe customer portal → { url }

Library & projects

Method & pathPurpose
GET /librarysearch assets (q, type, project, limit, offset)
GET /library/trashlist trashed assets (auto-purged after 10 days)
POST /library/{id}/trash, POST /library/{id}/restoretrash / restore an asset
POST /library/{id}/projectassign to a project ({ "project_id": "…|null" })
GET / POST /projectslist / create projects
GET /projects/activeget your active (default) project
PUT / POST /projects/activeset your active (default) project ({ "project_id": "…|null" })
PATCH / DELETE /projects/{id}rename·revisibility / delete (owner)

Keys, actors, orgs

Actors temporarily disabled

Actor endpoints are part of the actor feature, which is temporarily disabled while we rework it — see Actor models.

Method & pathPurpose
GET / POST /api-keys, DELETE /api-keys/{key}manage API keys (POST body { name?, expires_in_days? }expires_in_days an optional integer 1–3650 day lifetime, omitted = never expires; an expired key stops working)
GET /actorslist your actors
GET /orgs, /orgs/members, /orgs/spend, /orgs/spend/trendorganization info + daily spend
POST /orgs/invitesowner: invite by email; returns the join link ({ "email": "…", "role": "member|owner" })
POST /orgs/invites/acceptaccept an invite ({ "token": "…" })
PATCH /orgs/members/{userId}change role / suspend ({ "role": "admin" } or { "suspended": true })
DELETE /orgs/members/{userId}remove a member (owner)

TIP

The same operations are available over MCP without managing tokens — your client handles auth.

Framehood