← MSMS Desk / API
Tokens

Drive MSMS Desk from your own code

Annotation evidence, not identification. The model reads the scores your browser (or your script) computed; it never sees raw files, never rescores a spectrum and never computes a mass. Similarity is evidence, not proof of identity. It is not clinical, forensic or regulatory advice.

Everything the web page does is available over HTTP. Score one query MS/MS spectrum against its candidate library spectra with the page's own mskit.js (greedy cosine and greedy modified cosine, matchms semantics), send the scores, and get back a verdict (probable, tentative, unassigned), a Schymanski et al. (2014) confidence level, the best candidate, and either a judgement of every candidate or the methods text, results text and annotation table row for the feature. The natural use is a batch annotation pipeline: score features locally, send only the ones with a passing candidate, and hold anything below level 2a for a standard.

Two lanes: the task field

taskwhat you getextra input
annotateOne call per candidate sent (supported, analog, ambiguous, rejected) with evidence for and against and the query peaks that carry it; up to 8 fragment readings; caveats.none
reportA methods paragraph for the spectral matching, a results sentence, an annotation table row and limitations, with AUTHOR_INPUT_NEEDED where a fact is missing.annotation: the text of an earlier annotate run (optional)

Both lanes return the same envelope: lane, verdict, level, best_candidate, headline, tldr, the lane body, next_evidence and prescan_responses. Worked examples: annotate, report.

Input fields

Every field is a string.

fieldrequiredmeaning
taskyesannotate or report.
factsyesA JSON-encoded string with the browser's scores - see below. Build it with MSKit.buildInput.
titlenoA label for the feature, up to 160 characters.
contextnoYour notes: sample, LC, instrument, collision energy, library source, and any standard, isotope-pattern or retention-time evidence. Up to 3,000 characters. Level 1 is only possible when this says an authentic standard matched.
annotationreport onlyPlain text of an earlier annotation (the page builds it with Recon.annotationText). Up to 8,000 characters.
questionnoAnswered in tldr as a bullet starting "Answer:". Up to 1,200 characters.
retry_notenoOnly on a retry after a malformed reply.

The facts string

settings (tolerance in Da, minimum score, minimum matched peaks, relative-intensity floor, precursor tolerance in ppm, and plain-language descriptions of the processing and the scores); query (precursor m/z, ion mode, adduct, retention time, up to 60 peaks as [m/z, relative intensity]); library (spectra read, scored and passing); candidates - the top candidates by cosine plus up to two analogs only the modified cosine finds, each with id R1.., name, precursor, adduct, ion mode, formula, InChIKey, cosine / cosine_matches, modified_cosine / modified_matches, precursor difference in Da and ppm, explained intensity, browser_status, the matched pairs that contribute most and up to 30 peaks; flags (F1.. with severity, category, message and the candidates they concern); browser_verdict; and clipped.

Building the body

The simplest way to get a body that matches the page byte for byte is to run the page's own module in Node. mskit.js has no dependencies and exports itself with module.exports:

// make-body.js - build the exact body the page sends, with the page's own code.
// Save https://msms-desk.skillsafe.ai/mskit.js next to this file, then:
//   node make-body.js query.mgf library.msp annotate "Feature 195.0878" "notes" > body.json
const fs = require("fs");
const K = require("./mskit.js");
const [q, lib, lane = "annotate", title = "", context = "", annotation = ""] = process.argv.slice(2);
const A = K.analyze({ lane, title, context, query: fs.readFileSync(q, "utf8"), library: fs.readFileSync(lib, "utf8"),
  settings: { tolerance: 0.02, min_score: 0.6, min_matches: 5, intensity_from: 0.01, precursor_ppm: 10, top_n: 8 } });
if (A.empty) throw new Error("need one query spectrum with peaks and at least one reference");
const body = K.mustBeObject(K.buildInput(A, { question: "", annotation }));
console.error("browser verdict:", A.hint, "| passing:", A.passing, "| flags:", A.flags.map(f => f.id + " " + f.category).join(", "));
console.error("idempotency key: msms-desk:" + lane + ":" + K.hashInput(body) + ":a1");
process.stdout.write(JSON.stringify(body));

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses the same envelope, so one helper covers the whole API:

{"ok": true, "data": {"job_id": "job_...", "status": "queued"}}
{"ok": false, "error": {"code": "payment_required", "message": "..."}}

The token is minted for this app (the guest endpoint takes {"slug":"msms-desk"} in its body), so no slug header is needed afterwards. Send it as Authorization: Bearer ….

The input object IS the request body. There is no {"input": …} wrapper. A wrapped body is answered with an unknown field 'input' warning, and the model never sees your text.

Error codes

statuscodewhat to do
400validation_errorA field is missing or the wrong type. Every field is a string: facts must be a JSON-encoded string, not an object.
401unauthorizedThe token is missing, malformed or expired. Get a new one from the token page.
402payment_requiredThe balance is below min_credits. Call /estimate first and top up.
403forbiddenThe token is valid but not for this app, or a guest token tried a metered run. A guest cannot run; sign in for a personal token.
404not_foundUnknown job id, or the app slug does not exist.
409conflictThe same Idempotency-Key was replayed with a different body. Change the key or send the original input.
429rate_limitedToo many requests. Back off and retry; do not tight-loop.
5xxinternalA server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice.

1. A tiny client

One helper that sends the token, unwraps data and raises on ok: false. The token comes from the token page (Copy token or Copy shell export); step 2 covers the kinds of token and minting one from code.

# Every call is the same three things: the base URL, your bearer token,
# and a JSON body. Keep the token in a shell variable.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="msms-desk"
TOKEN="$SKILLSAFE_TOKEN"   # from https://msms-desk.skillsafe.ai/tokens.html

call() {                  # call <path> [json-body]
  if [ -n "$2" ]; then
    curl -sS -X POST "$BASE/$1" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d "$2"
  else
    curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN"
  fi
}

2. Get a token

The easiest route is the token page: it shows the token this browser already holds, with Copy token and Copy shell export buttons, and a sign-in button for a personal token. A guest token, minted with POST /guest and {"slug":"msms-desk"}, can call /me and /estimate; the run is metered, so /run and /run-stream need a personal token.

# The token page is the shortest path. It shows the token this browser holds and
# hands you a ready-made shell export:
#
#   https://msms-desk.skillsafe.ai/tokens.html
#   export SKILLSAFE_TOKEN="..."
#
# To mint a guest token from the command line instead. A guest token is enough
# for /me and /estimate; a run needs a personal token from signing in.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
  -H "Content-Type: application/json" -d '{"slug":"msms-desk"}'
# {"ok":true,"data":{"token":"…","subject_type":"guest"}}

3. Check the session and the balance

call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}

4. Price the run (free)

/estimate returns the model binding and the credits a run would reserve. It creates no job and charges nothing. Expect model_alias gpt-terra and markup_bps 1000 (a 10% markup). hold_credits is a reservation, not the price: it is held against your balance while the run executes and released afterwards. min_credits is the least balance that can start a run. What you actually pay is charged_credits, reported on the finished job and in the done event, and it is usually far lower than the hold. The body is the input object itself, with no {"input": …} wrapper. /estimate does not validate the body, so check the shape yourself: an object whose every value is a string, task equal to annotate or report, facts non-empty, and facts a JSON string that parses to an object (this is what the page's own guard, MSKit.mustBeObject, refuses to spend without).

# body.json is the input object itself - no {"input": ...} wrapper. Build it with
# make-body.js above, or by hand. estimate does not validate it, so check the shape first:
python3 -c 'import json;b=json.load(open("body.json"));assert isinstance(b,dict) and b.get("task") in ("annotate","report") and all(isinstance(v,str) for v in b.values()) and all(b.get(k,"").strip() for k in ("facts",)) and isinstance(json.loads(b["facts"]),dict)'
INPUT=$(cat body.json)

call estimate "$INPUT"
# {"ok":true,"data":{"model":"...","model_alias":"gpt-terra",
#   "markup_bps":1000,"hold_credits":...,"min_credits":...,"sponsor_enabled":false,
#   "warnings":[]}}
#
# estimate creates no job and charges nothing. hold_credits is RESERVED, not the
# price; charged_credits after the run is the actual cost, usually far lower.

5. Run it, then poll

POST /run returns a job_id; poll GET /jobs/{id} until it is terminal. The reply is a string at data.output.output: JSON.parse it (step 7). Send an Idempotency-Key built from the lane, a hash of the input and the attempt number, msms-desk:<lane>:<hash>:a<attempt> (for example msms-desk:annotate:1gliaxow6ay8q:a1), so a retried request returns the same job instead of billing a second run. Use one key per distinct input: changed spectra, settings or notes (so changed facts) or a changed annotation are a new hash, the same spectra in the other lane are a new key, and replaying an old key with a different body is a 409. The page uses MSKit.hashInput(body) for the hash (it covers task, title, context, facts, annotation and question; make-body.js prints the key); any stable digest of the body works from other languages. Leave retry_note out of the hash and bump the attempt instead.

# Always send an Idempotency-Key derived from the input. A retried request with
# the same key returns the SAME job instead of billing a second run.
LANE=$(printf '%s' "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["task"])')   # annotate or report
KEY="msms-desk:$LANE:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"

JOB=$(curl -sS -X POST "$BASE/run" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')

while :; do
  OUT=$(call "jobs/$JOB")
  STATUS=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
  [ "$STATUS" = "succeeded" ] && break
  [ "$STATUS" = "failed" ] && echo "$OUT" && exit 1
  sleep 2
done

# {"ok":true,"data":{"job_id":"job_...","status":"succeeded",
#   "output":{"output":"{\"lane\":\"annotate\",\"verdict\":\"probable\",\"headline\":\"...\", ...}"},
#   "charged_credits":...,"truncated":false}}
printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])' > reply.json

6. Or stream it

POST /run-stream takes the same body and headers and answers with server-sent events: job (the job id), delta (chunks of the reply) and done (the status, charged_credits, truncated and, when present, the full output). A browser page may receive only tick heartbeats and then done, never a delta, so take the reply from done.output.output when it is there, fall back to the concatenated deltas, and fall back again to GET /jobs/{id}.

# Server-sent events. `delta` events carry chunks of the reply; `done` carries the
# status, charged_credits and the truncated flag. Ignore `tick` heartbeats.
curl -N -X POST "$BASE/run-stream" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -H "Accept: text/event-stream" \
  -d "$INPUT"

# event: job    {"job_id":"job_..."}
# event: delta  {"text":"{\"lane\":\"annotate\",\"verdict\":\"probable\",\"level\":\"2a"}
# event: done   {"status":"succeeded","charged_credits":...,"truncated":false}

7. Parse the reply

The reply is a JSON object serialised as a string. Parse it, then check the lane.

# The reply is a JSON string inside data.output.output. Pull it out and parse it:
printf '%s' "$JOB" | python3 -c 'import sys,json;r=json.loads(json.load(sys.stdin)["output"]["output"]);print(r["verdict"],r["level"],r["best_candidate"])'

Invariants worth asserting

The output contract

{
  "lane": "annotate" | "report",
  "verdict": "probable" | "tentative" | "unassigned",
  "level": "1" | "2a" | "2b" | "3" | "4" | "5",
  "best_candidate": {"ref": "R1", "name": "..."},
  "headline": "...",
  "tldr": ["..."],
  // annotate:
  "candidates": [{"ref", "call": "supported|analog|ambiguous|rejected", "evidence_for", "evidence_against", "key_peaks": [m/z, ...]}],
  "fragments": [{"mz", "reading", "certainty": "tentative|supported"}],
  "caveats": ["..."],
  // report:
  "methods_text": "...", "results_text": "...",
  "table_row": {"feature", "precursor_mz", "adduct", "annotation", "level", "score", "matched_peaks", "reference", "evidence"},
  "limitations": ["..."],
  // both:
  "next_evidence": ["..."],
  "prescan_responses": [{"ref": "F1", "verdict": "confirmed|dismissed", "note": "..."}]
}

Worked example: annotate

The caffeine example from the page (illustrative spectra). The body, with the peak lists shortened here:

{
 "task": "annotate",
 "title": "Feature 195.0878 @ 5.20 min",
 "context": "Human plasma, protein-precipitated extract. C18 reversed-phase LC, ESI positive. The reference spectra were pasted from our in-house list. No authentic standard has been run yet.",
 "facts": "{\"settings\": {\"tolerance_da\": 0.02, \"min_score\": 0.6, \"min_matched_peaks\": 5, \"relative_intensity_floor\": 0.01, \"precursor_tolerance_ppm\": 10, \"processing\": \"same for query and references: non-positive peaks dropped, duplicate m/z merged (max), intensities scaled to the base peak, peaks below the relative-intensity floor removed\", \"scores\": \"greedy cosine and greedy modified cosine, mz_power 0, intensity_power 1, computed in the browser with matchms CosineGreedy / ModifiedCosineGreedy semantics\"}, \"query\": {\"title\": \"Feature 195.0878 @ 5.20 min\", \"precursor_mz\": 195.0878, \"ionmode\": \"positive\", \"adduct\": \"[M+H]+\", \"rt\": \"312\", \"instrument\": \"\", \"collision_energy\": \"\", \"peaks_raw\": 10, \"peaks_after_processing\": 10, \"peaks_sent\": 10, \"peaks\": [[42.0339, 0.1], [56.0496, 0.04], [69.0448, 0.15], [\"...\"]]}, \"library\": {\"spectra_read\": 8, \"spectra_scored\": 8, \"passing\": 1, \"candidates_sent\": 8, \"format\": \"msp\"}, \"candidates\": [{\"ref\": \"R1\", \"rank\": 1, \"name\": \"Caffeine\", \"id\": \"\", \"precursor_mz\": 195.0877, \"adduct\": \"[M+H]+\", \"ionmode\": \"positive\", \"formula\": \"C8H10N4O2\", \"inchikey\": \"RYYVLZVUVIJVGH-UHFFFAOYSA-N\", \"cosine\": 0.9937, \"cosine_matches\": 8, \"modified_cosine\": 0.9937, \"modified_matches\": 8, \"precursor_delta_da\": 0.0001, \"precursor_delta_ppm\": 0.5, \"explained_intensity\": 0.957, \"browser_status\": \"match\", \"top_pairs\": [{\"query_mz\": 138.0663, \"reference_mz\": 138.0662, \"shifted\": false, \"contribution\": 0.8213}, \"...\"], \"peaks\": [[42.0338, 0.08], [56.0495, 0.05], [\"...\"]]}, {\"ref\": \"R2\", \"rank\": 2, \"name\": \"Theobromine\", \"id\": \"\", \"precursor_mz\": 181.072, \"adduct\": \"[M+H]+\", \"ionmode\": \"positive\", \"formula\": \"C7H8N4O2\", \"inchikey\": \"YAPQBXQYLJRXSA-UHFFFAOYSA-N\", \"cosine\": 0.8663, \"cosine_matches\": 2, \"modified_cosine\": 0.9649, \"modified_matches\": 3, \"precursor_delta_da\": 14.0158, \"precursor_delta_ppm\": 77404.6, \"explained_intensity\": 0.616, \"browser_status\": \"weak\", \"top_pairs\": [{\"query_mz\": 138.0663, \"reference_mz\": 138.0662, \"shifted\": false, \"contribution\": 0.8219}, \"...\"], \"peaks\": [[67.0291, 0.1], [110.0713, 0.18], [\"...\"]]}, \"... 6 more\"], \"flags\": [{\"id\": \"F1\", \"severity\": \"medium\", \"category\": \"ion_mode_conflict\", \"message\": \"2 candidate(s) were recorded in the other ion mode from the query (positive); their spectra are not comparable as an identity match.\", \"refs\": [\"R6\", \"R7\"]}, {\"id\": \"F2\", \"severity\": \"medium\", \"category\": \"high_score_few_matches\", \"message\": \"1 reference(s) score at or above 0.6 on fewer than 5 matched peaks (Theobromine: 0.866 on 2); keep score and matched peaks together.\", \"refs\": [\"R2\"]}, {\"id\": \"F3\", \"severity\": \"low\", \"category\": \"analog_candidates\", \"message\": \"3 candidate(s) pass only on the modified cosine (1-Methylxanthine 0.965, shift +28.0314 Da; Theophylline 0.986, shift +14.0158 Da; Paraxanthine 0.967, shift +14.0158 Da): structural relatives, not identities.\", \"refs\": [\"R3\", \"R4\", \"R5\"]}], \"browser_verdict\": \"probable\", \"clipped\": {\"query_peaks_not_sent\": 0, \"library_spectra_not_scored\": 0}}",
 "question": ""
}

The reply (a saved model run):

{
 "lane": "annotate",
 "verdict": "probable",
 "level": "2a",
 "best_candidate": {
  "ref": "R1",
  "name": "Caffeine"
 },
 "headline": "The query spectrum matches the Caffeine reference with cosine 0.994 on 8 matched peaks and a precursor difference of 0.5 ppm, supporting a probable structure at level 2a pending an authentic standard.",
 "tldr": [
  "Caffeine (R1) is the only candidate with browser status match: cosine 0.9937 on 8 matched peaks, 95.7% of query intensity explained, precursor 195.0877 vs 195.0878 (0.5 ppm).",
  "Theophylline, Paraxanthine and 1-Methylxanthine pass only on the modified cosine with precursor shifts of 14.0158 or 28.0314 Da and are structural relatives, not identities.",
  "Theobromine scores 0.866 on only 2 matched peaks with a 14.0158 Da precursor difference and is below the matched-peak threshold.",
  "No authentic standard has been run, so level 1 is not supported; retention time confirmation is the next check."
 ],
 "candidates": [
  {
   "ref": "R1",
   "call": "supported",
   "evidence_for": "Cosine 0.9937 on 8 matched peaks (modified cosine identical), explained intensity 0.957, same positive ion mode and [M+H]+ adduct, precursor 195.0877 within 0.0001 Da (0.5 ppm). Unshifted matches at 138.07 (base peak), 110.07, 195.09, 69.04, 83.06, 42.03, 123.04 and 56.05.",
   "evidence_against": "Query peaks at 91.05 and 150.07 have no counterpart in the reference spectrum. No retention time or authentic standard evidence is supplied.",
   "key_peaks": [
    138.0663,
    110.0714,
    195.0879,
    69.0448,
    83.0605,
    42.0339
   ]
  },
  {
   "ref": "R2",
   "call": "rejected",
   "evidence_for": "Shares unshifted peaks at 138.07 and 110.07; modified cosine 0.9649 on 3 matched peaks.",
   "evidence_against": "Browser status weak: cosine 0.8663 on only 2 matched peaks, below the 5-peak threshold. Precursor 181.072 differs by 14.0158 Da (77404.6 ppm), so it cannot be the query compound.",
   "key_peaks": [
    138.0663,
    110.0714
   ]
  },
  {
   "ref": "R3",
   "call": "analog",
   "evidence_for": "Modified cosine 0.9652 on 5 matched peaks; query 138.07, 195.09 and 110.07 align with reference 110.03, 167.06 and 82.04 after the precursor shift.",
   "evidence_against": "Plain cosine 0.0315 on 2 matched peaks, explained intensity 0.118; precursor 167.0564 differs by 28.0314 Da. Passes only on the modified cosine.",
   "key_peaks": [
    138.0663,
    195.0879,
    110.0714
   ]
  },
  {
   "ref": "R4",
   "call": "analog",
   "evidence_for": "Modified cosine 0.9857 on 5 matched peaks; query 138.07, 195.09 and 110.07 align with reference 124.05, 181.07 and 96.06 after the shift.",
   "evidence_against": "Plain cosine 0.0286 on 2 matched peaks, explained intensity 0.118; precursor 181.072 differs by 14.0158 Da (77404.6 ppm). A shifted-fragment relative, not an identity match.",
   "key_peaks": [
    138.0663,
    195.0879,
    110.0714
   ]
  },
  {
   "ref": "R5",
   "call": "analog",
   "evidence_for": "Modified cosine 0.9671 on 5 matched peaks; query 138.07, 195.09 and 110.07 align with reference 124.05, 181.07 and 96.06 after the shift.",
   "evidence_against": "Plain cosine 0.0278 on 2 matched peaks, explained intensity 0.118; precursor 181.072 differs by 14.0158 Da (77404.6 ppm). A shifted-fragment relative, not an identity match.",
   "key_peaks": [
    138.0663,
    195.0879,
    110.0714
   ]
  },
  {
   "ref": "R6",
   "call": "rejected",
   "evidence_for": "None beyond a single shifted precursor pairing (modified cosine 0.0965 on 1 peak).",
   "evidence_against": "Recorded in negative ion mode as [M-H]- (ion mode conflict); cosine 0 on 0 matched peaks; precursor 179.035 differs by 16.0528 Da.",
   "key_peaks": []
  },
  {
   "ref": "R7",
   "call": "rejected",
   "evidence_for": "None beyond a single shifted precursor pairing (modified cosine 0.0607 on 1 peak).",
   "evidence_against": "Recorded in negative ion mode as [M-H]- (ion mode conflict); cosine 0 on 0 matched peaks; precursor 191.0197 differs by 4.0681 Da.",
   "key_peaks": []
  },
  {
   "ref": "R8",
   "call": "rejected",
   "evidence_for": "Same positive ion mode and [M+H]+ adduct.",
   "evidence_against": "Cosine 0 on 0 matched peaks, modified cosine 0.0508 on 1 peak, explained intensity 0; precursor 163.123 differs by 31.9648 Da.",
   "key_peaks": []
  }
 ],
 "fragments": [
  {
   "mz": 138.0663,
   "reading": "Base peak, matched unshifted to Caffeine at 138.0662 and carrying most of the cosine; appears shifted in Theophylline and Paraxanthine, consistent with a fragment retaining the extra methyl group (tentative).",
   "certainty": "supported"
  },
  {
   "mz": 195.0879,
   "reading": "Protonated precursor, matched unshifted to the Caffeine precursor at 195.0877.",
   "certainty": "supported"
  },
  {
   "mz": 110.0714,
   "reading": "Matched unshifted to Caffeine at 110.0713; shifted in the dimethylxanthine and 1-Methylxanthine references.",
   "certainty": "supported"
  },
  {
   "mz": 69.0448,
   "reading": "Matched unshifted to Caffeine and to the analog references, suggesting a fragment common to the xanthine core (tentative).",
   "certainty": "tentative"
  },
  {
   "mz": 42.0339,
   "reading": "Low-mass fragment shared unshifted with Caffeine and the analog references.",
   "certainty": "tentative"
  },
  {
   "mz": 83.0605,
   "reading": "Matched unshifted to Caffeine at 83.0604; not present in the analog references.",
   "certainty": "supported"
  },
  {
   "mz": 91.0542,
   "reading": "Unexplained by any candidate reference; low intensity, possibly co-isolated background or a collision-energy difference.",
   "certainty": "tentative"
  },
  {
   "mz": 150.066,
   "reading": "Unexplained by any candidate reference; low intensity, origin not supported by the supplied spectra.",
   "certainty": "tentative"
  }
 ],
 "caveats": [
  "Positional isomers or other compounds absent from the in-house list cannot be excluded by library matching alone.",
  "Two low-intensity query peaks are unexplained, which may reflect co-isolation in the plasma matrix or collision-energy differences between query and reference.",
  "The instrument and collision energy of the query are not stated, so comparability with the reference spectra is not documented."
 ],
 "next_evidence": [
  "Measure an authentic standard of Caffeine under the same C18 LC method and compare retention time and MS/MS with the feature at 5.20 min.",
  "Record the query and reference spectra at documented, matching collision energies and instrument settings.",
  "Check the isotope pattern and any other adducts of the 195.0878 feature to support the molecular formula.",
  "Compare against an independent public reference spectrum of Caffeine to confirm the in-house entry."
 ],
 "prescan_responses": [
  {
   "ref": "F1",
   "verdict": "confirmed",
   "note": "Caffeic acid and Citric acid were recorded as [M-H]- in negative mode while the query is positive; both are rejected and play no part in the annotation."
  },
  {
   "ref": "F2",
   "verdict": "confirmed",
   "note": "Theobromine scores 0.866 on only 2 matched peaks, below the 5-peak threshold, with a 14.0158 Da precursor difference; it is rejected and does not compete with Caffeine."
  },
  {
   "ref": "F3",
   "verdict": "confirmed",
   "note": "1-Methylxanthine, Theophylline and Paraxanthine pass only on the modified cosine with precursor shifts of 28.0314 or 14.0158 Da; they are called analogs, and only Caffeine matches the query precursor."
  }
 ]
}

Worked example: report

The same feature, with the annotation above handed over as annotation:

{
 "task": "report",
 "title": "Feature 195.0878 @ 5.20 min",
 "context": "Human plasma, protein-precipitated extract. C18 reversed-phase LC, ESI positive. The reference spectra were pasted from our in-house list. No authentic standard has been run yet.",
 "facts": "{\"settings\": {\"tolerance_da\": 0.02, \"min_score\": 0.6, \"min_matched_peaks\": 5, \"relative_intensity_floor\": 0.01, \"precursor_tolerance_ppm\": 10, \"processing\": \"same for query and references: non-positive peaks dropped, duplicate m/z merged (max), intensities scaled to the base peak, peaks below the relative-intensity floor removed\", \"scores\": \"greedy cosine and greedy modified cosine, mz_power 0, intensity_power 1, computed in the browser with matchms CosineGreedy / ModifiedCosineGreedy semantics\"}, \"query\": {\"title\": \"Feature 195.0878 @ 5.20 min\", \"precursor_mz\": 195.0878, \"ionmode\": \"positive\", \"adduct\": \"[M+H]+\", \"rt\": \"312\", \"instrument\": \"\", \"collision_energy\": \"\", \"peaks_raw\": 10, \"peaks_after_processing\": 10, \"peaks_sent\": 10, \"peaks\": [[42.0339, 0.1], [56.0496, 0.04], [69.0448, 0.15], [\"...\"]]}, \"library\": {\"spectra_read\": 8, \"spectra_scored\": 8, \"passing\": 1, \"candidates_sent\": 8, \"format\": \"msp\"}, \"candidates\": [{\"ref\": \"R1\", \"rank\": 1, \"name\": \"Caffeine\", \"id\": \"\", \"precursor_mz\": 195.0877, \"adduct\": \"[M+H]+\", \"ionmode\": \"positive\", \"formula\": \"C8H10N4O2\", \"inchikey\": \"RYYVLZVUVIJVGH-UHFFFAOYSA-N\", \"cosine\": 0.9937, \"cosine_matches\": 8, \"modified_cosine\": 0.9937, \"modified_matches\": 8, \"precursor_delta_da\": 0.0001, \"precursor_delta_ppm\": 0.5, \"explained_intensity\": 0.957, \"browser_status\": \"match\", \"top_pairs\": [{\"query_mz\": 138.0663, \"reference_mz\": 138.0662, \"shifted\": false, \"contribution\": 0.8213}, \"...\"], \"peaks\": [[42.0338, 0.08], [56.0495, 0.05], [\"...\"]]}, {\"ref\": \"R2\", \"rank\": 2, \"name\": \"Theobromine\", \"id\": \"\", \"precursor_mz\": 181.072, \"adduct\": \"[M+H]+\", \"ionmode\": \"positive\", \"formula\": \"C7H8N4O2\", \"inchikey\": \"YAPQBXQYLJRXSA-UHFFFAOYSA-N\", \"cosine\": 0.8663, \"cosine_matches\": 2, \"modified_cosine\": 0.9649, \"modified_matches\": 3, \"precursor_delta_da\": 14.0158, \"precursor_delta_ppm\": 77404.6, \"explained_intensity\": 0.616, \"browser_status\": \"weak\", \"top_pairs\": [{\"query_mz\": 138.0663, \"reference_mz\": 138.0662, \"shifted\": false, \"contribution\": 0.8219}, \"...\"], \"peaks\": [[67.0291, 0.1], [110.0713, 0.18], [\"...\"]]}, \"... 6 more\"], \"flags\": [{\"id\": \"F1\", \"severity\": \"medium\", \"category\": \"ion_mode_conflict\", \"message\": \"2 candidate(s) were recorded in the other ion mode from the query (positive); their spectra are not comparable as an identity match.\", \"refs\": [\"R6\", \"R7\"]}, {\"id\": \"F2\", \"severity\": \"medium\", \"category\": \"high_score_few_matches\", \"message\": \"1 reference(s) score at or above 0.6 on fewer than 5 matched peaks (Theobromine: 0.866 on 2); keep score and matched peaks together.\", \"refs\": [\"R2\"]}, {\"id\": \"F3\", \"severity\": \"low\", \"category\": \"analog_candidates\", \"message\": \"3 candidate(s) pass only on the modified cosine (1-Methylxanthine 0.965, shift +28.0314 Da; Theophylline 0.986, shift +14.0158 Da; Paraxanthine 0.967, shift +14.0158 Da): structural relatives, not identities.\", \"refs\": [\"R3\", \"R4\", \"R5\"]}], \"browser_verdict\": \"probable\", \"clipped\": {\"query_peaks_not_sent\": 0, \"library_spectra_not_scored\": 0}}",
 "question": "",
 "annotation": "Verdict: probable, level 2a, best candidate R1 Caffeine.\nHeadline: The query spectrum matches the Caffeine reference with cosine 0.994 on 8 matched peaks and a precursor difference of 0.5 ppm, supporting a probable structure at level 2a pen ..."
}

The reply:

{
 "lane": "report",
 "verdict": "probable",
 "level": "2a",
 "best_candidate": {
  "ref": "R1",
  "name": "Caffeine"
 },
 "headline": "The feature at precursor m/z 195.0878 is reported as Caffeine at level 2a (probable structure by library spectrum match), with cosine 0.994 on 8 matched peaks and a precursor difference of 0.5 ppm, pending an authentic standard.",
 "tldr": [
  "R1 Caffeine is the only candidate with browser status match: cosine 0.9937 on 8 matched peaks, explained intensity 0.957, same positive ion mode and [M+H]+ adduct, precursor 195.0877 within 0.0001 Da (0.5 ppm).",
  "No other distinct candidate comes close on the cosine: Theobromine scores 0.8663 on only 2 matched peaks, and Theophylline, Paraxanthine and 1-Methylxanthine pass only on the modified cosine with precursors differing by 14.0158 or 28.0314 Da.",
  "Level 1 is not supported because no authentic reference standard has been run; positional isomers absent from the in-house list cannot be excluded.",
  "Instrument, collision energy and library version are not stated and are marked AUTHOR_INPUT_NEEDED in the methods text."
 ],
 "methods_text": "Tandem mass spectra were acquired from a protein-precipitated human plasma extract by C18 reversed-phase LC with positive-mode electrospray ionisation on AUTHOR_INPUT_NEEDED (instrument) at AUTHOR_INPUT_NEEDED (collision energy). The query spectrum was compared with reference spectra from an in-house list (source and version: AUTHOR_INPUT_NEEDED). Query and reference spectra were processed identically: non-positive peaks were dropped, duplicate m/z values were merged by taking the maximum intensity, intensities were scaled to the base peak, and peaks below a relative intensity of 0.01 were removed. Spectral similarity was computed as greedy cosine and greedy modified cosine scores with a fragment m/z tolerance of 0.02 Da, m/z power 0 and intensity power 1. A reference was considered a match when it reached a score of at least 0.6 on at least 5 matched peaks, with the precursor m/z within 10 ppm and the same ion mode; references passing only on the modified cosine were treated as structural analogs. Eight library spectra were scored. Confidence levels were assigned following Schymanski et al. (2014): level 2a required a library spectrum match with precursor agreement and no competing distinct candidate of similar score, and level 1 was reserved for confirmation against an authentic standard measured under the same conditions.",
 "results_text": "The feature at precursor m/z 195.0878 ([M+H]+, 5.20 min) was annotated as Caffeine at confidence level 2a, with a cosine score of 0.9937 on 8 matched peaks and a precursor difference of 0.5 ppm against the in-house reference spectrum. The annotation is based on library spectral matching only; no authentic standard has been measured, so retention time confirmation and exclusion of isomers not in the library remain outstanding.",
 "table_row": {
  "feature": "Feature 195.0878 @ 5.20 min",
  "precursor_mz": "195.0878",
  "adduct": "[M+H]+",
  "annotation": "Caffeine",
  "level": "2a",
  "score": "0.9937",
  "matched_peaks": "8",
  "reference": "RYYVLZVUVIJVGH-UHFFFAOYSA-N",
  "evidence": "Library MS/MS match, cosine 0.994 on 8 peaks, precursor within 0.5 ppm; no authentic standard"
 },
 "limitations": [
  "The annotation rests on library spectral similarity alone; no authentic Caffeine standard was measured under the same LC method, so retention time and MS/MS were not confirmed.",
  "Positional isomers or other compounds absent from the in-house reference list cannot be excluded, and MS/MS does not resolve stereochemistry.",
  "Query peaks at 91.05 and 150.07 have no counterpart in the Caffeine reference spectrum, which may reflect co-isolation in the plasma matrix or collision-energy differences.",
  "The instrument and collision energy of the query and the provenance of the in-house reference spectra are not documented, so spectral comparability cannot be fully assessed."
 ],
 "next_evidence": [
  "Measure an authentic standard of Caffeine under the same C18 LC method and compare retention time and MS/MS with the feature at 5.20 min.",
  "Document the instrument and collision energy for the query and the in-house reference spectra, and re-acquire at matching settings if they differ.",
  "Compare the query against an independent public reference spectrum of Caffeine to corroborate the in-house entry.",
  "Check the isotope pattern and other adducts of the 195.0878 feature to support the molecular formula."
 ],
 "prescan_responses": [
  {
   "ref": "F1",
   "verdict": "confirmed",
   "note": "Caffeic acid (R6) and Citric acid (R7) are recorded as [M-H]- in negative ion mode against a positive-mode query; both are rejected and the flag does not affect the reported Caffeine annotation."
  },
  {
   "ref": "F2",
   "verdict": "confirmed",
   "note": "Theobromine (R2) scores 0.8663 on only 2 matched peaks, below the 5-peak threshold, and its precursor 181.072 differs by 14.0158 Da; it is not a competing identity for the feature."
  },
  {
   "ref": "F3",
   "verdict": "confirmed",
   "note": "1-Methylxanthine, Theophylline and Paraxanthine pass only on the modified cosine with plain cosine below 0.04 and precursors differing by 28.0314 or 14.0158 Da; they are structural relatives, not identities, and do not compete with Caffeine."
  }
 ]
}

Truncation and partial results

If your balance sits between min_credits and hold_credits, the run still executes with a smaller output cap and the job carries "truncated": true. The JSON may then stop mid-object: close it (the page's Recon.closeJson does this) and show the sections that arrived, saying how many of the lane's sections were recovered, rather than treating a clipped reply as complete.