{"openapi":"3.1.0","info":{"title":"SpecAI Mass Spectrometry API","description":"Rank candidate structures against an MS/MS spectrum. Candidates are selected from a\ncatalogue by exact mass, then ranked by the FLARE dual-tower model using per-peak to\nper-atom late interaction.\n\n## Authentication\n\nAll requests require an API key sent as a bearer token:\n`Authorization: Bearer <key>`. A workspace owner creates and revokes keys on the\n**API keys** tab of the portal at `/portal/`; the secret is shown once at creation and\nis not recoverable. A key is scoped to one workspace and carries a fixed scope set.\nThe key's prefix records the environment that issued it (`sfm_test` outside\nproduction, `sfm_live` in it), so a key only works against the deployment that\nissued it.\n\nFailures are `401` when the credential is missing, unknown or revoked, and `403`\nwhen it is valid but lacks the scope the route requires.\n\nThe scopes are `catalogues:read`, `catalogues:write`, `match`, `models:read`,\n`keys:write`, `members:write` and `admin`, which satisfies all of them. Creating or\nrevoking API keys requires `keys:write` and adding or removing members requires\n`members:write`, so a key omitting them can use the workspace but not administer it;\na signed-in owner manages both by role and needs neither. A key created without an\nexplicit `scopes` list gets the four integration scopes only.\n\nThe portal calls a tenant a **Workspace**. In this API the same thing is an account:\n`/api/v1/accounts/...`, the `X-SFM-Account-ID` selector and the `account_id` field all\nrefer to a Workspace. API keys already carry their workspace, so an integration does\nnot send that header.\n\n## Reading limits from the API\n\n`GET /api/v1/limits` reports the accepted precursor and peak ranges, the minimum\npeak count, candidate and result caps, and the supported adducts with their mass\nshifts. These values derive from the served model and change when it does, so\nclients should read them rather than hardcoding them.\n\n## Constraints\n\n**The adduct is required and is never inferred.** An unrecognised value returns 422.\nThe adduct determines the neutral mass, so an incorrect value shifts the search by\nwhole Daltons and returns a ranked set of structures that are all incorrect. Polarity\nis taken from the adduct, so there is no `ion_mode` field to supply; the resolved\nvalue is returned in the response.\n\n**Precursor m/z is the most consequential field.** Beyond setting the neutral mass\nfor candidate selection, the spectrum tower derives a neutral-loss token per peak\nfrom it. An incorrect precursor therefore corrupts roughly half the spectrum tokens\nrather than merely shifting the search window.\n\n**`instrument` and `collision_energy` are recorded, not scored.** Both materially\naffect a spectrum, and that effect reaches the model through the peaks. The model was\ntrained to be invariant to them by domain randomization over\n(adduct, collision energy, instrument), so it has no input for either and supplying\nthem cannot change a ranking. They are accepted so a stored result records its\nacquisition conditions and so failures can be analysed by energy or instrument.\n\n**Exact mass does not identify a structure.** Isomers share an identical\nmonoisotopic mass rather than a similar one; glucose and fructose both resolve to\n180.063388 at 0.00 ppm error. Mass selection narrows the candidate set, and all\ndiscrimination within that set is performed by the model.\n\n**Clients must inspect `truncated`.** When a mass window contains more entries than\n`max_candidates`, the excess is discarded arbitrarily rather than by relevance,\nbecause candidates with identical mass cannot be ordered by mass error. The correct\nstructure may be among the discarded entries, and the returned ranking gives no\nindication of this.\n\n**Scores are not probabilities.** A score is the mean of cosine similarities in\n[-1, 1]. Scores are comparable between candidates for a single spectrum. They are\nnot calibrated and are not comparable across spectra. A small margin between the\nfirst and second result indicates the model has not separated those candidates.\n\n**Peaks are submitted unnormalised.** The service applies the square-root and\nmaximum-normalisation transform the model was trained with. Peaks outside the\nreported range are discarded before scoring rather than rejected, so a request may\nsucceed with fewer peaks than were submitted; the response reports both counts.","version":"0.1.0"},"paths":{"/api/v1/health":{"get":{"tags":["health"],"summary":"Health","description":"Liveness probe. Does not touch downstream dependencies.","operationId":"health_api_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}},"security":[]}},"/api/v1/ready":{"get":{"tags":["health"],"summary":"Ready","description":"Readiness probe. Add downstream dependency checks here as they land.","operationId":"ready_api_v1_ready_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}},"security":[]}},"/api/v1/me":{"get":{"tags":["meta"],"summary":"Identify the calling credential","description":"Resolve the caller to the account and scopes it is acting with.\n\nWorks for either credential kind. `api_key_id` is null for a signed-in person\nand `user_id` is null for a machine, so a client can tell which it is holding.","operationId":"whoami_api_v1_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeOut"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/api/v1/models":{"get":{"tags":["models"],"summary":"List Models","operationId":"list_models_api_v1_models_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/FamilySummary"},"type":"array","title":"Response List Models Api V1 Models Get"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/api/v1/models/{model_id}/versions":{"get":{"tags":["models"],"summary":"List Versions","operationId":"list_versions_api_v1_models__model_id__versions_get","parameters":[{"name":"model_id","in":"path","required":true,"schema":{"type":"string","title":"Model Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/VersionSummary"},"title":"Response List Versions Api V1 Models  Model Id  Versions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/api/v1/models/{model_id}/versions/{version}":{"get":{"tags":["models"],"summary":"Read Version","description":"Fetch one card. `version` accepts `latest`; the response resolves it.","operationId":"read_version_api_v1_models__model_id__versions__version__get","parameters":[{"name":"model_id","in":"path","required":true,"schema":{"type":"string","title":"Model Id"}},{"name":"version","in":"path","required":true,"schema":{"type":"string","title":"Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/api/v1/catalogues":{"get":{"tags":["catalogues"],"summary":"List Catalogues","description":"Own catalogues plus every shared reference catalogue.","operationId":"list_catalogues_api_v1_catalogues_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CatalogueOut"},"type":"array","title":"Response List Catalogues Api V1 Catalogues Get"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]},"post":{"tags":["catalogues"],"summary":"Create Catalogue","operationId":"create_catalogue_api_v1_catalogues_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogueCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogueOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/api/v1/catalogues/template":{"get":{"tags":["catalogues"],"summary":"Download a catalogue file template","description":"A two-row example of the catalogue file format.\n\nServed rather than written into the documentation prose so there is exactly\none copy of the example, and it is the one the parser is tested against. It\ncontains no tenant data, so a plain browser download does not require auth.","operationId":"catalogue_template_api_v1_catalogues_template_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}},"text/csv":{}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/api/v1/catalogues/{catalogue_id}":{"get":{"tags":["catalogues"],"summary":"Read Catalogue","operationId":"read_catalogue_api_v1_catalogues__catalogue_id__get","parameters":[{"name":"catalogue_id","in":"path","required":true,"schema":{"type":"string","title":"Catalogue Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogueOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]},"patch":{"tags":["catalogues"],"summary":"Update Catalogue","description":"Rename a catalogue, or change its description.\n\nThe seal covers contents, not the label. A stored match result has to keep\nmeaning the same set of structures, which a rename does not touch; and a\nshared catalogue's name is visible to every account, so fixing one should not\nrequire deleting the catalogue and loading the file again.","operationId":"update_catalogue_api_v1_catalogues__catalogue_id__patch","parameters":[{"name":"catalogue_id","in":"path","required":true,"schema":{"type":"string","title":"Catalogue Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogueUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogueOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]},"delete":{"tags":["catalogues"],"summary":"Delete Catalogue","operationId":"delete_catalogue_api_v1_catalogues__catalogue_id__delete","parameters":[{"name":"catalogue_id","in":"path","required":true,"schema":{"type":"string","title":"Catalogue Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/api/v1/catalogues/{catalogue_id}/entries":{"post":{"tags":["catalogues"],"summary":"Load a catalogue file","description":"Load structures into an empty catalogue.\n\nLoading is allowed exactly once. A sealed catalogue returns 409, because a\nmatch result cites a catalogue by id and an id whose contents can change is\nnot reproducible. Replacing contents is a delete and a fresh load.\n\nSynchronous, so bounded per request. Files larger than the cap need the async\njob path, which does not exist yet; the bound is enforced rather than letting\na request run unbounded.","operationId":"load_catalogue_file_api_v1_catalogues__catalogue_id__entries_post","parameters":[{"name":"catalogue_id","in":"path","required":true,"schema":{"type":"string","title":"Catalogue Id"}}],"requestBody":{"required":true,"content":{"text/csv":{"schema":{"type":"string","contentMediaType":"application/octet-stream","description":"The catalogue file itself as the request body, not multipart. UTF-8 text, comma or tab delimited, with a header row naming its columns. `smiles` is required; `name` and `source_id` are optional and any other column is ignored. Blank lines and lines starting with `#` are skipped. Formula, InChIKey and monoisotopic mass are derived from the structure with RDKit, so they are not accepted as columns: a file cannot then disagree with itself about what a structure weighs. GET /catalogues/template returns a working example.","title":"File"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"409":{"description":"The catalogue is already loaded. A sealed catalogue is replaced by deleting it and loading a fresh one, never by loading again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]},"get":{"tags":["catalogues"],"summary":"List Entries","description":"Page through a catalogue's structures, ordered by monoisotopic mass.\n\nFor reading a catalogue, not for matching. `GET .../search` answers \"what would\nthis spectrum be compared against\"; this answers \"what is in here at all\".","operationId":"list_entries_api_v1_catalogues__catalogue_id__entries_get","parameters":[{"name":"catalogue_id","in":"path","required":true,"schema":{"type":"string","title":"Catalogue Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"include_depictions","in":"query","required":false,"schema":{"type":"boolean","description":"Attach a 2D structure SVG to each entry. Several KB per entry, so a full page of them is a different order of response size; opt in only when something is going to draw them.","default":false,"title":"Include Depictions"},"description":"Attach a 2D structure SVG to each entry. Several KB per entry, so a full page of them is a different order of response size; opt in only when something is going to draw them."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntryPageOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/api/v1/catalogues/{catalogue_id}/search":{"get":{"tags":["catalogues"],"summary":"Search By Mass","description":"Mass-window candidate lookup, for inspecting a catalogue directly.\n\nTakes a neutral mass rather than a precursor m/z: converting m/z to neutral\nmass needs the adduct, which belongs to the match endpoint. This endpoint is\nthe raw index query.","operationId":"search_by_mass_api_v1_catalogues__catalogue_id__search_get","parameters":[{"name":"catalogue_id","in":"path","required":true,"schema":{"type":"string","title":"Catalogue Id"}},{"name":"neutral_mass","in":"query","required":true,"schema":{"type":"number","exclusiveMinimum":0,"description":"Neutral monoisotopic mass in Da.","title":"Neutral Mass"},"description":"Neutral monoisotopic mass in Da."},{"name":"tolerance_ppm","in":"query","required":false,"schema":{"type":"number","maximum":1000,"exclusiveMinimum":0,"default":10.0,"title":"Tolerance Ppm"}},{"name":"max_candidates","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"default":256,"title":"Max Candidates"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CandidateSetOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/api/v1/match":{"post":{"tags":["match"],"summary":"Match","operationId":"match_api_v1_match_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/api/v1/limits":{"get":{"tags":["meta"],"summary":"Read Limits","operationId":"read_limits_api_v1_limits_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LimitsOut"}}}},"401":{"description":"No usable credential: the API key is missing, unknown or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"403":{"description":"The credential is valid but lacks the scope this route requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}}},"security":[{"ApiKeyBearer":[]}]}}},"components":{"schemas":{"AccountOut":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"slug":{"type":"string","title":"Slug"},"status":{"type":"string","title":"Status"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","name","slug","status","created_at"],"title":"AccountOut"},"AdductOut":{"properties":{"name":{"type":"string","title":"Name"},"mass_shift":{"type":"number","title":"Mass Shift"},"ion_mode":{"type":"string","title":"Ion Mode"},"charge":{"type":"integer","title":"Charge"}},"type":"object","required":["name","mass_shift","ion_mode","charge"],"title":"AdductOut","description":"One supported adduct.\n\n`mass_shift` is applied to the neutral monoisotopic mass to reach the ion, so a\nclient can compute the precursor m/z it should be sending, or check the one it\nhas."},"CandidateOut":{"properties":{"id":{"type":"string","title":"Id"},"smiles":{"type":"string","title":"Smiles"},"inchikey":{"type":"string","title":"Inchikey"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"formula":{"type":"string","title":"Formula"},"monoisotopic_mass":{"type":"number","title":"Monoisotopic Mass"},"n_atoms":{"type":"integer","title":"N Atoms"},"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id"},"unknown_elements":{"items":{"type":"string"},"type":"array","title":"Unknown Elements"},"depiction_svg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Depiction Svg"},"mass_error_ppm":{"type":"number","title":"Mass Error Ppm"}},"type":"object","required":["id","smiles","inchikey","name","formula","monoisotopic_mass","n_atoms","source_id","unknown_elements","mass_error_ppm"],"title":"CandidateOut"},"CandidateSelection":{"properties":{"tolerance_ppm":{"type":"number","maximum":1000.0,"exclusiveMinimum":0.0,"title":"Tolerance Ppm","default":10.0},"max_candidates":{"type":"integer","maximum":2000.0,"minimum":1.0,"title":"Max Candidates","default":256}},"type":"object","title":"CandidateSelection"},"CandidateSetOut":{"properties":{"neutral_mass":{"type":"number","title":"Neutral Mass"},"window_da":{"type":"number","title":"Window Da"},"total_in_window":{"type":"integer","title":"Total In Window"},"truncated":{"type":"boolean","title":"Truncated"},"candidates":{"items":{"$ref":"#/components/schemas/CandidateOut"},"type":"array","title":"Candidates"}},"type":"object","required":["neutral_mass","window_da","total_in_window","truncated","candidates"],"title":"CandidateSetOut","description":"A mass-window query result.\n\n`truncated` must be checked. Exact isomers have identical mass, so a capped\nwindow discards tied structures near-arbitrarily and may have dropped the\ncorrect one."},"CatalogueCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"description":{"type":"string","maxLength":2000,"title":"Description","default":""},"shared":{"type":"boolean","title":"Shared","description":"Admin only. Creates an unowned reference catalogue readable by every workspace.","default":false}},"type":"object","required":["name"],"title":"CatalogueCreate"},"CatalogueOut":{"properties":{"id":{"type":"string","title":"Id"},"account_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Account Id"},"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"entry_count":{"type":"integer","title":"Entry Count"},"is_shared":{"type":"boolean","title":"Is Shared"},"sealed":{"type":"boolean","title":"Sealed","description":"True once structures have been loaded. A sealed catalogue accepts no further loads and cannot be edited, so a match result that cites it stays reproducible. Replacing its contents means deleting it and loading a new one."}},"type":"object","required":["id","account_id","name","description","entry_count","is_shared","sealed"],"title":"CatalogueOut"},"CatalogueUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Description"}},"type":"object","title":"CatalogueUpdate","description":"Metadata only. Contents are sealed after loading and are not editable."},"EntryOut":{"properties":{"id":{"type":"string","title":"Id"},"smiles":{"type":"string","title":"Smiles"},"inchikey":{"type":"string","title":"Inchikey"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"formula":{"type":"string","title":"Formula"},"monoisotopic_mass":{"type":"number","title":"Monoisotopic Mass"},"n_atoms":{"type":"integer","title":"N Atoms"},"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id"},"unknown_elements":{"items":{"type":"string"},"type":"array","title":"Unknown Elements"},"depiction_svg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Depiction Svg"}},"type":"object","required":["id","smiles","inchikey","name","formula","monoisotopic_mass","n_atoms","source_id","unknown_elements"],"title":"EntryOut"},"EntryPageOut":{"properties":{"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"entries":{"items":{"$ref":"#/components/schemas/EntryOut"},"type":"array","title":"Entries"}},"type":"object","required":["total","limit","offset","entries"],"title":"EntryPageOut","description":"One page of a catalogue, ordered by monoisotopic mass.\n\nThe order is total, so a row cannot appear on two pages or on none while\npaging. `total` is the whole catalogue, not the page."},"FamilySummary":{"properties":{"id":{"type":"string","title":"Id"},"modality":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Modality"},"latest_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Latest Version"},"versions":{"items":{"type":"string"},"type":"array","title":"Versions"}},"type":"object","required":["id","modality","latest_version","versions"],"title":"FamilySummary"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","title":"Status"},"version":{"type":"string","title":"Version"}},"type":"object","required":["status","version"],"title":"HealthResponse"},"HitOut":{"properties":{"rank":{"type":"integer","title":"Rank"},"score":{"type":"number","title":"Score"},"smiles":{"type":"string","title":"Smiles"},"inchikey":{"type":"string","title":"Inchikey"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"formula":{"type":"string","title":"Formula"},"monoisotopic_mass":{"type":"number","title":"Monoisotopic Mass"},"mass_error_ppm":{"type":"number","title":"Mass Error Ppm"},"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id"},"warnings":{"items":{"type":"string"},"type":"array","title":"Warnings"},"depiction_svg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Depiction Svg"}},"type":"object","required":["rank","score","smiles","inchikey","name","formula","monoisotopic_mass","mass_error_ppm","source_id","warnings"],"title":"HitOut"},"LimitsOut":{"properties":{"precursor_mz":{"$ref":"#/components/schemas/Range"},"peak_mz":{"$ref":"#/components/schemas/Range"},"min_peaks":{"type":"integer","title":"Min Peaks"},"max_peaks_retained":{"type":"integer","title":"Max Peaks Retained"},"default_tolerance_ppm":{"type":"number","title":"Default Tolerance Ppm"},"max_tolerance_ppm":{"type":"number","title":"Max Tolerance Ppm"},"default_max_candidates":{"type":"integer","title":"Default Max Candidates"},"max_max_candidates":{"type":"integer","title":"Max Max Candidates"},"max_top_k":{"type":"integer","title":"Max Top K"},"max_entries_per_ingest":{"type":"integer","title":"Max Entries Per Ingest"},"adducts":{"items":{"$ref":"#/components/schemas/AdductOut"},"type":"array","title":"Adducts"}},"type":"object","required":["precursor_mz","peak_mz","min_peaks","max_peaks_retained","default_tolerance_ppm","max_tolerance_ppm","default_max_candidates","max_max_candidates","max_top_k","max_entries_per_ingest","adducts"],"title":"LimitsOut","description":"What the match endpoint will accept.\n\nFetch this instead of hardcoding bounds. Peaks outside `peak_mz` are dropped\nbefore scoring rather than rejected, so a spectrum can be accepted and still\nlose peaks; the match response reports how many."},"LoadResponse":{"properties":{"added":{"type":"integer","title":"Added"},"duplicates":{"type":"integer","title":"Duplicates"},"rejected":{"items":{"$ref":"#/components/schemas/RejectedRowOut"},"type":"array","title":"Rejected"},"entry_count":{"type":"integer","title":"Entry Count"}},"type":"object","required":["added","duplicates","rejected","entry_count"],"title":"LoadResponse","description":"Per-row outcome of a load. A rejected row does not fail the file."},"MatchRequest":{"properties":{"catalogue_id":{"type":"string","title":"Catalogue Id"},"spectrum":{"$ref":"#/components/schemas/SpectrumIn"},"model":{"type":"string","title":"Model","default":"sfm-mass-spec"},"version":{"type":"string","title":"Version","default":"latest"},"candidate_selection":{"$ref":"#/components/schemas/CandidateSelection"},"top_k":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Top K","default":10},"include_depictions":{"type":"boolean","title":"Include Depictions","description":"Attach a 2D structure SVG to each hit. Several KB per hit, so it is opt-in: the portal wants pictures, most API consumers want identifiers.","default":false}},"type":"object","required":["catalogue_id","spectrum"],"title":"MatchRequest"},"MatchResponse":{"properties":{"model":{"type":"string","title":"Model"},"version":{"type":"string","title":"Version"},"catalogue_id":{"type":"string","title":"Catalogue Id"},"spectrum":{"$ref":"#/components/schemas/SpectrumEcho"},"neutral_mass":{"type":"number","title":"Neutral Mass"},"window_da":{"type":"number","title":"Window Da"},"candidates_in_window":{"type":"integer","title":"Candidates In Window"},"candidates_scored":{"type":"integer","title":"Candidates Scored"},"truncated":{"type":"boolean","title":"Truncated"},"hits":{"items":{"$ref":"#/components/schemas/HitOut"},"type":"array","title":"Hits"},"warnings":{"items":{"type":"string"},"type":"array","title":"Warnings"}},"type":"object","required":["model","version","catalogue_id","spectrum","neutral_mass","window_da","candidates_in_window","candidates_scored","truncated","hits","warnings"],"title":"MatchResponse","description":"A ranked match.\n\n`truncated` must be checked before treating the ranking as complete. Isomers\nshare an exact mass, so a capped window discards tied structures arbitrarily\nrather than by relevance, and the correct one may be among them.\n\nScores are means of cosine similarities in [-1, 1]. They are comparable across\ncandidates for the same spectrum, and are not calibrated probabilities nor\ncomparable across spectra."},"MeOut":{"properties":{"account":{"$ref":"#/components/schemas/AccountOut"},"api_key_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key Id"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id"},"role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes"}},"type":"object","required":["account","scopes"],"title":"MeOut","description":"Whoever is calling, and what they may do.\n\nExactly one of `api_key_id` or `user_id` is set, so a client can tell whether it\nauthenticated as a machine or a person."},"PeakIn":{"properties":{"mz":{"type":"number","exclusiveMinimum":0.0,"title":"Mz"},"intensity":{"type":"number","exclusiveMinimum":0.0,"title":"Intensity"}},"type":"object","required":["mz","intensity"],"title":"PeakIn","description":"One raw peak. Intensities are normalized server-side; do not pre-scale."},"Range":{"properties":{"min":{"type":"number","title":"Min"},"max":{"type":"number","title":"Max"}},"type":"object","required":["min","max"],"title":"Range"},"RejectedRowOut":{"properties":{"line":{"type":"integer","title":"Line"},"smiles":{"type":"string","title":"Smiles"},"reason":{"type":"string","title":"Reason"}},"type":"object","required":["line","smiles","reason"],"title":"RejectedRowOut","description":"A row that was not stored, identified by its line number in the file.\n\nThe line number rather than a row index: a caller fixing a file needs the\nplace to go, and a row index does not survive comments and blank lines."},"SpectrumEcho":{"properties":{"precursor_mz":{"type":"number","title":"Precursor Mz"},"ion_mode":{"type":"string","title":"Ion Mode"},"adduct":{"type":"string","title":"Adduct"},"peaks_submitted":{"type":"integer","title":"Peaks Submitted"},"peaks_used":{"type":"integer","title":"Peaks Used"},"instrument":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Instrument"},"collision_energy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Collision Energy"}},"type":"object","required":["precursor_mz","ion_mode","adduct","peaks_submitted","peaks_used"],"title":"SpectrumEcho","description":"What the service made of the submitted spectrum.\n\n`ion_mode` is derived from the adduct and returned so a stored result records\nthe polarity that was searched."},"SpectrumIn":{"properties":{"precursor_mz":{"type":"number","exclusiveMinimum":0.0,"title":"Precursor Mz","description":"Observed precursor m/z. The most consequential field: it sets the neutral mass for candidate selection, and the spectrum tower also derives a neutral-loss token per peak from it, so an incorrect value corrupts roughly half the tokens rather than merely shifting the window."},"adduct":{"type":"string","title":"Adduct","description":"Required, not defaulted. Guessing shifts the neutral mass by whole Daltons and searches the wrong structures. Known: ['[M+H]+', '[M+NH4]+', '[M+Na]+', '[M-H]-']. Polarity is taken from here; there is no separate ion_mode field."},"peaks":{"items":{"$ref":"#/components/schemas/PeakIn"},"type":"array","maxItems":5000,"minItems":1,"title":"Peaks"},"instrument":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Instrument","description":"Optional. Recorded and echoed back, never scored. The model was trained to be invariant to instrument by domain randomization and has no input for it; its effect reaches the model through the peaks."},"collision_energy":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Collision Energy","description":"Optional, free text as the instrument reports it (\"20\", \"20 eV\", \"NCE 35\", \"HCD 35%\"). Recorded and echoed back, never scored, for the same reason as instrument. A leading number is extracted where present so energies can be grouped for analysis; the raw string is authoritative."}},"type":"object","required":["precursor_mz","adduct","peaks"],"title":"SpectrumIn"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VersionDetail":{"properties":{"version":{"type":"string","title":"Version"},"status":{"type":"string","title":"Status"},"description":{"type":"string","title":"Description"},"epoch":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Epoch"},"params_trainable_m":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Params Trainable M"},"metrics":{"additionalProperties":{"additionalProperties":{"type":"number"},"type":"object"},"type":"object","title":"Metrics"},"name":{"type":"string","title":"Name"},"file":{"type":"string","title":"File"},"sha256":{"type":"string","title":"Sha256"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"code_repo_commit":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Repo Commit"},"committed":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Committed"},"config":{"additionalProperties":true,"type":"object","title":"Config"}},"type":"object","required":["version","status","description","epoch","params_trainable_m","metrics","name","file","sha256","source","code_repo_commit","committed","config"],"title":"VersionDetail","description":"Full card, including provenance needed to reproduce a result."},"VersionSummary":{"properties":{"version":{"type":"string","title":"Version"},"status":{"type":"string","title":"Status"},"description":{"type":"string","title":"Description"},"epoch":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Epoch"},"params_trainable_m":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Params Trainable M"},"metrics":{"additionalProperties":{"additionalProperties":{"type":"number"},"type":"object"},"type":"object","title":"Metrics"}},"type":"object","required":["version","status","description","epoch","params_trainable_m","metrics"],"title":"VersionSummary"},"ErrorDetail":{"title":"ErrorDetail","type":"object","properties":{"detail":{"type":"string","title":"Detail"}},"required":["detail"]}},"securitySchemes":{"ApiKeyBearer":{"type":"http","scheme":"bearer","description":"In this page, paste the API key on its own. Over HTTP, send it as `Authorization: Bearer <key>`."}}},"tags":[{"name":"match","description":"Rank catalogue structures against a spectrum."},{"name":"catalogues","description":"Sets of candidate structures. A catalogue is loaded once from a delimited text file and its contents are then final: a match response cites a catalogue by id, so an id whose contents can change is not reproducible. Loading again returns 409; replacing contents is a delete and a fresh load. Canonical SMILES, InChIKey, formula and monoisotopic mass are derived with RDKit, so they are not columns in the file. Unreadable rows are reported with their line number rather than failing the file, and duplicates collapse by InChIKey. A catalogue with a null `account_id` is a shared reference catalogue: readable by every workspace, writable only by an admin."},{"name":"models","description":"Released checkpoints and their provenance. `latest` resolves to the newest non-deprecated version; responses always echo the concrete version so a result can be reproduced from the response alone."},{"name":"meta","description":"Caller identity and the validation bounds the service enforces."},{"name":"health","description":"Liveness and readiness probes."}]}