MFannotWeb
···

HTTP API

The /api/v1 surface lets scripts and CLI clients submit, watch, and retrieve jobs. Everything the web UI does is available here.

Overview

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.

Authentication

Two mechanisms are accepted on every /api/v1 route:

  • Session cookie (sid) — set by POST /api/auth/login from a browser. Cookie callers implicitly hold every scope.
  • Bearer tokenAuthorization: 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.

Scopes

Bearer tokens carry one or more scopes:

ScopeGrants
jobs:readList, fetch, log, events, results, download.
jobs:writeSubmit a new job.
jobs:cancelCancel a queued job.
tokens:manageList, create, revoke API tokens.

Cookie-authed identities return scopes: ["*"] from GET /api/v1. Treat "*" as “all scopes”.

Errors

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"
}
CodeStatusWhen
validation_failed400Bad parameters (gencode, base64, scope name, etc.).
unauthenticated401No cookie or bearer token supplied.
forbidden_scope403Token is valid but missing the required scope.
not_found404Job, token, or file does not exist (or is not yours).
cancellable_only_when_queued409Cancel called on a running/done/failed job.
archive_not_ready409Download or manifest requested before the job finished.
archive_missing404Result archive is no longer on disk (likely expired).
concurrency_limit_exceeded409Per-user in-flight cap reached.
rate_limit_exceeded429Per-principal rate limit tripped.

Rate limits

Limits are keyed per principal (per token, per session, or per IP for the legacy surface).

EndpointLimit
POST /api/v1/jobs30 / hour, 5 / minute
POST /api/v1/jobs/{id}/cancel30 / minute
POST /api/v1/tokens5 / hour
POST /api/auth/signup5 / minute (per IP)
POST /api/auth/login10 / 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.

Identity & usage

GET /api/v1

Whoami. 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/usage

How 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
}

Pipeline discovery

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/pipelines

List 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/gencodes

Full 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/models

RNA model names accepted by the rnaweasel rna parameter. Order is significant.

{
  "models": ["tRNA", "intronI", "intronII", "rnpB", "rrn5", "rns", "ssrA"]
}

Jobs

POST /api/v1/jobs

Submit a new job. JSON body — multipart uploads stay on the legacy /api/jobs route.

Body fields:

FieldTypeNotes
kindstringmfannot or rnaweasel.
gencodeintMust be present in the gencode table above.
rnastring[]Required for rnaweasel; ignored otherwise.
input_filenamestring1–255 chars, used only as a label.
fastastringInline FASTA. Exactly one of fasta/fasta_b64.
fasta_b64stringBase64-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/jobs

List 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}/cancel

Cancel a job that is still queued. Returns 409 if already running or terminal. Required scope: jobs:cancel.

Job object shape

{
  "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.

Event stream

GET /api/v1/jobs/{id}/events

Server-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:

EventPayload
statusInitial snapshot: {"status": "...", "created_at": "..."}
log{"line": "..."} — one per line of pipeline output
stageOptional progress label: {"label": "..."}
doneTerminal success with download_url
errorTerminal 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.

Log access

GET /api/v1/jobs/{id}/log

Fetch 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/stream

Same 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.

Results

GET /api/v1/jobs/{id}/download

Stream the result archive (results.zip). 409 until status is done; 404 after the TTL purge removes the archive.

GET /api/v1/jobs/{id}/results

JSON 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.

Token management

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/tokens

List the caller’s active tokens. The plaintext is never returned here — only at creation.

POST /api/v1/tokens

Create 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.

Example requests

Submit a job

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…"
  }'

Watch the live log

curl -N https://mfannot.example.org/api/v1/jobs/$ID/events \
  -H "Authorization: Bearer $MFW_TOKEN"

Download results

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/.