The /api/v1 surface lets scripts and CLI clients submit, watch,
and retrieve jobs. Everything the web UI does is available here.
Versioned routes live under /api/v1. They are the documented
contract for programmatic clients. The unversioned /api/* endpoints (used by the web UI for cookie-authed multipart uploads) are not part of this contract and may change without notice.
All responses are JSON unless noted (event streams are SSE; downloads are binary). Timestamps are ISO 8601 in UTC.
Two mechanisms are accepted on every /api/v1 route:
sid) — set by POST /api/auth/login from a browser. Cookie callers
implicitly hold every scope.Authorization: Bearer mfw_…. Tokens have a fixed scope set
assigned at creation and cannot escalate.Anonymous (no auth) calls only work against the legacy /api/jobs surface used by the web form; /api/v1 always requires authentication.
Bearer tokens carry one or more scopes:
| Scope | Grants |
|---|---|
jobs:read | List, fetch, log, events, results, download. |
jobs:write | Submit a new job. |
jobs:cancel | Cancel a queued job. |
tokens:manage | List, create, revoke API tokens. |
Cookie-authed identities return scopes: ["*"] from GET /api/v1. Treat "*" as “all scopes”.
Failures return a non-2xx status with a JSON body. The /api/v1 handlers include a stable code string
that callers can branch on without parsing the human message.
{
"detail": "job is running, not done",
"code": "cancellable_only_when_queued"
} | Code | Status | When |
|---|---|---|
validation_failed | 400 | Bad parameters (gencode, base64, scope name, etc.). |
unauthenticated | 401 | No cookie or bearer token supplied. |
forbidden_scope | 403 | Token is valid but missing the required scope. |
not_found | 404 | Job, token, or file does not exist (or is not yours). |
cancellable_only_when_queued | 409 | Cancel called on a running/done/failed job. |
archive_not_ready | 409 | Download or manifest requested before the job finished. |
archive_missing | 404 | Result archive is no longer on disk (likely expired). |
concurrency_limit_exceeded | 409 | Per-user in-flight cap reached. |
rate_limit_exceeded | 429 | Per-principal rate limit tripped. |
Limits are keyed per principal (per token, per session, or per IP for the legacy surface).
| Endpoint | Limit |
|---|---|
POST /api/v1/jobs | 30 / hour, 5 / minute |
POST /api/v1/jobs/{id}/cancel | 30 / minute |
POST /api/v1/tokens | 5 / hour |
POST /api/auth/signup | 5 / minute (per IP) |
POST /api/auth/login | 10 / minute (per IP) |
There is also a hard cap on concurrent in-flight jobs per user
(default 3). Submitting beyond it returns 409 concurrency_limit_exceeded.
GET /api/v1Whoami. Returns the version and, if authenticated, the caller’s identity and scopes.
{
"version": "1",
"authenticated": true,
"identity": {
"id": 42,
"email": "you@example.org",
"role": "user",
"auth_method": "token",
"scopes": ["jobs:read", "jobs:write"]
}
} GET /api/v1/me/usageHow close you are to your concurrency cap and how many jobs you ran recently. Poll this instead of trying a submit and getting a 409.
{
"concurrent_in_flight": 1,
"concurrent_limit": 3,
"jobs_last_24h": 7,
"jobs_last_7d": 22,
"ttl_days": 7
}The three endpoints in this group are unauthenticated and cacheable. Use them so your client never hard-codes lists that drift out of sync with the server.
GET /api/v1/pipelinesList of available pipelines and their required/optional parameters.
[
{
"kind": "mfannot",
"description": "MFannot mitochondrial / plastid genome annotation",
"required_params": ["gencode"],
"optional_params": []
},
{
"kind": "rnaweasel",
"description": "RNA model search (RNAfinder + CMsearchWrapper for ssrA)",
"required_params": ["gencode", "rna"],
"optional_params": []
}
] GET /api/v1/pipelines/gencodesFull NCBI gencode table. Maps the integer code to its human-readable name. Submissions with a value outside this set are rejected with 400.
{
"1": "Standard",
"2": "Vertebrate Mitochondrial",
"3": "Yeast Mitochondrial",
"4": "Mold, Protozoan, Coelenterate Mitochondrial; Mycoplasma/Spiroplasma",
"5": "Invertebrate Mitochondrial",
"6": "Ciliate, Dasycladacean and Hexamita Nuclear",
"9": "Echinoderm and Flatworm Mitochondrial",
"10": "Euplotid Nuclear",
"11": "Bacterial, Archaeal and Plant Plastid",
"12": "Alternative Yeast Nuclear",
"13": "Ascidian Mitochondrial",
"14": "Alternative Flatworm Mitochondrial",
"15": "Blepharisma Nuclear",
"16": "Chlorophycean Mitochondrial",
"21": "Trematode Mitochondrial",
"22": "Scenedesmus Obliquus Mitochondrial",
"23": "Thraustochytrium Mitochondrial"
} GET /api/v1/pipelines/rnaweasel/modelsRNA model names accepted by the rnaweasel rna parameter. Order is significant.
{
"models": ["tRNA", "intronI", "intronII", "rnpB", "rrn5", "rns", "ssrA"]
}POST /api/v1/jobsSubmit a new job. JSON body — multipart uploads stay on the legacy /api/jobs route.
Body fields:
| Field | Type | Notes |
|---|---|---|
kind | string | mfannot or rnaweasel. |
gencode | int | Must be present in the gencode table above. |
rna | string[] | Required for rnaweasel; ignored otherwise. |
input_filename | string | 1–255 chars, used only as a label. |
fasta | string | Inline FASTA. Exactly one of fasta/fasta_b64. |
fasta_b64 | string | Base64-encoded FASTA bytes; useful when escaping is awkward. |
Optional header Idempotency-Key: <1–200 chars> de-dupes
retries for 24 hours. Reusing a key returns the originally-created job.
Required scope: jobs:write. Returns 201 with a JobOutV1.
GET /api/v1/jobsList the caller’s jobs (most recent 100, newest first). Required scope: jobs:read.
GET /api/v1/jobs/{id}Fetch a single job. Returns 404 if the job does not exist or is not yours. Required scope: jobs:read.
POST /api/v1/jobs/{id}/cancelCancel a job that is still queued. Returns 409 if already running or terminal. Required scope: jobs:cancel.
{
"id": "f8c1…",
"kind": "mfannot",
"status": "queued | running | done | failed | cancelled | expired",
"params": { "gencode": 4, "rna": [] },
"input_filename": "mito.fasta",
"created_at": "2026-05-14T19:02:11Z",
"started_at": null,
"finished_at": null,
"error_msg": null,
"links": {
"self": "/api/v1/jobs/f8c1…",
"events": "/api/v1/jobs/f8c1…/events",
"log": "/api/v1/jobs/f8c1…/log",
"download": null,
"results": null,
"cancel": "/api/v1/jobs/f8c1…/cancel"
}
} links.download, links.results, and links.cancel are null when the corresponding
action isn’t valid for the current status — clients should follow
them rather than constructing URLs by hand.
GET /api/v1/jobs/{id}/eventsServer-Sent Events. Replays up to 500 lines of run.log and then tails live until the job reaches a terminal state. Required scope: jobs:read.
Event types emitted:
| Event | Payload |
|---|---|
status | Initial snapshot: {"status": "...", "created_at": "..."} |
log | {"line": "..."} — one per line of pipeline output |
stage | Optional progress label: {"label": "..."} |
done | Terminal success with download_url |
error | Terminal failure (failed / cancelled / expired) with error_msg |
Keep-alive comments (: keep-alive) are emitted every ~15 s so intermediate proxies don’t drop the connection.
GET /api/v1/jobs/{id}/logFetch the run log as a single response. Query parameters:
tail=N — return only the last N lines (1–100 000).format=text|ndjson — default text; ndjson emits one {"line": "..."} per line.Required scope: jobs:read. Returns 404 if the job hasn’t started or predates log persistence.
GET /api/v1/jobs/{id}/log/streamSame content as /log but streamed as plain text or NDJSON
(no SSE framing). Useful for tail-style consumers that don’t want
SSE’s event headers.
GET /api/v1/jobs/{id}/downloadStream the result archive (results.zip). 409 until status is done; 404 after the TTL purge removes the archive.
GET /api/v1/jobs/{id}/resultsJSON manifest of every file in the archive with byte counts and SHA-256 digests.
{
"files": [
{ "name": "mfannot.out", "size_bytes": 18432, "sha256": "…" },
{ "name": "mfannot.html", "size_bytes": 41021, "sha256": "…" }
]
} GET /api/v1/jobs/{id}/results/{filename}Stream a single file out of the archive without downloading the whole zip. The filename must match an entry in the manifest exactly.
Self-service token CRUD. Browser users hit these via cookie auth; tokens
can also call them, but only if they themselves hold tokens:manage. A read-only token cannot mint a write token.
Each user is capped at 10 active (non-revoked) tokens.
GET /api/v1/tokensList the caller’s active tokens. The plaintext is never returned here — only at creation.
POST /api/v1/tokensCreate a new token.
POST /api/v1/tokens
Content-Type: application/json
{
"name": "ci-uploads",
"scopes": ["jobs:read", "jobs:write"],
"expires_in_days": 90
} The response includes a one-time plaintext field — store it immediately, it cannot be retrieved again.
{
"id": "5b2e…",
"name": "ci-uploads",
"prefix": "mfw_aBcD",
"scopes": ["jobs:read", "jobs:write"],
"created_at": "2026-05-14T19:02:11Z",
"last_used_at": null,
"expires_at": "2026-08-12T19:02:11Z",
"plaintext": "mfw_aBcD…full-token…"
} DELETE /api/v1/tokens/{id}Revoke a token. Idempotent: revoking an already-revoked token is a 204 no-op.
curl -X POST https://mfannot.example.org/api/v1/jobs \
-H "Authorization: Bearer $MFW_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: nightly-2026-05-14" \
-d '{
"kind": "mfannot",
"gencode": 4,
"rna": [],
"input_filename": "sample.fasta",
"fasta": ">seq1\nACGTACGT…"
}' curl -N https://mfannot.example.org/api/v1/jobs/$ID/events \
-H "Authorization: Bearer $MFW_TOKEN" curl -L -o results.zip \
https://mfannot.example.org/api/v1/jobs/$ID/download \
-H "Authorization: Bearer $MFW_TOKEN" A first-party CLI (mfw) wraps these endpoints with no
runtime dependencies beyond Python’s stdlib. See the source in cli/.