Skip to Content
ReferenceApp API

App API

This page documents HTTP endpoints exposed by an installed SeqDesk instance. They are the endpoints the SeqDesk interface itself calls, which makes them stable enough to script against — but it also means they are shaped for the interface, not designed as a public REST product. Read the response shapes here rather than assuming a convention.

For the public seqdesk.org endpoints (pipeline catalog, MIxS registry, release feed) see the Pipeline Registry API instead. Nothing on this page is served by seqdesk.org, and nothing on that page can see your data.

The auth model in one paragraph

There is no API-key mechanism and no middleware. Every route calls getServerSession() itself and makes its own decision, so authentication means sending a valid NextAuth session cookie. Four patterns recur:

PatternTypical response when it fails
Any authenticated user401 {"error":"Unauthorized"}
Facility admin only403 {"error":"Unauthorized"} — note the status, not 401
Facility admin, and not a demo session403 with a specific message, e.g. "Pipeline execution is disabled in the public demo."
Shared secret, no sessionthe weblog endpoint only

Because each route decides independently, the same failure can be a 401 on one endpoint and a 403 on another. The per-endpoint sections below state which.

A handful of endpoints are deliberately unauthenticated because they run before anyone can log in or because they contain no facility data: GET /api/version, GET /api/setup/status, GET /api/mixs-checklists, and GET /api/sequencing-tech. Everything else requires a session — including GET /api/modules, which reports which modules an instance has enabled and is therefore not something to expose to an anonymous caller.

Instance metadata

GET /api/version

The installed version, read from the release’s package.json. No session required. Useful as a liveness probe and for asserting which release a host is actually running after an update.

curl -s https://your-host/api/version
{ "version": "1.1.125" }

Do not confuse this with GET /api/version on seqdesk.org, which returns the latest published release and has an entirely different shape.

GET /api/setup/status

The install-status probe the setup and login pages poll before anyone can sign in. Unauthenticated by design, and deliberately narrow: it reports database reachability and bootstrap progress, never account addresses.

Top-level keys: exists, configured, phase, steps, nextAction, database, install, plus bootstrapAccounts when a seed pass ran and error when something failed. The response is served Cache-Control: no-store.

GET /api/modules

Which optional modules this instance has enabled. Requires a session — any signed-in role, because the forms every user sees are module-driven. Without a session cookie it answers 401 {"error":"Unauthorized"}.

{ "modules": { "ai-validation": true, "mixs-metadata": true, "account-validation": false, "funding-info": false, "billing-info": false, "ena-sample-fields": true, "dynamic-studies": false, "notifications": false }, "globalDisabled": false }

modules is the stored configuration merged over the built-in defaults, so every module id that has a default is always present, whatever the instance saved. An instance that has never saved a module configuration gets exactly the defaults shown above. Changing the configuration is a separate admin-only endpoint; see Administration → Modules.

Form schemas

The dynamic form configuration is exposed read-only so external tooling can render the same field set the SeqDesk interface renders.

GET /api/form-schema

Returns the sequencing-order form schema as configured in the Order Form Builder. Requires a session (401 otherwise).

{ "fields": [ { "id": "field_library_strategy", "name": "library_strategy", "type": "select", "options": ["WGS", "AMPLICON"], "required": true, "groupId": "group_sequencing", "visible": true, "perSample": false } ], "groups": [ { "id": "group_sequencing", "name": "Sequencing Information", "icon": "Dna", "order": 1 } ], "version": 3, "enabledMixsChecklists": ["ERC000022"], "perSampleFields": [ { "id": "field_barcode", "name": "barcode", "type": "barcode", "perSample": true, "visible": true } ] }

Three things about this response are easy to get wrong:

  • version is an integerOrderFormConfig.version, which starts at 1 and is bumped on every save. It is not a timestamp. When no form has been saved yet, the endpoint returns the built-in default schema with version: 1.
  • perSampleFields contains whole field objects, not names. It is the subset of fields where perSample && visible, precomputed so callers do not have to re-derive it.
  • The field list is filtered twice: by enabled modules, then by role. Fields marked adminOnly are omitted entirely for researchers, and enabledMixsChecklists comes back as [] unless the mixs-metadata module is on.

GET /api/study-form-schema

The study metadata form. Requires a session.

GET /api/study-form-schema GET /api/study-form-schema?studyId=<id>

Returns { fields, studyFields, perSampleFields, groups, modules } — note that unlike the order schema there is no version and no enabledMixsChecklists key. studyFields and perSampleFields split the field list into study-level and sample-level; modules reports which modules’ fields are currently contributing.

?studyId= matters only when the dynamic-studies module is enabled: it resolves that study’s own StudyFormConfig row instead of the global study form.

MIxS checklists

MIxS  is the Genomic Standards Consortium’s Minimum Information about a Sequence checklist family. SeqDesk ships checklist definitions, admins enable specific checklists in the form builder, and researchers then see the corresponding required fields.

GET /api/mixs-checklists

Lists the active checklists. Unauthenticated.

curl -s https://your-host/api/mixs-checklists
{ "checklists": [ { "name": "soil", "accession": "ERC000022", "description": "GSC MIxS soil environmental package", "fieldCount": 53, "mandatoryCount": 12, "deprecated": false } ], "total": 16, "version": 4 }

total is the number of returned checklists, and version is the MIxS registry version currently active on this instance — the same integer a study pins in Study.mixsVersion.

Single-checklist lookup returns the full definition (every field, its data type, unit options, MIxS section, and whether it is mandatory), not the summary above:

curl -s "https://your-host/api/mixs-checklists?accession=ERC000022" curl -s "https://your-host/api/mixs-checklists?name=soil" curl -s "https://your-host/api/mixs-checklists?accession=ERC000022&version=3"

?version= resolves the definition as it existed at that registry version, which is how a study pinned to an older MIxS release keeps rendering the fields it was authored with. An unknown accession or name returns 404 {"error":"Checklist not found: ERC000022"}.

GET /api/mixs-templates

The same definitions in template form, ready to be embedded in the form builder. Requires a session (401 otherwise). ?name=<checklist> narrows to one template and falls back to fuzzy matching — normalized name equality, then substring matching — before giving up with 404 {"error":"Template not found"}. When several templates match, the one with the most fields wins. ?version= behaves as above.

Sequencing uploads

The sequencing tab uses a three-call chunked protocol when reads or artifacts are attached to a sample. All three calls are facility-admin only and blocked in the public demo. Mimic the same flow when bulk-loading data.

The order must be SUBMITTED or COMPLETED — otherwise you get 400 {"error":"Sequencing data can only be managed on submitted or completed sequencing orders"}.

Initiate

POST /api/orders/[id]/sequencing/uploads

{ "targetKind": "read", "targetRole": "R1", "originalName": "S1_R1.fastq.gz", "expectedSize": 1843921023, "checksumProvided": "6f1b9c0d4e2a...", "mimeType": "application/gzip", "sampleId": "cm2x7q9r80001abcd", "metadata": { "dataClass": "raw", "sequencingRunId": "cm2x7qa1z0002abcd" } }

targetKind, targetRole, originalName, and expectedSize are required; a missing one returns 400 {"error":"targetKind, targetRole, originalName, and expectedSize are required"}. For targetKind: "read", targetRole must be R1 or R2 and the filename must carry one of the configured sequencing-file extensions (.fastq.gz, .fq.gz, .fastq, .fq by default) — otherwise "Read uploads must use an allowed sequencing file extension".

{ "success": true, "uploadId": "cm2x7qb4k0003abcd", "tempPath": "orders/<orderId>/.tmp/cm2x7qb4k0003abcd/S1_R1.fastq.gz", "status": "PENDING", "receivedSize": 0 }

Send chunks

PATCH /api/orders/[id]/sequencing/uploads/[uploadId]

The chunk is the raw request body, and the byte offset goes in the x-seqdesk-offset header. There is no multipart wrapper and no JSON envelope.

curl -X PATCH "https://your-host/api/orders/$ORDER/sequencing/uploads/$UPLOAD" \ -H "x-seqdesk-offset: 0" \ --data-binary @chunk-0000 \ --cookie "$COOKIE_JAR"
{ "success": true, "uploadId": "cm2x7qb4k0003abcd", "receivedSize": 8388608, "expectedSize": 1843921023, "status": "UPLOADING" }

The offset must equal the server’s current receivedSize exactly. Anything else is rejected with 400 {"error":"Upload offset does not match current upload size"} — which is also how you resume: read receivedSize back and continue from there. Status flips from UPLOADING to READY automatically once receivedSize reaches expectedSize.

Complete

POST /api/orders/[id]/sequencing/uploads/[uploadId]/complete

No body. The server promotes the file out of .tmp into its final location and, for a read upload, creates or updates the sample’s Read record.

{ "success": true, "uploadId": "cm2x7qb4k0003abcd", "finalPath": "orders/<orderId>/reads/S1/...", "size": 1843921023, "status": "COMPLETED" }

An incomplete upload fails with 400 {"error":"Upload is incomplete"}; a read upload with no sampleId fails with 400 {"error":"Read uploads require a target sample"}.

To abandon a session, DELETE /api/orders/[id]/sequencing/uploads/[uploadId] removes the partial temp file and marks the row CANCELLED.

checksumProvided is recorded, not verified. The value you supply at initiate is stored on the resulting Read or SequencingArtifact as its checksum; nothing recomputes the MD5 during complete. If you want a verified checksum, run POST /api/files/checksum afterwards — it hashes the file on disk and overwrites the stored value.

File operations

These endpoints back the in-app file browser and downloader. They respect demo-session restrictions (Demo mode) and the delivery-publication rules.

GET /api/files/download?path=<rel>

Streams a file as application/octet-stream with a Content-Disposition attachment header. When the path resolves to a Read, the response also carries X-SeqDesk-Read-Data-Class so a client can tell raw from cleaned data without a second request.

Requires a session. Demo sessions get 403 {"error":"Downloads are disabled in the public demo."}.

The path is relative to the configured data base path — absolute paths and .. traversal are rejected with 400 {"error":"Invalid file path"}, and an unconfigured data base path is 400 {"error":"Data base path not configured"}.

Authorization is per-file, not per-role-only. A facility admin may download anything with an allowed extension. A researcher must satisfy one of:

File isResearcher may download when
a Readthey own the order, the delivery has been published (sequencingFilesPublishedAt is set), and the read’s dataClass is cleaned
a SequencingArtifactthey own the order, the delivery has been published, and visibility is customer
an AssemblyallowUserAssemblyDownload is enabled, they own the order or the study, and the order is COMPLETED

Assemblies are additionally gated by extension (.fa, .fasta, .fna, and their .gz variants). A researcher hitting an assembly while the setting is off gets 403 {"error":"Assembly downloads are disabled by the facility administrator."}; any other failure is 403 {"error":"Access denied"}.

GET /api/files/preview?path=<path>

Serves a file inline for in-browser viewing — HTML QC reports are the main use. Allowed extensions are html, htm, pdf, txt, tsv, csv, log, json, and the file must be at most 100 MB. Access is validated against the pipeline run the file belongs to, using the same rule as the run endpoints below. In demo mode the endpoint serves generated stand-in reports instead of reading disk.

Unlike download, preview also accepts an absolute path (still checked against traversal), because pipeline artifacts are recorded with absolute run paths.

POST /api/files/checksum

Facility-admin only (403 {"error":"Only facility admins can calculate checksums"}). Computes MD5 for up to 50 paths per call and writes each result onto the matching Read record. Checksums are raw lowercase hex with no md5: prefix.

// Request { "filePaths": ["orders/abc/S1_R1.fastq.gz", "orders/abc/S1_R2.fastq.gz"] }
// Response { "success": true, "results": [ { "filePath": "orders/abc/S1_R1.fastq.gz", "checksum": "6f1b...", "updatedReadRecord": true }, { "filePath": "orders/abc/S1_R2.fastq.gz", "checksum": "a3d9...", "updatedReadRecord": false, "warning": "No assigned read record found; checksum was not stored in database" } ], "summary": { "total": 2, "successful": 2, "failed": 0, "updatedReadRecords": 1, "notLinkedToRead": 1 } }

Per-file failures do not fail the request: a path outside the data base path, or a missing file, comes back as a results entry with an error field while the rest are still hashed. More than 50 paths is a hard 400 {"error":"Maximum 50 files at a time"}.

POST /api/files/delete

Facility-admin only (401 here, not 403). Deletes each path from disk and repairs the referencing Read records: if the deleted file was the read’s only file the row is deleted, otherwise the corresponding file1/file2 column is cleared and the row survives. Already-deleted entries are reported per path in errors rather than aborting the batch.

Pipeline runs

A run is created first and launched second, which is why there are two calls. Both go through /api/pipelines/runs.

GET /api/pipelines/runs

Lists runs. Requires a session. Facility admins see everything; researchers see only runs whose target they own and which have been selected as the final result for that target. Query parameters: pipelineId, status, studyId, orderId, limit (default 50), offset (default 0), and publishedOnly=true (aliases: userVisible=true, visible=user), which applies the researcher-visibility filter to an admin listing too.

Returns { "runs": [ … ], "total": <n>, "limit": <n>, "offset": <n> }. Each run is enriched with its resolved result files, so the listing is heavier than a raw PipelineRun row.

POST /api/pipelines/runs

Creates a pending run. Facility-admin only (403). It validates aggressively before writing anything, so most integration mistakes surface here rather than at launch.

{ "pipelineId": "fastqc", "orderId": "cm2x7q8lp0000abcd", "sampleIds": ["cm2x7q9r80001abcd", "cm2x7q9r80002abcd"], "config": {}, "executionMode": "local" }
{ "success": true, "run": { "id": "cm2x7qc9v0004abcd", "runNumber": "FASTQC-1776240000000-K3Q7M", "status": "pending", "pipelineId": "fastqc", "studyId": null, "orderId": "cm2x7q8lp0000abcd", "targetType": "order", "executionMode": "local" } }
ConditionResponse
Neither or both of studyId / orderId400 {"error":"Exactly one of studyId or orderId is required"}
Unknown pipelineId400 {"error":"Invalid pipeline ID"}
Pipeline installed but not enabled403 {"error":"Pipeline fastqc is disabled"}
Pipeline does not support that target400 {"error":"Pipeline mag does not support order targets"}
sampleIds not in the target400 {"error":"Invalid sample IDs: …"}
executionMode other than default/local/slurm400 {"error":"executionMode must be one of: default, local, slurm"}
Metadata, input, or config validation fails400 {"error":"…validation failed","details":["…"]}
Target does not exist404 {"error":"Study not found"} / 404 {"error":"Sequencing Order not found"}

The runNumber you get back is provisional. It is replaced with the canonical {PIPELINE}-YYYYMMDD-NNN form when the run is actually staged — see PipelineRun.

POST /api/pipelines/runs/[id]/start

Stages and launches the run. Facility-admin only, blocked in the demo. The body is optional; {} or no body is fine. Recognized keys are sampleIds (only used when the run was created without a selection) and executionMode.

The response differs by execution backend:

// SLURM { "success": true, "status": "queued", "jobId": "184312", "runFolder": "/data/pipeline_runs/FASTQC-20260430-001", "executionMode": "slurm", "warnings": [] }
// Local { "success": true, "status": "running", "pid": 48213, "runFolder": "/data/pipeline_runs/FASTQC-20260430-001", "executionMode": "local", "message": "Pipeline started in background. Check the Analysis dashboard for status.", "warnings": [] }
ConditionResponse
Run is not pending400 {"error":"Cannot start run with status: running"}
Another request already claimed the launch409 {"error":"Run has already been started (status: queued)"}
Data base path or run directory unset400 with the specific message
Metadata / config validation fails at launch400 {"error":"…validation failed","details":[…]} and the run is moved to failed
Execution itself fails500 {"error":"<launcher message>"} and the run is moved to failed

The 409 is not a race you need to handle by retrying — the launch claim is a conditional update, so exactly one caller wins and a double-click can never double-start a run.

GET /api/pipelines/runs/[id]

Run details. Requires a session plus read access to the run (below).

DELETE /api/pipelines/runs/[id]

Cancels a run — it does not delete it. Facility-admin only, blocked in the demo. Only pending, queued, and running runs can be cancelled; anything else is 400 {"error":"Cannot cancel a completed or failed run"}. Cancelling a local run signals the whole process group; cancelling a SLURM run calls scancel. Returns { "success": true, "status": "cancelled" }, or { "success": true, "status": "…", "alreadyFinalized": true } when the run had already reached a terminal state in the meantime.

POST /api/pipelines/runs/[id]/delete

Actually deletes the run record and cleans up its output data. Facility-admin only. A running run is refused with 400 {"error":"Cannot delete a running run. Cancel it first."}.

GET /api/pipelines/runs/[id]/logs

GET /api/pipelines/runs/[id]/logs?type=output&tail=200

type is output (default) or error, mapping to logs/pipeline.out and logs/pipeline.err. tail defaults to 100 lines. The endpoint prefers the file on disk and falls back to the cached tail stored on the run — fromFile tells you which you got. While the run is running it also parses the Nextflow trace and returns per-process progress:

{ "content": "…last 200 lines…", "fromFile": true, "status": "running", "progress": 62, "currentStep": "Running MEGAHIT...", "steps": [ { "process": "MAG:ASSEMBLY:MEGAHIT", "status": "RUNNING", "tasks": 4 } ] }

POST /api/pipelines/runs/[id]/sync

Reconciles a run against its execution backend — the SLURM queue or local process, plus the on-disk trace file. Requires a session and read access to the run.

// Trace file present { "success": true, "synced": true, "progress": 62, "processes": 7, "tasks": 41, "currentStep": "Running MEGAHIT..." }
// No trace yet (still queued, or the run never produced one) { "success": true, "synced": false, "message": "No trace file found yet", "status": "queued", "queueStatus": "PENDING", "queueSource": "squeue" }

400 {"error":"Run folder not set"} if the run was never staged.

Read access to a run is narrower than “owns the target”. A facility admin can read any run. A researcher can read a run only if they own its study or order and the run has been selected as the final result (a PipelineResultSelection row exists for it). Otherwise the endpoint returns 403 {"error":"Forbidden"}. This applies to GET /[id], logs, and sync.

PUT /api/pipelines/runs/[id]/selection

Marks a completed run as the final result for its pipeline + target, upserting a PipelineResultSelection. Facility-admin only (403), blocked in the demo. This is also what makes the run visible to the researcher who owns the target, so it is the publication step, not just a bookmark.

Returns { "success": true, "selection": { …, "selectedBy": { "id", "firstName", "lastName", "email" } } }.

ConditionResponse
Run is not completed400 {"error":"Only completed pipeline runs can be selected as final."}
Run has no study or order400 {"error":"Pipeline run does not have a study or order target."}
Unknown run404 {"error":"Run not found"}

Selecting a second run for the same pipeline and target silently replaces the first — the (pipelineId, targetKey) unique constraint guarantees one winner.

DELETE /api/pipelines/runs/[id]/selection

Clears the selection, but only if this run is the currently selected one. Returns { "success": true, "cleared": true }, or cleared: false when the target’s selection pointed at a different run.

POST /api/pipelines/runs/[id]/resolve-outputs

Re-runs output discovery for a completed or failed run and writes the resulting Assembly, Bin, and PipelineArtifact records. Facility-admin only, blocked in the demo. Use it when a run finished but its results never appeared — usually because the weblog callback was lost.

{ "success": true, "discovered": { "assembliesFound": 1, "binsFound": 12, "artifactsFound": 4, "reportsFound": 2 }, "resolved": { "assembliesCreated": 1, "binsCreated": 12, "artifactsCreated": 4 }, "errors": [], "warnings": [] }

discovered is the adapter’s own summary. The generic manifest-driven adapter reports the four …Found counts above; a pipeline that ships a bespoke adapter may report a different set of keys, so treat this block as informational rather than as a fixed schema. resolved is always the three …Created counts.

Re-running it is safe: the unique constraints on Assembly, Bin, and PipelineArtifact make resolution idempotent.

ConditionResponse
Run is neither completed nor failed400 {"error":"Can only resolve outputs for completed or failed runs"}
runFolder is unset400 {"error":"Run folder not set"}
Target has no samples400 {"error":"No samples found for this run"}
No adapter and no manifest to build one from400 {"error":"No adapter found for pipeline: <id>"}

GET / POST /api/pipelines/runs/[id]/pending-writebacks

The review-and-promote flow for order-scoped runs that stage read candidates instead of writing reads directly — read cleaning is the canonical case. Both verbs are facility-admin only (403); POST is additionally blocked in the demo.

GET lists what is waiting:

{ "run": { "id": "…", "runNumber": "READ-CLEANING-20260430-002", "pipelineId": "read-cleaning", "status": "completed", "orderId": "…" }, "readCandidates": [ { "artifactId": "…", "outputId": "cleaned_reads", "outputLabel": "Cleaned reads", "sampleId": "…", "sampleCode": "S1", "file1": "…/S1_R1.trimmed.fastq.gz", "file2": "…/S1_R2.trimmed.fastq.gz", "readLayout": "paired", "targetDataClass": "cleaned", "status": "candidate", "currentRead": { "id": "…", "file1": "…", "file2": "…", "dataClass": "raw", "dataClassLabel": "Raw / protected", "isProtectedRaw": true } } ], "reports": [ { "id": "…", "name": "multiqc_report.html", "path": "…", "outputId": "qc_summary" } ], "review": { "title": "Review pending read outputs", "…": "UI copy strings" } }

POST promotes them. Optional body { "sampleIds": ["…"] } scopes the promotion; omit it to promote every candidate that is not already promoted.

{ "success": true, "promoted": 2, "readIds": ["cm2x7qd…", "cm2x7qe…"] }

Promotion copies the candidate files into the order’s canonical read location, creates a new active Read, and supersedes the previous active read rather than deleting it. A candidate can never claim a protected data class — see Read.

ConditionResponse
Run is not order-scoped400 {"error":"Pending read promotion requires an order-scoped run"}
Nothing left to promote400 {"error":"No pending read candidates selected for promotion"}
Unknown run404 {"error":"Pipeline run not found"}

GET / POST /api/pipelines/runs/[id]/cleaned-reads

The narrower, read-cleaning-specific variant of the same review flow, kept for the dedicated cleaned-reads panel. Same auth (403 for non-admins, POST blocked in the demo) and the same { "sampleIds": [...] } body. Errors follow the same convention: 404 when the message contains “not found”, 400 otherwise.

Pipeline weblog

Nextflow runs POST their execution events to this endpoint. It also accepts events from other adapters (SLURM trace, queue polling, internal step transitions). See Pipeline Runtime for the client-side configuration.

POST /api/pipelines/weblog?runId=<id>&token=<secret>

The target run is identified by the runId query parameter, and the call is authenticated by the shared weblog secret in the token query parameter. There is no session cookie and no runId in the body.

ConditionStatus
No weblog secret configured on the instance503 {"error":"Weblog secret is not configured"}
token does not match403 {"error":"Invalid token"}
Missing runId400 {"error":"runId is required"}
Unknown runId404 {"error":"Run not found"}

The secret is set under Admin → Application Settings → Pipeline execution. Until it is set the endpoint fails closed with 503, so this state-mutating webhook is never left unauthenticated.

curl -X POST "https://your-host/api/pipelines/weblog?runId=cm2x7qc9v0004abcd&token=$WEBLOG_SECRET" \ -H "Content-Type: application/json" \ -d '{ "event": "process_completed", "trace": { "process": "MAG:ASSEMBLY:MEGAHIT", "status": "COMPLETED", "exit": 0, "duration": "00:43:12", "%cpu": "812", "rss": "12.3 GB" } }'

On success the endpoint returns { "success": true }.

How a payload is interpreted

The event name is read from event, eventType, or type, lowercased, and matched by substringworkflow_complete, process_start, process_completed, workflow_error and their variants all work. Process and trace details come from trace, falling back to task. The event timestamp is taken from utcTime, timestamp, or the trace’s complete/start/submit fields, and is clamped to the receipt time if it is more than 6 hours in the future or more than 30 days in the past.

Behaviour worth knowing before you build an adapter:

  • Terminal runs are never resurrected. Once a run is not in pending, queued, or running, a late workflow_complete no longer changes its status or timestamps. The event row is still recorded.
  • A failed process is not a failed run. errorStrategy 'ignore' is legitimate, so a process_failed event sets currentStep and leaves the run active; only a workflow-level event finalizes it.
  • Completion waits for outputs. On workflow_complete the endpoint first checks the scheduler: if the job is still active the run stays running at 99%. Otherwise it resolves outputs, and only marks the run completed if that succeeded.
  • Duplicate suppression. An event identical to one already stored within ±2 seconds is dropped.
  • Retention. Only the newest 100 events per run are kept; older rows are deleted in the same transaction.
  • Payload truncation. The stored payload is capped at 12,000 characters and message at 500.

Event sources

PipelineRunEvent.source records where an event came from. This endpoint always writes weblog; the other values are written by internal code paths.

SourceMeaning
weblogNextflow -with-weblog payload posted here
traceParsed trace.txt row, written by sync or the monitor
queueSLURM queue poller, e.g. a job moving PD → R
processInternal step lifecycle event
launcherWritten at launch, e.g. preparation_warning

eventType is not gate-kept — an unrecognized value is stored verbatim, so a custom adapter can define its own.

Sequencing run import

POST /api/orders/[id]/sequencing/runs/import

Facility-admin only. Accepts a multipart form with an Excel attachment under the field name file, describing a run plan of (run, sample, barcode) rows. This is what the Import Excel button on the sequencing tab calls. CSV is not accepted — the body is parsed with ExcelJS.

Limits: 5 MB, 1000 data rows, 80 columns. Exceeding the size limit returns 413; the others return 400.

The worksheet is resolved by name, in order: Run Samples, Samples, Tabelle2, then the first worksheet with more than one row. Header cells are normalized (lowercased, punctuation stripped, µ folded to u) and matched against an alias table, so real-world spreadsheets usually map without editing:

Spreadsheet header (any of)Mapped field
Run, Run ID, Run name, Sequencing runrunId
Sample, Sample ID, Sample code, Patient, Patient ID, Internal sample codesampleCode
Barcode, Barcode ID, Barcode namebarcode
Material, Body sitematerial_body_site
Date, Sampling datesampling_date
DNA ng/µl, Concentration ng/µlconcentration_ng_ul
Storage box, Storage position, Bufferstorage_box, storage_position, storage_buffer

Unrecognized columns are not dropped — they come back in unmappedColumns and per row in unmapped, so you can see what was ignored.

This is a two-phase flow. Without ?apply=true the endpoint returns a preview and never mutates anything:

{ "sheet": "Run Samples", "rows": [ { "rowNumber": 2, "runId": "RUN-2026-04-30-001", "sampleCode": "S1", "barcode": "BC01", "customFields": { "material_body_site": "stool" }, "unmapped": { "Notes": "repeat extraction" } } ], "rowCount": 1, "unmappedColumns": ["Notes"], "missingSamples": ["S99"], "duplicateBarcodes": [ { "runId": "RUN-2026-04-30-001", "barcode": "BC01", "count": 2 } ], "rowErrors": [ { "rowNumber": null, "message": "Sample not found on this order: S99" } ], "applyReady": false }

applyReady is true only when there is at least one row and rowErrors is empty. Rows missing a run or sample column produce a row-level error; missing samples and duplicate barcodes produce errors with rowNumber: null because they are detected across the whole sheet.

With ?apply=true and a clean preview, the endpoint upserts SequencingRun and SequencingRunSample rows and returns the preview fields plus:

{ "success": true, "createdOrUpdated": [ { "runId": "RUN-2026-04-30-001", "assignments": 2 } ] }

Applying also splits the mapped custom fields: fields that belong to the ONT sample field set are merged into Sample.customFields, and everything else is stored on the SequencingRunSample row. A field like sampling date therefore follows the sample, while a run-specific note stays with the assignment.

Sending apply=true while rowErrors is non-empty returns 400 with {"error":"Import contains rows that need review before saving", …preview} — the full preview is echoed so a client can render the problems without re-uploading.

Live stream (MinKNOW ingest)

These endpoints drive live Oxford Nanopore ingest. The read endpoints accept a facility-admin read session (demo sessions may look); the mutating ones require a full facility-admin session. See the stream data models.

GET /api/orders/[id]/stream

The order’s stream runs, newest first, up to 50, each with its most recent event. totalBases is serialized as a string because it is a BigInt. This is the list behind the Stream page’s run history strip, so stopped runs in the response are readable in the interface; runs older than the 50 returned here are reachable only through this API.

{ "runs": [ { "id": "cm2x7qf…", "orderId": "cm2x7q8lp0000abcd", "minknowRunId": null, "flowCellId": "FAX12345", "deviceId": "MN12345", "outputDir": "/data/minknow/exp1/no_sample/20260430_1200", "status": "ACTIVE", "totalBases": "18342190231", "totalReads": 412003, "barcodeMap": { "barcode01": "S1" }, "startedAt": "2026-04-30T12:00:00.000Z", "lastSeenAt": "2026-04-30T13:42:11.000Z", "stoppedAt": null, "latestEvent": { "kind": "FILE_INGESTED", "ts": "2026-04-30T13:42:11.000Z", "payload": { } } } ] }

POST /api/orders/[id]/stream

Starts a stream run. outputDir is required and must resolve — after symlink resolution — under the configured MinKNOW output root. That containment check runs once, here; from then on the boundary is held by the monitor’s watcher, which does not follow symlinks. Optional: deviceId, flowCellId, minknowRunId, and a barcodeMap object (keys are lowercased on write).

Returns 201 with { "id": "cm2x7qf…" }.

ConditionResponse
outputDir missing400 {"error":"outputDir is required"}
outputDir outside the configured root, or unresolvable400 with the validation reason
Another ACTIVE run already watches that directory409 naming the conflicting run and order

The conflict check, the run insert, and the RUN_STARTED event run in one SERIALIZABLE transaction, so two simultaneous starts cannot both succeed — the loser also gets a 409.

GET /api/orders/[id]/stream/[streamRunId]/events

Cursor-paginated event feed. Without a cursor it returns the newest events (the initial live-tail load); with ?after=<seq> it pages forward from that sequence. ?limit=<n> is clamped to [1, 500], default 100. Events are always presented newest-first regardless of paging direction.

{ "events": [ { "id": "…", "seq": 42, "ts": "2026-04-30T13:42:11.000Z", "kind": "FILE_INGESTED", "payload": { "barcode": "barcode01", "reads": 4000 } } ], "cursor": 42 }

Advance your client cursor to the returned cursor and poll again. Because paging is by the monotonic seq and forward pages are fetched oldest-first, a backlog larger than limit is delivered incrementally instead of skipping the middle.

A run id that does not belong to this order returns 404 {"error":"Stream run not found"}.

POST /api/orders/[id]/stream/[streamRunId]/stop

Requests a stop. This is a soft stop: the API sets the run to STOPPING and emits a RUN_STOP_REQUESTED event; the monitor daemon closes its watcher on the next tick and writes STOPPED. The API never touches the watcher, which lives in a different process.

Returns { "ok": true }, or { "ok": true, "alreadyStopped": true } / { "ok": true, "alreadyStopping": true } when the run was already there.

GET /api/orders/[id]/stream/[streamRunId]/by-barcode

Aggregates the run’s FILE_INGESTED events by barcode, sorted by barcode. Files whose event carried no barcode are grouped under (unknown).

{ "barcodes": [ { "barcode": "barcode01", "fileCount": 12, "totalSize": 1048576, "totalReads": 4000, "totalBases": 1200000, "lastFileAt": "2026-04-30T13:42:11.000Z", "lastFilePath": "/data/minknow/…/fastq_pass/barcode01/FAX12345_pass_barcode01_12.fastq.gz" } ] }

Troubleshooting

SymptomLikely cause
401 {"error":"Unauthorized"} on every callNo session cookie. There is no API key — sign in and reuse the cookie jar.
403 {"error":"Unauthorized"} on a writeYou are authenticated but not a FACILITY_ADMIN.
403 "… disabled in the public demo."You are on a demo session; demo workspaces block every mutating operation.
Weblog returns 503No weblog secret configured. Set it under Admin → Application Settings → Pipeline execution.
Weblog returns 403Secret mismatch. The value in the run’s generated nextflow.config was written when the run was staged — regenerate the run after changing the secret.
400 {"error":"Data base path not configured"}site.dataBasePath is unset in settings.json, the environment, and the database. See Data Storage.
400 "Sequencing data can only be managed on submitted or completed sequencing orders"The order is still DRAFT.
Upload PATCH keeps returning 400 about the offsetYour offset drifted. GET the upload session or re-read receivedSize from the last successful chunk response and resume from exactly that byte.
Run finished on the cluster but SeqDesk still shows runningThe weblog could not reach the app — common when compute nodes are network-isolated. Call POST /api/pipelines/runs/[id]/sync, then resolve-outputs if artifacts are missing.
A researcher cannot see a completed runThe run has not been selected as the final result. PUT …/selection publishes it.

Conventions

  • POST bodies are application/json unless the endpoint takes multipart (the run-plan import) or a raw binary body (upload chunks).
  • Errors are { "error": "<message>" } with a 4xx/5xx status. Validation errors that have several causes add details: [ … ].
  • BigInt columns (totalBases, size, expectedSize) are serialized as strings or numbers depending on the endpoint — check the example rather than assuming.
  • Pagination is ?limit=&offset= for run listings and ?after=<seq>&limit= for the stream event feed. There is no global pagination convention.
  • Sequencing data management, run control, and result promotion require FACILITY_ADMIN. Read-only run endpoints additionally admit the researcher who owns the target, but only for published runs.