Fathom API Reference Open the app

Fathom API

Everything the Fathom web app does, it does through this API. Create samples, upload reads, run analyses, and read results and artifacts from your own scripts and pipelines.

Base URLhttps://app.fathomgenomics.com — every path below starts with /api/
FormatJSON request bodies (Content-Type: application/json) and JSON responses. File bytes go to pre-signed S3 URLs, never through the API itself.
AuthenticationBearer token from sign in, valid 30 days. Endpoints marked 🔒 require it.
BillingAnalyses cost credits; the price is quoted by the server before you commit, and a failed analysis is refunded. See Pricing & credits.
Supportsupport@fathom-genomics.com

Authentication

Every call carries a bearer token in the Authorization header. There are two kinds:

API tokens (recommended)

In the app, open your account from the header (your email address) and create a token under API tokens. Give it a name and an expiry (30, 90 or 365 days, or none). The token starts with fthm_ and is shown once; copy it then. It works however you sign in to the app, including with Google or Microsoft.

TOKEN=fthm_...          # from Account → API tokens
curl -s https://app.fathomgenomics.com/api/account \
  -H "Authorization: Bearer $TOKEN"
  • A token has the reach of your account, except that it cannot create, list or revoke tokens, link or unlink sign-in methods, connect or disconnect Box or S3, or buy credits. Those calls answer 403 {"code":"session_required"}; do them in the app.
  • Revoke a token in the same place at any time; the next call with it fails immediately. The account page shows when each token was last used.
  • Keep tokens secret: one lets its holder read everything in your account, upload reads and spend your credits on analyses. Fathom stores only a hash of it and can never show it again.

Session tokens

Accounts with a password can also sign in from a script. The answer is the same 30-day session token the web app uses; sign in again to get a new one (there is no refresh call).

curl -s https://app.fathomgenomics.com/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"your-password"}'
# → {"token":"eyJhbGciOi...","user":{"user_id":"...","email":"...","credit_balance":2,...}}

With either kind, a missing or malformed header answers 401 {"error":"Unauthorized"}; an expired token 401 {"error":"Token expired"}; anything else invalid 401 {"error":"Invalid token"}.

Quickstart: upload paired reads and run a human pathogen analysis

The whole flow with curl and jq. Each step is documented in full further down.

API=https://app.fathomgenomics.com/api

# Your API token, from Account → API tokens in the app (or a session token from /api/auth/login)
TOKEN=fthm_...
AUTH="Authorization: Bearer $TOKEN"

# Create a sample (accession is your own identifier, unique within your account)
SAMPLE=$(curl -s $API/samples -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"accession":"RUN-2026-001","description":"nasopharyngeal swab"}' | jq -r .sample.sample_id)

# Upload a read file: presign → PUT the bytes → finalize. Repeat with asset_type fastq_r2 for R2.
FILE=reads_R1.fastq.gz
SIZE=$(stat -c%s $FILE 2>/dev/null || stat -f%z $FILE)
PRESIGN=$(curl -s $API/samples/$SAMPLE/assets/presign -H "$AUTH" -H 'Content-Type: application/json' \
  -d "{\"filename\":\"$FILE\",\"asset_type\":\"fastq_r1\",\"size_bytes\":$SIZE}")
ASSET=$(echo "$PRESIGN" | jq -r .asset.asset_id)
curl -s -X PUT "$(echo "$PRESIGN" | jq -r .upload.url)" \
  -H 'Content-Type: application/octet-stream' --data-binary @$FILE
curl -s -X POST $API/samples/$SAMPLE/assets/$ASSET/finalize -H "$AUTH"

# Ask the price (dry_run), then submit with that price as expected_credits
BODY='{"analysis_type":"human_pathogens","input_refs":{"fastq_asset_ids":["'$ASSET'"]},"parameters":{}}'
CREDITS=$(curl -s $API/samples/$SAMPLE/analyses -H "$AUTH" -H 'Content-Type: application/json' \
  -d "$(jq -c '. + {dry_run:true}' <<<"$BODY")" | jq -r .credits)
ANALYSIS=$(curl -s $API/samples/$SAMPLE/analyses -H "$AUTH" -H 'Content-Type: application/json' \
  -d "$(jq -c --argjson c $CREDITS '. + {expected_credits:$c}' <<<"$BODY")" | jq -r .analysis_id)

# Poll until status is complete (or failed), then read the result
curl -s $API/samples/$SAMPLE/analyses/$ANALYSIS -H "$AUTH" | jq '.analysis.status'
curl -s $API/samples/$SAMPLE/analyses/$ANALYSIS -H "$AUTH" | jq '.result | keys'

# Download an artifact (the answer is a 15-minute pre-signed URL)
curl -s $API/samples/$SAMPLE/analyses/$ANALYSIS/artifacts/bacteria_classification.json -H "$AUTH" | jq -r .url | xargs curl -sO -J

Errors & codes

Every error is JSON with a human-readable error. Many also carry a stable code to branch on, and validation errors name the offending field.

{"error": "This analysis costs 2 credits. Press Run again to confirm.", "code": "quote_required", "credits": 2, "quote": {...}}
StatusMeaning
400Malformed request: a missing or invalid field (see field), an unsupported analysis type, a wrong file extension, a file too large for a single PUT (file_too_large).
401No, expired or invalid token.
402insufficient_credits: the analysis costs more than your balance (required, balance).
403session_required: an API token was used on a call only a signed-in session may make (managing tokens, sign-in methods, Box/S3 connections, purchases). Also registration closed, or a dev-only endpoint in production.
404Not yours or does not exist: samples, assets, analyses and artifacts you do not own all read as not found.
409State conflict. quote_required and price_changed (resend with the quoted expected_credits), sample_changed (re-read and retry), file_changed (a read file changed after it was measured), slot_taken (that read slot already holds a file), upload_missing / upload_too_late, missing_r1, reads_expired, duplicate accession or project name.
410artifact_expired: a read-level artifact whose reads have been deleted under the retention policy.
422Admitted inputs only: the selection is larger than this analysis type can process today (see input_limits in config), or an SRA run the policy refuses.
429Per-user SRA lookup limit.
503busy (retry in a moment), a disabled feature (SRA, Stripe), or reference data that failed to load. Safe to retry.

Service

GET /api/health public

Liveness. Answers {"status":"ok"}.

GET /api/config public · token optional

Non-secret runtime settings. Send your token to get the upload block as it applies to you.

Response

input_limitsPer analysis type, the largest input admitted today: max_bytes, max_bases, max_records under default and per-type overrides (vcf_kmer_support, protein_kmer_support).
credit_pricingHow analyses are priced: base credits per type (default 1, human_pathogens 2) × bands of unit_bytes of gzipped reads; anything up to first_unit_bytes is one band.
credit_purchaseunit_cents, volume tiers (min_credits, percent_off), max_credits per purchase, expiry_days, sales_email.
uploadsingle_put_max_bytes (the presign path's per-file limit), multipart_enabled, multipart_threshold_bytes, window_hours (a multipart upload must finish inside it), largest_processable_bytes.
read_retention_hours, read_max_lifetime_days, retention_modeThe read retention policy: uploads are deleted this many hours after the sample's last analysis (results are kept), and never kept past the lifetime cap. Enforced only when retention_mode is enforce.
sra_enabled, stripe_enabled, stripe_publishable_keyFeature switches.

Auth & account

POST /api/auth/register public

Create an account. New accounts start with 2 credits, one human pathogen analysis of up to the first pricing band.

Body

namestring, required
emailstring, required; stored lower-cased
passwordstring, required, at least 8 characters
organizationstring, optional

Response 201

{"token": "...", "user": {user_id, email, name, organization, credit_balance, samples_processed}}. 409 when the email is taken, 403 when registration is closed.

POST /api/auth/login public

Body {"email", "password"}. Answers {"token", "user"} as above, or 401 {"error":"Invalid email or password"}.

GET /api/auth/providers public

{"google": bool, "microsoft": bool, "registration": bool}: which single sign-on providers are enabled and whether self-service registration is open.

GET /api/account token

Your account.

Response

user_id, email, name, organizationIdentity.
credit_balanceWhole credits available now.
samples_processedLifetime count of analyses run.
has_password, identitiesWhether a password is set, and the linked SSO identities (provider, email).
credit_expiry_days, credit_next_expiryYour credit expiry term and when the soonest unused credits expire (ISO timestamp or null).

API tokens

All three need a session token (sign in with a password, or do it in the app); an API token answers 403 session_required.

GET /api/tokens session

{"tokens": [{token_id, name, prefix, created_at, expires_at, last_used_at}]}, newest first. The token value itself is never returned.

POST /api/tokens session

Body {"name", "expires_in_days": 30 | 90 | 365 | null} (default 365). 201 {"token": "fthm_...", "record": {...}}: the only time the token is shown. 409 for a duplicate name, 400 token_limit at 20 tokens.

DELETE /api/tokens/{token_id} session

Revokes the token immediately. {"deleted": true}; 404 if it is not yours.

Projects

A project is a label you can attach to samples and filter by. Names are unique per account (case-insensitive), at most 60 characters, at most 200 projects.

GET /api/projects token

{"projects": [{"name", "created_at"}]}, sorted by name.

POST /api/projects token

Body {"name"}. Answers 201 with the full list; 409 if the name exists.

Samples

A sample holds one set of reads (uploaded FASTQ or BAM files, or an NCBI SRA run) and any number of analyses over them.

The sample object

sample_idUUID; the identifier used in every path.
accessionYour identifier, unique within your account, up to 120 characters.
descriptionFree text, up to 1000 characters.
statedraft until a read file is ready, then ready; archived hides it from the default listing.
projectProject name or absent.
analysis_count, active_analysis_countAnalyses ever run, and those still pending or running.
sra_run, sra_accessionPresent on an SRA-backed sample: the run's accession, metadata, read class and the frozen billing size.
reads_hold_until, reads_expired_atRetention bookkeeping (see config).
created_at, updated_atISO 8601 UTC.
POST /api/samples token

Body

accessionrequired
descriptionoptional
sra_accessionoptional: an SRA run (SRR…, ERR…, DRR…). The sample is created ready with no uploads; see NCBI SRA input.

201 {"sample": {...}}. 409 when the accession exists; 400/422/503 for a refused SRA run.

GET /api/samples token

Your samples, newest activity first, paged by cursor.

Query

limit1 to 150, default 50
cursorthe cursor from the previous page
qsubstring of accession, description or SRA accession
project / no_project=trueone project, or samples with none
from, toYYYY-MM-DD, on updated_at
include_archived=trueinclude archived samples

{"samples": [...], "cursor": "..." | null, "total": n}. total is sent on the first page only.

GET /api/samples/{sample_id} token

{"sample", "assets": [...], "credit_quotes": {type: {"credits", "billable_bytes", "units"}}}. The quotes are what each analysis type would cost on the reads currently on the sample; a type reads {"blocked", "code"} instead when it cannot be quoted (for example file_changed).

PATCH /api/samples/{sample_id} token

Body with any of description, state (draft | ready | archived), project (an existing project name, or "" to clear). Answers {"sample"}; 400 for an unknown project; 409 sample_changed if the state moved underneath you.

Assets & uploads

An asset is one file attached to a sample. Bytes are uploaded straight to S3 through pre-signed URLs; the API only mints the URLs and records the result.

Asset types

asset_typeWhat it isExtensions
fastq_r1, fastq_r2Reads: one single-end file, or an R1/R2 pair. Gzipped is strongly preferred (it is also what pricing measures)..fastq, .fq, .fastq.gz, .fq.gz
reads_bamReads as an aligned or unaligned BAM (the usual PacBio and ONT delivery). An alternative to FASTQs, not an addition..bam
reference_fastaYour reference for the custom and vcf_kmer_support analyses..fa, .fasta, .fna, each also .gz
vcfVariants for vcf_kmer_support..vcf, .vcf.gz, .bcf
protein_variants_tsv, protein_reference_fastaInputs for protein_kmer_support..tsv; .faa, .fa, .fasta, each also .gz

A sample holds at most one file per read slot (fastq_r1, fastq_r2, reads_bam); a second upload to an occupied slot is refused with 409 slot_taken. The first read file to become ready moves the sample to ready.

The asset object

asset_id, sample_id, asset_type, filenameIdentity.
stateuploading → ready, via completing for multipart; failed; expiring / expired once retention has deleted the reads.
size_bytes, etag, finalized_atSet when the upload is confirmed.
source, upload_modeupload, box or s3; multipart when it was.
expires_at, cap_at, liveRetention: when the reads will be deleted (null while an analysis holds them), the hard lifetime cap, and whether the file is usable now.
GET /api/samples/{sample_id}/assets token

{"assets": [...]}, newest first.

Single-request upload (files up to upload.single_put_max_bytes, 5 GiB)

POST /api/samples/{sample_id}/assets/presign token

Body

filenamerequired; the extension must match the type
asset_typerequired
size_bytesrequired for read types, ignored otherwise. The exact byte count: it is signed into the URL.

201 {"asset": {... "state":"uploading"}, "upload": {"url": "..."}}. The URL is valid for one hour.

PUT the upload URL pre-signed

Send the file body with Content-Type: application/octet-stream and, for reads, exactly size_bytes bytes. No Authorization header: the URL carries its own signature. A 403 from S3 means the URL expired or the headers or length differ from what was signed.

POST /api/samples/{sample_id}/assets/{asset_id}/finalize token

Confirms the object is in place, records its size and ETag, measures read files for pricing, and marks the asset ready. Call it within 6 hours of presigning, or the upload is discarded (upload_too_late). Answers {"asset"}; 400 if the object is missing or empty.

Multipart upload (any size)

For read files larger than multipart_threshold_bytes, or whenever you prefer resumable parts. The whole upload must complete within upload.window_hours (48 h) of starting.

POST /api/samples/{sample_id}/assets/multipart token

Body {"filename", "asset_type", "size_bytes"} (read types only). 201 {"asset", "upload": {"part_bytes", "part_count", "window_ends_at"}}. Part n (1-based) covers bytes (n-1)·part_bytes up to min(n·part_bytes, size_bytes).

POST /api/samples/{sample_id}/assets/multipart/{asset_id}/parts token

Body {"part_numbers": [1, 2, ...]}, up to 8 distinct parts per call. Answers {"urls": {"1": "...", "2": "..."}}. Mint URLs just before you PUT each part; a URL that has expired answers 403 from S3, so mint it again rather than retrying.

PUT each part URL pre-signed

The part's bytes, with Content-Length equal to that part's size. Parts may be uploaded in parallel.

GET /api/samples/{sample_id}/assets/multipart/{asset_id}/parts token

What S3 holds so far: {"parts": [{"part_number", "size", "etag"}], "missing": [n, ...], "state", "part_bytes", "part_count", "expected_bytes", "window_ends_at"}. Use it to resume after an interruption.

POST /api/samples/{sample_id}/assets/multipart/{asset_id}/complete token

Assembles the parts. 202 {"asset": {"state": "completing"}}; poll the sample or asset list until the asset is ready (or back to uploading with missing_parts if parts were absent). 409 upload_missing if S3 no longer has the upload.

DELETE /api/samples/{sample_id}/assets/multipart/{asset_id} token

Abandon an in-progress multipart upload. Answers {"asset"}.

DELETE /api/samples/{sample_id}/assets/{asset_id} token

Remove an asset and its bytes. Answers {"deleted": true}. A ready read file that a pending or still-staging analysis needs is refused with 409; a ready file an existing analysis already used is kept as that analysis's provenance.

Pricing & credits

Analyses are paid in credits. The price of an analysis is base[type] × bands, where the bands are unit_bytes (10 GiB) of gzipped reads and anything up to first_unit_bytes (16 GiB) counts as one band. human_pathogens has a base of 2; every other type 1. A plain, uncompressed FASTQ is priced on an estimate of its compressed size until its upload has been sampled. The server quotes every price; nothing is charged until you submit with the quoted expected_credits, and a failed analysis is refunded automatically.

POST /api/credits/estimate token

What a read selection would cost before uploading it, from filenames and sizes alone. Body {"files": [{"filename", "size_bytes"}]} (one BAM, or one or two FASTQs). Answers per-type estimates plus processable and the pricing table. For gzipped reads the estimate equals the eventual quote.

POST /api/samples/{sample_id}/analyses dry_run token

The authoritative quote for a real submission: send the full submission body with "dry_run": true. Answers 200 {"credits", "billing", "size_tier", "admissible"}, with the refusal's fields merged in when admissible is false. Nothing is charged.

Buying credits

Credits are bought by card through Stripe. The flow is browser-oriented (it needs Stripe's client-side confirmation), so use the app's Buy credits dialog; the endpoints are listed for completeness.

POST /api/credits/create-payment-intent token

Body {"credits": n} (1 to credit_purchase.max_credits). Answers {"client_secret", "credits", "amount_cents"} priced from credit_purchase in config.

POST /api/credits/confirm token

Body {"payment_intent_id"} after the card is confirmed. Answers {"credits_added", "already_credited"}; idempotent with the webhook.

For volume pricing or invoicing, contact sales@fathom-genomics.com.

Analyses

Analysis types

analysis_typeWhat it runsInputs beyond readsparameters
human_pathogensRead QC, sample composition, then bacterial, viral and fungal pathogen identification with genome coverage; influenza A subtype and SARS-CoV-2 lineage when detected. Base price 2.—skip_qc (bool, default false)
amrAssembles the AMR-binned reads and types them with NCBI AMRFinderPlus.—skip_qc
influenza_resistance_typingOseltamivir and adamantane resistance markers in influenza A reads.—skip_qc
ribosomal_rnaSpecies-level typing of rRNA reads.—subunit: "16S" (bacteria & archaea) or "18S_28S" (fungi), required; skip_qc
customClassifies the reads against a FASTA you supply.input_refs.referenceskip_qc
vcf_kmer_supportChecks whether each variant in a VCF is supported by the sample's k-mers.input_refs.vcf_asset_id, input_refs.referencekmer_size (default 31), min_alt_frac (0–1, default 0.15), min_kmer_count (≥1, default 2), coverage (optional override)
protein_kmer_supportTranslates reads and verifies proposed amino-acid variants.input_refs.protein_variants_asset_id, input_refs.protein_reference_asset_idprotein_kmer_size (default 9), min_frame_ref_kmer_hits (≥1, default 1), min_alt_frac, min_kmer_count, coverage

Older type names (pathogen_classification, who_priority, bacteria, viral, fungal) are retired: existing results still load, new submissions answer 400 naming the replacement.

POST /api/samples/{sample_id}/analyses token

Submit an analysis. The sample must be ready.

Body

analysis_typerequired, from the table above
input_refsWhich reads: {"fastq_asset_ids": [r1_id]} or [r1_id, r2_id], or {"bam_asset_id": id}, or for an SRA sample {"sra_accession": "SRR..."} (the sample's own run). Plus, per type: "reference": {"kind": "sample_asset", "asset_id": id}, "vcf_asset_id", "protein_variants_asset_id", "protein_reference_asset_id".
parametersper type, see above; {} is fine where everything has a default
labeloptional, up to 120 characters
expected_creditsthe price you agree to pay: the credits from a dry_run quote, or from the last quote_required / price_changed answer
dry_runtrue to quote without submitting

Responses

202{"analysis_id", "status": "pending", "credits_charged", "billing", "size_tier", "dispatch"}. The charge is made; the analysis runs asynchronously.
409quote_required (no expected_credits) or price_changed (it differed): both carry credits and quote; resend with that value.
402insufficient_credits with required and balance.
422The input is larger than this type can process; the body explains the limit.
400Wrong type, wrong asset types for the refs, R2 without R1 (409 missing_r1), bad parameters.
503busy: nothing was charged, retry shortly.

The analysis object

analysis_id, sample_id, sample_accession, analysis_type, labelIdentity.
statuspending → running → complete or failed.
input_refs, parametersExactly what was run, with each input asset frozen (id, filename, size, ETag).
credits_charged, billing, size_tierWhat it cost and why.
created_at, completed_atISO 8601 UTC.
error, error_code, partial_resultOn failure: the reason, and whether a partial result is still served.
refunded_at, refund_reasonSet when a failed analysis was refunded.
pipeline_version, seed_align_version / tool_versionProvenance: the pipeline and classifier build, or the k-mer tool version.
GET /api/samples/{sample_id}/analyses token

{"analyses": [...]}, newest first; limit 1 to 100 (default 100).

GET /api/samples/{sample_id}/analyses/{analysis_id} token

{"analysis": {...}, "result": {...}}. result is present once the status is complete (or failed with partial_result). Poll this to track progress; a few seconds between polls is plenty.

Results & artifacts

The result object

A JSON document assembled by the pipeline. Its top-level sections depend on the analysis type; the common ones are:

total_reads_processed, classified_reads, unclassified_reads, reporting_categories, reporting_fractions, top_reporting_categorySample composition: read counts and fractions by category (human, bacteria, virus, fungi, ambiguous, unknown).
qcHeadline read-QC numbers from fastp (the full report is the qc_metrics.json artifact).
bacteria_classification, viral_classification, fungal_classificationPer kingdom: organisms with reads, coverage, spread and detection status, plus totals. Reporting names and complex roll-ups (for example Mycobacterium tuberculosis complex) are applied.
influenza, sarscov2_lineage, viral_gatesInfluenza A subtype, SARS-CoV-2 lineage, and whether each was evaluated (so "looked for, not found" is distinct from "never run").
amrAMRFinderPlus calls by gene and drug class.
rrna_classification, rrna_taxonomyRibosomal RNA typing.
model_confidencePer-kingdom BLAST read-back sidecars where computed. For each assessed organism, n reads with the most k-mer hits for it were BLASTed against the kingdom database and target_pid_over_90 of them aligned back to it at over 90% identity; the application shows that count as Low (fewer than 25), Medium (25–75) or High (more than 75). A read count, not a probability.
pipelineProvenance: pipeline name and version, databases, thresholds.
partialPresent on a partial result from a failed run.
GET /api/samples/{sample_id}/analyses/{analysis_id}/qc token

The complete normalised read-QC report (every fastp field, including the per-cycle curves) as JSON. 404 when QC was skipped.

GET /api/samples/{sample_id}/analyses/{analysis_id}/variants token

Paged variant calls for vcf_kmer_support and protein_kmer_support. Query call filters by verdict (ALL, SUPPORTED, NOT_SUPPORTED, NO_INFO, SKIPPED; protein also NO_COVERAGE, AMBIGUOUS) and cursor is the page number from 0. Answers {"variants": [...], "next_cursor": n | null}.

GET /api/samples/{sample_id}/analyses/{analysis_id}/artifacts/{name} token

A download link for one of the files the analysis published: {"url": "..."}, a pre-signed URL valid for 15 minutes that serves the file as an attachment named <accession>.<run>.<name>. 404 for a name this analysis cannot have written; 410 artifact_expired for read-level files (amr_reads_*.fastq.gz, *tied_reads.tsv.gz) after the sample's reads were deleted. SRA-backed analyses publish no read-level files.

Artifact names by type

every type except the k-mer toolsqc_metrics.json, qc_report.html
human_pathogensper kingdom <k>_classification.json, <k>_taxonomy.json, <k>_taxa.tsv, <k>_taxonomy.tsv.gz, <k>_coverage.json, <k>_subset.json, <k>_no_coverage.tsv for k in bacteria, viral, fungal; plus viral_gates.json, influenza.json, sarscov2.json
amramrfinder.tsv, amr_report.json, amr_coverage.json, amr_organisms.json, amr_contigs.fasta.gz, amr_reads_R1.fastq.gz, amr_reads_R2.fastq.gz
influenza_resistance_typinginfluenza_resistance.json, oseltamivir.report.tsv, oseltamivir.annotated.vcf.gz, adamantane.report.tsv, adamantane.tied_reads.tsv.gz, adamantane.annotated.vcf.gz
ribosomal_rnarrna_classification.json, rrna_taxonomy.json, rrna_taxa.tsv, rrna_taxonomy.tsv.gz, rrna_alignment.tsv, rrna_consensus.fasta, rrna_pid.tsv
customthe QC pair only; the classification is in the result
vcf_kmer_supportreport.tsv, annotated.vcf.gz
protein_kmer_supportreport.tsv, tied_reads.tsv.gz

NCBI SRA input

Instead of uploading, a sample can point at a public SRA run. The reads are downloaded fresh for each analysis and never stored. Availability is per account while the feature rolls out: check allowed first.

GET /api/sra/status token

{"enabled": bool, "allowed": bool}: whether SRA input is on for everyone, and whether you may use it.

GET /api/sra/lookup?accession=SRR... token

Resolve a run before creating a sample. 200 {"ok": true, "run": {...}, "derived": {...}, "existing_sample_id"}, or 200 {"ok": false, "code", "message", "run"} when the policy refuses it (controlled access, unsupported platform, too large). 400 for a malformed or container accession (give a run, not a study or experiment); 429 on the per-user limit; 503 when SRA is off or NCBI is unreachable.

Then POST /api/samples with sra_accession, and submit analyses with "input_refs": {"sra_accession": "SRR..."}. Pricing uses the run's compressed size as frozen on the sample.

Box, S3 & single sign-on

These flows involve a browser redirect or a stored connection and are meant to be driven from the app. They are listed so the surface is complete.

EndpointPurpose
GET /api/box/status, GET /api/box/auth-url, GET /api/box/callback, POST /api/box/disconnect, GET /api/box/filesConnect a Box account (OAuth) and browse it.
POST /api/samples/{sample_id}/assets/import-boxImport read files from Box into a sample as a background job; the assets appear with source: "box".
GET /api/s3/status, POST /api/s3/connect, POST /api/s3/disconnect, GET /api/s3/objectsStore a scoped AWS key (encrypted at rest) and list a bucket.
POST /api/samples/{sample_id}/assets/import-s3Import read objects from your bucket into a sample.
GET /api/auth/{provider}/start, GET /api/auth/{provider}/link-url, GET /api/auth/{provider}/callback, POST /api/auth/sso/unlinkGoogle and Microsoft sign-in and account linking (provider is google or microsoft).
POST /api/credits/webhookStripe's webhook; not for clients.