{"data":{"version":"v1","base_url":"https://api.softon.dev/v1","datasets":[{"slug":"jobs","name":"Job Data API","status":"live","operations":[{"id":"list-jobs","verb":"GET","path":"/v1/jobs","summary":"List job postings","returns":"array","params":[{"name":"q","in":"query","type":"string","required":false,"desc":"Full-text match on title, company and description.","example":"FINCA"},{"name":"remote","in":"query","type":"boolean","required":false,"desc":"Restrict to remote-eligible postings.","example":"false"},{"name":"company","in":"query","type":"string","required":false,"desc":"Company slug or name."},{"name":"updated_after","in":"query","type":"timestamp","required":false,"desc":"Only rows whose `ingested_at` is later than this instant — everything that CHANGED since your last poll, new and re-seen alike. Pass the newest `ingested_at` you have stored. Also returns a posting whose only change was going inactive: retirement deliberately does not move `ingested_at`, so this matches `deactivated_at` as well and a mirror is told when a posting dies.","example":"2026-08-16T09:00:00Z"},{"name":"first_seen_after","in":"query","type":"timestamp","required":false,"desc":"Only postings this platform saw for the FIRST time after this instant. A refresh re-observes thousands of rows; across a day about **1,400** are genuinely new, which is roughly 15 requests — measured by walking a 24-hour window to exhaustion, not estimated. `first_seen_at` never moves once set, so it is safe as a watermark.","example":"2026-08-16T09:00:00Z"},{"name":"posted_after","in":"query","type":"date","required":false,"desc":"ISO-8601 date. Filters on posted_at -- the date the SOURCE published the posting, not the date we first saw it. Page by order=ingested_at for the latter.","example":"2026-08-01"},{"name":"location","in":"query","type":"string","required":false,"desc":"Substring match on `location`, case-insensitive. That field is free text at whatever precision the source publishes, so this matches a city inside \"Bakı, Azərbaycan\" -- and equally matches a country on the sources that publish only one. Not a place lookup: nothing is geocoded, and a spelling the source does not use will not match.","example":"Bakı"},{"name":"employment_type","in":"query","type":"string","required":false,"desc":"Exact match on `employment_type`, case-insensitive, and treating `-` and `_` as the same character. Two vocabularies are in this column: sources that derive the value from body text emit a schema.org token, and the rest pass through the employer's own label. The fold joins the pairs that differ only in punctuation — `FULL_TIME`/`Full-time`, `PART_TIME`/`Part-time`, `TEMPORARY`/`Temporary` — and it does NOT join `CONTRACTOR`/`Contract` or `INTERN`/`Internship`, which are different words. Query those separately; nothing here maps one vocabulary onto the other.","example":"Full-time"},{"name":"job_function","in":"query","type":"string","required":false,"desc":"Substring match on `job_function`, case-insensitive. Each source's own taxonomy, not normalised between them, so treat this as a search rather than an enum.","example":"Bank services"},{"name":"career_level","in":"query","type":"string","required":false,"desc":"Substring match on `career_level`, case-insensitive. The source's own label, not normalised.","example":"Professional"},{"name":"has_salary","in":"query","type":"boolean","required":false,"desc":"Only postings that publish pay (`true`), or only those that do not (`false`). Most do not: a minority of sources quote a figure at all, so this is usually the first filter to reach for if you are working with pay. Either end counts — a posting advertising a floor with no ceiling publishes pay.","example":"true"},{"name":"salary_from_at_least","in":"query","type":"number","required":false,"desc":"Minimum published pay, compared against `salary_from` — the FLOOR a posting quotes, not the ceiling. A posting offering 800–2000 does not pay at least 1500, and matching it on `salary_to` would answer a different question. Postings that publish no floor are excluded rather than assumed: null means the source did not say. Nothing is converted between currencies — pair this with `salary_currency`.","example":"1500"},{"name":"salary_currency","in":"query","type":"string","required":false,"desc":"Exact match on the source's own currency code, case-insensitive, usually AZN. Pay figures are not converted, so a bare number means different things on different postings; this is how you pin one.","example":"AZN"},{"name":"count","in":"query","type":"boolean","required":false,"desc":"Adds `meta.total` — how many rows the filter matches in ALL, across every page. `meta.count` is and remains the size of THIS page.\n\nOpt-in because it is a second query over the whole match, and a caller paging a feed should not pay for a number they never read. Exact rather than estimated: a filtered total is something you put on a page (\"1,247 jobs in Sumqayıt\"), and an estimate that is wrong by an unbounded amount is worse than no number.\n\nIf the count fails the page still returns, without `total` — the rows are the answer.","example":"true"},{"name":"include","in":"query","type":"string","required":false,"desc":"Opt a collection into fields that are off by default. One value today: `bodies`, which adds `description_html` and `requirements_html` to every row.\n\nThey are off by default because a body roughly doubles the size of a row that has one, and most callers walking 100 at a time do not want that. Ask for them when you do — hydrating a corpus one `/v1/jobs/{id}` call at a time would cost more requests than any plan allows, which made a documented feature unreachable.\n\nComma-separated for future values. An unrecognised token is a 400 rather than being ignored, so a typo cannot look like it worked.","example":"bodies"},{"name":"city","in":"query","type":"string","required":false,"desc":"Exact match on the normalised `city`, case-insensitive so the diacritics do not have to be typed. Not a substring search, deliberately: matching a city name loosely is what makes an employer called \"Bakı Bank\" in Sumqayıt show up under Bakı. Only postings whose source published a place are returned; ones whose location was recovered from the posting body are not, because that city may be the head office.","example":"Sumqayıt"},{"name":"district","in":"query","type":"string","required":false,"desc":"Exact match on the Baku rayon, case-insensitive. Narrower than `city` and only ever set for Baku postings whose source named a rayon.","example":"Nəsimi"},{"name":"country","in":"query","type":"string","required":false,"desc":"Exact match on the ISO 3166-1 alpha-2 `country`, case-insensitive — `?country=AZ`. This is the filter for \"only postings I can actually serve\": before it existed, an Azerbaijan-market board had no way to ask this API for Azerbaijan, and a source publishing its global board put Houston roles on two Azerbaijani job boards.\n\n**ISO 3166-1 alpha-2 — the two-letter code, not a name and not a language tag.** The United Kingdom is `GB`, never `UK`; `EN` is a language and not a country at all. One code, not a list: `?country=AZ,GE` and a repeated `?country=` do not select two countries. Any value that is not two letters is a `400` naming what you sent — except an empty `?country=`, which is the same as not sending it and applies no filter. `UK` gets a `400` telling you the code is `GB` — an empty page for a mistyped code reads as \"you hold nothing there\", which is the one thing this filter must never say by accident. A code the standard DOES define but this corpus has no rows for returns an empty page, because that answer is true.\n\n**It returns only postings whose source PUBLISHED a country**, which is a minority of the corpus — most rows are null because the board never said. So `?country=AZ` is much narrower than \"jobs in Azerbaijan\", and there is no way to ask for the nulls. If you want everything except the foreign rows, filter them out on your side by the country you get back rather than asking for one here.","example":"AZ"},{"name":"deadline_after","in":"query","type":"date","required":false,"desc":"ISO-8601 date. Only postings whose application deadline is on or after it — pass today's date for \"still open\". Postings with no deadline are excluded, and most have none, so this narrows hard. Unlike `posted_at` a future date is kept rather than nulled: that is what a deadline is.","example":"2026-09-01"},{"name":"active","in":"query","type":"string","required":false,"desc":"Which postings to return. `any` — **the default today** — is every posting this platform holds; `true` is only those whose source was still publishing them at its last complete scrape; `false` is only the retired ones. The default is `any` rather than `true` on purpose — a retired posting is accurate data with an archival use, unlike the inactive half of the quotes pool — but if you are showing postings to jobseekers, pass `true`.\n\n**The default stays `any`.** Moving it to `true` was announced on 2026-10-03 and withdrawn on 2026-10-15, before it took effect, so that no integration's results change under it.\n\n**Most of the corpus is retired**: 39,046 postings were still open of 149,383 held when this was measured (2026-08-28). So the default walk returns roughly four rows of history for every live vacancy, and coverage of the optional fields is markedly worse on the retired ones. If you are sizing the dataset rather than archiving it, pass `true` and read `live_records` on `GET /v1/sources`.","example":"true"},{"name":"source_type","in":"query","type":"string","required":false,"desc":"Restrict to whole CLASSES of publisher, rather than to named sources. Comma-separated; the one filter here that takes a list, because the question it answers is a union.\n\n`employer` — one organisation's own careers page, government bodies and universities included. `platform` — a multi-tenant careers platform, where each posting is still an employer's own (Glorri). `agency` — a recruitment or staffing firm posting for clients. `board` — a multi-employer job board. `aggregator` — republishes other boards' postings. `unknown` — four archive stems with no module and no registry entry, classified honestly rather than guessed.\n\nSo **everything that did not come from a job board** is `?source_type=employer,platform,agency` — one filter instead of fifty `?source=` calls. Measured 2026-08-28, that is 2,150 of the 39,046 live postings; the boards are 31,775 of them. Size it before you build on it.\n\n**This is our classification, not a fact the source asserts.** It is the one editorial field in this API: a value can change when a source is re-read, and the calls most likely to be argued with are recorded in the repository. The SET of values is stable. `GET /v1/sources` carries each source's `source_type` so you can see what a filter will include.","example":"employer,platform,agency"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `ingested_at`, `posted_at`, each optionally prefixed with `-` to reverse. Default is `-posted_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-posted_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003csource id\u003e\". Pass it to /v1/jobs/{id}.\n\nThe source id is the posting number the site itself publishes, for every source but eight. Those eight publish none: `ddla_gov_az`, `ishelanlari_az`, `kaspi_edu_az`, `konsis`, `mdm`, `regulator`, `royalesb` and `ziraat`, and for them it is the first sixteen hex digits of the SHA-1 of the posting's URL, or of its title for `mdm`, whose postings all share one URL. So an id changes only when the site renumbers a posting, or, for those eight, changes its URL (`mdm`: its title, and the old row stays until a run retires it for being absent). A record from any other source that arrives without its id is dropped, never given a new one."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper produced this posting. A stable slug, and half of `id` — this is what `?source=` takes, so it never changes even when a board renames itself. `GET /v1/sources` lists every slug with its display name, its dataset, how many rows it currently has and when it last landed a batch. That endpoint needs no key."},{"name":"source_name","type":"string","nullable":true,"desc":"The same source as a person would write it — `ABB` for `abb`, `1is.az` for `1is_az`. This is the one to display. Null for the handful of sources the scraper registry has no entry for, rather than repeating the slug."},{"name":"source_url","type":"string","nullable":true,"desc":"The source's own website — `https://glorri.com` for `glorri`, `https://abb-bank.az` for `abb`. This is the one to link `source_name` to.\n\n**Not this posting's link** — that is `url` — and not the source's listing page either. `GET /v1/sources` carries both, as `url` and `jobs_url`, and needs no key; `jobs_url` there is the page this platform reads the source's vacancies from, which is the better link if you are sending somebody to browse a source rather than to read about it.\n\nSet for nearly every source; `GET /v1/sources` says which, live. Null rather than guessed where there is no honest answer: sources hosted entirely on a third party's applicant-tracking system, whose own domain appears nowhere in our scrapers (their `jobs_url` on `/v1/sources` still points at the ATS page the postings are actually on), a source with only its ATS vendor's domain recorded against it, `other`, a catch-all bucket with no website, and **sources we have measured as gone** — a hosting placeholder on every path, an organisation that no longer exists and now redirects to a different agency, a decommissioned ATS tenant. Those keep their rows and lose their link, because a link to nothing is worse than none."},{"name":"title","type":"string","nullable":false,"desc":"Posting title as published."},{"name":"company","type":"object","nullable":true,"desc":"The hiring company. `company.name` is always present when the object is; `company.slug` is nullable and is null on most rows -- do not type it as required. The whole object is null when the source omits the company, and is ALWAYS null on classifieds sources: there the advert is placed by a private individual rather than an employer, so there is no hiring company to name and publishing the poster under this field would be publishing a person's name as a company (#83)."},{"name":"location","type":"string","nullable":true,"desc":"Free text at whatever precision the source itself publishes — a city, a city and country, a district, an office or branch name, or the country alone. Not normalised, not geocoded, and not comparable between sources. Some boards publish no city at all and this is their country; nothing is inferred from the posting body to fill the gap, because a city read out of prose is as likely to be the employer's head office as the job's location."},{"name":"city","type":"string","nullable":true,"desc":"**25% of live postings** — 9,690 of 39,046, counted exactly on 2026-08-28 by summing `?city=` across the whole vocabulary. This said 60% and climbing, which was a projection rather than a measurement and is not where it is heading: the field fills only from sources that publish a structured place, about twenty of a hundred and thirty do, and several of the largest sources never will. Nine in ten of the values are `Bakı`. The same place as `location`, normalised to a closed vocabulary of Azerbaijani cities so it can be filtered and grouped — which `location` cannot be. **Null unless the source published a place of its own**: a city inferred from advert prose is as likely to be the employer's head office as the job's, so postings whose location was recovered that way carry a null city and keep their `location`. Azerbaijani cities only — a posting in Moscow or Warsaw is null here and unchanged in `location`. `location` is untouched and stays the higher-precision value; this is the comparable one."},{"name":"district","type":"string","nullable":true,"desc":"The Baku rayon, where the source names one — `Nəsimi`, `Binəqədi`, `Xətai` and the other nine. Null everywhere else, including for Baku postings that name no rayon. Implies `city`: a rayon is Baku's, so `city` is always `Bakı` when this is set. Present because one source publishes a rayon where every other publishes a city, and because a per-rayon page needs rayon granularity that a city vocabulary cannot express."},{"name":"country","type":"string","nullable":true,"desc":"The posting's country as **ISO 3166-1 alpha-2** — `AZ`, `PL`, `US`. **Null unless the source itself published a country**, which most do not: an Azerbaijani board's postings are Azerbaijani by construction and it never says so on the row. So null means *nobody said*, NOT *somewhere else* — use this to narrow what you serve, never to conclude a posting is foreign.\n\nIndependent of `city`, not a widening of it. `city` is an Azerbaijani-only vocabulary, so a Warsaw posting is null there and `PL` here, while a Baku posting whose source named no country is `Bakı` there and null here.\n\nIt comes from a structured country field where the source publishes one (Oracle ATS `PrimaryLocationCountry`, jobpilot's `country_code`) and from a country component of `location` where the source appends one — the same provenance rule as `city`: nothing is inferred from the posting body, and a token that is not an ISO code is stored as null rather than published."},{"name":"remote","type":"boolean","nullable":true,"desc":"Whether the posting is remote-eligible."},{"name":"employment_type","type":"string","nullable":true,"desc":"Full-time, Part-time, Contract, as the source labels it."},{"name":"job_function","type":"string","nullable":true,"desc":"The source's own function taxonomy, not normalised across sources."},{"name":"career_level","type":"string","nullable":true,"desc":"The source's own seniority label. Not normalised, so it is not called \"seniority\"."},{"name":"salary_from","type":"number","nullable":true,"desc":"Bottom of the published pay range, in `salary_currency`. Null where the source does not publish pay, which is most postings -- and null rather than 0, which would be a figure. Nothing is inferred from the posting body."},{"name":"salary_to","type":"number","nullable":true,"desc":"Top of the range. Either end can be null on its own: plenty of postings advertise a floor with no ceiling."},{"name":"salary_currency","type":"string","nullable":true,"desc":"The source's own currency code for the range, usually AZN. Not converted, so two postings in different currencies are not comparable by number alone."},{"name":"salary_period","type":"string","nullable":true,"desc":"What the figures are per: `HOUR`, `DAY`, `WEEK`, `MONTH` or `YEAR`. **Null unless the source itself stated it**, which today is about 1% of live postings and 6% of those that carry pay at all, and always `MONTH` — the eight sources that publish a period all publish that one. Null is the useful answer, not a gap: it means nobody knows, so do not assume. If you emit JobPosting `unitText`, omit it when this is null rather than defaulting to `MONTH`; a figure published under the wrong period is wrong by 12x. Nothing here is inferred from the amount, and nothing is read out of the posting body — a period word near a salary is usually about a shift, a rest day or required experience, not about pay."},{"name":"deadline_at","type":"timestamp","nullable":true,"desc":"Application deadline as published, RFC 3339. Unlike `posted_at` a future date is kept rather than nulled -- a date the clock has not reached is what a deadline is. Not a liveness signal: a posting is not withdrawn here when its deadline passes, and most postings have none."},{"name":"posted_at","type":"timestamp","nullable":true,"desc":"When the posting first appeared, RFC 3339."},{"name":"url","type":"string","nullable":true,"desc":"The posting's page on the source site — usually canonical, but see the last paragraph: on some sources it is a third-party redirect carrying search-session state rather than a canonical page. **Not unique**, in two different ways, and **not stable over time** in a third.\n\n**Same source.** A board that lists one vacancy under two of its own ids yields two rows that differ by `id` and match by `url`, and they can land in the SAME page of a collection — so a consumer keying a table on `url` must deduplicate before writing, or one duplicated pair will reject the whole statement and take the other 98 rows of the page with it. Collapse these by keeping the newer `ingested_at`.\n\n**Different sources.** An aggregator republishes another board's posting under its own id, carrying the ORIGINAL board's url. You then hold two records for one job with the same `url`, different `source`, and different `id`. **This pair cannot be collapsed the same way** — the two disagree about which `\u003csource\u003e:\u003csource_job_id\u003e` addresses the posting, so whichever you keep decides the id you can look it up by afterwards. Pick the record whose `source` owns the url's domain, or skip aggregators entirely: `GET /v1/sources` marks them with `\"aggregator\": true`.\n\n**Over time.** The same `id` can return a DIFFERENT `url` on any scrape, so a mirror cannot key on it — this is the arity the other two paragraphs do not cover, and unlike them it cannot be absorbed by deduplicating a page. Measured on `jobpilot_remote`: one id emitted **39 distinct urls over five weeks** for one unchanged posting. Both the path and the query rotate (`/away/` vs `/desc/`), and `jobAge` increments by exactly 24 every scrape, so the url is **guaranteed** to differ on every daily run. It is source-specific rather than fleet-wide: 11,722 of 11,722 sampled active urls on that source carry tracking parameters, against 0 of 100 on each of boss.az, tap.az, jobsearch.az and Glorri. Those redirect urls are also not canonical pages — `ckey=` is the search query somebody typed, not part of the posting's address.\n\nSo: **`id` is the identifier, and this is a link that moves.** Key on `id`, store `url` as an attribute, and if you must group by url, strip the query string first."},{"name":"description","type":"string","nullable":true,"desc":"Full posting body. Present when the crawler has fetched the posting's own page, null otherwise — on collections and on /v1/jobs/{id} alike, since neither fetches on demand.\n\n**92% of live postings**, counted over a complete walk of them on 2026-08-28, and 97% on `source_type=employer,platform,agency`. The gap is per-source and mostly upstream: a handful of sites give a listing and no reachable detail page. It is markedly worse on RETIRED postings, so a walk left at today's default `?active=any` will look thinner than the live data actually is."},{"name":"requirements","type":"string","nullable":true,"desc":"Requirements section when the source separates it, subject to the same condition as description. **Present on about 11% of live postings** (this said 5%, measured against a different denominator) — most boards write one prose block and it lands in `description`. Concentrated rather than spread: Glorri separates it on virtually every posting and boss.az on most, and the rest of the fleet barely at all. Do not build a layout that needs this."},{"name":"facts","type":"object","nullable":true,"desc":"The upstream columns this platform does not publish as their own field, under one key because only a handful of sources fill them. Five of the keys the live fleet fills — `work_type`, `company_logo`, `view_count`, `contact_email` and `contact_phone` — are typed fields of their own now and are still echoed here as strings; the typed fields are the ones to read, and `view_count` in particular can appear here as an abbreviated string like `1.3K` that will not cast. `company_logo`, where it is here at all, is the typed field's value, cleaned the same way, and it is never here when that is null. `slug` is the source's own. The two contact keys are rare — see `contact_email` for where each one comes from and for the two conditions an address named in a posting body has to meet before it is published. Note that a minority of rows imported from the upstream board's database carry an address THAT board extracted from advert text before the import, under rules this platform does not apply to its own crawling. They are the employer's own application contact for that posting, given for applications: do not use them to build a contact list, and do not send anything to them that is not a reply to the posting they came with. Postings imported from the upstream job board's own database — a large part of the archive — carry more, including that board's internal bookkeeping (`is_active`, `flag_status`, `created_at` and `click_count`). **Do not read those back.** They are the origin board's own columns about its own copy, frozen at the moment we imported its database — a mirror reading `facts.is_active` would resurrect its own retired rows from its own stale data. The `active` field is this platform's answer and the only one that moves. Treat the key set as per-row and per-source rather than as a schema: check for a key, do not assume it. Every value is rendered as a string whatever its underlying type, and a nested object or array is dropped rather than serialised. Unlike every other nullable field here the key is ABSENT rather than null on a posting with none of them. Salary and the deadline used to live here and are now typed fields of their own; they are still echoed inside `facts` as strings, and the typed columns are the ones to read."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this version was stored. Moves every time a scrape re-sees the posting."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first saw the posting. Never changes — this is what distinguishes a new posting from one you already hold, since ingested_at moves on every re-sighting."},{"name":"times_seen","type":"integer","nullable":false,"desc":"How many scrapes have seen this posting. A source whose rows are all at 1 is changing its own identifiers rather than posting new work."},{"name":"active","type":"boolean","nullable":false,"desc":"**A verdict, and a real one needs the source to have run.** `false` is usually a finding: the source delivered a complete scrape, this posting was not in it, and nothing had seen it for 24 hours — roughly three consecutive misses on the published schedule. It is also written when a source has stored nothing at all for 14 days, by a daily pass that retires every posting that source still holds — and that `false` is **not** a finding. The posting may well still be open; what it says is that we stopped vouching for it. `inactive_reason` says which kind of `false` you have.\n\n`true` is **weaker than it looks, and you should not read it as \"verified live today\"**. It is the default state: it means nothing has retired the posting, not that anything confirmed it. A source that stops delivering gives no verdict on its postings, so they stay `true` until the daily pass retires them, at most about 15 days after the source's last delivery. The pass can also decline, and then they stay `true` until someone has looked: it changes nothing while fewer than half of the sources that delivered in the last 44 days have delivered in the last 36 hours — an outage on our side, not theirs — or when what it would retire is more than a tenth of every live posting. Either way, a posting is `true` again on the next run that carries it.\n\n**You do not have to take our word for the size of it.** `GET /v1/sources` publishes `stale_live_records` per source: how many of its `live_records` no run has carried for seven days, which is the population whose `true` carries no verdict, counted. On a source whose `last_observed_at` is old it is all of them.\n\n**If liveness matters to you, derive it yourself** — you have everything you need and it is four lines. Compare this posting's `ingested_at` against its source's `last_observed_at` from `GET /v1/sources`: the source delivered recently AND carried this row means live; the source delivered recently and did NOT means gone; the source has not delivered means nobody knows, including us, whatever `active` says. Use `last_observed_at` and not `last_ingested_at`, which moves whenever a batch lands even if nothing in it could be stored. See the worked example in the freshness docs. Nothing stops you being stricter than we are, and for a jobseeker-facing product you probably should be.\n\n**It stays a boolean.** Making it nullable was announced on 2026-10-03 and withdrawn on 2026-10-15, before it took effect: `inactive_reason` now says what a `null` would have said, beside an `active` that does not change."},{"name":"deactivated_at","type":"timestamp","nullable":true,"desc":"When this platform retired the posting, RFC 3339, and null while it is active. Not when the employer took it down — nothing here can observe that. After a complete scrape that did not carry it, this is at most one grace window after the posting was last seen. For a posting retired because its source went silent, it is the day the daily pass ran, about two weeks after that source's last delivery, and no evidence the posting closed then or at all. `ingested_at` deliberately does not move when a posting is retired, so this is the timestamp that changed; `updated_after` matches on either."},{"name":"inactive_reason","type":"string","nullable":true,"desc":"Why the posting is inactive, and null while `active` is `true`. One of seven values, fixed by a database constraint, so a new one is an announced addition, never a typo:\n\n- `absent_from_source`: the source delivered a complete scrape without it, and nothing had seen it for a day. **A finding about the posting.**\n- `source_gone`: the site that published it no longer exists. **A finding.**\n- `source_unreachable`: the site still publishes, and our address can no longer fetch it. **Not a finding**: the posting may well still be open.\n- `source_origin_down`: the site's edge answers and the server behind it does not. **Not a finding**, and usually temporary.\n- `source_dark`: we have stored nothing from the source for 14 days. **Not a finding**: a fact about us, not about the posting.\n- `imported_inactive`, `imported_flagged`: imported from an earlier archive that had already retired it, or held it as a suspected scam.\n\nA posting seen again is `active` again, and this goes back to null. To keep only postings retired on evidence, filter on `absent_from_source` and `source_gone`."},{"name":"work_type","type":"string","nullable":true,"desc":"The source's own working-arrangement label — on-site, hybrid, shift — as it writes it. Not normalised, and not the same axis as `employment_type`, which is about the contract. Null on most postings."},{"name":"company_logo","type":"string","nullable":true,"desc":"The company's logo as an **absolute `http://` or `https://` URL, or null** — never a relative path, never a bare file name, and never HTML-escaped: `\u0026amp;`, `\u0026lt;`, `\u0026gt;`, `\u0026quot;`, `\u0026#39;`, `\u0026#039;` and `\u0026#x27;` are decoded wherever a source left them, however many times over. It is the address the source serves the image from: not mirrored, not fetched by us and not revalidated, so it can 404 independently of the posting — treat a broken image as expected rather than as an error.\n\nWhere a source gives the logo as a path, it is resolved only where that source is known to serve its uploads from a fixed address, and a space in a file name is sent as `%20`. A path is never resolved against the posting's own page: the posting is not always the page the path was read from, and a guessed address that 404s is worse than none. Anything that cannot be made into a URL that way is null, and so is an image a source shows to mean the company has none — a `nologo.png`, a placeholder, the board's own brand mark. **Null is common and it is a correct answer**: render your fallback, and leave `logo` out of JobPosting markup."},{"name":"view_count","type":"integer","nullable":true,"desc":"How many times the SOURCE says the posting has been viewed — its counter, not ours, and not comparable between sources. Approximate where the source abbreviated it: a board showing 1.3K has published somewhere in 1,250–1,349, and that precision is gone before it reaches us. Null where the source publishes no counter."},{"name":"contact_email","type":"string","nullable":true,"desc":"**About 6% of live postings, and the distribution matters more than the number.** About 85% of these rows are on job boards, nearly all of that on four of them — jobsearch.az, boss.az, work.az and position.az — because a board's posting form asks the employer for a contact address. On `source_type=employer` it is about 28%, and on a board like boss.az it is every row. **If you filter to direct employers and also need an address, those two requirements pull against each other**: a company's own careers page routes applications through its own form or ATS, so most such postings publish no address at all and `url` is the apply path.\n\nWhere it is present it is the employer's own application mailbox for this posting, lower-cased, from one of two places and no others:\n\n1. a **structured field** the source published — an API field, a `mailto:` or a `tel:` link — read by the few scrapers whose source publishes one;\n2. an **application mailbox named in the posting itself**, under three conditions that all have to hold: the **whole local part** is a role name from a closed list (`cv@`, `hr@`, `karyera@`, `vacancy@`, `tecrube@` …) with nothing appended, so `hr.aliyeva@` does not qualify; the **domain is the posting's own**; and the posting comes from a source whose url identifies **one organisation** — a direct employer or a named recruitment agency, never a job board, a multi-tenant careers platform or a hosted ATS domain shared between employers. Together those mean a named individual's address cannot qualify and neither can a third party's. Ten distinct mailboxes account for every row filled this way.\n\nNothing else is read out of advert prose, deliberately. Use it to reply to the posting it arrived with; do not use it to build a contact list."},{"name":"contact_phone","type":"string","nullable":true,"desc":"**About 4% of live postings**, and 98% of those are on jobsearch.az and boss.az alone — on employer career pages it is effectively absent (one row in the whole segment). Formatted exactly as the source published it, with no canonicalisation, because a number's formatting is theirs and normalising it here would be inventing one. Do not assume E.164.\n\nUnlike `contact_email` this comes ONLY from a structured field — an API field or a `tel:` link. Nothing is read from the posting body, and the asymmetry is deliberate: a number has no local part, so there is no way to tell an employer's application line from someone's mobile the way a role mailbox can be told from a person's. Across a complete walk of the live corpus there were no `tel:` links in any posting body, so there is nothing being withheld."}],"defaults":{"active":"any","count":"false","limit":"25","order":"-posted_at"},"deprecations":[{"kind":"field","name":"facts.birjob_source_name","announced":"2026-10-31","sunset":"2027-01-30","instead":"`source_name` or `source`, which name the same source on every posting","notice":"https://softon.dev/docs/shared/versioning#announced-birjob-source-name"}]},{"id":"get-job","verb":"GET","path":"/v1/jobs/{id}","summary":"Retrieve a job","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"The posting id from a list response. Ids are stable and are never reissued, and this endpoint resolves a retired posting as readily as a live one — an id you already hold never starts returning 404. A posting is not deleted when its source removes it: `active` becomes false and `deactivated_at` is stamped, every other field keeps the last value that was scraped, and the row stays retrievable.\n\nRetrieving one by id is not the same as walking a collection. This endpoint resolves a retired posting whatever `?active=` would have done with it, and that does not change. What a COLLECTION contains does depend on the filter: rows withdrawn on request are the only ones that ever leave one regardless of how you ask, and `?active=true` leaves out a retired posting that still resolves here. The default, `any`, keeps a collection matching this endpoint's reach, and it is not going to change: moving it to `true` was announced for 2027-01-08 and withdrawn on 2026-10-15.","example":"smartjob_az:36586"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003csource id\u003e\". Pass it to /v1/jobs/{id}.\n\nThe source id is the posting number the site itself publishes, for every source but eight. Those eight publish none: `ddla_gov_az`, `ishelanlari_az`, `kaspi_edu_az`, `konsis`, `mdm`, `regulator`, `royalesb` and `ziraat`, and for them it is the first sixteen hex digits of the SHA-1 of the posting's URL, or of its title for `mdm`, whose postings all share one URL. So an id changes only when the site renumbers a posting, or, for those eight, changes its URL (`mdm`: its title, and the old row stays until a run retires it for being absent). A record from any other source that arrives without its id is dropped, never given a new one."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper produced this posting. A stable slug, and half of `id` — this is what `?source=` takes, so it never changes even when a board renames itself. `GET /v1/sources` lists every slug with its display name, its dataset, how many rows it currently has and when it last landed a batch. That endpoint needs no key."},{"name":"source_name","type":"string","nullable":true,"desc":"The same source as a person would write it — `ABB` for `abb`, `1is.az` for `1is_az`. This is the one to display. Null for the handful of sources the scraper registry has no entry for, rather than repeating the slug."},{"name":"source_url","type":"string","nullable":true,"desc":"The source's own website — `https://glorri.com` for `glorri`, `https://abb-bank.az` for `abb`. This is the one to link `source_name` to.\n\n**Not this posting's link** — that is `url` — and not the source's listing page either. `GET /v1/sources` carries both, as `url` and `jobs_url`, and needs no key; `jobs_url` there is the page this platform reads the source's vacancies from, which is the better link if you are sending somebody to browse a source rather than to read about it.\n\nSet for nearly every source; `GET /v1/sources` says which, live. Null rather than guessed where there is no honest answer: sources hosted entirely on a third party's applicant-tracking system, whose own domain appears nowhere in our scrapers (their `jobs_url` on `/v1/sources` still points at the ATS page the postings are actually on), a source with only its ATS vendor's domain recorded against it, `other`, a catch-all bucket with no website, and **sources we have measured as gone** — a hosting placeholder on every path, an organisation that no longer exists and now redirects to a different agency, a decommissioned ATS tenant. Those keep their rows and lose their link, because a link to nothing is worse than none."},{"name":"title","type":"string","nullable":false,"desc":"Posting title as published."},{"name":"company","type":"object","nullable":true,"desc":"The hiring company. `company.name` is always present when the object is; `company.slug` is nullable and is null on most rows -- do not type it as required. The whole object is null when the source omits the company, and is ALWAYS null on classifieds sources: there the advert is placed by a private individual rather than an employer, so there is no hiring company to name and publishing the poster under this field would be publishing a person's name as a company (#83)."},{"name":"location","type":"string","nullable":true,"desc":"Free text at whatever precision the source itself publishes — a city, a city and country, a district, an office or branch name, or the country alone. Not normalised, not geocoded, and not comparable between sources. Some boards publish no city at all and this is their country; nothing is inferred from the posting body to fill the gap, because a city read out of prose is as likely to be the employer's head office as the job's location."},{"name":"city","type":"string","nullable":true,"desc":"**25% of live postings** — 9,690 of 39,046, counted exactly on 2026-08-28 by summing `?city=` across the whole vocabulary. This said 60% and climbing, which was a projection rather than a measurement and is not where it is heading: the field fills only from sources that publish a structured place, about twenty of a hundred and thirty do, and several of the largest sources never will. Nine in ten of the values are `Bakı`. The same place as `location`, normalised to a closed vocabulary of Azerbaijani cities so it can be filtered and grouped — which `location` cannot be. **Null unless the source published a place of its own**: a city inferred from advert prose is as likely to be the employer's head office as the job's, so postings whose location was recovered that way carry a null city and keep their `location`. Azerbaijani cities only — a posting in Moscow or Warsaw is null here and unchanged in `location`. `location` is untouched and stays the higher-precision value; this is the comparable one."},{"name":"district","type":"string","nullable":true,"desc":"The Baku rayon, where the source names one — `Nəsimi`, `Binəqədi`, `Xətai` and the other nine. Null everywhere else, including for Baku postings that name no rayon. Implies `city`: a rayon is Baku's, so `city` is always `Bakı` when this is set. Present because one source publishes a rayon where every other publishes a city, and because a per-rayon page needs rayon granularity that a city vocabulary cannot express."},{"name":"country","type":"string","nullable":true,"desc":"The posting's country as **ISO 3166-1 alpha-2** — `AZ`, `PL`, `US`. **Null unless the source itself published a country**, which most do not: an Azerbaijani board's postings are Azerbaijani by construction and it never says so on the row. So null means *nobody said*, NOT *somewhere else* — use this to narrow what you serve, never to conclude a posting is foreign.\n\nIndependent of `city`, not a widening of it. `city` is an Azerbaijani-only vocabulary, so a Warsaw posting is null there and `PL` here, while a Baku posting whose source named no country is `Bakı` there and null here.\n\nIt comes from a structured country field where the source publishes one (Oracle ATS `PrimaryLocationCountry`, jobpilot's `country_code`) and from a country component of `location` where the source appends one — the same provenance rule as `city`: nothing is inferred from the posting body, and a token that is not an ISO code is stored as null rather than published."},{"name":"remote","type":"boolean","nullable":true,"desc":"Whether the posting is remote-eligible."},{"name":"employment_type","type":"string","nullable":true,"desc":"Full-time, Part-time, Contract, as the source labels it."},{"name":"job_function","type":"string","nullable":true,"desc":"The source's own function taxonomy, not normalised across sources."},{"name":"career_level","type":"string","nullable":true,"desc":"The source's own seniority label. Not normalised, so it is not called \"seniority\"."},{"name":"salary_from","type":"number","nullable":true,"desc":"Bottom of the published pay range, in `salary_currency`. Null where the source does not publish pay, which is most postings -- and null rather than 0, which would be a figure. Nothing is inferred from the posting body."},{"name":"salary_to","type":"number","nullable":true,"desc":"Top of the range. Either end can be null on its own: plenty of postings advertise a floor with no ceiling."},{"name":"salary_currency","type":"string","nullable":true,"desc":"The source's own currency code for the range, usually AZN. Not converted, so two postings in different currencies are not comparable by number alone."},{"name":"salary_period","type":"string","nullable":true,"desc":"What the figures are per: `HOUR`, `DAY`, `WEEK`, `MONTH` or `YEAR`. **Null unless the source itself stated it**, which today is about 1% of live postings and 6% of those that carry pay at all, and always `MONTH` — the eight sources that publish a period all publish that one. Null is the useful answer, not a gap: it means nobody knows, so do not assume. If you emit JobPosting `unitText`, omit it when this is null rather than defaulting to `MONTH`; a figure published under the wrong period is wrong by 12x. Nothing here is inferred from the amount, and nothing is read out of the posting body — a period word near a salary is usually about a shift, a rest day or required experience, not about pay."},{"name":"deadline_at","type":"timestamp","nullable":true,"desc":"Application deadline as published, RFC 3339. Unlike `posted_at` a future date is kept rather than nulled -- a date the clock has not reached is what a deadline is. Not a liveness signal: a posting is not withdrawn here when its deadline passes, and most postings have none."},{"name":"posted_at","type":"timestamp","nullable":true,"desc":"When the posting first appeared, RFC 3339."},{"name":"url","type":"string","nullable":true,"desc":"The posting's page on the source site — usually canonical, but see the last paragraph: on some sources it is a third-party redirect carrying search-session state rather than a canonical page. **Not unique**, in two different ways, and **not stable over time** in a third.\n\n**Same source.** A board that lists one vacancy under two of its own ids yields two rows that differ by `id` and match by `url`, and they can land in the SAME page of a collection — so a consumer keying a table on `url` must deduplicate before writing, or one duplicated pair will reject the whole statement and take the other 98 rows of the page with it. Collapse these by keeping the newer `ingested_at`.\n\n**Different sources.** An aggregator republishes another board's posting under its own id, carrying the ORIGINAL board's url. You then hold two records for one job with the same `url`, different `source`, and different `id`. **This pair cannot be collapsed the same way** — the two disagree about which `\u003csource\u003e:\u003csource_job_id\u003e` addresses the posting, so whichever you keep decides the id you can look it up by afterwards. Pick the record whose `source` owns the url's domain, or skip aggregators entirely: `GET /v1/sources` marks them with `\"aggregator\": true`.\n\n**Over time.** The same `id` can return a DIFFERENT `url` on any scrape, so a mirror cannot key on it — this is the arity the other two paragraphs do not cover, and unlike them it cannot be absorbed by deduplicating a page. Measured on `jobpilot_remote`: one id emitted **39 distinct urls over five weeks** for one unchanged posting. Both the path and the query rotate (`/away/` vs `/desc/`), and `jobAge` increments by exactly 24 every scrape, so the url is **guaranteed** to differ on every daily run. It is source-specific rather than fleet-wide: 11,722 of 11,722 sampled active urls on that source carry tracking parameters, against 0 of 100 on each of boss.az, tap.az, jobsearch.az and Glorri. Those redirect urls are also not canonical pages — `ckey=` is the search query somebody typed, not part of the posting's address.\n\nSo: **`id` is the identifier, and this is a link that moves.** Key on `id`, store `url` as an attribute, and if you must group by url, strip the query string first."},{"name":"description","type":"string","nullable":true,"desc":"Full posting body. Present when the crawler has fetched the posting's own page, null otherwise — on collections and on /v1/jobs/{id} alike, since neither fetches on demand.\n\n**92% of live postings**, counted over a complete walk of them on 2026-08-28, and 97% on `source_type=employer,platform,agency`. The gap is per-source and mostly upstream: a handful of sites give a listing and no reachable detail page. It is markedly worse on RETIRED postings, so a walk left at today's default `?active=any` will look thinner than the live data actually is."},{"name":"requirements","type":"string","nullable":true,"desc":"Requirements section when the source separates it, subject to the same condition as description. **Present on about 11% of live postings** (this said 5%, measured against a different denominator) — most boards write one prose block and it lands in `description`. Concentrated rather than spread: Glorri separates it on virtually every posting and boss.az on most, and the rest of the fleet barely at all. Do not build a layout that needs this."},{"name":"description_html","type":"string","nullable":true,"desc":"Markup for the body — `\u003cp\u003e`, `\u003cul\u003e`, `\u003cli\u003e`. **Returned by `GET /v1/jobs/{id}`, and on a collection when you ask for it with `include=bodies`.** Otherwise it is absent, not null, on a collection: a body roughly doubles the size of a row that has one, so a walk of 100 carries it only on request. A mirror walking the corpus asks for it on the walk, not one id at a time.\n\n**Two provenances, and you cannot tell them apart from the field alone.** Where the source published HTML — a minority of sources — this is that source's own markup, passed through, and the only changes we make to it are cosmetic: script and style elements removed at capture, a block element holding nothing but a space or an `\u0026nbsp;` dropped, and a bullet glyph glued to its first word given the space back. Where the source published only plain text, **this markup is ours**, generated from `description` — paragraphs become `\u003cp\u003e`, a run of bullet lines becomes a real `\u003cul\u003e`/`\u003cli\u003e`, and everything is escaped. It carries no information `description` does not; what it gives you is one shape to render whichever scraper produced the row.\n\nGenerated markup never replaces a source's own: it only ever fills a value that was empty. A row not re-observed since 2026-08-31 can still be null — there is no backfill, for the reason recorded in migration 0037. **Escape it before rendering either way**: the pass-through half is a stranger's HTML."},{"name":"requirements_html","type":"string","nullable":true,"desc":"As `description_html`, for the requirements section where the source separates it. Same two provenances, same condition, same endpoint."},{"name":"facts","type":"object","nullable":true,"desc":"The upstream columns this platform does not publish as their own field, under one key because only a handful of sources fill them. Five of the keys the live fleet fills — `work_type`, `company_logo`, `view_count`, `contact_email` and `contact_phone` — are typed fields of their own now and are still echoed here as strings; the typed fields are the ones to read, and `view_count` in particular can appear here as an abbreviated string like `1.3K` that will not cast. `company_logo`, where it is here at all, is the typed field's value, cleaned the same way, and it is never here when that is null. `slug` is the source's own. The two contact keys are rare — see `contact_email` for where each one comes from and for the two conditions an address named in a posting body has to meet before it is published. Note that a minority of rows imported from the upstream board's database carry an address THAT board extracted from advert text before the import, under rules this platform does not apply to its own crawling. They are the employer's own application contact for that posting, given for applications: do not use them to build a contact list, and do not send anything to them that is not a reply to the posting they came with. Postings imported from the upstream job board's own database — a large part of the archive — carry more, including that board's internal bookkeeping (`is_active`, `flag_status`, `created_at` and `click_count`). **Do not read those back.** They are the origin board's own columns about its own copy, frozen at the moment we imported its database — a mirror reading `facts.is_active` would resurrect its own retired rows from its own stale data. The `active` field is this platform's answer and the only one that moves. Treat the key set as per-row and per-source rather than as a schema: check for a key, do not assume it. Every value is rendered as a string whatever its underlying type, and a nested object or array is dropped rather than serialised. Unlike every other nullable field here the key is ABSENT rather than null on a posting with none of them. Salary and the deadline used to live here and are now typed fields of their own; they are still echoed inside `facts` as strings, and the typed columns are the ones to read."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this version was stored. Moves every time a scrape re-sees the posting."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first saw the posting. Never changes — this is what distinguishes a new posting from one you already hold, since ingested_at moves on every re-sighting."},{"name":"times_seen","type":"integer","nullable":false,"desc":"How many scrapes have seen this posting. A source whose rows are all at 1 is changing its own identifiers rather than posting new work."},{"name":"active","type":"boolean","nullable":false,"desc":"**A verdict, and a real one needs the source to have run.** `false` is usually a finding: the source delivered a complete scrape, this posting was not in it, and nothing had seen it for 24 hours — roughly three consecutive misses on the published schedule. It is also written when a source has stored nothing at all for 14 days, by a daily pass that retires every posting that source still holds — and that `false` is **not** a finding. The posting may well still be open; what it says is that we stopped vouching for it. `inactive_reason` says which kind of `false` you have.\n\n`true` is **weaker than it looks, and you should not read it as \"verified live today\"**. It is the default state: it means nothing has retired the posting, not that anything confirmed it. A source that stops delivering gives no verdict on its postings, so they stay `true` until the daily pass retires them, at most about 15 days after the source's last delivery. The pass can also decline, and then they stay `true` until someone has looked: it changes nothing while fewer than half of the sources that delivered in the last 44 days have delivered in the last 36 hours — an outage on our side, not theirs — or when what it would retire is more than a tenth of every live posting. Either way, a posting is `true` again on the next run that carries it.\n\n**You do not have to take our word for the size of it.** `GET /v1/sources` publishes `stale_live_records` per source: how many of its `live_records` no run has carried for seven days, which is the population whose `true` carries no verdict, counted. On a source whose `last_observed_at` is old it is all of them.\n\n**If liveness matters to you, derive it yourself** — you have everything you need and it is four lines. Compare this posting's `ingested_at` against its source's `last_observed_at` from `GET /v1/sources`: the source delivered recently AND carried this row means live; the source delivered recently and did NOT means gone; the source has not delivered means nobody knows, including us, whatever `active` says. Use `last_observed_at` and not `last_ingested_at`, which moves whenever a batch lands even if nothing in it could be stored. See the worked example in the freshness docs. Nothing stops you being stricter than we are, and for a jobseeker-facing product you probably should be.\n\n**It stays a boolean.** Making it nullable was announced on 2026-10-03 and withdrawn on 2026-10-15, before it took effect: `inactive_reason` now says what a `null` would have said, beside an `active` that does not change."},{"name":"deactivated_at","type":"timestamp","nullable":true,"desc":"When this platform retired the posting, RFC 3339, and null while it is active. Not when the employer took it down — nothing here can observe that. After a complete scrape that did not carry it, this is at most one grace window after the posting was last seen. For a posting retired because its source went silent, it is the day the daily pass ran, about two weeks after that source's last delivery, and no evidence the posting closed then or at all. `ingested_at` deliberately does not move when a posting is retired, so this is the timestamp that changed; `updated_after` matches on either."},{"name":"inactive_reason","type":"string","nullable":true,"desc":"Why the posting is inactive, and null while `active` is `true`. One of seven values, fixed by a database constraint, so a new one is an announced addition, never a typo:\n\n- `absent_from_source`: the source delivered a complete scrape without it, and nothing had seen it for a day. **A finding about the posting.**\n- `source_gone`: the site that published it no longer exists. **A finding.**\n- `source_unreachable`: the site still publishes, and our address can no longer fetch it. **Not a finding**: the posting may well still be open.\n- `source_origin_down`: the site's edge answers and the server behind it does not. **Not a finding**, and usually temporary.\n- `source_dark`: we have stored nothing from the source for 14 days. **Not a finding**: a fact about us, not about the posting.\n- `imported_inactive`, `imported_flagged`: imported from an earlier archive that had already retired it, or held it as a suspected scam.\n\nA posting seen again is `active` again, and this goes back to null. To keep only postings retired on evidence, filter on `absent_from_source` and `source_gone`."},{"name":"work_type","type":"string","nullable":true,"desc":"The source's own working-arrangement label — on-site, hybrid, shift — as it writes it. Not normalised, and not the same axis as `employment_type`, which is about the contract. Null on most postings."},{"name":"company_logo","type":"string","nullable":true,"desc":"The company's logo as an **absolute `http://` or `https://` URL, or null** — never a relative path, never a bare file name, and never HTML-escaped: `\u0026amp;`, `\u0026lt;`, `\u0026gt;`, `\u0026quot;`, `\u0026#39;`, `\u0026#039;` and `\u0026#x27;` are decoded wherever a source left them, however many times over. It is the address the source serves the image from: not mirrored, not fetched by us and not revalidated, so it can 404 independently of the posting — treat a broken image as expected rather than as an error.\n\nWhere a source gives the logo as a path, it is resolved only where that source is known to serve its uploads from a fixed address, and a space in a file name is sent as `%20`. A path is never resolved against the posting's own page: the posting is not always the page the path was read from, and a guessed address that 404s is worse than none. Anything that cannot be made into a URL that way is null, and so is an image a source shows to mean the company has none — a `nologo.png`, a placeholder, the board's own brand mark. **Null is common and it is a correct answer**: render your fallback, and leave `logo` out of JobPosting markup."},{"name":"view_count","type":"integer","nullable":true,"desc":"How many times the SOURCE says the posting has been viewed — its counter, not ours, and not comparable between sources. Approximate where the source abbreviated it: a board showing 1.3K has published somewhere in 1,250–1,349, and that precision is gone before it reaches us. Null where the source publishes no counter."},{"name":"contact_email","type":"string","nullable":true,"desc":"**About 6% of live postings, and the distribution matters more than the number.** About 85% of these rows are on job boards, nearly all of that on four of them — jobsearch.az, boss.az, work.az and position.az — because a board's posting form asks the employer for a contact address. On `source_type=employer` it is about 28%, and on a board like boss.az it is every row. **If you filter to direct employers and also need an address, those two requirements pull against each other**: a company's own careers page routes applications through its own form or ATS, so most such postings publish no address at all and `url` is the apply path.\n\nWhere it is present it is the employer's own application mailbox for this posting, lower-cased, from one of two places and no others:\n\n1. a **structured field** the source published — an API field, a `mailto:` or a `tel:` link — read by the few scrapers whose source publishes one;\n2. an **application mailbox named in the posting itself**, under three conditions that all have to hold: the **whole local part** is a role name from a closed list (`cv@`, `hr@`, `karyera@`, `vacancy@`, `tecrube@` …) with nothing appended, so `hr.aliyeva@` does not qualify; the **domain is the posting's own**; and the posting comes from a source whose url identifies **one organisation** — a direct employer or a named recruitment agency, never a job board, a multi-tenant careers platform or a hosted ATS domain shared between employers. Together those mean a named individual's address cannot qualify and neither can a third party's. Ten distinct mailboxes account for every row filled this way.\n\nNothing else is read out of advert prose, deliberately. Use it to reply to the posting it arrived with; do not use it to build a contact list."},{"name":"contact_phone","type":"string","nullable":true,"desc":"**About 4% of live postings**, and 98% of those are on jobsearch.az and boss.az alone — on employer career pages it is effectively absent (one row in the whole segment). Formatted exactly as the source published it, with no canonicalisation, because a number's formatting is theirs and normalising it here would be inventing one. Do not assume E.164.\n\nUnlike `contact_email` this comes ONLY from a structured field — an API field or a `tel:` link. Nothing is read from the posting body, and the asymmetry is deliberate: a number has no local part, so there is no way to tell an employer's application line from someone's mobile the way a role mailbox can be told from a person's. Across a complete walk of the live corpus there were no `tel:` links in any posting body, so there is nothing being withheld."}],"deprecations":[{"kind":"field","name":"facts.birjob_source_name","announced":"2026-10-31","sunset":"2027-01-30","instead":"`source_name` or `source`, which name the same source on every posting","notice":"https://softon.dev/docs/shared/versioning#announced-birjob-source-name"}]},{"id":"job-liveness","verb":"POST","path":"/v1/jobs/liveness","summary":"Batch liveness","returns":"array","params":[],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Echoed back exactly as you sent it, including an id this platform could not parse. Match on it; it is not normalised."},{"name":"state","type":"string","nullable":false,"desc":"One of `active`, `inactive`, `missing`, `unknown`, and that set is closed.\n\n`active` — the posting is here and its source still lists it. `inactive` — it is here and retired: its source's last complete run did not list it, or the source has stored nothing for 14 days and the daily pass retired what it held. Only the first says the posting closed; the source's `last_observed_at` on `GET /v1/sources` tells them apart. `missing` — this platform holds no such posting, either because it never did or because it was withdrawn on request; both are \"gone\" to a caller reconciling their own copy. `unknown` — no verdict, because the id could not be resolved into one. Treat `unknown` as \"change nothing, ask again\"; it is the safe answer and it is why every id gets a row rather than being dropped."},{"name":"active","type":"boolean","nullable":true,"desc":"A real boolean whenever `state` is `active` or `inactive`, and null otherwise. Never absent."},{"name":"checked_at","type":"string (RFC 3339)","nullable":false,"desc":"When this verdict was read. One timestamp for the whole batch, because it is one query."}]},{"id":"list-company-jobs","verb":"GET","path":"/v1/companies/{id}/jobs","summary":"Company jobs","returns":"array","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"Company slug or name.","example":"fincaazerbaijan"},{"name":"q","in":"query","type":"string","required":false,"desc":"Full-text match within this company's postings."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `ingested_at`, `posted_at`, each optionally prefixed with `-` to reverse. Default is `-posted_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-posted_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003csource id\u003e\". Pass it to /v1/jobs/{id}.\n\nThe source id is the posting number the site itself publishes, for every source but eight. Those eight publish none: `ddla_gov_az`, `ishelanlari_az`, `kaspi_edu_az`, `konsis`, `mdm`, `regulator`, `royalesb` and `ziraat`, and for them it is the first sixteen hex digits of the SHA-1 of the posting's URL, or of its title for `mdm`, whose postings all share one URL. So an id changes only when the site renumbers a posting, or, for those eight, changes its URL (`mdm`: its title, and the old row stays until a run retires it for being absent). A record from any other source that arrives without its id is dropped, never given a new one."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper produced this posting. A stable slug, and half of `id` — this is what `?source=` takes, so it never changes even when a board renames itself. `GET /v1/sources` lists every slug with its display name, its dataset, how many rows it currently has and when it last landed a batch. That endpoint needs no key."},{"name":"source_name","type":"string","nullable":true,"desc":"The same source as a person would write it — `ABB` for `abb`, `1is.az` for `1is_az`. This is the one to display. Null for the handful of sources the scraper registry has no entry for, rather than repeating the slug."},{"name":"source_url","type":"string","nullable":true,"desc":"The source's own website — `https://glorri.com` for `glorri`, `https://abb-bank.az` for `abb`. This is the one to link `source_name` to.\n\n**Not this posting's link** — that is `url` — and not the source's listing page either. `GET /v1/sources` carries both, as `url` and `jobs_url`, and needs no key; `jobs_url` there is the page this platform reads the source's vacancies from, which is the better link if you are sending somebody to browse a source rather than to read about it.\n\nSet for nearly every source; `GET /v1/sources` says which, live. Null rather than guessed where there is no honest answer: sources hosted entirely on a third party's applicant-tracking system, whose own domain appears nowhere in our scrapers (their `jobs_url` on `/v1/sources` still points at the ATS page the postings are actually on), a source with only its ATS vendor's domain recorded against it, `other`, a catch-all bucket with no website, and **sources we have measured as gone** — a hosting placeholder on every path, an organisation that no longer exists and now redirects to a different agency, a decommissioned ATS tenant. Those keep their rows and lose their link, because a link to nothing is worse than none."},{"name":"title","type":"string","nullable":false,"desc":"Posting title as published."},{"name":"company","type":"object","nullable":true,"desc":"The hiring company. `company.name` is always present when the object is; `company.slug` is nullable and is null on most rows -- do not type it as required. The whole object is null when the source omits the company, and is ALWAYS null on classifieds sources: there the advert is placed by a private individual rather than an employer, so there is no hiring company to name and publishing the poster under this field would be publishing a person's name as a company (#83)."},{"name":"location","type":"string","nullable":true,"desc":"Free text at whatever precision the source itself publishes — a city, a city and country, a district, an office or branch name, or the country alone. Not normalised, not geocoded, and not comparable between sources. Some boards publish no city at all and this is their country; nothing is inferred from the posting body to fill the gap, because a city read out of prose is as likely to be the employer's head office as the job's location."},{"name":"city","type":"string","nullable":true,"desc":"**25% of live postings** — 9,690 of 39,046, counted exactly on 2026-08-28 by summing `?city=` across the whole vocabulary. This said 60% and climbing, which was a projection rather than a measurement and is not where it is heading: the field fills only from sources that publish a structured place, about twenty of a hundred and thirty do, and several of the largest sources never will. Nine in ten of the values are `Bakı`. The same place as `location`, normalised to a closed vocabulary of Azerbaijani cities so it can be filtered and grouped — which `location` cannot be. **Null unless the source published a place of its own**: a city inferred from advert prose is as likely to be the employer's head office as the job's, so postings whose location was recovered that way carry a null city and keep their `location`. Azerbaijani cities only — a posting in Moscow or Warsaw is null here and unchanged in `location`. `location` is untouched and stays the higher-precision value; this is the comparable one."},{"name":"district","type":"string","nullable":true,"desc":"The Baku rayon, where the source names one — `Nəsimi`, `Binəqədi`, `Xətai` and the other nine. Null everywhere else, including for Baku postings that name no rayon. Implies `city`: a rayon is Baku's, so `city` is always `Bakı` when this is set. Present because one source publishes a rayon where every other publishes a city, and because a per-rayon page needs rayon granularity that a city vocabulary cannot express."},{"name":"country","type":"string","nullable":true,"desc":"The posting's country as **ISO 3166-1 alpha-2** — `AZ`, `PL`, `US`. **Null unless the source itself published a country**, which most do not: an Azerbaijani board's postings are Azerbaijani by construction and it never says so on the row. So null means *nobody said*, NOT *somewhere else* — use this to narrow what you serve, never to conclude a posting is foreign.\n\nIndependent of `city`, not a widening of it. `city` is an Azerbaijani-only vocabulary, so a Warsaw posting is null there and `PL` here, while a Baku posting whose source named no country is `Bakı` there and null here.\n\nIt comes from a structured country field where the source publishes one (Oracle ATS `PrimaryLocationCountry`, jobpilot's `country_code`) and from a country component of `location` where the source appends one — the same provenance rule as `city`: nothing is inferred from the posting body, and a token that is not an ISO code is stored as null rather than published."},{"name":"remote","type":"boolean","nullable":true,"desc":"Whether the posting is remote-eligible."},{"name":"employment_type","type":"string","nullable":true,"desc":"Full-time, Part-time, Contract, as the source labels it."},{"name":"job_function","type":"string","nullable":true,"desc":"The source's own function taxonomy, not normalised across sources."},{"name":"career_level","type":"string","nullable":true,"desc":"The source's own seniority label. Not normalised, so it is not called \"seniority\"."},{"name":"salary_from","type":"number","nullable":true,"desc":"Bottom of the published pay range, in `salary_currency`. Null where the source does not publish pay, which is most postings -- and null rather than 0, which would be a figure. Nothing is inferred from the posting body."},{"name":"salary_to","type":"number","nullable":true,"desc":"Top of the range. Either end can be null on its own: plenty of postings advertise a floor with no ceiling."},{"name":"salary_currency","type":"string","nullable":true,"desc":"The source's own currency code for the range, usually AZN. Not converted, so two postings in different currencies are not comparable by number alone."},{"name":"salary_period","type":"string","nullable":true,"desc":"What the figures are per: `HOUR`, `DAY`, `WEEK`, `MONTH` or `YEAR`. **Null unless the source itself stated it**, which today is about 1% of live postings and 6% of those that carry pay at all, and always `MONTH` — the eight sources that publish a period all publish that one. Null is the useful answer, not a gap: it means nobody knows, so do not assume. If you emit JobPosting `unitText`, omit it when this is null rather than defaulting to `MONTH`; a figure published under the wrong period is wrong by 12x. Nothing here is inferred from the amount, and nothing is read out of the posting body — a period word near a salary is usually about a shift, a rest day or required experience, not about pay."},{"name":"deadline_at","type":"timestamp","nullable":true,"desc":"Application deadline as published, RFC 3339. Unlike `posted_at` a future date is kept rather than nulled -- a date the clock has not reached is what a deadline is. Not a liveness signal: a posting is not withdrawn here when its deadline passes, and most postings have none."},{"name":"posted_at","type":"timestamp","nullable":true,"desc":"When the posting first appeared, RFC 3339."},{"name":"url","type":"string","nullable":true,"desc":"The posting's page on the source site — usually canonical, but see the last paragraph: on some sources it is a third-party redirect carrying search-session state rather than a canonical page. **Not unique**, in two different ways, and **not stable over time** in a third.\n\n**Same source.** A board that lists one vacancy under two of its own ids yields two rows that differ by `id` and match by `url`, and they can land in the SAME page of a collection — so a consumer keying a table on `url` must deduplicate before writing, or one duplicated pair will reject the whole statement and take the other 98 rows of the page with it. Collapse these by keeping the newer `ingested_at`.\n\n**Different sources.** An aggregator republishes another board's posting under its own id, carrying the ORIGINAL board's url. You then hold two records for one job with the same `url`, different `source`, and different `id`. **This pair cannot be collapsed the same way** — the two disagree about which `\u003csource\u003e:\u003csource_job_id\u003e` addresses the posting, so whichever you keep decides the id you can look it up by afterwards. Pick the record whose `source` owns the url's domain, or skip aggregators entirely: `GET /v1/sources` marks them with `\"aggregator\": true`.\n\n**Over time.** The same `id` can return a DIFFERENT `url` on any scrape, so a mirror cannot key on it — this is the arity the other two paragraphs do not cover, and unlike them it cannot be absorbed by deduplicating a page. Measured on `jobpilot_remote`: one id emitted **39 distinct urls over five weeks** for one unchanged posting. Both the path and the query rotate (`/away/` vs `/desc/`), and `jobAge` increments by exactly 24 every scrape, so the url is **guaranteed** to differ on every daily run. It is source-specific rather than fleet-wide: 11,722 of 11,722 sampled active urls on that source carry tracking parameters, against 0 of 100 on each of boss.az, tap.az, jobsearch.az and Glorri. Those redirect urls are also not canonical pages — `ckey=` is the search query somebody typed, not part of the posting's address.\n\nSo: **`id` is the identifier, and this is a link that moves.** Key on `id`, store `url` as an attribute, and if you must group by url, strip the query string first."},{"name":"description","type":"string","nullable":true,"desc":"Full posting body. Present when the crawler has fetched the posting's own page, null otherwise — on collections and on /v1/jobs/{id} alike, since neither fetches on demand.\n\n**92% of live postings**, counted over a complete walk of them on 2026-08-28, and 97% on `source_type=employer,platform,agency`. The gap is per-source and mostly upstream: a handful of sites give a listing and no reachable detail page. It is markedly worse on RETIRED postings, so a walk left at today's default `?active=any` will look thinner than the live data actually is."},{"name":"requirements","type":"string","nullable":true,"desc":"Requirements section when the source separates it, subject to the same condition as description. **Present on about 11% of live postings** (this said 5%, measured against a different denominator) — most boards write one prose block and it lands in `description`. Concentrated rather than spread: Glorri separates it on virtually every posting and boss.az on most, and the rest of the fleet barely at all. Do not build a layout that needs this."},{"name":"facts","type":"object","nullable":true,"desc":"The upstream columns this platform does not publish as their own field, under one key because only a handful of sources fill them. Five of the keys the live fleet fills — `work_type`, `company_logo`, `view_count`, `contact_email` and `contact_phone` — are typed fields of their own now and are still echoed here as strings; the typed fields are the ones to read, and `view_count` in particular can appear here as an abbreviated string like `1.3K` that will not cast. `company_logo`, where it is here at all, is the typed field's value, cleaned the same way, and it is never here when that is null. `slug` is the source's own. The two contact keys are rare — see `contact_email` for where each one comes from and for the two conditions an address named in a posting body has to meet before it is published. Note that a minority of rows imported from the upstream board's database carry an address THAT board extracted from advert text before the import, under rules this platform does not apply to its own crawling. They are the employer's own application contact for that posting, given for applications: do not use them to build a contact list, and do not send anything to them that is not a reply to the posting they came with. Postings imported from the upstream job board's own database — a large part of the archive — carry more, including that board's internal bookkeeping (`is_active`, `flag_status`, `created_at` and `click_count`). **Do not read those back.** They are the origin board's own columns about its own copy, frozen at the moment we imported its database — a mirror reading `facts.is_active` would resurrect its own retired rows from its own stale data. The `active` field is this platform's answer and the only one that moves. Treat the key set as per-row and per-source rather than as a schema: check for a key, do not assume it. Every value is rendered as a string whatever its underlying type, and a nested object or array is dropped rather than serialised. Unlike every other nullable field here the key is ABSENT rather than null on a posting with none of them. Salary and the deadline used to live here and are now typed fields of their own; they are still echoed inside `facts` as strings, and the typed columns are the ones to read."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this version was stored. Moves every time a scrape re-sees the posting."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first saw the posting. Never changes — this is what distinguishes a new posting from one you already hold, since ingested_at moves on every re-sighting."},{"name":"times_seen","type":"integer","nullable":false,"desc":"How many scrapes have seen this posting. A source whose rows are all at 1 is changing its own identifiers rather than posting new work."},{"name":"active","type":"boolean","nullable":false,"desc":"**A verdict, and a real one needs the source to have run.** `false` is usually a finding: the source delivered a complete scrape, this posting was not in it, and nothing had seen it for 24 hours — roughly three consecutive misses on the published schedule. It is also written when a source has stored nothing at all for 14 days, by a daily pass that retires every posting that source still holds — and that `false` is **not** a finding. The posting may well still be open; what it says is that we stopped vouching for it. `inactive_reason` says which kind of `false` you have.\n\n`true` is **weaker than it looks, and you should not read it as \"verified live today\"**. It is the default state: it means nothing has retired the posting, not that anything confirmed it. A source that stops delivering gives no verdict on its postings, so they stay `true` until the daily pass retires them, at most about 15 days after the source's last delivery. The pass can also decline, and then they stay `true` until someone has looked: it changes nothing while fewer than half of the sources that delivered in the last 44 days have delivered in the last 36 hours — an outage on our side, not theirs — or when what it would retire is more than a tenth of every live posting. Either way, a posting is `true` again on the next run that carries it.\n\n**You do not have to take our word for the size of it.** `GET /v1/sources` publishes `stale_live_records` per source: how many of its `live_records` no run has carried for seven days, which is the population whose `true` carries no verdict, counted. On a source whose `last_observed_at` is old it is all of them.\n\n**If liveness matters to you, derive it yourself** — you have everything you need and it is four lines. Compare this posting's `ingested_at` against its source's `last_observed_at` from `GET /v1/sources`: the source delivered recently AND carried this row means live; the source delivered recently and did NOT means gone; the source has not delivered means nobody knows, including us, whatever `active` says. Use `last_observed_at` and not `last_ingested_at`, which moves whenever a batch lands even if nothing in it could be stored. See the worked example in the freshness docs. Nothing stops you being stricter than we are, and for a jobseeker-facing product you probably should be.\n\n**It stays a boolean.** Making it nullable was announced on 2026-10-03 and withdrawn on 2026-10-15, before it took effect: `inactive_reason` now says what a `null` would have said, beside an `active` that does not change."},{"name":"deactivated_at","type":"timestamp","nullable":true,"desc":"When this platform retired the posting, RFC 3339, and null while it is active. Not when the employer took it down — nothing here can observe that. After a complete scrape that did not carry it, this is at most one grace window after the posting was last seen. For a posting retired because its source went silent, it is the day the daily pass ran, about two weeks after that source's last delivery, and no evidence the posting closed then or at all. `ingested_at` deliberately does not move when a posting is retired, so this is the timestamp that changed; `updated_after` matches on either."},{"name":"inactive_reason","type":"string","nullable":true,"desc":"Why the posting is inactive, and null while `active` is `true`. One of seven values, fixed by a database constraint, so a new one is an announced addition, never a typo:\n\n- `absent_from_source`: the source delivered a complete scrape without it, and nothing had seen it for a day. **A finding about the posting.**\n- `source_gone`: the site that published it no longer exists. **A finding.**\n- `source_unreachable`: the site still publishes, and our address can no longer fetch it. **Not a finding**: the posting may well still be open.\n- `source_origin_down`: the site's edge answers and the server behind it does not. **Not a finding**, and usually temporary.\n- `source_dark`: we have stored nothing from the source for 14 days. **Not a finding**: a fact about us, not about the posting.\n- `imported_inactive`, `imported_flagged`: imported from an earlier archive that had already retired it, or held it as a suspected scam.\n\nA posting seen again is `active` again, and this goes back to null. To keep only postings retired on evidence, filter on `absent_from_source` and `source_gone`."},{"name":"work_type","type":"string","nullable":true,"desc":"The source's own working-arrangement label — on-site, hybrid, shift — as it writes it. Not normalised, and not the same axis as `employment_type`, which is about the contract. Null on most postings."},{"name":"company_logo","type":"string","nullable":true,"desc":"The company's logo as an **absolute `http://` or `https://` URL, or null** — never a relative path, never a bare file name, and never HTML-escaped: `\u0026amp;`, `\u0026lt;`, `\u0026gt;`, `\u0026quot;`, `\u0026#39;`, `\u0026#039;` and `\u0026#x27;` are decoded wherever a source left them, however many times over. It is the address the source serves the image from: not mirrored, not fetched by us and not revalidated, so it can 404 independently of the posting — treat a broken image as expected rather than as an error.\n\nWhere a source gives the logo as a path, it is resolved only where that source is known to serve its uploads from a fixed address, and a space in a file name is sent as `%20`. A path is never resolved against the posting's own page: the posting is not always the page the path was read from, and a guessed address that 404s is worse than none. Anything that cannot be made into a URL that way is null, and so is an image a source shows to mean the company has none — a `nologo.png`, a placeholder, the board's own brand mark. **Null is common and it is a correct answer**: render your fallback, and leave `logo` out of JobPosting markup."},{"name":"view_count","type":"integer","nullable":true,"desc":"How many times the SOURCE says the posting has been viewed — its counter, not ours, and not comparable between sources. Approximate where the source abbreviated it: a board showing 1.3K has published somewhere in 1,250–1,349, and that precision is gone before it reaches us. Null where the source publishes no counter."},{"name":"contact_email","type":"string","nullable":true,"desc":"**About 6% of live postings, and the distribution matters more than the number.** About 85% of these rows are on job boards, nearly all of that on four of them — jobsearch.az, boss.az, work.az and position.az — because a board's posting form asks the employer for a contact address. On `source_type=employer` it is about 28%, and on a board like boss.az it is every row. **If you filter to direct employers and also need an address, those two requirements pull against each other**: a company's own careers page routes applications through its own form or ATS, so most such postings publish no address at all and `url` is the apply path.\n\nWhere it is present it is the employer's own application mailbox for this posting, lower-cased, from one of two places and no others:\n\n1. a **structured field** the source published — an API field, a `mailto:` or a `tel:` link — read by the few scrapers whose source publishes one;\n2. an **application mailbox named in the posting itself**, under three conditions that all have to hold: the **whole local part** is a role name from a closed list (`cv@`, `hr@`, `karyera@`, `vacancy@`, `tecrube@` …) with nothing appended, so `hr.aliyeva@` does not qualify; the **domain is the posting's own**; and the posting comes from a source whose url identifies **one organisation** — a direct employer or a named recruitment agency, never a job board, a multi-tenant careers platform or a hosted ATS domain shared between employers. Together those mean a named individual's address cannot qualify and neither can a third party's. Ten distinct mailboxes account for every row filled this way.\n\nNothing else is read out of advert prose, deliberately. Use it to reply to the posting it arrived with; do not use it to build a contact list."},{"name":"contact_phone","type":"string","nullable":true,"desc":"**About 4% of live postings**, and 98% of those are on jobsearch.az and boss.az alone — on employer career pages it is effectively absent (one row in the whole segment). Formatted exactly as the source published it, with no canonicalisation, because a number's formatting is theirs and normalising it here would be inventing one. Do not assume E.164.\n\nUnlike `contact_email` this comes ONLY from a structured field — an API field or a `tel:` link. Nothing is read from the posting body, and the asymmetry is deliberate: a number has no local part, so there is no way to tell an employer's application line from someone's mobile the way a role mailbox can be told from a person's. Across a complete walk of the live corpus there were no `tel:` links in any posting body, so there is nothing being withheld."}],"defaults":{"limit":"25","order":"-posted_at"},"deprecations":[{"kind":"field","name":"facts.birjob_source_name","announced":"2026-10-31","sunset":"2027-01-30","instead":"`source_name` or `source`, which name the same source on every posting","notice":"https://softon.dev/docs/shared/versioning#announced-birjob-source-name"}]}]},{"slug":"events","name":"Event Data API","status":"retired","operations":[{"id":"list-events","verb":"GET","path":"/v1/events","summary":"List events","returns":"array","params":[{"name":"q","in":"query","type":"string","required":false,"desc":"Full-text match on name, venue and description.","example":"Bergen"},{"name":"category","in":"query","type":"string","required":false,"desc":"Exact match on the source's own category slug, which is not a closed set — see the field description. `GET /v1/events` with no filter is the only reliable way to learn which values exist today.","example":"cinema"},{"name":"updated_after","in":"query","type":"timestamp","required":false,"desc":"Only rows whose `ingested_at` is later than this instant — everything that CHANGED since your last poll, new and re-seen alike. Pass the newest `ingested_at` you have stored. This is what a mirror of the collection should page on; `starts_after` filters on when the EVENT happens, which is a different question.","example":"2026-08-16T09:00:00Z"},{"name":"starts_after","in":"query","type":"timestamp","required":false,"desc":"Events starting at or after this instant."},{"name":"starts_before","in":"query","type":"timestamp","required":false,"desc":"Events starting at or before this instant."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `ingested_at`, `starts_at`, each optionally prefixed with `-` to reverse. Default is `starts_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"starts_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003csource id\u003e\"."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper produced this event."},{"name":"slug","type":"string","nullable":true,"desc":"The source's own readable identifier for the event — what to build a stable, human-readable URL from, rather than the opaque `id`. NOT unique and not an identifier: one source derives it from a film title and a cinema name, so the same film at two cinemas shares a slug. Null where the source publishes none, and a value that stops arriving is kept rather than nulled, because a stale slug is a working link and an empty one is a 404."},{"name":"name","type":"string","nullable":false,"desc":"Event name as published."},{"name":"category","type":"string","nullable":true,"desc":"The source's own category slug, not normalised and not a closed set — it follows the upstream, so it also contains venue and festival names (`hayal-kahvesi`, `gabalafest`). `cinema`, `concerts` and `theatre` are three quarters of the corpus between them; `kids`, `sport`, `seminar`, `master-class`, `circus`, `other` and `exhibitions` make up most of the rest. Do not build a fixed facet from this list — read the values you actually receive."},{"name":"venue","type":"object","nullable":true,"desc":"id, name, address, lat, lng. Coordinates where the source geocodes."},{"name":"starts_at","type":"timestamp","nullable":true,"desc":"Start time as an RFC 3339 instant in UTC. Convert with the venue's timezone for display — all current venues are Asia/Baku (UTC+4)."},{"name":"ends_at","type":"timestamp","nullable":true,"desc":"End time in UTC, when the source publishes one."},{"name":"price","type":"object","nullable":true,"desc":"min, max and currency across ticket tiers."},{"name":"age_limit","type":"string","nullable":true,"desc":"Age restriction as published, e.g. \"16+\"."},{"name":"tickets_available","type":"integer","nullable":true,"desc":"Live inventory. null where the source does not expose it — never 0 as a stand-in."},{"name":"description","type":"string","nullable":true,"desc":"Editorial copy for the event."},{"name":"url","type":"string","nullable":true,"desc":"Canonical page on the source site."},{"name":"external_url","type":"string","nullable":true,"desc":"Information or ticketing page. NOT guaranteed to be a purchase link — cinema sources have no dereferenceable buy href."},{"name":"poster_url","type":"string","nullable":true,"desc":"Artwork, highest resolution the source offers."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this version was stored."}]},{"id":"get-event","verb":"GET","path":"/v1/events/{id}","summary":"Retrieve an event","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"The event id from a list response.","example":"iticket:12299"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003csource id\u003e\"."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper produced this event."},{"name":"slug","type":"string","nullable":true,"desc":"The source's own readable identifier for the event — what to build a stable, human-readable URL from, rather than the opaque `id`. NOT unique and not an identifier: one source derives it from a film title and a cinema name, so the same film at two cinemas shares a slug. Null where the source publishes none, and a value that stops arriving is kept rather than nulled, because a stale slug is a working link and an empty one is a 404."},{"name":"name","type":"string","nullable":false,"desc":"Event name as published."},{"name":"category","type":"string","nullable":true,"desc":"The source's own category slug, not normalised and not a closed set — it follows the upstream, so it also contains venue and festival names (`hayal-kahvesi`, `gabalafest`). `cinema`, `concerts` and `theatre` are three quarters of the corpus between them; `kids`, `sport`, `seminar`, `master-class`, `circus`, `other` and `exhibitions` make up most of the rest. Do not build a fixed facet from this list — read the values you actually receive."},{"name":"venue","type":"object","nullable":true,"desc":"id, name, address, lat, lng. Coordinates where the source geocodes."},{"name":"starts_at","type":"timestamp","nullable":true,"desc":"Start time as an RFC 3339 instant in UTC. Convert with the venue's timezone for display — all current venues are Asia/Baku (UTC+4)."},{"name":"ends_at","type":"timestamp","nullable":true,"desc":"End time in UTC, when the source publishes one."},{"name":"price","type":"object","nullable":true,"desc":"min, max and currency across ticket tiers."},{"name":"age_limit","type":"string","nullable":true,"desc":"Age restriction as published, e.g. \"16+\"."},{"name":"tickets_available","type":"integer","nullable":true,"desc":"Live inventory. null where the source does not expose it — never 0 as a stand-in."},{"name":"description","type":"string","nullable":true,"desc":"Editorial copy for the event."},{"name":"url","type":"string","nullable":true,"desc":"Canonical page on the source site."},{"name":"external_url","type":"string","nullable":true,"desc":"Information or ticketing page. NOT guaranteed to be a purchase link — cinema sources have no dereferenceable buy href."},{"name":"poster_url","type":"string","nullable":true,"desc":"Artwork, highest resolution the source offers."},{"name":"sessions","type":"array","nullable":true,"desc":"Every individual showing, each with its own price range and inventory. Returned by `GET /v1/events/{id}` only, **and only where the source publishes per-showing inventory** — which is often not the case. The key is ABSENT rather than null when it does not, so check for it before iterating: one of the two current sources publishes none at all, and the other omits it on about a third of its events. `tickets_available` is null on the same rows."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this version was stored."},{"name":"sessions[].id","type":"string","nullable":false,"desc":"Session identifier from the source."},{"name":"sessions[].starts_at","type":"timestamp","nullable":true,"desc":"When this showing starts, UTC."},{"name":"sessions[].ends_at","type":"timestamp","nullable":true,"desc":"When it ends, when the source publishes one."},{"name":"sessions[].sell_ends_at","type":"timestamp","nullable":true,"desc":"When sales close for this showing."},{"name":"sessions[].price","type":"object","nullable":true,"desc":"min, max and currency for this showing, which can differ from the event's overall range."},{"name":"sessions[].tickets_available","type":"integer","nullable":true,"desc":"Remaining inventory for THIS showing at observed_at, not the event total."},{"name":"sessions[].language","type":"string","nullable":true,"desc":"Presentation language. null where the source does not label it."},{"name":"sessions[].observed_at","type":"timestamp","nullable":true,"desc":"When this inventory figure was scraped — successive observations form a sell-through time series."}]},{"id":"get-venue","verb":"GET","path":"/v1/venues/{id}","summary":"Retrieve a venue","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"Venue slug from an event's venue.id.","example":"museum-center"}],"fields":[{"name":"id","type":"string","nullable":true,"desc":"Slugified venue name. Venues have no source-issued id."},{"name":"name","type":"string","nullable":false,"desc":"Venue name as published."},{"name":"address","type":"string","nullable":true,"desc":"Street address as published."},{"name":"lat","type":"number","nullable":true,"desc":"Latitude, WGS 84."},{"name":"lng","type":"number","nullable":true,"desc":"Longitude, WGS 84."}]}]},{"slug":"quotes","name":"Quotes Data API","status":"retired","operations":[{"id":"list-quotes","verb":"GET","path":"/v1/quotes","summary":"List quotations","returns":"array","params":[{"name":"q","in":"query","type":"string","required":false,"desc":"Full-text match on the quotation and its author.","example":"self-doubt"},{"name":"author","in":"query","type":"string","required":false,"desc":"Author name."},{"name":"category","in":"query","type":"string","required":false,"desc":"The upstream's own label."},{"name":"active","in":"query","type":"string","required":false,"desc":"Which half of the pool. `true` (the default) is the curated quotations; `false` is the bulk archive imported from the upstream and never published there; `any` is both. The archive is large and unvetted -- some of it is a mis-parsed CSV whose author field holds part of the quotation -- which is why it is opt-in rather than the default."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `ingested_at`, each optionally prefixed with `-` to reverse. Default is `-ingested_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-ingested_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003csource id\u003e\"."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered this quotation."},{"name":"text","type":"string","nullable":false,"desc":"The quotation."},{"name":"author","type":"string","nullable":true,"desc":"Attributed author. null where the collection does not name one."},{"name":"category","type":"string","nullable":true,"desc":"The upstream's own label. A folksonomy, not an enum -- tens of thousands of distinct values -- so treat it as a tag rather than a fixed set."},{"name":"origin","type":"string","nullable":true,"desc":"Where the quotation was collected from, e.g. \"passiton\". Distinct from source, which is the scraper that delivered it."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this version was stored."}]},{"id":"get-quote","verb":"GET","path":"/v1/quotes/{id}","summary":"Retrieve a quotation","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"The quotation id from a list response. Ids are `\u003csource\u003e:\u003csource_id\u003e` and are stable for the life of the record.\n\nQuotations carry a `birjob:` prefix because that is the name of the corpus they were imported from — the same project this platform's crawler used to run under, which is why it still answers to `BirJobBot`. It is a name, not a namespace you need to handle: treat the whole string as opaque and store it as you received it. It is not going to change, because an id you already hold changing would cost you a re-import to buy us a tidier prefix.","example":"birjob:1663"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003csource id\u003e\"."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered this quotation."},{"name":"text","type":"string","nullable":false,"desc":"The quotation."},{"name":"author","type":"string","nullable":true,"desc":"Attributed author. null where the collection does not name one."},{"name":"category","type":"string","nullable":true,"desc":"The upstream's own label. A folksonomy, not an enum -- tens of thousands of distinct values -- so treat it as a tag rather than a fixed set."},{"name":"origin","type":"string","nullable":true,"desc":"Where the quotation was collected from, e.g. \"passiton\". Distinct from source, which is the scraper that delivered it."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this version was stored."}]},{"id":"get-random-quote","verb":"GET","path":"/v1/quotes/random","summary":"Quote of the day","returns":"object","params":[{"name":"date","in":"query","type":"date","required":false,"desc":"ISO-8601 date in UTC. The same date and filters always return the same quotation, so it is safe to cache for the day. Defaults to today.","example":"2026-10-06"},{"name":"category","in":"query","type":"string","required":false,"desc":"Restrict the pool to one category."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003csource id\u003e\"."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered this quotation."},{"name":"text","type":"string","nullable":false,"desc":"The quotation."},{"name":"author","type":"string","nullable":true,"desc":"Attributed author. null where the collection does not name one."},{"name":"category","type":"string","nullable":true,"desc":"The upstream's own label. A folksonomy, not an enum -- tens of thousands of distinct values -- so treat it as a tag rather than a fixed set."},{"name":"origin","type":"string","nullable":true,"desc":"Where the quotation was collected from, e.g. \"passiton\". Distinct from source, which is the scraper that delivered it."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this version was stored."}]}]},{"slug":"rates","name":"Rates Data API","status":"retired","operations":[{"id":"list-rates","verb":"GET","path":"/v1/rates","summary":"List exchange rates","returns":"array","params":[{"name":"from","in":"query","type":"date","required":false,"desc":"Earliest `date`, inclusive.","example":"2026-09-01"},{"name":"to","in":"query","type":"date","required":false,"desc":"Latest `date`, inclusive.","example":"2026-09-05"},{"name":"codes","in":"query","type":"string","required":false,"desc":"Currency or metal codes, comma-separated — `USD,EUR`. Case is normalised, and a repeated `?codes=` is unioned with the list rather than overwriting it. Omit for all 42.","example":"USD,EUR"},{"name":"kind","in":"query","type":"string","required":false,"desc":"`currency` or `metal`. The metals are the four the bank publishes per troy ounce.","example":"currency"},{"name":"carried_forward","in":"query","type":"boolean","required":false,"desc":"`false` returns only the dates the bank actually published on — a strict business-day series. `true` returns only the repeats. Omit for the dense daily series, which is both.","example":"false"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `ingested_at`, each optionally prefixed with `-` to reverse. Default is `-ingested_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-ingested_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003cdate\u003e:\u003ccode\u003e\" — `cbar:2026-09-05:USD`. Opaque: treat the whole string as the id rather than parsing the halves out of it."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered this bulletin. `cbar` is the Central Bank of Azerbaijan."},{"name":"date","type":"date","nullable":false,"desc":"The calendar date this row answers for, **Asia/Baku**. Azerbaijan is UTC+4 year-round with no DST, so a consumer computing \"today\" in UTC gets yesterday's date for the four hours after 20:00 Baku — which is a real bug this feed's predecessor shipped, not a hypothetical one."},{"name":"effective_date","type":"date","nullable":false,"desc":"The date the bank says these rates take effect from — the bulletin's own `ValCurs Date=`, never inferred from the date requested. On a business day it equals `date`; on a weekend, a public holiday or a future date it is the last business day before it."},{"name":"carried_forward","type":"boolean","nullable":false,"desc":"`date != effective_date`: these numbers were published earlier and are still standing. **Filter `carried_forward=false` for a strict business-day series**; take every row for a dense daily one and know which values are repeats. The upstream answers `200` with a full bulletin for every date string including weekends and dates in the future, so this flag is the only thing that distinguishes a fresh publication from a repeat."},{"name":"kind","type":"string","nullable":false,"desc":"`currency` or `metal`. The four bank metals (XAU, XPD, XPT, XAG) are the metals."},{"name":"base","type":"string","nullable":false,"desc":"The currency the rate is expressed in. Always `AZN` for this source."},{"name":"code","type":"string","nullable":false,"desc":"ISO 4217 code, or the metal's X-code. 42 per bulletin: 38 currencies and 4 bank metals. `SDR` is the IMF basket rather than a currency, and is carried as the bank publishes it."},{"name":"name","type":"string","nullable":true,"desc":"The bank's own label, in Azerbaijani, including the multiplier it prints there — \"1 ABŞ dolları\", \"100 Yapon iyeni\"."},{"name":"unit","type":"string","nullable":false,"desc":"What one of `nominal` is: `unit` for a currency, `troy_ounce` for a metal."},{"name":"nominal","type":"integer","nullable":false,"desc":"How many units `value` is quoted for. Seven currencies quote per 100 — KRW, KZT, HUF, UZS, PKR, RUB and JPY — and everything else per 1. Reading `value` without dividing by this is how a rouble comes out a hundred times too large."},{"name":"value","type":"number","nullable":false,"desc":"The published figure, unrounded: AZN per `nominal` units."},{"name":"rate_per_unit","type":"number","nullable":false,"desc":"`value / nominal`, unrounded. **This is the number you almost certainly want**: AZN for one unit of `code`, or for one troy ounce of a metal."},{"name":"source_url","type":"string","nullable":true,"desc":"The document requested for `date`."},{"name":"bulletin_url","type":"string","nullable":true,"desc":"The document these numbers were published in, i.e. the one for `effective_date`. Differs from `source_url` on a carried-forward row."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this version was stored."}]},{"id":"get-latest-rates","verb":"GET","path":"/v1/rates/latest","summary":"Latest bulletin","returns":"array","params":[{"name":"from","in":"query","type":"date","required":false,"desc":"Ignore dates before this one when resolving \"latest\"."},{"name":"to","in":"query","type":"date","required":false,"desc":"Resolve \"latest\" as of this date rather than now — the bulletin that was in force on a given day."},{"name":"codes","in":"query","type":"string","required":false,"desc":"Currency or metal codes, comma-separated — `USD,EUR`.","example":"USD,XAU"},{"name":"kind","in":"query","type":"string","required":false,"desc":"`currency` or `metal`."},{"name":"carried_forward","in":"query","type":"boolean","required":false,"desc":"`false` resolves to the last date the bank actually published on, skipping a weekend's repeats."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `ingested_at`, each optionally prefixed with `-` to reverse. Default is `-ingested_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-ingested_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003cdate\u003e:\u003ccode\u003e\" — `cbar:2026-09-05:USD`. Opaque: treat the whole string as the id rather than parsing the halves out of it."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered this bulletin. `cbar` is the Central Bank of Azerbaijan."},{"name":"date","type":"date","nullable":false,"desc":"The calendar date this row answers for, **Asia/Baku**. Azerbaijan is UTC+4 year-round with no DST, so a consumer computing \"today\" in UTC gets yesterday's date for the four hours after 20:00 Baku — which is a real bug this feed's predecessor shipped, not a hypothetical one."},{"name":"effective_date","type":"date","nullable":false,"desc":"The date the bank says these rates take effect from — the bulletin's own `ValCurs Date=`, never inferred from the date requested. On a business day it equals `date`; on a weekend, a public holiday or a future date it is the last business day before it."},{"name":"carried_forward","type":"boolean","nullable":false,"desc":"`date != effective_date`: these numbers were published earlier and are still standing. **Filter `carried_forward=false` for a strict business-day series**; take every row for a dense daily one and know which values are repeats. The upstream answers `200` with a full bulletin for every date string including weekends and dates in the future, so this flag is the only thing that distinguishes a fresh publication from a repeat."},{"name":"kind","type":"string","nullable":false,"desc":"`currency` or `metal`. The four bank metals (XAU, XPD, XPT, XAG) are the metals."},{"name":"base","type":"string","nullable":false,"desc":"The currency the rate is expressed in. Always `AZN` for this source."},{"name":"code","type":"string","nullable":false,"desc":"ISO 4217 code, or the metal's X-code. 42 per bulletin: 38 currencies and 4 bank metals. `SDR` is the IMF basket rather than a currency, and is carried as the bank publishes it."},{"name":"name","type":"string","nullable":true,"desc":"The bank's own label, in Azerbaijani, including the multiplier it prints there — \"1 ABŞ dolları\", \"100 Yapon iyeni\"."},{"name":"unit","type":"string","nullable":false,"desc":"What one of `nominal` is: `unit` for a currency, `troy_ounce` for a metal."},{"name":"nominal","type":"integer","nullable":false,"desc":"How many units `value` is quoted for. Seven currencies quote per 100 — KRW, KZT, HUF, UZS, PKR, RUB and JPY — and everything else per 1. Reading `value` without dividing by this is how a rouble comes out a hundred times too large."},{"name":"value","type":"number","nullable":false,"desc":"The published figure, unrounded: AZN per `nominal` units."},{"name":"rate_per_unit","type":"number","nullable":false,"desc":"`value / nominal`, unrounded. **This is the number you almost certainly want**: AZN for one unit of `code`, or for one troy ounce of a metal."},{"name":"source_url","type":"string","nullable":true,"desc":"The document requested for `date`."},{"name":"bulletin_url","type":"string","nullable":true,"desc":"The document these numbers were published in, i.e. the one for `effective_date`. Differs from `source_url` on a carried-forward row."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this version was stored."}]},{"id":"get-rate","verb":"GET","path":"/v1/rates/{id}","summary":"Retrieve a rate","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"The rate id from a list response, `\u003csource\u003e:\u003cdate\u003e:\u003ccode\u003e`. Stable for the life of the record: a past bulletin is immutable, so the row this resolves to does not change once it exists.","example":"cbar:2026-09-04:RUB"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003cdate\u003e:\u003ccode\u003e\" — `cbar:2026-09-05:USD`. Opaque: treat the whole string as the id rather than parsing the halves out of it."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered this bulletin. `cbar` is the Central Bank of Azerbaijan."},{"name":"date","type":"date","nullable":false,"desc":"The calendar date this row answers for, **Asia/Baku**. Azerbaijan is UTC+4 year-round with no DST, so a consumer computing \"today\" in UTC gets yesterday's date for the four hours after 20:00 Baku — which is a real bug this feed's predecessor shipped, not a hypothetical one."},{"name":"effective_date","type":"date","nullable":false,"desc":"The date the bank says these rates take effect from — the bulletin's own `ValCurs Date=`, never inferred from the date requested. On a business day it equals `date`; on a weekend, a public holiday or a future date it is the last business day before it."},{"name":"carried_forward","type":"boolean","nullable":false,"desc":"`date != effective_date`: these numbers were published earlier and are still standing. **Filter `carried_forward=false` for a strict business-day series**; take every row for a dense daily one and know which values are repeats. The upstream answers `200` with a full bulletin for every date string including weekends and dates in the future, so this flag is the only thing that distinguishes a fresh publication from a repeat."},{"name":"kind","type":"string","nullable":false,"desc":"`currency` or `metal`. The four bank metals (XAU, XPD, XPT, XAG) are the metals."},{"name":"base","type":"string","nullable":false,"desc":"The currency the rate is expressed in. Always `AZN` for this source."},{"name":"code","type":"string","nullable":false,"desc":"ISO 4217 code, or the metal's X-code. 42 per bulletin: 38 currencies and 4 bank metals. `SDR` is the IMF basket rather than a currency, and is carried as the bank publishes it."},{"name":"name","type":"string","nullable":true,"desc":"The bank's own label, in Azerbaijani, including the multiplier it prints there — \"1 ABŞ dolları\", \"100 Yapon iyeni\"."},{"name":"unit","type":"string","nullable":false,"desc":"What one of `nominal` is: `unit` for a currency, `troy_ounce` for a metal."},{"name":"nominal","type":"integer","nullable":false,"desc":"How many units `value` is quoted for. Seven currencies quote per 100 — KRW, KZT, HUF, UZS, PKR, RUB and JPY — and everything else per 1. Reading `value` without dividing by this is how a rouble comes out a hundred times too large."},{"name":"value","type":"number","nullable":false,"desc":"The published figure, unrounded: AZN per `nominal` units."},{"name":"rate_per_unit","type":"number","nullable":false,"desc":"`value / nominal`, unrounded. **This is the number you almost certainly want**: AZN for one unit of `code`, or for one troy ounce of a metal."},{"name":"source_url","type":"string","nullable":true,"desc":"The document requested for `date`."},{"name":"bulletin_url","type":"string","nullable":true,"desc":"The document these numbers were published in, i.e. the one for `effective_date`. Differs from `source_url` on a carried-forward row."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this version was stored."}]}]},{"slug":"genome","name":"Genome Data API","status":"retired","operations":[{"id":"list-genome-releases","verb":"GET","path":"/v1/genome/releases","summary":"List genome source releases","returns":"array","params":[{"name":"status","in":"query","type":"string","required":false,"desc":"How the latest probe went: `ok`, `degraded`, `unreachable` or `error`. `?status=ok` is how you ask for the rows to trust today — a failing source still carries its last good `release_version`.","example":"ok"},{"name":"authority","in":"query","type":"string","required":false,"desc":"The publishing body, exactly as the row carries it — `NCBI`, `EMBL-EBI`. Case-sensitive, and an unknown value answers an empty page rather than a 400, because this vocabulary is the upstream registry's and grows when a source from a new body is added.","example":"NCBI"},{"name":"category","in":"query","type":"string","required":false,"desc":"The registry's own kind label — `reference_sequence`, `genome_metadata`, `variation`, `population_reference`. Open for the same reason `authority` is.","example":"reference_sequence"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `ingested_at`, each optionally prefixed with `-` to reverse. Default is `-ingested_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-ingested_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"The upstream registry's own key — `genbank`, `ncbi_taxonomy`, `clinvar`, `gwas_catalog`, `ncbi_datasets`, `gnomad`, `gtex`, `igsr_1000g`, `dbsnp`, `refseq`, `ncbi_gene`. **No `\u003csource\u003e:` prefix** — one of two collections on this API without one, `/v1/seismic/faults` being the other — because the row's identity IS an upstream source; it is the same string as `source_id` and it is what `/v1/genome/releases/{id}` takes."},{"name":"source","type":"string","nullable":false,"desc":"Which softon scraper delivered this row. One probe pass reads every source here, so it is the same value on every row — `source_id` is the field that varies, and the one you almost certainly mean."},{"name":"source_id","type":"string","nullable":false,"desc":"The upstream registry's key, identical to `id`. Published under this name because it is the name the registry this replaces uses, so a consumer writing softon's answer back into that registry keys on it without a mapper."},{"name":"source_name","type":"string","nullable":false,"desc":"What the source calls itself: `GenBank`, `ClinVar`, `gnomAD`, `1000 Genomes / IGSR`, `NCBI Gene`."},{"name":"authority","type":"string","nullable":false,"desc":"The body that publishes it — `NCBI`, `EMBL-EBI` or `Broad Institute`."},{"name":"category","type":"string","nullable":false,"desc":"The registry's own label for what kind of source this is: `sequence_archive`, `taxonomy`, `clinical_variation`, `trait_association`, `genome_metadata`, `population_variation`, `population_reference`, `variation`, `reference_sequence`, `gene_dimension`. A shared free-form vocabulary rather than a closed set, so `?category=` accepts anything and answers an empty page for a label no source carries. It is matched exactly: `variation` is dbSNP's catalogue of variants and does not include ClinVar's `clinical_variation`."},{"name":"landing_url","type":"string","nullable":true,"desc":"The human-facing page for this source. Never the FTP index — this is the one to link a reader to."},{"name":"api_url","type":"string","nullable":true,"desc":"The source's machine-facing API root, where the registry records one. Null for GenBank, NCBI Taxonomy, ClinVar, IGSR, RefSeq and NCBI Gene."},{"name":"bulk_url","type":"string","nullable":true,"desc":"The source's bulk archive root, where it has one. Null for NCBI Datasets. For gnomAD it is the release root of the public storage bucket, which is listed rather than browsed. **Recorded, never mirrored**: nothing on this platform downloads it, and this API serves no data from behind it."},{"name":"update_frequency","type":"string","nullable":false,"desc":"How often the SOURCE cuts a release — `bimonthly_release`, `weekly`, `daily_dump`, `periodic_release`, `release_based`, `continuous`. Not how often softon probes it: the probe runs daily whatever this says."},{"name":"release_version","type":"string","nullable":true,"desc":"The release the source is currently on, **as a string and never a number**: `\"273.0\"`, `\"20260928\"`, `\"4.1.2\"`, `\"237\"`, `\"20130502\"`. Release names are not all numeric and a JSON number would also defeat a `?? \"—\"` null check the day one of them is `0`.\n\n**Compare it for equality; do not parse it.** The shapes differ by source and are not converted to one: where the upstream registry this replaces published a source's release, the value keeps that form, so a consumer writing it back keys on the same string — which is why ClinVar's is `\"20260928\"`, the form ClinVar names its files with, while the GWAS Catalog's is `\"2026-09-13\"`. gnomAD's is the one deliberate break: the registry published the API's dataset name, `gnomad_r4`, which cannot say which release is served, and `\"4.1.2\"` is that release. Where a release has a date, `release_date` carries it as ISO-8601.\n\nTwo sources publish no version of their own, and say so differently. `ncbi_taxonomy` has **none**: it is null on a healthy row and the release signal is `checksum`. `ncbi_gene` is the date NCBI last wrote the file (`\"2026-10-03\"`), because that date is the one property of it NCBI publishes. NCBI rewrites the file daily, so it moves every day **whether or not a gene changed**: a new value there says the file was written, not that its content moved.\n\n**This is a release MARKER and nothing else.** It pins no download, selects no file and carries no sequence, assembly, annotation or variant data — for RefSeq, for instance, assembly facts come from the separate ASSEMBLY_REPORTS bulk tables and not from anything here.\n\nNull until a probe has succeeded once. After that it is **sticky**: a probe that fails leaves the last good value standing and moves `status` instead, so read the two together rather than treating a non-null version as fresh."},{"name":"release_date","type":"date","nullable":true,"desc":"The release's own date where the source publishes one: `clinvar` (its VCF's `##fileDate`), `gwas_catalog` (the catalog's release date) and `igsr_1000g`, where it is the **freeze name** sliced out of a directory name (`20130502` → `2013-05-02`) — an identity, not a publication date. Deliberately not filled from an upstream `Last-Modified`, which is a different fact."},{"name":"checksum","type":"string","nullable":true,"desc":"The release signal for a source identified by a digest rather than a number. Only `ncbi_taxonomy` has one: NCBI rebuilds the taxonomy dump daily at one unversioned path and publishes no version, so this is the md5 it publishes for that day's `taxdump.tar.gz`. Sticky like `release_version`."},{"name":"format","type":"string","nullable":true,"desc":"How the UPSTREAM distributes its data — `flatfile`, `rest_v2`, `ftp_release_dirs`, `text_release_notes`, `taxdump`, `tab_delimited`. Not the format of this response, which is JSON for all of them."},{"name":"retrieved_at","type":"timestamp","nullable":true,"desc":"When softon last **successfully** read this upstream. Not when your request arrived, and never advanced by a probe that failed — it is a provenance claim, and a failed refresh that moved it would assert a read that did not happen. Compare it with `ingested_at`, which is the last probe of any kind, to see how stale the answer is. Null only for a source that has never answered."},{"name":"status","type":"string","nullable":false,"desc":"How the LATEST probe went. Four values, and they are four because the distinctions matter to different people:\n\n`ok` — a release was read. A read made beside the release — gnomAD's dataset list from its GraphQL API, ClinVar's md5 and its file date — that fails does not change that: its field in `extra` is null and `\u003cfield\u003e_error` says why, and the release still moves. `degraded` — the host answered with something that is not the file: a 2xx with nothing parseable in it, or a redirect, which is never followed. The upstream's format or address moved and a person has to fix the probe. `unreachable` — the host never answered: DNS, TLS, connection or timeout, including a probe the daily pass ran out of time to make. Nothing is wrong with softon's parser and nothing is wrong with the data. `error` — the host answered with a non-2xx, or its `robots.txt` names our crawler and disallows the path, in which case nothing was requested.\n\nThe feed this replaces collapsed `unreachable` into `degraded` and then counted the source as reachable anyway, so an outage read as a parser bug."},{"name":"extra","type":"object","nullable":true,"desc":"Per-source probe metadata: which URL was read, what it answered, and the handful of fields the upstream published beside the version — for gnomAD, the bucket's constraint releases and the object the version was read from, kept apart from the API's dataset list. `error` says why the release was not read; a supporting field that could not be read is null with `\u003cfield\u003e_error` beside it — `api_datasets_error`, `md5_error`, `file_last_modified_error`. **An allow-listed projection, not the upstream body** — one of these endpoints echoes a submitted API key back inside its own error body, so a verbatim pass-through would publish a credential. Null where a source records none."},{"name":"sort_order","type":"integer","nullable":false,"desc":"This source's position in the upstream registry's declaration order: 1–10 for the registry's ten, `gtex` among them at 7, and 11 for NCBI Gene, which it never had. Sort on it to reproduce that order: this collection pages on `ingested_at` like every other one here, and one probe pass stores every row at the same instant, so the order rows come back in is the id's and not the registry's."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When the latest probe ran and this version was stored — successful or not. See `retrieved_at` for the last successful read."}]},{"id":"get-genome-release","verb":"GET","path":"/v1/genome/releases/{id}","summary":"Retrieve one genome source release","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"The upstream registry key, e.g. `refseq`. **No `\u003csource\u003e:` prefix on this collection** — the id of a release marker is the source it describes, and it is stable for the life of the source.","example":"igsr_1000g"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"The upstream registry's own key — `genbank`, `ncbi_taxonomy`, `clinvar`, `gwas_catalog`, `ncbi_datasets`, `gnomad`, `gtex`, `igsr_1000g`, `dbsnp`, `refseq`, `ncbi_gene`. **No `\u003csource\u003e:` prefix** — one of two collections on this API without one, `/v1/seismic/faults` being the other — because the row's identity IS an upstream source; it is the same string as `source_id` and it is what `/v1/genome/releases/{id}` takes."},{"name":"source","type":"string","nullable":false,"desc":"Which softon scraper delivered this row. One probe pass reads every source here, so it is the same value on every row — `source_id` is the field that varies, and the one you almost certainly mean."},{"name":"source_id","type":"string","nullable":false,"desc":"The upstream registry's key, identical to `id`. Published under this name because it is the name the registry this replaces uses, so a consumer writing softon's answer back into that registry keys on it without a mapper."},{"name":"source_name","type":"string","nullable":false,"desc":"What the source calls itself: `GenBank`, `ClinVar`, `gnomAD`, `1000 Genomes / IGSR`, `NCBI Gene`."},{"name":"authority","type":"string","nullable":false,"desc":"The body that publishes it — `NCBI`, `EMBL-EBI` or `Broad Institute`."},{"name":"category","type":"string","nullable":false,"desc":"The registry's own label for what kind of source this is: `sequence_archive`, `taxonomy`, `clinical_variation`, `trait_association`, `genome_metadata`, `population_variation`, `population_reference`, `variation`, `reference_sequence`, `gene_dimension`. A shared free-form vocabulary rather than a closed set, so `?category=` accepts anything and answers an empty page for a label no source carries. It is matched exactly: `variation` is dbSNP's catalogue of variants and does not include ClinVar's `clinical_variation`."},{"name":"landing_url","type":"string","nullable":true,"desc":"The human-facing page for this source. Never the FTP index — this is the one to link a reader to."},{"name":"api_url","type":"string","nullable":true,"desc":"The source's machine-facing API root, where the registry records one. Null for GenBank, NCBI Taxonomy, ClinVar, IGSR, RefSeq and NCBI Gene."},{"name":"bulk_url","type":"string","nullable":true,"desc":"The source's bulk archive root, where it has one. Null for NCBI Datasets. For gnomAD it is the release root of the public storage bucket, which is listed rather than browsed. **Recorded, never mirrored**: nothing on this platform downloads it, and this API serves no data from behind it."},{"name":"update_frequency","type":"string","nullable":false,"desc":"How often the SOURCE cuts a release — `bimonthly_release`, `weekly`, `daily_dump`, `periodic_release`, `release_based`, `continuous`. Not how often softon probes it: the probe runs daily whatever this says."},{"name":"release_version","type":"string","nullable":true,"desc":"The release the source is currently on, **as a string and never a number**: `\"273.0\"`, `\"20260928\"`, `\"4.1.2\"`, `\"237\"`, `\"20130502\"`. Release names are not all numeric and a JSON number would also defeat a `?? \"—\"` null check the day one of them is `0`.\n\n**Compare it for equality; do not parse it.** The shapes differ by source and are not converted to one: where the upstream registry this replaces published a source's release, the value keeps that form, so a consumer writing it back keys on the same string — which is why ClinVar's is `\"20260928\"`, the form ClinVar names its files with, while the GWAS Catalog's is `\"2026-09-13\"`. gnomAD's is the one deliberate break: the registry published the API's dataset name, `gnomad_r4`, which cannot say which release is served, and `\"4.1.2\"` is that release. Where a release has a date, `release_date` carries it as ISO-8601.\n\nTwo sources publish no version of their own, and say so differently. `ncbi_taxonomy` has **none**: it is null on a healthy row and the release signal is `checksum`. `ncbi_gene` is the date NCBI last wrote the file (`\"2026-10-03\"`), because that date is the one property of it NCBI publishes. NCBI rewrites the file daily, so it moves every day **whether or not a gene changed**: a new value there says the file was written, not that its content moved.\n\n**This is a release MARKER and nothing else.** It pins no download, selects no file and carries no sequence, assembly, annotation or variant data — for RefSeq, for instance, assembly facts come from the separate ASSEMBLY_REPORTS bulk tables and not from anything here.\n\nNull until a probe has succeeded once. After that it is **sticky**: a probe that fails leaves the last good value standing and moves `status` instead, so read the two together rather than treating a non-null version as fresh."},{"name":"release_date","type":"date","nullable":true,"desc":"The release's own date where the source publishes one: `clinvar` (its VCF's `##fileDate`), `gwas_catalog` (the catalog's release date) and `igsr_1000g`, where it is the **freeze name** sliced out of a directory name (`20130502` → `2013-05-02`) — an identity, not a publication date. Deliberately not filled from an upstream `Last-Modified`, which is a different fact."},{"name":"checksum","type":"string","nullable":true,"desc":"The release signal for a source identified by a digest rather than a number. Only `ncbi_taxonomy` has one: NCBI rebuilds the taxonomy dump daily at one unversioned path and publishes no version, so this is the md5 it publishes for that day's `taxdump.tar.gz`. Sticky like `release_version`."},{"name":"format","type":"string","nullable":true,"desc":"How the UPSTREAM distributes its data — `flatfile`, `rest_v2`, `ftp_release_dirs`, `text_release_notes`, `taxdump`, `tab_delimited`. Not the format of this response, which is JSON for all of them."},{"name":"retrieved_at","type":"timestamp","nullable":true,"desc":"When softon last **successfully** read this upstream. Not when your request arrived, and never advanced by a probe that failed — it is a provenance claim, and a failed refresh that moved it would assert a read that did not happen. Compare it with `ingested_at`, which is the last probe of any kind, to see how stale the answer is. Null only for a source that has never answered."},{"name":"status","type":"string","nullable":false,"desc":"How the LATEST probe went. Four values, and they are four because the distinctions matter to different people:\n\n`ok` — a release was read. A read made beside the release — gnomAD's dataset list from its GraphQL API, ClinVar's md5 and its file date — that fails does not change that: its field in `extra` is null and `\u003cfield\u003e_error` says why, and the release still moves. `degraded` — the host answered with something that is not the file: a 2xx with nothing parseable in it, or a redirect, which is never followed. The upstream's format or address moved and a person has to fix the probe. `unreachable` — the host never answered: DNS, TLS, connection or timeout, including a probe the daily pass ran out of time to make. Nothing is wrong with softon's parser and nothing is wrong with the data. `error` — the host answered with a non-2xx, or its `robots.txt` names our crawler and disallows the path, in which case nothing was requested.\n\nThe feed this replaces collapsed `unreachable` into `degraded` and then counted the source as reachable anyway, so an outage read as a parser bug."},{"name":"extra","type":"object","nullable":true,"desc":"Per-source probe metadata: which URL was read, what it answered, and the handful of fields the upstream published beside the version — for gnomAD, the bucket's constraint releases and the object the version was read from, kept apart from the API's dataset list. `error` says why the release was not read; a supporting field that could not be read is null with `\u003cfield\u003e_error` beside it — `api_datasets_error`, `md5_error`, `file_last_modified_error`. **An allow-listed projection, not the upstream body** — one of these endpoints echoes a submitted API key back inside its own error body, so a verbatim pass-through would publish a credential. Null where a source records none."},{"name":"sort_order","type":"integer","nullable":false,"desc":"This source's position in the upstream registry's declaration order: 1–10 for the registry's ten, `gtex` among them at 7, and 11 for NCBI Gene, which it never had. Sort on it to reproduce that order: this collection pages on `ingested_at` like every other one here, and one probe pass stores every row at the same instant, so the order rows come back in is the id's and not the registry's."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When the latest probe ran and this version was stored — successful or not. See `retrieved_at` for the last successful read."}]},{"id":"get-genbank-growth","verb":"GET","path":"/v1/genome/genbank/growth","summary":"GenBank's growth, release by release","returns":"object","params":[],"fields":[{"name":"source","type":"object","nullable":false,"desc":"Where these figures come from: the release notes, the release they describe, when they were read, and the attribution to carry with them. In the document rather than in `meta`, so `data` is the whole file a consumer stores. See `source.*`."},{"name":"series_definitions","type":"object","nullable":false,"desc":"What each series counts — `traditional`, `wgs`, `tsa`, `tls` and `set_based` — as one sentence each, naming the release and month each series starts at: its first row in `timeline`, the same as `coverage.\u003cseries\u003e.first_release`. GenBank's notes say the TSA and TLS tables will be back-filled with earlier releases; when they are, those two sentences name the earlier start."},{"name":"coverage","type":"object","nullable":false,"desc":"Each series' extent: `traditional`, `wgs`, `tsa` and `tls`, each with `releases` (how many report it), `first_release`, `first_date`, `last_release` and `last_date`, dated by that series' own table."},{"name":"latest","type":"object","nullable":true,"desc":"The newest release **with traditional figures**, in the shape of a `timeline` row. Not the last row of `timeline` by definition: the two coincide today, and differ on any release whose traditional row is missing. Read headline figures here."},{"name":"timeline","type":"array","nullable":false,"desc":"One row per GenBank release, strictly ascending by `release`, merged across the four tables. 237 rows on release 273.0's notes. See `timeline[].*`."},{"name":"gaps","type":"array","nullable":false,"desc":"Months the tables mark with an `n/a` row — a release that was not delivered — each with `date`, the table's own `note` and the `series` whose tables carry it. Not releases, so not rows of `timeline`. One today: February 2024."},{"name":"advisories","type":"array","nullable":false,"desc":"Releases whose `traditional_entries` fell more than 5% below the release before, each with `release`, `entries`, `previous_release`, `previous_entries` and `drop_pct`. **Flagged, never refused**: GenBank does re-baseline, and its notes say so where it does. `[]` today — the three re-baselined releases dropped under a percent."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`genbank`, GenBank's key in the genome registry: `/v1/genome/releases/genbank` says which release GenBank is on today, checked every morning."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`GenBank`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`NCBI`."},{"name":"source.url","type":"string","nullable":false,"desc":"The release notes the figures were read from, `https://ftp.ncbi.nlm.nih.gov/genbank/gbrel.txt`."},{"name":"source.section","type":"string","nullable":false,"desc":"The notes' heading for the four tables, `2.2.8 Growth of GenBank`."},{"name":"source.release_version","type":"string","nullable":false,"desc":"The release whose notes these are, `273.0` — a **string with a decimal**, as GenBank's `GB_Release_Number` writes it, while each row's `release` is the tables' number, `273`. The same release in two shapes; both are kept."},{"name":"source.release_date","type":"date","nullable":false,"desc":"The day the notes are dated, `2026-08-15`, read from their title. The tables give a month and nothing finer, so this is the one release date GenBank states."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When the notes these figures were parsed from were **first** read. The notes are read again whenever the release number moves, and every ten days regardless; a read that finds the same bytes, parsed the same way, leaves this standing — so the document, and its `ETag`, change only when the figures do, and a conditional GET answers `304`."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The parse rules that produced the rows. It moves when a change to the parser would read the same file into different rows."},{"name":"source.checksum_sha256","type":"string","nullable":false,"desc":"`sha256` of the notes as served: `sha256sum gbrel.txt` over the same file gives the same string."},{"name":"source.content_length","type":"integer","nullable":true,"desc":"The notes' size in bytes: 1,235,842 for release 273.0."},{"name":"source.last_modified","type":"string","nullable":true,"desc":"The `Last-Modified` header NCBI sent with those bytes, verbatim."},{"name":"source.scraper","type":"string","nullable":false,"desc":"The program that read the notes, `genbank_growth` — its row on `/v1/sources`."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with any figure republished from this document: `GenBank (NCBI), gbrel.txt section 2.2.8, release 273.0; NCBI places no restrictions on use or distribution`. A GenBank figure without its release is unciteable."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"NCBI's Website and Data Usage Policies: \"NCBI itself places no restrictions on the use or distribution of the data contained therein.\" These figures are NCBI's own counts of its database, not any submitter's record."},{"name":"timeline[].release","type":"number","nullable":false,"desc":"GenBank's release number as the tables write it: `273`, and `57.1` for the interim release of October 1988 — so a JSON number, an integer on every row but that one."},{"name":"timeline[].date","type":"date","nullable":false,"desc":"The first of the month the tables date the release, `2026-08-01`: the traditional table's month, or the first table that has the release. The `-01` is not a publication day; `source.release_date` is the one GenBank states."},{"name":"timeline[].year","type":"integer","nullable":false,"desc":"`date`'s year, for a chart's x axis."},{"name":"timeline[].traditional_bases","type":"integer","nullable":false,"desc":"Base pairs in the traditional series (non-set-based records, CON division excluded) at this release, unscaled. **Absent — not null and not 0 — on a release this series' table does not have**. Every release has one today: 227, 235 and 267 carry figures and a `traditional_note` explaining a decrease, where the file this replaces showed none."},{"name":"timeline[].traditional_entries","type":"integer","nullable":false,"desc":"Sequence records in the traditional series (non-set-based records, CON division excluded) at this release. Absent where `traditional_bases` is."},{"name":"timeline[].traditional_unchanged","type":"boolean","nullable":false,"desc":"`true` where this series' table marks the row `(unchanged)`: the release is real and the table's statistics were not regenerated for it. **Present only when true**, never `false`."},{"name":"timeline[].traditional_note","type":"string","nullable":false,"desc":"Any other text this series' row carries after its figures, verbatim. Present only where there is some."},{"name":"timeline[].traditional_date","type":"date","nullable":false,"desc":"This series' own month for the release, present only where its table disagrees with `date`. Release 272 is June 2026 in the traditional and WGS tables and April 2026 in the TSA and TLS tables."},{"name":"timeline[].wgs_bases","type":"integer","nullable":false,"desc":"Base pairs in the Whole Genome Shotgun series at this release, unscaled. **Absent — not null and not 0 — on a release this series' table does not have**: every release before 129, 92 of them."},{"name":"timeline[].wgs_entries","type":"integer","nullable":false,"desc":"Sequence records in the Whole Genome Shotgun series at this release. Absent where `wgs_bases` is."},{"name":"timeline[].wgs_unchanged","type":"boolean","nullable":false,"desc":"`true` where this series' table marks the row `(unchanged)`: the release is real and the table's statistics were not regenerated for it. **Present only when true**, never `false`."},{"name":"timeline[].wgs_note","type":"string","nullable":false,"desc":"Any other text this series' row carries after its figures, verbatim. Present only where there is some."},{"name":"timeline[].wgs_date","type":"date","nullable":false,"desc":"This series' own month for the release, present only where its table disagrees with `date`. Release 272 is June 2026 in the traditional and WGS tables and April 2026 in the TSA and TLS tables."},{"name":"timeline[].tsa_bases","type":"integer","nullable":false,"desc":"Base pairs in the Transcriptome Shotgun Assembly series at this release, unscaled. **Absent — not null and not 0 — on a release this series' table does not have**: every release before 201."},{"name":"timeline[].tsa_entries","type":"integer","nullable":false,"desc":"Sequence records in the Transcriptome Shotgun Assembly series at this release. Absent where `tsa_bases` is."},{"name":"timeline[].tsa_unchanged","type":"boolean","nullable":false,"desc":"`true` where this series' table marks the row `(unchanged)`: the release is real and the table's statistics were not regenerated for it. **Present only when true**, never `false`."},{"name":"timeline[].tsa_note","type":"string","nullable":false,"desc":"Any other text this series' row carries after its figures, verbatim. Present only where there is some."},{"name":"timeline[].tsa_date","type":"date","nullable":false,"desc":"This series' own month for the release, present only where its table disagrees with `date`. Release 272 is June 2026 in the traditional and WGS tables and April 2026 in the TSA and TLS tables."},{"name":"timeline[].tls_bases","type":"integer","nullable":false,"desc":"Base pairs in the Targeted Locus Study series at this release, unscaled. **Absent — not null and not 0 — on a release this series' table does not have**: every release before 217."},{"name":"timeline[].tls_entries","type":"integer","nullable":false,"desc":"Sequence records in the Targeted Locus Study series at this release. Absent where `tls_bases` is."},{"name":"timeline[].tls_unchanged","type":"boolean","nullable":false,"desc":"`true` where this series' table marks the row `(unchanged)`: the release is real and the table's statistics were not regenerated for it. **Present only when true**, never `false`."},{"name":"timeline[].tls_note","type":"string","nullable":false,"desc":"Any other text this series' row carries after its figures, verbatim. Present only where there is some."},{"name":"timeline[].tls_date","type":"date","nullable":false,"desc":"This series' own month for the release, present only where its table disagrees with `date`. Release 272 is June 2026 in the traditional and WGS tables and April 2026 in the TSA and TLS tables."},{"name":"timeline[].set_based_bases","type":"integer","nullable":true,"desc":"WGS + TSA + TLS bases, summed over the series that reported this release. **Null when none did — missing is not zero.**"},{"name":"timeline[].set_based_entries","type":"integer","nullable":true,"desc":"The same sum over records."},{"name":"timeline[].combined_bases","type":"integer","nullable":true,"desc":"Traditional plus set-based bases. **Null whenever `traditional_bases` is absent**, even beside set-based figures: a total without its largest series would chart as a collapse."},{"name":"timeline[].combined_entries","type":"integer","nullable":true,"desc":"The same sum over records."}]},{"id":"get-clinvar-overview","verb":"GET","path":"/v1/genome/clinvar/overview","summary":"ClinVar's totals and classifications","returns":"object","params":[],"fields":[{"name":"source","type":"object","nullable":false,"desc":"The file these counts were made from and the attribution to carry with them — the same block on every ClinVar document. See `source.*`."},{"name":"totals","type":"object","nullable":false,"desc":"Rows in the file, rows on GRCh38, distinct VariationIDs and rsIDs, the rows that could not be read, and the size of the two collections. See `totals.*`."},{"name":"classification_exact","type":"array","nullable":false,"desc":"Every `ClinicalSignificance` string ClinVar wrote on a GRCh38 row, exactly as written, with its count — **uncapped**, busiest first: 107 strings on the 2026-10-05 file, down to singletons like `Benign; Affects; association; other`. A tie keeps the order the strings first appear in the file."},{"name":"classification_grouped","type":"array","nullable":false,"desc":"The same rows in ten groups, busiest first and **always all ten**, a group with no rows as `0`: `Conflicting`, `Pathogenic / Likely pathogenic`, `Likely pathogenic`, `Pathogenic`, `Uncertain significance`, `Likely benign`, `Benign`, `Other clinical assertion`, `Not provided` and `Other`. The strings are an interface — chart colours key off them — and the grouping is additional to `classification_exact`, never a replacement. ClinVar's absent-value marker `-` is `Not provided`, not `Other`."},{"name":"review_status","type":"array","nullable":false,"desc":"Each `ReviewStatus` with its count and stars, busiest first. ClinVar's `-` is counted under `no classification provided` — **one row**, where the file this replaces carried two with the same key."},{"name":"variant_type","type":"array","nullable":false,"desc":"Each value of ClinVar's `Type` column, busiest first."},{"name":"origin","type":"array","nullable":false,"desc":"Each value of ClinVar's `OriginSimple` column — six: `germline`, `not applicable`, `unknown`, `somatic`, `not provided`, `germline/somatic` — busiest first, capped at 25 as the file this replaces was. Not the `Origin` column, which has dozens."},{"name":"last_evaluated_year","type":"array","nullable":false,"desc":"How many GRCh38 rows were last evaluated in each year, **ascending by year**: the last four characters of `LastEvaluated`, kept from 1990 to the year the file was counted in. Rows with no date (`-`) or an earlier one are in no year."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`clinvar`, ClinVar's key in the genome registry: `/v1/genome/releases/clinvar` says which weekly release ClinVar is on today, checked every morning."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`ClinVar`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`NCBI`."},{"name":"source.url","type":"string","nullable":false,"desc":"The file the counts were made from, `https://ftp.ncbi.nlm.nih.gov/pub/clinvar/tab_delimited/variant_summary.txt.gz`."},{"name":"source.file_last_modified","type":"string","nullable":true,"desc":"The `Last-Modified` header NCBI sent with the file, **verbatim** — `Mon, 05 Oct 2026 13:02:37 GMT`, an RFC 7231 date, never normalised to ISO 8601."},{"name":"source.genome_build","type":"string","nullable":false,"desc":"`GRCh38`. ClinVar's file repeats every variant once per assembly, and every count here is over the GRCh38 rows alone."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When this file was **first** read by this parser. A later read of the same bytes by the same parser leaves it standing, so the document and its `ETag` change only when ClinVar writes a new file — send the `ETag` back as `If-None-Match` and the answer is a `304`, which is not billed."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The counting rules that produced these numbers. It moves when a change to them would count the same file differently, and the file is then counted again."},{"name":"source.note","type":"string","nullable":false,"desc":"The note the files this replaces carried, on the two decisions every count rests on, verbatim."},{"name":"source.release_version","type":"string","nullable":true,"desc":"The weekly release the file was written for, as ClinVar names its dated VCFs: `20261004`. Read from the `##fileDate` of ClinVar's GRCh38 VCF in the week the file was read, which is dated a day before the file's own `Last-Modified` — by design, so both are kept. `null`, with `release_note` saying why, when the VCF had already moved on to another week."},{"name":"source.release_date","type":"date","nullable":true,"desc":"`release_version` as a date, `2026-10-04`."},{"name":"source.release_note","type":"string","nullable":true,"desc":"Why `release_version` is `null`, when it is; `null` otherwise."},{"name":"source.file_md5","type":"string","nullable":false,"desc":"The md5 NCBI publishes beside the file, `variant_summary.txt.gz.md5`, which the bytes read were checked against: `md5sum variant_summary.txt.gz` over the same file gives the same string."},{"name":"source.content_length","type":"integer","nullable":false,"desc":"The file's size in bytes: 455,128,718 on 2026-10-05."},{"name":"source.scraper","type":"string","nullable":false,"desc":"The program that counted the file, `clinvar_summary` — its row on `/v1/sources`."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with any figure republished from ClinVar's reads: `ClinVar (NCBI), variant_summary.txt.gz, release 2026-10-04; NCBI places no restrictions on use or distribution`. ClinVar asks whoever distributes its data to name ClinVar as the source."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"NCBI's Website and Data Usage Policies: \"NCBI itself places no restrictions on the use or distribution of the data contained therein.\" These are counts over ClinVar's export, not any submitter's record."},{"name":"totals.rows_in_file","type":"integer","nullable":false,"desc":"Every row of the file that is not a `#` line, on any assembly: 9,290,347 on 2026-10-05."},{"name":"totals.rows_on_build","type":"integer","nullable":false,"desc":"The rows whose `Assembly` is `GRCh38`: 4,611,511. About half of `rows_in_file`, because ClinVar writes every variant once per assembly; a file nearly all GRCh38 is refused, because it means the filter stopped meaning what it meant."},{"name":"totals.distinct_variation_ids","type":"integer","nullable":false,"desc":"Distinct `VariationID`s over GRCh38 rows: the number of variants."},{"name":"totals.distinct_rs_ids","type":"integer","nullable":false,"desc":"Distinct dbSNP rsIDs over GRCh38 rows. An rsID of `-1` or `-` is none."},{"name":"totals.rows_malformed","type":"integer","nullable":false,"desc":"Rows that did not line up with the header — more or fewer fields than it names, or a VariationID or rsID that is not a number — counted in `rows_in_file` and in nothing else. `0` on 2026-10-05; a file with more than one in a thousand is refused, and the last good counts stay served."},{"name":"totals.genes","type":"integer","nullable":false,"desc":"How many rows `/v1/genome/clinvar/genes` serves for this file: every gene."},{"name":"totals.gene_condition_pairs","type":"integer","nullable":false,"desc":"How many gene-condition pairs the file holds: 233,232 on 2026-10-05."},{"name":"totals.gene_conditions_served","type":"integer","nullable":false,"desc":"How many of the busiest pairs `/v1/genome/clinvar/gene-conditions` serves: 10,000."},{"name":"classification_exact[].clinical_significance","type":"string","nullable":false,"desc":"A `ClinicalSignificance` string exactly as ClinVar wrote it, `-` included."},{"name":"classification_exact[].count","type":"integer","nullable":false,"desc":"GRCh38 rows carrying it."},{"name":"classification_grouped[].group","type":"string","nullable":false,"desc":"One of the ten group strings, character for character."},{"name":"classification_grouped[].count","type":"integer","nullable":false,"desc":"GRCh38 rows in the group; `0` for a group with none."},{"name":"review_status[].review_status","type":"string","nullable":false,"desc":"A `ReviewStatus`, lower-case as ClinVar writes it."},{"name":"review_status[].count","type":"integer","nullable":false,"desc":"GRCh38 rows carrying it."},{"name":"review_status[].stars","type":"integer","nullable":true,"desc":"ClinVar's review stars for the status: `practice guideline` 4, `reviewed by expert panel` 3, `criteria provided, multiple submitters, no conflicts` 2, `criteria provided, conflicting classifications` and `criteria provided, single submitter` 1, and 0 for the four statuses with no assertion or classification. `null` for a status the table does not rate."},{"name":"variant_type[].variant_type","type":"string","nullable":false,"desc":"A value of the `Type` column: `single nucleotide variant`, `Deletion`, …"},{"name":"variant_type[].count","type":"integer","nullable":false,"desc":"GRCh38 rows of that type."},{"name":"origin[].origin","type":"string","nullable":false,"desc":"A value of the `OriginSimple` column."},{"name":"origin[].count","type":"integer","nullable":false,"desc":"GRCh38 rows of that origin."},{"name":"last_evaluated_year[].year","type":"integer","nullable":false,"desc":"A year, 1990 or later."},{"name":"last_evaluated_year[].count","type":"integer","nullable":false,"desc":"GRCh38 rows last evaluated in it."}]},{"id":"get-clinvar-chromosomes","verb":"GET","path":"/v1/genome/clinvar/chromosomes","summary":"ClinVar's variants per chromosome","returns":"object","params":[],"fields":[{"name":"source","type":"object","nullable":false,"desc":"The file these counts were made from — the same block as the overview's. See `source.*`."},{"name":"chromosomes","type":"array","nullable":false,"desc":"One entry per chromosome a GRCh38 row names, in chromosome order: `1`–`22`, `X`, `Y`, `MT`, then anything else. **`Un` is kept** — three variants on 2026-10-05 — so the counts add up to `rows_on_build`; a chart can leave it out. See `chromosomes[].*`."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`clinvar`, ClinVar's key in the genome registry: `/v1/genome/releases/clinvar` says which weekly release ClinVar is on today, checked every morning."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`ClinVar`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`NCBI`."},{"name":"source.url","type":"string","nullable":false,"desc":"The file the counts were made from, `https://ftp.ncbi.nlm.nih.gov/pub/clinvar/tab_delimited/variant_summary.txt.gz`."},{"name":"source.file_last_modified","type":"string","nullable":true,"desc":"The `Last-Modified` header NCBI sent with the file, **verbatim** — `Mon, 05 Oct 2026 13:02:37 GMT`, an RFC 7231 date, never normalised to ISO 8601."},{"name":"source.genome_build","type":"string","nullable":false,"desc":"`GRCh38`. ClinVar's file repeats every variant once per assembly, and every count here is over the GRCh38 rows alone."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When this file was **first** read by this parser. A later read of the same bytes by the same parser leaves it standing, so the document and its `ETag` change only when ClinVar writes a new file — send the `ETag` back as `If-None-Match` and the answer is a `304`, which is not billed."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The counting rules that produced these numbers. It moves when a change to them would count the same file differently, and the file is then counted again."},{"name":"source.note","type":"string","nullable":false,"desc":"The note the files this replaces carried, on the two decisions every count rests on, verbatim."},{"name":"source.release_version","type":"string","nullable":true,"desc":"The weekly release the file was written for, as ClinVar names its dated VCFs: `20261004`. Read from the `##fileDate` of ClinVar's GRCh38 VCF in the week the file was read, which is dated a day before the file's own `Last-Modified` — by design, so both are kept. `null`, with `release_note` saying why, when the VCF had already moved on to another week."},{"name":"source.release_date","type":"date","nullable":true,"desc":"`release_version` as a date, `2026-10-04`."},{"name":"source.release_note","type":"string","nullable":true,"desc":"Why `release_version` is `null`, when it is; `null` otherwise."},{"name":"source.file_md5","type":"string","nullable":false,"desc":"The md5 NCBI publishes beside the file, `variant_summary.txt.gz.md5`, which the bytes read were checked against: `md5sum variant_summary.txt.gz` over the same file gives the same string."},{"name":"source.content_length","type":"integer","nullable":false,"desc":"The file's size in bytes: 455,128,718 on 2026-10-05."},{"name":"source.scraper","type":"string","nullable":false,"desc":"The program that counted the file, `clinvar_summary` — its row on `/v1/sources`."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with any figure republished from ClinVar's reads: `ClinVar (NCBI), variant_summary.txt.gz, release 2026-10-04; NCBI places no restrictions on use or distribution`. ClinVar asks whoever distributes its data to name ClinVar as the source."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"NCBI's Website and Data Usage Policies: \"NCBI itself places no restrictions on the use or distribution of the data contained therein.\" These are counts over ClinVar's export, not any submitter's record."},{"name":"chromosomes[].chromosome","type":"string","nullable":false,"desc":"ClinVar's `Chromosome` value: `1`, `X`, `MT`, `Un`."},{"name":"chromosomes[].variants","type":"integer","nullable":false,"desc":"GRCh38 rows on the chromosome."},{"name":"chromosomes[].pathogenic","type":"integer","nullable":false,"desc":"Those in the three pathogenic groups: `Pathogenic`, `Likely pathogenic` and `Pathogenic / Likely pathogenic`."},{"name":"chromosomes[].uncertain","type":"integer","nullable":false,"desc":"Those in the group `Uncertain significance`."}]},{"id":"list-clinvar-genes","verb":"GET","path":"/v1/genome/clinvar/genes","summary":"ClinVar's variants per gene","returns":"array","max_page_size":30000,"params":[{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–30000. Defaults to 25. **Every gene is served, and the whole list fits in one page**: 24,031 genes on the 2026-10-05 file, about 650 more a year, so `limit=30000` returns all of them in one request until about 2035. Follow `meta.next` anyway — it is `null` when the page is the whole list, and a client that stops at page 1 without reading it would one day stop short.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's `meta.next`. Do not construct one. It carries the ClinVar file the walk started on, so a page asked for after the next weekly file has landed is a `400` saying so — start the walk again — never that file's rows appended to the pages you hold."}],"fields":[{"name":"gene","type":"string","nullable":false,"desc":"ClinVar's `GeneSymbol`, verbatim. A row whose symbol is `-`, or a `;`-joined list of several genes, is counted under no gene — treating either as one would invent the busiest gene in the database. Only those two are left out, so what ClinVar writes instead of a symbol for a large copy-number variant — `covers 17 genes, none of which curated to show dosage sensitivity`, `subset of 102 genes: PRRT2` — is a row of its own: 3,707 such rows on 2026-10-05, and a join by symbol matches none of them."},{"name":"variants","type":"integer","nullable":false,"desc":"GRCh38 rows naming the gene. The order of the collection: busiest first, a tie in the order the file first names the genes."},{"name":"pathogenic","type":"integer","nullable":false,"desc":"Those in the three pathogenic groups: `Pathogenic`, `Likely pathogenic` and `Pathogenic / Likely pathogenic`."}]},{"id":"list-clinvar-gene-conditions","verb":"GET","path":"/v1/genome/clinvar/gene-conditions","summary":"ClinVar's busiest gene-condition pairs","returns":"array","params":[{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25. The busiest 10,000 pairs of the file are served, in order, so `limit` and `meta.next` take you as deep into them as you need: the first 4,000 are the file the pipeline this replaces published. `totals.gene_condition_pairs` on the overview says how many the file holds in all.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's `meta.next`. Do not construct one. It carries the ClinVar file the walk started on, so a page asked for after the next weekly file has landed is a `400` saying so — start the walk again — never that file's rows appended to the pages you hold."}],"fields":[{"name":"gene","type":"string","nullable":false,"desc":"The gene, as `/v1/genome/clinvar/genes` names it."},{"name":"condition","type":"string","nullable":false,"desc":"One `|`-separated part of ClinVar's `PhenotypeList`, trimmed and verbatim. A `;` inside it is part of ClinVar's label, not a separator: `Dilated cardiomyopathy 1G;Autosomal recessive limb-girdle muscular dystrophy type 2J` is one condition. `not provided`, `not specified` and `See cases` are no condition."},{"name":"variants","type":"integer","nullable":false,"desc":"GRCh38 rows naming both. The order of the collection: busiest first, a tie in the order the file first names the pair."}]},{"id":"get-biosample-geography","verb":"GET","path":"/v1/genome/biosample-geography","summary":"BioSamples by country, counted from each record","returns":"object","params":[{"name":"focus","in":"query","type":"string","required":false,"desc":"The country to put in `focus`, ignoring case. One of `countries` — Azerbaijan, Georgia, Armenia, Turkey, Kazakhstan, Uzbekistan, Iran. Defaults to Azerbaijan. Any other answers `404` naming the seven: only these are counted, weekly, and a country is never looked up while you wait.","example":"Azerbaijan"},{"name":"compare","in":"query","type":"string","required":false,"desc":"The countries to put in `comparison`, comma-separated or repeated, in the order you want them, ignoring case. Defaults to every counted country but the focus. Each must be one of `countries` (a `404` names them if not), and naming the focus is a `400`.","example":"Georgia,Armenia,Turkey,Kazakhstan,Uzbekistan,Iran"}],"fields":[{"name":"source","type":"object","nullable":false,"desc":"Where these counts come from, when the run read them, and the terms they come under. In the document rather than in `meta`, so `data` is the whole file a consumer stores. See `source.*`."},{"name":"focus","type":"object","nullable":false,"desc":"The country `?focus=` names — by default the run's first, `Azerbaijan` — counted from its records. See `focus.*`, which every row of `comparison` shares."},{"name":"comparison","type":"array","nullable":false,"desc":"The countries `?compare=` names, in the order named; by default every other country the run counted, in the run's order: Georgia, Armenia, Turkey, Kazakhstan, Uzbekistan, Iran. **Not sorted by any figure** — sort it yourself."},{"name":"methodology","type":"object","nullable":false,"desc":"What a country is, the cap, how an estimate is made, what the sample is and which name a country is counted under. See `methodology.*`."},{"name":"countries","type":"array","nullable":false,"desc":"Every country this document can be asked about, in the run's order: the values `?focus=` and `?compare=` take. Seven today. Any other is a `404` naming these — a country is never looked up while you wait."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`ncbi_biosample`, the key the file this replaces gave the document."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`NCBI BioSample`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`NCBI`."},{"name":"source.url","type":"string","nullable":false,"desc":"BioSample's own page, `https://www.ncbi.nlm.nih.gov/biosample/`."},{"name":"source.api","type":"string","nullable":false,"desc":"The two E-utilities programs a run calls: `esearch.fcgi` for each country's record ids, `esummary.fcgi` for those records, 400 at a time."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When the run's last country was read. Weekly: a run reads BioSample when the document served is six and a half days old, so this is at most about a week old on a healthy source, and `stale` says when it is older than that allows."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The reading rules — what `geo_loc_name` is, what a country is, the estimate's arithmetic. It moves when a change would count the same records differently, and a run under a new one is read the next morning rather than at the end of the week."},{"name":"source.note","type":"string","nullable":false,"desc":"What these counts are, in one paragraph: each record's `geo_loc_name` parsed and only the country before the colon compared, exactly; why a text search overstates every country; sample geography only, never author affiliation."},{"name":"source.stale","type":"boolean","nullable":false,"desc":"`true` once `retrieved_at` is more than seven and a half days old: the weekly read that should have replaced this run is half a day late, because it failed or was refused and the last good document was kept. Measured when your request is answered, so it can turn `true` while every figure stands still — and the `ETag` moves with it. It says how recently the counts were confirmed, never that they are wrong. Added beside the file this replaces."},{"name":"source.run","type":"string","nullable":false,"desc":"The run's name, the UTC instant it started: `20261009T011215Z`."},{"name":"source.scraper","type":"string","nullable":false,"desc":"The program that counted, `ncbi_eutils` — its row on `/v1/sources`."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with a figure republished from this document: what was read and the day it was read, with NCBI's terms. BioSample has no release a count could be cited by instead."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"NCBI's Website and Data Usage Policies: \"NCBI itself places no restrictions on the use or distribution of the data contained therein.\" These are counts of records and tallies of three of their fields, not any submitter's record."},{"name":"focus.country","type":"string","nullable":false,"desc":"The country as counted, `Azerbaijan`: the name the run searched for and compared each record's country with, ignoring case — that name and no other spelling, so Turkey's records spelled `Turkiye` are not counted. See `methodology.name_rule`."},{"name":"focus.reported_by_search","type":"integer","nullable":false,"desc":"**The wrong number, kept on purpose.** esearch's own count for `geo_loc_name \u003ccountry\u003e`, which NCBI reads as the two words anywhere in a record — see `query_translation` — so `USA: Georgia` is counted for Georgia and `Iran: East Azerbaijan` for Azerbaijan. It is never a country's count: it is here so the size of the error is visible, and `reported_by_search − exact_country_matches` is how many records a text search wrongly credits."},{"name":"focus.records_examined","type":"integer","nullable":false,"desc":"How many records were read and parsed: every id in `ids_sampled`, less any NCBI had no summary for."},{"name":"focus.exact_country_matches","type":"integer","nullable":false,"desc":"How many of those name this country before the first colon of their `geo_loc_name`, compared exactly: **the answer**, for the records examined."},{"name":"focus.without_geo_value","type":"integer","nullable":false,"desc":"How many had no place at all — no `geo_loc_name`, an empty one, `missing` or `not applicable`. Neither a match nor a mismatch. A run in which a country's share of these jumps past a quarter, where the run served had it under one in twenty, is refused and the served document stays: it is how a parse that stopped finding the attribute shows."},{"name":"focus.match_rate","type":"number","nullable":true,"desc":"`exact_country_matches / records_examined` to four places — a **fraction**, 0 to 1, not a percentage. **Null when nothing was examined**, never 0."},{"name":"focus.estimated_total","type":"integer","nullable":false,"desc":"The country's count. Where every record was read, it **is** `exact_country_matches`. Where the search counts more than the cap, it is an estimate — `match_rate`, unrounded, times `reported_by_search` — and `is_estimate` says so."},{"name":"focus.is_estimate","type":"boolean","nullable":false,"desc":"Whether `estimated_total` is a sampled estimate rather than an exact count: exactly `ids_sampled \u003c reported_by_search`. **An estimate and an exact count must not look the same** — show it, with a `≈` or similar."},{"name":"focus.organisms","type":"array","nullable":false,"desc":"The matched records' organisms, `{organism, samples}`, the 60 most frequent, descending — so `organisms.length` is a count capped at 60. `samples` are records, not sequences or bases."},{"name":"focus.by_year","type":"array","nullable":false,"desc":"The matched records by year, `{year, samples}`, ascending, 1990 to the run's year: each record's `collection_date`, or the record's own date where it has none. A year with no record has no entry."},{"name":"focus.packages","type":"array","nullable":false,"desc":"The matched records' BioSample packages, `{name, samples}` — `Plant; version 1.0` — the 20 most frequent, descending."},{"name":"focus.ids_sampled","type":"integer","nullable":false,"desc":"How many of the search's records were read. Every one where the search counts no more than `methodology.cap_per_country`; otherwise that many, one from the middle of each equal stretch of every id the search returns — **spread over all of them, not the first 6,000**, which are a few submission batches. Added beside the file this replaces."},{"name":"focus.summaries_missing","type":"integer","nullable":false,"desc":"How many of those ids NCBI answered without a summary — records withdrawn between the search and the read. `records_examined + summaries_missing = ids_sampled`; a run with more than 2% missing for a country is refused."},{"name":"focus.unparsed_sampledata","type":"integer","nullable":false,"desc":"How many records' attributes were not readable XML, counted in `without_geo_value`. 0 on every row today."},{"name":"focus.query_translation","type":"string","nullable":true,"desc":"NCBI's own reading of the search, `geo_loc_name[All Fields] AND Georgia[All Fields]`: the receipt for why `reported_by_search` is the wrong number."},{"name":"comparison[].country","type":"string","nullable":false,"desc":"The country as counted, `Azerbaijan`: the name the run searched for and compared each record's country with, ignoring case — that name and no other spelling, so Turkey's records spelled `Turkiye` are not counted. See `methodology.name_rule`."},{"name":"comparison[].reported_by_search","type":"integer","nullable":false,"desc":"**The wrong number, kept on purpose.** esearch's own count for `geo_loc_name \u003ccountry\u003e`, which NCBI reads as the two words anywhere in a record — see `query_translation` — so `USA: Georgia` is counted for Georgia and `Iran: East Azerbaijan` for Azerbaijan. It is never a country's count: it is here so the size of the error is visible, and `reported_by_search − exact_country_matches` is how many records a text search wrongly credits."},{"name":"comparison[].records_examined","type":"integer","nullable":false,"desc":"How many records were read and parsed: every id in `ids_sampled`, less any NCBI had no summary for."},{"name":"comparison[].exact_country_matches","type":"integer","nullable":false,"desc":"How many of those name this country before the first colon of their `geo_loc_name`, compared exactly: **the answer**, for the records examined."},{"name":"comparison[].without_geo_value","type":"integer","nullable":false,"desc":"How many had no place at all — no `geo_loc_name`, an empty one, `missing` or `not applicable`. Neither a match nor a mismatch. A run in which a country's share of these jumps past a quarter, where the run served had it under one in twenty, is refused and the served document stays: it is how a parse that stopped finding the attribute shows."},{"name":"comparison[].match_rate","type":"number","nullable":true,"desc":"`exact_country_matches / records_examined` to four places — a **fraction**, 0 to 1, not a percentage. **Null when nothing was examined**, never 0."},{"name":"comparison[].estimated_total","type":"integer","nullable":false,"desc":"The country's count. Where every record was read, it **is** `exact_country_matches`. Where the search counts more than the cap, it is an estimate — `match_rate`, unrounded, times `reported_by_search` — and `is_estimate` says so."},{"name":"comparison[].is_estimate","type":"boolean","nullable":false,"desc":"Whether `estimated_total` is a sampled estimate rather than an exact count: exactly `ids_sampled \u003c reported_by_search`. **An estimate and an exact count must not look the same** — show it, with a `≈` or similar."},{"name":"comparison[].organisms","type":"array","nullable":false,"desc":"The matched records' organisms, `{organism, samples}`, the 60 most frequent, descending — so `organisms.length` is a count capped at 60. `samples` are records, not sequences or bases."},{"name":"comparison[].by_year","type":"array","nullable":false,"desc":"The matched records by year, `{year, samples}`, ascending, 1990 to the run's year: each record's `collection_date`, or the record's own date where it has none. A year with no record has no entry."},{"name":"comparison[].packages","type":"array","nullable":false,"desc":"The matched records' BioSample packages, `{name, samples}` — `Plant; version 1.0` — the 20 most frequent, descending."},{"name":"comparison[].ids_sampled","type":"integer","nullable":false,"desc":"How many of the search's records were read. Every one where the search counts no more than `methodology.cap_per_country`; otherwise that many, one from the middle of each equal stretch of every id the search returns — **spread over all of them, not the first 6,000**, which are a few submission batches. Added beside the file this replaces."},{"name":"comparison[].summaries_missing","type":"integer","nullable":false,"desc":"How many of those ids NCBI answered without a summary — records withdrawn between the search and the read. `records_examined + summaries_missing = ids_sampled`; a run with more than 2% missing for a country is refused."},{"name":"comparison[].unparsed_sampledata","type":"integer","nullable":false,"desc":"How many records' attributes were not readable XML, counted in `without_geo_value`. 0 on every row today."},{"name":"comparison[].query_translation","type":"string","nullable":true,"desc":"NCBI's own reading of the search, `geo_loc_name[All Fields] AND Georgia[All Fields]`: the receipt for why `reported_by_search` is the wrong number."},{"name":"methodology.country_field","type":"string","nullable":false,"desc":"`Portion of geo_loc_name before the first colon`."},{"name":"methodology.cap_per_country","type":"integer","nullable":false,"desc":"How many of a country's records a run reads at most: 6,000. A country whose search counts more is estimated from a sample of that many."},{"name":"methodology.estimate_rule","type":"string","nullable":false,"desc":"How an estimate is made: the exact-match rate observed in the sample applied to the search total, flagged `is_estimate`."},{"name":"methodology.sample_rule","type":"string","nullable":false,"desc":"What the sample is: one record from the middle of each of `cap_per_country` equal stretches of every id the search returns — **spread over all of them, not the first `cap_per_country`**. A country's records are deposited in batches that sit together in NCBI's order, so the first 6,000 are a few batches: on 2026-10-10 they put Georgia at 86,073, where two spread samples agreed on about 22,000."},{"name":"methodology.name_rule","type":"string","nullable":false,"desc":"Which name a country is counted under: the one in `countries`, searched for and compared exactly, and no other spelling. **Turkey counts records spelled `Turkey`; records using INSDC's current spelling, `Turkiye`, are not counted** — 2,864 of them on 2026-10-10, when Turkey was estimated at 38,633. Added beside the file this replaces."}]},{"id":"get-sequencing-activity","verb":"GET","path":"/v1/genome/sequencing-activity","summary":"Viral sequences and SRA runs, counted daily","returns":"object","params":[],"fields":[{"name":"source","type":"object","nullable":false,"desc":"Where these counts come from, when the run read them, and the terms they come under. See `source.*`."},{"name":"viruses","type":"array","nullable":false,"desc":"Ten viruses, **descending by `sequences`**, each named by tax_id. See `viruses[].*`."},{"name":"virus_by_year","type":"array","nullable":false,"desc":"Every virus sequence (`txid10239[Organism:exp]`) by **publication** year (`[PDAT]`), `{year, sequences}`, ascending and unbroken from 2015 to the run's year — a January run adds the new year. Publication date is when a record entered the archive, not when its sample was collected; only the first is searchable."},{"name":"sra","type":"object","nullable":false,"desc":"The Sequence Read Archive's runs: `total_runs`, and `by_strategy` (`{strategy, runs}`, eight library strategies) and `by_platform` (`{platform, runs}`, five named platforms), each **descending by `runs`**, and `by_year` (`{year, runs}`, by publication year, ascending and unbroken from 2010 to the run's year). `runs` are SRA run records, counted, never read. `by_platform` is the five platforms the file this replaces named — Illumina, Oxford Nanopore, PacBio SMRT, Ion Torrent and BGISEQ — **not every platform SRA indexes**: DNBSEQ, the name BGISEQ's line is sold under now, is a platform of its own there and is not among them (384,303 runs on 2026-10-10, against BGISEQ's 257,863)."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`ncbi_entrez`, the key the file this replaces gave the document."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`NCBI Nucleotide / SRA`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`NCBI`."},{"name":"source.api","type":"string","nullable":false,"desc":"The one E-utilities program a run calls, `esearch.fcgi`, with `retmax=0`."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When the run's counts were read. Daily, and `stale` says when it is older than that allows."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The counting rules. It moves when a change would count the same answers differently."},{"name":"source.note","type":"string","nullable":false,"desc":"What these counts are, in one paragraph: counts only; viruses by tax_id with their descendants; years are publication years; the segmented-virus undercount."},{"name":"source.stale","type":"boolean","nullable":false,"desc":"`true` once `retrieved_at` is more than a day and a half old: the morning's read that should have replaced this run is half a day late. Measured when your request is answered; it says how recently the counts were read, never that they are wrong. Added beside the file this replaces."},{"name":"source.run","type":"string","nullable":false,"desc":"The run's name, the UTC instant it started: `20261009T044012Z`."},{"name":"source.scraper","type":"string","nullable":false,"desc":"The program that counted, `ncbi_eutils` — its row on `/v1/sources`."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with a figure republished from this document: what was read and the day it was read, with NCBI's terms."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"NCBI's Website and Data Usage Policies: \"NCBI itself places no restrictions on the use or distribution of the data contained therein.\""},{"name":"viruses[].name","type":"string","nullable":false,"desc":"The virus, `Influenza A virus`."},{"name":"viruses[].taxid","type":"integer","nullable":false,"desc":"Its NCBI Taxonomy id, `11320`. Viruses are counted by tax_id with `[Organism:exp]` — never by name, which as free text matches every record that merely mentions it — so descendant taxa are included."},{"name":"viruses[].sequences","type":"integer","nullable":false,"desc":"Nucleotide records (GenBank and RefSeq) under the tax_id."},{"name":"viruses[].complete_genomes","type":"integer","nullable":false,"desc":"Those records titled `complete genome`. **Undercounts segmented viruses by orders of magnitude**: their genomes are deposited one record per segment, and a segment is not titled a complete genome — Influenza A has 56 against 1.68 million sequences. Kept, and flagged by `segmented`."},{"name":"viruses[].segmented","type":"boolean","nullable":false,"desc":"`true` for the two viruses whose `complete_genomes` undercounts — Influenza A virus and Rotavirus A. Show it beside the figure."}]},{"id":"get-taxonomy-backbone","verb":"GET","path":"/v1/genome/taxonomy/backbone","summary":"NCBI Taxonomy's backbone, as one document","returns":"object","params":[{"name":"depth","in":"query","type":"string","required":false,"desc":"`family` (the default) for the whole backbone — root and every node ranked domain down to family, about 14,800 — or `class` for the backbone cut at class, about 1,020 nodes and 160 KB: the small first payload of a tree a viewer expands. Each node's parent is its nearest ancestor IN the cut, and `totals.by_rank` counts the cut you asked for. Anything else is a `400`.","example":"class"}],"fields":[{"name":"source","type":"object","nullable":false,"desc":"The dump the backbone was cut from — NCBI's md5 for it, when it was read — and the attribution to carry with it. In the document rather than in `meta`, so `data` is the whole file a consumer stores. See `source.*`."},{"name":"methodology","type":"object","nullable":false,"desc":"The ranks the nodes were cut from and what the counts count. See `methodology.*`."},{"name":"totals","type":"object","nullable":false,"desc":"The whole tree's taxa and species, and **this document's** nodes by rank. See `totals.*`."},{"name":"depth","type":"string","nullable":false,"desc":"The cut this document is: `family`, the whole backbone, or `class`, the nodes ranked class or above. The `?depth=` asked for, or `family`."},{"name":"nodes","type":"array","nullable":false,"desc":"The backbone, sorted by `species_count` descending and then `tax_id` ascending — the order is part of the contract, as in the file this replaces. 14,819 nodes on the dump of 2026-10-10, about 1,020 cut at class. See `nodes[].*`."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`ncbi_taxonomy`, NCBI Taxonomy's key in the genome registry: `/v1/genome/releases/ncbi_taxonomy` names the md5 NCBI publishes this morning."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`NCBI Taxonomy`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`NCBI`."},{"name":"source.url","type":"string","nullable":false,"desc":"The archive the dump was read from, `https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump.tar.gz` — one unversioned path NCBI rebuilds more than once a day."},{"name":"source.checksum","type":"string","nullable":false,"desc":"The md5 NCBI publishes beside the archive, which the bytes read hashed to: the dump's only identity. Quote it with anything you cite from this document."},{"name":"source.checksum_algorithm","type":"string","nullable":false,"desc":"`md5`, NCBI's choice."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When this dump was **first** read. The backbone is re-read about weekly; the same md5 read again leaves this standing, so the document, and its `ETag`, change only when a different dump's backbone has landed whole."},{"name":"source.last_modified","type":"string","nullable":true,"desc":"The `Last-Modified` header NCBI sent with the archive, verbatim."},{"name":"source.content_length","type":"integer","nullable":true,"desc":"The archive's size in bytes: 79,796,663 on 2026-10-10."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The rules that cut the backbone. It moves when a change to them would read the same dump into different nodes."},{"name":"source.scraper","type":"string","nullable":false,"desc":"The program that read the dump, `ncbi_taxdump` — its row on `/v1/sources`."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with anything republished from this document: `NCBI Taxonomy (NCBI), taxdump.tar.gz md5 \u003cchecksum\u003e; NCBI places no restrictions on use or distribution`."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"NCBI's Website and Data Usage Policies: \"NCBI itself places no restrictions on the use or distribution of the data contained therein.\" The taxonomy is NCBI's own classification, not any submitter's record."},{"name":"methodology.backbone_ranks","type":"array","nullable":false,"desc":"The ranks this document's nodes hold, root aside, in #147's order: `domain`, `superkingdom`, `acellular root`, `realm`, `kingdom`, `phylum`, `class`, `order`, `family` — the last two absent cut at class. `superkingdom` holds no node today (NCBI renamed it `domain`) and is listed so the next rename shows as an empty rank rather than a missing branch. Viruses hang off root at `acellular root`, under no domain."},{"name":"methodology.note","type":"string","nullable":false,"desc":"What the nodes are and what their counts count, in one paragraph."},{"name":"totals.taxa_in_source","type":"integer","nullable":false,"desc":"Every node in the dump — root's `descendant_taxa`. 3,020,947 on 2026-10-10."},{"name":"totals.species_in_source","type":"integer","nullable":false,"desc":"Every node ranked `species` in the dump — root's `species_count`. 2,493,818 on 2026-10-10."},{"name":"totals.backbone_nodes","type":"integer","nullable":false,"desc":"How many nodes **this document** holds: the whole backbone, or the class cut."},{"name":"totals.by_rank","type":"object","nullable":false,"desc":"This document's nodes per rank, `root` included, summing to `backbone_nodes`. **Computed for the document you are reading** — cut at class it counts no order and no family. The file this replaces copied the whole backbone's counts into its class cut; this is the correction #147 asked for, so the two differ there on purpose."},{"name":"nodes[].tax_id","type":"integer","nullable":false,"desc":"NCBI's tax id."},{"name":"nodes[].parent_tax_id","type":"integer","nullable":true,"desc":"The nearest ancestor that is a node **of this document** — past every unranked and minor level between them. Null on root alone, and every other value is a `tax_id` in the same `nodes`: #147's `parents_resolve`, which the platform checks before a backbone is served."},{"name":"nodes[].rank","type":"string","nullable":false,"desc":"NCBI's literal rank string, `root` on tax id 1. A rank NCBI adds is served as it is spelt and reported to the platform's operator the day it appears."},{"name":"nodes[].scientific_name","type":"string","nullable":false,"desc":"NCBI's scientific name. Never null: a dump with an unnamed backbone node is refused whole rather than served with a hole in the tree."},{"name":"nodes[].common_name","type":"string","nullable":true,"desc":"NCBI's GenBank common name where it has one, else the first plain common name in NCBI's own order, else null."},{"name":"nodes[].species_count","type":"integer","nullable":false,"desc":"Taxa ranked `species` anywhere beneath the node in the whole NCBI tree — not the number of species on Earth. Never negative, and always fewer than `descendant_taxa`: a backbone node is never itself a species."},{"name":"nodes[].descendant_taxa","type":"integer","nullable":false,"desc":"Every taxon beneath the node in the whole tree, the node itself included."}]},{"id":"lookup-taxonomy-lineage","verb":"GET","path":"/v1/genome/taxonomy/lineage","summary":"Walk NCBI tax ids to root","returns":"array","params":[{"name":"tax_id","in":"query","type":"string","required":true,"desc":"**Required.** Up to 100 NCBI tax ids, comma-separated — `9606,10090` — or the parameter repeated. Any taxon NCBI holds: a species, a strain, an unranked clade. A value that is not a positive integer is a `400` naming it, and a 101st id a `400` naming the count, never a truncation: a dropped id would come back in `meta.not_found`, which means \"NCBI holds no such taxon\". An id NCBI has merged into another or deleted IS in `not_found`; look it up again under its current id.\n\nThere is no way to page the whole tree here, and that is deliberate: it is three million taxa, and NCBI's monthly archive at `https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump_archive/` is the file to take if you need all of it.","example":"9606,10090"}],"fields":[{"name":"source","type":"string","nullable":false,"desc":"The scraper that read the tree, `ncbi_taxdump`."},{"name":"tax_id","type":"integer","nullable":false,"desc":"The tax id asked for, as NCBI holds it."},{"name":"rank","type":"string","nullable":false,"desc":"NCBI's literal rank string — `species`, `strain`, `no rank`, `clade` — and `root` on tax id 1, as in the backbone."},{"name":"scientific_name","type":"string","nullable":true,"desc":"NCBI's scientific name. Every taxon NCBI holds has one today."},{"name":"common_name","type":"string","nullable":true,"desc":"NCBI's GenBank common name where it has one, else the first plain common name, else null."},{"name":"parent_tax_id","type":"integer","nullable":true,"desc":"The taxon's own parent in NCBI's tree, null on root."},{"name":"lineage","type":"array","nullable":false,"desc":"Every ancestor, **the parent first and root last** — NCBI's whole chain, every unranked and minor level on it, which is the walk an assembly roll-up performs and the one the backbone's own links cannot make: they stop at family. `[]` for root. See `lineage[].*`."},{"name":"reaches_root","type":"boolean","nullable":false,"desc":"Whether the walk ended at tax id 1. Always `true` once a dump has landed whole; `false` only while a refresh is part-way and the walk met a parent the tree does not hold yet, in which case `lineage` stops at the last ancestor it does."},{"name":"mixed_dumps","type":"boolean","nullable":false,"desc":"Whether any step of the walk was read from a block a newer dump stated after the tree `dump` names was whole — that dump's refresh arriving in parts, which a walk can cross before it is whole, so the lineage may mix the two. `false` on every answer between refreshes and once a refresh has landed whole; when it is `true`, ask again later for a lineage of one dump."},{"name":"dump","type":"string","nullable":false,"desc":"The md5 of the newest dump the whole tree landed for — what the walk was answered from, `mixed_dumps` aside. The backbone's `source.checksum` names the same dump once both have landed."},{"name":"retrieved_at","type":"timestamp","nullable":false,"desc":"When that dump was first read."},{"name":"lineage[].tax_id","type":"integer","nullable":false,"desc":"NCBI's tax id."},{"name":"lineage[].rank","type":"string","nullable":false,"desc":"NCBI's literal rank string, `root` on tax id 1."},{"name":"lineage[].scientific_name","type":"string","nullable":true,"desc":"NCBI's scientific name."},{"name":"lineage[].common_name","type":"string","nullable":true,"desc":"GenBank's common name, else the first plain one, else null."}]},{"id":"get-gtex-expression","verb":"GET","path":"/v1/genome/expression/{symbol}","summary":"One gene's median expression in every GTEx tissue","returns":"object","params":[{"name":"symbol","in":"path","type":"string","required":true,"desc":"A gene symbol as GENCODE writes it, matched case-insensitively — `BRCA1`, `brca1` — or a GENCODE gene id, `ENSG00000012048.20`, or one without its version, `ENSG00000012048`. Percent-encode a `/` as `%2F`. **A well-formed symbol the release does not name is a `200` with `data: null`** and `meta.reason: \"not_in_reference\"` — the one by-name read here whose miss is not a `404` — while something that is no symbol or id at all is a `400`. A made-up symbol in the right shape cannot be told from a real one GENCODE lacks, so it is the same null.","example":"BRCA1"},{"name":"dataset","in":"query","type":"string","required":false,"desc":"The GTEx release: `gtex_v10` (the default; GENCODE v39, the release GTEx's own API serves) or `gtex_v8` (GENCODE v26). Anything else is a `400`. A release this deployment has not loaded yet is a `404`.","example":"gtex_v8"}],"fields":[{"name":"symbol","type":"string","nullable":true,"desc":"GENCODE's name for the gene in this release, as GTEx's file writes it — `BRCA1` whichever case you asked in. Null for a gene GENCODE gave no name (19,118 of `gtex_v10`'s), which only its GENCODE id answers."},{"name":"gencode_id","type":"string","nullable":false,"desc":"The gene's versioned GENCODE id in this release: `ENSG00000012048.20` in `gtex_v8` (GENCODE v26), `ENSG00000012048.23` in `gtex_v10` (v39). A gene's copy on chrY's pseudoautosomal region ends `_PAR_Y`."},{"name":"dataset","type":"string","nullable":false,"desc":"GTEx's id for the release answered from — `gtex_v10` or `gtex_v8` — exactly as its own API names it."},{"name":"gencode_version","type":"string","nullable":false,"desc":"The GENCODE release the dataset was quantified against: `v26` for `gtex_v8`, `v39` for `gtex_v10`. A gene absent from one can be present in the other."},{"name":"genome_build","type":"string","nullable":false,"desc":"`GRCh38/hg38` for both releases."},{"name":"unit","type":"string","nullable":false,"desc":"`TPM`, as on every tissue."},{"name":"tissues","type":"array of object","nullable":false,"desc":"The gene's median in every tissue of the release — 54 for both — **sorted by `median` descending**, a tie in `tissue_id` order, so the first is the tissue that expresses it most and a chart can normalise against it as served."},{"name":"matches","type":"integer","nullable":false,"desc":"How many genes of this release your request named: `1` for almost every symbol, `738` for `Y_RNA` in `gtex_v8`. The answer is the first in the file's order, which is GENCODE's genomic order and the order GTEx's own lookup answers in."},{"name":"other_gencode_ids","type":"array of string","nullable":false,"desc":"The other genes your request named, in file order, at most 100 — each is answered by its own `gencode_id`. `[]` when it named one."},{"name":"tissues[].tissue","type":"string","nullable":false,"desc":"The display label: GTEx's `tissueSiteDetailId` with its underscores turned to spaces and its case untouched — `Whole Blood`, `Brain Anterior cingulate cortex BA24`. Unique within the array."},{"name":"tissues[].tissue_id","type":"string","nullable":false,"desc":"GTEx's `tissueSiteDetailId`: `Whole_Blood`."},{"name":"tissues[].median","type":"number","nullable":false,"desc":"The median TPM across the release's samples of this tissue, as GTEx's file prints it — the number its own API answers. Always a number, `0` where the gene was not detected."},{"name":"tissues[].unit","type":"string","nullable":false,"desc":"`TPM`."}]},{"id":"list-gtex-tissues","verb":"GET","path":"/v1/genome/expression/tissues","summary":"The tissues of a GTEx release","returns":"array","params":[{"name":"dataset","in":"query","type":"string","required":false,"desc":"The GTEx release, as on the gene route: `gtex_v10` (default) or `gtex_v8`.","example":"gtex_v8"}],"fields":[{"name":"tissue_id","type":"string","nullable":false,"desc":"GTEx's `tissueSiteDetailId`, the id every median carries."},{"name":"tissue","type":"string","nullable":false,"desc":"The label every median of this tissue carries: the id with its underscores turned to spaces."},{"name":"name","type":"string","nullable":false,"desc":"GTEx's own display name, `Brain - Anterior cingulate cortex (BA24)`."},{"name":"site","type":"string","nullable":true,"desc":"The tissue site it belongs to, `Brain`."},{"name":"ontology_id","type":"string","nullable":true,"desc":"Its UBERON or EFO term, `UBERON:0009835`."},{"name":"color_hex","type":"string","nullable":true,"desc":"The colour GTEx draws it in, six hex digits."},{"name":"rna_seq_samples","type":"integer","nullable":true,"desc":"How many RNA-seq samples of this tissue the release has, per GTEx's API."},{"name":"column","type":"string","nullable":false,"desc":"The header of the median file's column this tissue was read from: `gtex_v8`'s file is headed by display names, `gtex_v10`'s by ids."}]},{"id":"get-assembly-coverage","verb":"GET","path":"/v1/genome/assemblies/coverage","summary":"Genome assemblies on every taxonomy backbone node","returns":"object","params":[{"name":"rank","in":"query","type":"string","required":false,"desc":"`top` (the default) for root and every node ranked domain down to class — about 1,020 nodes and 340 KB, the small first payload of a tree a viewer expands — or `all` for every backbone node down to family, about 14,800 and 4.9 MB. Each node's parent is its nearest ancestor IN the cut, and every count is the whole subtree's at either cut. Anything else is a `400`.","example":"top"}],"fields":[{"name":"source","type":"object","nullable":false,"desc":"The read the counts were made by — both tables by `Last-Modified`, their row counts, the taxdump rolled up over — and the attribution to carry with them. In the document rather than in `meta`, so `data` is the whole file a consumer stores. Every assembly document carries the same block. See `source.*`."},{"name":"totals","type":"object","nullable":false,"desc":"Each table's `latest` rows, and how many nodes **this document** holds. See `totals.*`."},{"name":"rank","type":"string","nullable":false,"desc":"The cut this document is: `top` (the default), root and every node ranked domain down to class, or `all`, every backbone node down to family."},{"name":"nodes","type":"array","nullable":false,"desc":"The backbone nodes, in the backbone's own order: `species_in_taxonomy` descending, then `tax_id`. 14,819 at `rank=all` on the read of 2026-10-10, 1,022 at `top`. See `nodes[].*`."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`ncbi_assemblies`, as the documents this replaces named themselves."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`NCBI Assembly summary`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`NCBI`."},{"name":"source.urls","type":"array","nullable":false,"desc":"The two tables the read was made from, RefSeq's first: `https://ftp.ncbi.nlm.nih.gov/genomes/ASSEMBLY_REPORTS/assembly_summary_refseq.txt` and `…/assembly_summary_genbank.txt`."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When the read began. The tables are read weekly; the same tables read again by the same parser over the same taxdump leave this standing, so the document, and its `ETag`, change only when a different read lands."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The counting rules. It moves when a change to them would count the same tables differently."},{"name":"source.note","type":"string","nullable":false,"desc":"#149's note, verbatim: counts exclude assemblies whose `version_status` is not `latest`, and **GenBank and RefSeq counts must not be summed** — a RefSeq record is derived from a GenBank submission, not an additional one."},{"name":"source.read_id","type":"string","nullable":false,"desc":"The read's identity: the sha256 of both tables' `Last-Modified`, the taxdump's md5 and the parser version. Two documents with the same `read_id` were counted together."},{"name":"source.genbank","type":"object","nullable":false,"desc":"GenBank's table as the read found it. Every count on a node but `refseq_assemblies` is GenBank's. See `source.genbank.*`."},{"name":"source.refseq","type":"object","nullable":false,"desc":"RefSeq's table as the read found it: `refseq_assemblies` on a node is its own roll-up. See `source.refseq.*`."},{"name":"source.taxonomy","type":"object","nullable":false,"desc":"The NCBI Taxonomy dump the counts were rolled up over, read by the same run: its backbone is this document's, and `species_in_taxonomy` is counted in its tree. See `source.taxonomy.*`."},{"name":"source.scraper","type":"string","nullable":false,"desc":"The program that read the tables, `ncbi_assemblies` — its row on `/v1/sources`."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with any figure republished from these documents: both tables by `Last-Modified`, the taxdump by md5, and NCBI's terms."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"NCBI's Website and Data Usage Policies: \"NCBI itself places no restrictions on the use or distribution of the data contained therein.\" These are counts over NCBI's own assembly metadata and taxonomy, not any submitter's record."},{"name":"source.genbank.url","type":"string","nullable":false,"desc":"The table's address on NCBI's FTP host, rewritten in place every day."},{"name":"source.genbank.last_modified","type":"string","nullable":false,"desc":"The `Last-Modified` header NCBI sent with the table, **verbatim** — the table's only identity, since the path is rewritten daily. Copy it into an `as_of`."},{"name":"source.genbank.content_length","type":"integer","nullable":false,"desc":"The table's text in bytes, as NCBI's `HEAD` announced it for the same `Last-Modified`: 1,763,129,784 for GenBank's on 2026-10-10. It arrives gzipped."},{"name":"source.genbank.rows","type":"integer","nullable":false,"desc":"Every row under the table's header."},{"name":"source.genbank.assemblies","type":"integer","nullable":false,"desc":"The rows whose `version_status` is `latest`: every count's base, and `totals.*_assemblies` on the coverage document."},{"name":"source.genbank.excluded","type":"integer","nullable":false,"desc":"Rows whose `version_status` is not `latest`, counted nowhere else. NCBI keeps replaced and suppressed assemblies in separate `_historical` tables, so this is 0 today, and still reported."},{"name":"source.genbank.malformed","type":"integer","nullable":false,"desc":"Rows that did not line up with the header or named a tax id that is not a number. A read with more than one in a thousand is refused."},{"name":"source.genbank.unresolved","type":"integer","nullable":false,"desc":"`latest` rows whose tax id the taxdump does not hold — merged, deleted, or newer than the dump. Counted in `assemblies` and on no node, so root's `assemblies` is `assemblies` less exactly these: 239 GenBank rows on 2026-10-10."},{"name":"source.refseq.url","type":"string","nullable":false,"desc":"The table's address on NCBI's FTP host, rewritten in place every day."},{"name":"source.refseq.last_modified","type":"string","nullable":false,"desc":"The `Last-Modified` header NCBI sent with the table, **verbatim** — the table's only identity, since the path is rewritten daily. Copy it into an `as_of`."},{"name":"source.refseq.content_length","type":"integer","nullable":false,"desc":"The table's text in bytes, as NCBI's `HEAD` announced it for the same `Last-Modified`: 1,763,129,784 for GenBank's on 2026-10-10. It arrives gzipped."},{"name":"source.refseq.rows","type":"integer","nullable":false,"desc":"Every row under the table's header."},{"name":"source.refseq.assemblies","type":"integer","nullable":false,"desc":"The rows whose `version_status` is `latest`: every count's base, and `totals.*_assemblies` on the coverage document."},{"name":"source.refseq.excluded","type":"integer","nullable":false,"desc":"Rows whose `version_status` is not `latest`, counted nowhere else. NCBI keeps replaced and suppressed assemblies in separate `_historical` tables, so this is 0 today, and still reported."},{"name":"source.refseq.malformed","type":"integer","nullable":false,"desc":"Rows that did not line up with the header or named a tax id that is not a number. A read with more than one in a thousand is refused."},{"name":"source.refseq.unresolved","type":"integer","nullable":false,"desc":"`latest` rows whose tax id the taxdump does not hold — merged, deleted, or newer than the dump. Counted in `assemblies` and on no node, so root's `assemblies` is `assemblies` less exactly these: 239 GenBank rows on 2026-10-10."},{"name":"source.taxonomy.url","type":"string","nullable":false,"desc":"`https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump.tar.gz`."},{"name":"source.taxonomy.checksum","type":"string","nullable":false,"desc":"The md5 NCBI publishes beside the archive, which the bytes read hashed to: the dump's only identity."},{"name":"source.taxonomy.checksum_algorithm","type":"string","nullable":false,"desc":"`md5`, NCBI's choice."},{"name":"source.taxonomy.last_modified","type":"string","nullable":true,"desc":"The `Last-Modified` header NCBI sent with the archive, verbatim."},{"name":"source.taxonomy.taxa_in_source","type":"integer","nullable":false,"desc":"Every node in the dump: 3,020,947 on 2026-10-10."},{"name":"source.taxonomy.species_in_source","type":"integer","nullable":false,"desc":"Every node ranked `species` in the dump — root's `species_in_taxonomy`."},{"name":"totals.genbank_assemblies","type":"integer","nullable":false,"desc":"GenBank's `latest` rows — the headline count, 3,820,648 on 2026-10-10. **Not** root's `assemblies`, which loses the rows on a tax id the taxdump does not hold (`source.genbank.unresolved`); #149 keeps both, unreconciled."},{"name":"totals.refseq_assemblies","type":"integer","nullable":false,"desc":"RefSeq's `latest` rows, likewise beside root's `refseq_assemblies`. Never add it to `genbank_assemblies`."},{"name":"totals.backbone_nodes","type":"integer","nullable":false,"desc":"How many nodes **this document** holds: the top cut, or every node."},{"name":"nodes[].tax_id","type":"integer","nullable":false,"desc":"NCBI's tax id."},{"name":"nodes[].parent_tax_id","type":"integer","nullable":true,"desc":"The nearest ancestor that is a node **of this document**. Null on root alone; every other value is a `tax_id` in the same `nodes`."},{"name":"nodes[].rank","type":"string","nullable":false,"desc":"NCBI's literal rank string, `root` on tax id 1."},{"name":"nodes[].scientific_name","type":"string","nullable":false,"desc":"NCBI's scientific name — `Mammalia`, `Insecta`, `Bacteria`: the key a consumer looks a node up by."},{"name":"nodes[].common_name","type":"string","nullable":true,"desc":"NCBI's GenBank common name where it has one, else a plain common name, else null."},{"name":"nodes[].species_in_taxonomy","type":"integer","nullable":false,"desc":"Taxa ranked `species` anywhere beneath the node in the taxdump's whole tree — not from the assembly tables."},{"name":"nodes[].assemblies","type":"integer","nullable":false,"desc":"GenBank's `latest` assemblies anywhere beneath the node, each counted once, on the node its tax id's nearest backbone ancestor is and every node above."},{"name":"nodes[].reference_assemblies","type":"integer","nullable":false,"desc":"Of those, the ones whose `refseq_category` is `reference genome`. Never more than `assemblies`."},{"name":"nodes[].representative_assemblies","type":"integer","nullable":false,"desc":"Of those, `representative genome`. NCBI retired the category: **0 on every node**, and kept because the consumer reads the key."},{"name":"nodes[].annotated_assemblies","type":"integer","nullable":false,"desc":"Of those, the ones whose `annotation_provider` is not `na`."},{"name":"nodes[].complete_assemblies","type":"integer","nullable":false,"desc":"Of those, the ones whose `assembly_level` is `Complete Genome` — the tables' spelling."},{"name":"nodes[].species_with_assembly","type":"integer","nullable":false,"desc":"The distinct species (`species_taxid`) those assemblies are of, each counted once however many assemblies it has. Never more than `species_in_taxonomy` where that is above zero: #149's invariant, checked before a read is served."},{"name":"nodes[].refseq_assemblies","type":"integer","nullable":false,"desc":"RefSeq's `latest` assemblies beneath the node — its own roll-up of its own table. **Never add it to `assemblies`**: a RefSeq assembly is a curated copy of a GenBank one."},{"name":"nodes[].latest_assembly_year","type":"integer","nullable":true,"desc":"The newest year a GenBank assembly beneath the node was released (`seq_rel_date`), never after the year of the read. Null where none was."}]},{"id":"get-assembly-genome-size","verb":"GET","path":"/v1/genome/assemblies/genome-size","summary":"Genome size, gene count and GC, one assembly per species","returns":"object","params":[{"name":"group","in":"query","type":"string","required":false,"desc":"One of NCBI's assembly groups, ignoring case — `bacteria`, `vertebrate_mammalian` — to list that group's species alone; `per_group` still counts every group. A group the read does not hold is a `404` naming the ones it does. Defaults to every group.","example":"plant"}],"fields":[{"name":"source","type":"object","nullable":false,"desc":"The read, as on every assembly document. See `source.*`."},{"name":"note","type":"string","nullable":false,"desc":"#149 section 3.3's note, verbatim: how a species and a group's sample are chosen."},{"name":"group","type":"string","nullable":true,"desc":"The `?group=` asked for, lower-cased, or null for every group."},{"name":"cap","type":"integer","nullable":false,"desc":"The most species one group ships, 2,500. A group at or under it ships whole; a group over it ships every reference genome and an evenly spaced sample of the rest across their genome sizes — the i-th of k at i·n/k, so the same tables give the same export."},{"name":"per_group","type":"object","nullable":false,"desc":"Every group of the read, keyed by NCBI's group name — whichever `?group=` was asked for — with `total_species`, the species eligible; `in_export`, the ones shipped; and `reference_species`, those with a reference genome. Render \"12,190 of 108,859 species shown\" from it."},{"name":"species","type":"array","nullable":false,"desc":"One species per row, genome size descending and then `tax_id`. See `species[].*`."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`ncbi_assemblies`, as the documents this replaces named themselves."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`NCBI Assembly summary`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`NCBI`."},{"name":"source.urls","type":"array","nullable":false,"desc":"The two tables the read was made from, RefSeq's first: `https://ftp.ncbi.nlm.nih.gov/genomes/ASSEMBLY_REPORTS/assembly_summary_refseq.txt` and `…/assembly_summary_genbank.txt`."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When the read began. The tables are read weekly; the same tables read again by the same parser over the same taxdump leave this standing, so the document, and its `ETag`, change only when a different read lands."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The counting rules. It moves when a change to them would count the same tables differently."},{"name":"source.note","type":"string","nullable":false,"desc":"#149's note, verbatim: counts exclude assemblies whose `version_status` is not `latest`, and **GenBank and RefSeq counts must not be summed** — a RefSeq record is derived from a GenBank submission, not an additional one."},{"name":"source.read_id","type":"string","nullable":false,"desc":"The read's identity: the sha256 of both tables' `Last-Modified`, the taxdump's md5 and the parser version. Two documents with the same `read_id` were counted together."},{"name":"source.genbank","type":"object","nullable":false,"desc":"GenBank's table as the read found it. Every count on a node but `refseq_assemblies` is GenBank's. See `source.genbank.*`."},{"name":"source.refseq","type":"object","nullable":false,"desc":"RefSeq's table as the read found it: `refseq_assemblies` on a node is its own roll-up. See `source.refseq.*`."},{"name":"source.taxonomy","type":"object","nullable":false,"desc":"The NCBI Taxonomy dump the counts were rolled up over, read by the same run: its backbone is this document's, and `species_in_taxonomy` is counted in its tree. See `source.taxonomy.*`."},{"name":"source.scraper","type":"string","nullable":false,"desc":"The program that read the tables, `ncbi_assemblies` — its row on `/v1/sources`."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with any figure republished from these documents: both tables by `Last-Modified`, the taxdump by md5, and NCBI's terms."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"NCBI's Website and Data Usage Policies: \"NCBI itself places no restrictions on the use or distribution of the data contained therein.\" These are counts over NCBI's own assembly metadata and taxonomy, not any submitter's record."},{"name":"source.genbank.url","type":"string","nullable":false,"desc":"The table's address on NCBI's FTP host, rewritten in place every day."},{"name":"source.genbank.last_modified","type":"string","nullable":false,"desc":"The `Last-Modified` header NCBI sent with the table, **verbatim** — the table's only identity, since the path is rewritten daily. Copy it into an `as_of`."},{"name":"source.genbank.content_length","type":"integer","nullable":false,"desc":"The table's text in bytes, as NCBI's `HEAD` announced it for the same `Last-Modified`: 1,763,129,784 for GenBank's on 2026-10-10. It arrives gzipped."},{"name":"source.genbank.rows","type":"integer","nullable":false,"desc":"Every row under the table's header."},{"name":"source.genbank.assemblies","type":"integer","nullable":false,"desc":"The rows whose `version_status` is `latest`: every count's base, and `totals.*_assemblies` on the coverage document."},{"name":"source.genbank.excluded","type":"integer","nullable":false,"desc":"Rows whose `version_status` is not `latest`, counted nowhere else. NCBI keeps replaced and suppressed assemblies in separate `_historical` tables, so this is 0 today, and still reported."},{"name":"source.genbank.malformed","type":"integer","nullable":false,"desc":"Rows that did not line up with the header or named a tax id that is not a number. A read with more than one in a thousand is refused."},{"name":"source.genbank.unresolved","type":"integer","nullable":false,"desc":"`latest` rows whose tax id the taxdump does not hold — merged, deleted, or newer than the dump. Counted in `assemblies` and on no node, so root's `assemblies` is `assemblies` less exactly these: 239 GenBank rows on 2026-10-10."},{"name":"source.refseq.url","type":"string","nullable":false,"desc":"The table's address on NCBI's FTP host, rewritten in place every day."},{"name":"source.refseq.last_modified","type":"string","nullable":false,"desc":"The `Last-Modified` header NCBI sent with the table, **verbatim** — the table's only identity, since the path is rewritten daily. Copy it into an `as_of`."},{"name":"source.refseq.content_length","type":"integer","nullable":false,"desc":"The table's text in bytes, as NCBI's `HEAD` announced it for the same `Last-Modified`: 1,763,129,784 for GenBank's on 2026-10-10. It arrives gzipped."},{"name":"source.refseq.rows","type":"integer","nullable":false,"desc":"Every row under the table's header."},{"name":"source.refseq.assemblies","type":"integer","nullable":false,"desc":"The rows whose `version_status` is `latest`: every count's base, and `totals.*_assemblies` on the coverage document."},{"name":"source.refseq.excluded","type":"integer","nullable":false,"desc":"Rows whose `version_status` is not `latest`, counted nowhere else. NCBI keeps replaced and suppressed assemblies in separate `_historical` tables, so this is 0 today, and still reported."},{"name":"source.refseq.malformed","type":"integer","nullable":false,"desc":"Rows that did not line up with the header or named a tax id that is not a number. A read with more than one in a thousand is refused."},{"name":"source.refseq.unresolved","type":"integer","nullable":false,"desc":"`latest` rows whose tax id the taxdump does not hold — merged, deleted, or newer than the dump. Counted in `assemblies` and on no node, so root's `assemblies` is `assemblies` less exactly these: 239 GenBank rows on 2026-10-10."},{"name":"source.taxonomy.url","type":"string","nullable":false,"desc":"`https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump.tar.gz`."},{"name":"source.taxonomy.checksum","type":"string","nullable":false,"desc":"The md5 NCBI publishes beside the archive, which the bytes read hashed to: the dump's only identity."},{"name":"source.taxonomy.checksum_algorithm","type":"string","nullable":false,"desc":"`md5`, NCBI's choice."},{"name":"source.taxonomy.last_modified","type":"string","nullable":true,"desc":"The `Last-Modified` header NCBI sent with the archive, verbatim."},{"name":"source.taxonomy.taxa_in_source","type":"integer","nullable":false,"desc":"Every node in the dump: 3,020,947 on 2026-10-10."},{"name":"source.taxonomy.species_in_source","type":"integer","nullable":false,"desc":"Every node ranked `species` in the dump — root's `species_in_taxonomy`."},{"name":"species[].tax_id","type":"integer","nullable":false,"desc":"The species' tax id (`species_taxid`): one row per species."},{"name":"species[].organism","type":"string","nullable":false,"desc":"The `organism_name` of the assembly chosen."},{"name":"species[].group","type":"string","nullable":false,"desc":"NCBI's `group`: `archaea`, `bacteria`, `fungi`, `invertebrate`, `metagenomes`, `other`, `plant`, `protozoa`, `vertebrate_mammalian`, `vertebrate_other`, `viral`."},{"name":"species[].genome_size","type":"integer","nullable":false,"desc":"Bases, from the `genome_size` column — never the ungapped one beside it. A species without both a size and a gene count is absent, not zero."},{"name":"species[].gene_count","type":"integer","nullable":false,"desc":"`total_gene_count`, not the protein-coding count."},{"name":"species[].gc_percent","type":"number","nullable":true,"desc":"GC content in percent, rounded to one decimal; null where NCBI states none."},{"name":"species[].assembly_level","type":"string","nullable":false,"desc":"`Complete Genome`, `Chromosome`, `Scaffold` or `Contig`, as the tables spell it."},{"name":"species[].refseq_category","type":"string","nullable":false,"desc":"Why this assembly was chosen for the species: `reference genome` over `representative genome` over `na`, the first row seen among equals."}]},{"id":"get-assembly-top-organisms","verb":"GET","path":"/v1/genome/assemblies/top-organisms","summary":"The 300 organisms with the most genome assemblies","returns":"object","params":[],"fields":[{"name":"source","type":"object","nullable":false,"desc":"The read, as on every assembly document. See `source.*`."},{"name":"organisms","type":"array","nullable":false,"desc":"The 300 organism names with the most GenBank assemblies, most first; a tie keeps the order the names first appear in the table. See `organisms[].*`."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`ncbi_assemblies`, as the documents this replaces named themselves."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`NCBI Assembly summary`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`NCBI`."},{"name":"source.urls","type":"array","nullable":false,"desc":"The two tables the read was made from, RefSeq's first: `https://ftp.ncbi.nlm.nih.gov/genomes/ASSEMBLY_REPORTS/assembly_summary_refseq.txt` and `…/assembly_summary_genbank.txt`."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When the read began. The tables are read weekly; the same tables read again by the same parser over the same taxdump leave this standing, so the document, and its `ETag`, change only when a different read lands."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The counting rules. It moves when a change to them would count the same tables differently."},{"name":"source.note","type":"string","nullable":false,"desc":"#149's note, verbatim: counts exclude assemblies whose `version_status` is not `latest`, and **GenBank and RefSeq counts must not be summed** — a RefSeq record is derived from a GenBank submission, not an additional one."},{"name":"source.read_id","type":"string","nullable":false,"desc":"The read's identity: the sha256 of both tables' `Last-Modified`, the taxdump's md5 and the parser version. Two documents with the same `read_id` were counted together."},{"name":"source.genbank","type":"object","nullable":false,"desc":"GenBank's table as the read found it. Every count on a node but `refseq_assemblies` is GenBank's. See `source.genbank.*`."},{"name":"source.refseq","type":"object","nullable":false,"desc":"RefSeq's table as the read found it: `refseq_assemblies` on a node is its own roll-up. See `source.refseq.*`."},{"name":"source.taxonomy","type":"object","nullable":false,"desc":"The NCBI Taxonomy dump the counts were rolled up over, read by the same run: its backbone is this document's, and `species_in_taxonomy` is counted in its tree. See `source.taxonomy.*`."},{"name":"source.scraper","type":"string","nullable":false,"desc":"The program that read the tables, `ncbi_assemblies` — its row on `/v1/sources`."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with any figure republished from these documents: both tables by `Last-Modified`, the taxdump by md5, and NCBI's terms."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"NCBI's Website and Data Usage Policies: \"NCBI itself places no restrictions on the use or distribution of the data contained therein.\" These are counts over NCBI's own assembly metadata and taxonomy, not any submitter's record."},{"name":"source.genbank.url","type":"string","nullable":false,"desc":"The table's address on NCBI's FTP host, rewritten in place every day."},{"name":"source.genbank.last_modified","type":"string","nullable":false,"desc":"The `Last-Modified` header NCBI sent with the table, **verbatim** — the table's only identity, since the path is rewritten daily. Copy it into an `as_of`."},{"name":"source.genbank.content_length","type":"integer","nullable":false,"desc":"The table's text in bytes, as NCBI's `HEAD` announced it for the same `Last-Modified`: 1,763,129,784 for GenBank's on 2026-10-10. It arrives gzipped."},{"name":"source.genbank.rows","type":"integer","nullable":false,"desc":"Every row under the table's header."},{"name":"source.genbank.assemblies","type":"integer","nullable":false,"desc":"The rows whose `version_status` is `latest`: every count's base, and `totals.*_assemblies` on the coverage document."},{"name":"source.genbank.excluded","type":"integer","nullable":false,"desc":"Rows whose `version_status` is not `latest`, counted nowhere else. NCBI keeps replaced and suppressed assemblies in separate `_historical` tables, so this is 0 today, and still reported."},{"name":"source.genbank.malformed","type":"integer","nullable":false,"desc":"Rows that did not line up with the header or named a tax id that is not a number. A read with more than one in a thousand is refused."},{"name":"source.genbank.unresolved","type":"integer","nullable":false,"desc":"`latest` rows whose tax id the taxdump does not hold — merged, deleted, or newer than the dump. Counted in `assemblies` and on no node, so root's `assemblies` is `assemblies` less exactly these: 239 GenBank rows on 2026-10-10."},{"name":"source.refseq.url","type":"string","nullable":false,"desc":"The table's address on NCBI's FTP host, rewritten in place every day."},{"name":"source.refseq.last_modified","type":"string","nullable":false,"desc":"The `Last-Modified` header NCBI sent with the table, **verbatim** — the table's only identity, since the path is rewritten daily. Copy it into an `as_of`."},{"name":"source.refseq.content_length","type":"integer","nullable":false,"desc":"The table's text in bytes, as NCBI's `HEAD` announced it for the same `Last-Modified`: 1,763,129,784 for GenBank's on 2026-10-10. It arrives gzipped."},{"name":"source.refseq.rows","type":"integer","nullable":false,"desc":"Every row under the table's header."},{"name":"source.refseq.assemblies","type":"integer","nullable":false,"desc":"The rows whose `version_status` is `latest`: every count's base, and `totals.*_assemblies` on the coverage document."},{"name":"source.refseq.excluded","type":"integer","nullable":false,"desc":"Rows whose `version_status` is not `latest`, counted nowhere else. NCBI keeps replaced and suppressed assemblies in separate `_historical` tables, so this is 0 today, and still reported."},{"name":"source.refseq.malformed","type":"integer","nullable":false,"desc":"Rows that did not line up with the header or named a tax id that is not a number. A read with more than one in a thousand is refused."},{"name":"source.refseq.unresolved","type":"integer","nullable":false,"desc":"`latest` rows whose tax id the taxdump does not hold — merged, deleted, or newer than the dump. Counted in `assemblies` and on no node, so root's `assemblies` is `assemblies` less exactly these: 239 GenBank rows on 2026-10-10."},{"name":"source.taxonomy.url","type":"string","nullable":false,"desc":"`https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump.tar.gz`."},{"name":"source.taxonomy.checksum","type":"string","nullable":false,"desc":"The md5 NCBI publishes beside the archive, which the bytes read hashed to: the dump's only identity."},{"name":"source.taxonomy.checksum_algorithm","type":"string","nullable":false,"desc":"`md5`, NCBI's choice."},{"name":"source.taxonomy.last_modified","type":"string","nullable":true,"desc":"The `Last-Modified` header NCBI sent with the archive, verbatim."},{"name":"source.taxonomy.taxa_in_source","type":"integer","nullable":false,"desc":"Every node in the dump: 3,020,947 on 2026-10-10."},{"name":"source.taxonomy.species_in_source","type":"integer","nullable":false,"desc":"Every node ranked `species` in the dump — root's `species_in_taxonomy`."},{"name":"organisms[].organism","type":"string","nullable":false,"desc":"The `organism_name`, as NCBI writes it: the key, not the tax id, as in the file this replaces."},{"name":"organisms[].tax_id","type":"integer","nullable":false,"desc":"The tax id of the first row with that name."},{"name":"organisms[].assemblies","type":"integer","nullable":false,"desc":"GenBank's `latest` assemblies with that name."},{"name":"organisms[].reference","type":"integer","nullable":false,"desc":"How many of them are reference genomes."},{"name":"organisms[].group","type":"string","nullable":false,"desc":"NCBI's `group` of the first row with that name."}]},{"id":"get-assembly-timeline","verb":"GET","path":"/v1/genome/assemblies/timeline","summary":"Genome assemblies released per year","returns":"object","params":[],"fields":[{"name":"source","type":"object","nullable":false,"desc":"The read, as on every assembly document. See `source.*`."},{"name":"timeline","type":"array","nullable":false,"desc":"GenBank's assemblies per release year, 1990 to the year of the read, ascending; a year with none is absent. See `timeline[].*`."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`ncbi_assemblies`, as the documents this replaces named themselves."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`NCBI Assembly summary`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`NCBI`."},{"name":"source.urls","type":"array","nullable":false,"desc":"The two tables the read was made from, RefSeq's first: `https://ftp.ncbi.nlm.nih.gov/genomes/ASSEMBLY_REPORTS/assembly_summary_refseq.txt` and `…/assembly_summary_genbank.txt`."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When the read began. The tables are read weekly; the same tables read again by the same parser over the same taxdump leave this standing, so the document, and its `ETag`, change only when a different read lands."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The counting rules. It moves when a change to them would count the same tables differently."},{"name":"source.note","type":"string","nullable":false,"desc":"#149's note, verbatim: counts exclude assemblies whose `version_status` is not `latest`, and **GenBank and RefSeq counts must not be summed** — a RefSeq record is derived from a GenBank submission, not an additional one."},{"name":"source.read_id","type":"string","nullable":false,"desc":"The read's identity: the sha256 of both tables' `Last-Modified`, the taxdump's md5 and the parser version. Two documents with the same `read_id` were counted together."},{"name":"source.genbank","type":"object","nullable":false,"desc":"GenBank's table as the read found it. Every count on a node but `refseq_assemblies` is GenBank's. See `source.genbank.*`."},{"name":"source.refseq","type":"object","nullable":false,"desc":"RefSeq's table as the read found it: `refseq_assemblies` on a node is its own roll-up. See `source.refseq.*`."},{"name":"source.taxonomy","type":"object","nullable":false,"desc":"The NCBI Taxonomy dump the counts were rolled up over, read by the same run: its backbone is this document's, and `species_in_taxonomy` is counted in its tree. See `source.taxonomy.*`."},{"name":"source.scraper","type":"string","nullable":false,"desc":"The program that read the tables, `ncbi_assemblies` — its row on `/v1/sources`."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with any figure republished from these documents: both tables by `Last-Modified`, the taxdump by md5, and NCBI's terms."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"NCBI's Website and Data Usage Policies: \"NCBI itself places no restrictions on the use or distribution of the data contained therein.\" These are counts over NCBI's own assembly metadata and taxonomy, not any submitter's record."},{"name":"source.genbank.url","type":"string","nullable":false,"desc":"The table's address on NCBI's FTP host, rewritten in place every day."},{"name":"source.genbank.last_modified","type":"string","nullable":false,"desc":"The `Last-Modified` header NCBI sent with the table, **verbatim** — the table's only identity, since the path is rewritten daily. Copy it into an `as_of`."},{"name":"source.genbank.content_length","type":"integer","nullable":false,"desc":"The table's text in bytes, as NCBI's `HEAD` announced it for the same `Last-Modified`: 1,763,129,784 for GenBank's on 2026-10-10. It arrives gzipped."},{"name":"source.genbank.rows","type":"integer","nullable":false,"desc":"Every row under the table's header."},{"name":"source.genbank.assemblies","type":"integer","nullable":false,"desc":"The rows whose `version_status` is `latest`: every count's base, and `totals.*_assemblies` on the coverage document."},{"name":"source.genbank.excluded","type":"integer","nullable":false,"desc":"Rows whose `version_status` is not `latest`, counted nowhere else. NCBI keeps replaced and suppressed assemblies in separate `_historical` tables, so this is 0 today, and still reported."},{"name":"source.genbank.malformed","type":"integer","nullable":false,"desc":"Rows that did not line up with the header or named a tax id that is not a number. A read with more than one in a thousand is refused."},{"name":"source.genbank.unresolved","type":"integer","nullable":false,"desc":"`latest` rows whose tax id the taxdump does not hold — merged, deleted, or newer than the dump. Counted in `assemblies` and on no node, so root's `assemblies` is `assemblies` less exactly these: 239 GenBank rows on 2026-10-10."},{"name":"source.refseq.url","type":"string","nullable":false,"desc":"The table's address on NCBI's FTP host, rewritten in place every day."},{"name":"source.refseq.last_modified","type":"string","nullable":false,"desc":"The `Last-Modified` header NCBI sent with the table, **verbatim** — the table's only identity, since the path is rewritten daily. Copy it into an `as_of`."},{"name":"source.refseq.content_length","type":"integer","nullable":false,"desc":"The table's text in bytes, as NCBI's `HEAD` announced it for the same `Last-Modified`: 1,763,129,784 for GenBank's on 2026-10-10. It arrives gzipped."},{"name":"source.refseq.rows","type":"integer","nullable":false,"desc":"Every row under the table's header."},{"name":"source.refseq.assemblies","type":"integer","nullable":false,"desc":"The rows whose `version_status` is `latest`: every count's base, and `totals.*_assemblies` on the coverage document."},{"name":"source.refseq.excluded","type":"integer","nullable":false,"desc":"Rows whose `version_status` is not `latest`, counted nowhere else. NCBI keeps replaced and suppressed assemblies in separate `_historical` tables, so this is 0 today, and still reported."},{"name":"source.refseq.malformed","type":"integer","nullable":false,"desc":"Rows that did not line up with the header or named a tax id that is not a number. A read with more than one in a thousand is refused."},{"name":"source.refseq.unresolved","type":"integer","nullable":false,"desc":"`latest` rows whose tax id the taxdump does not hold — merged, deleted, or newer than the dump. Counted in `assemblies` and on no node, so root's `assemblies` is `assemblies` less exactly these: 239 GenBank rows on 2026-10-10."},{"name":"source.taxonomy.url","type":"string","nullable":false,"desc":"`https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump.tar.gz`."},{"name":"source.taxonomy.checksum","type":"string","nullable":false,"desc":"The md5 NCBI publishes beside the archive, which the bytes read hashed to: the dump's only identity."},{"name":"source.taxonomy.checksum_algorithm","type":"string","nullable":false,"desc":"`md5`, NCBI's choice."},{"name":"source.taxonomy.last_modified","type":"string","nullable":true,"desc":"The `Last-Modified` header NCBI sent with the archive, verbatim."},{"name":"source.taxonomy.taxa_in_source","type":"integer","nullable":false,"desc":"Every node in the dump: 3,020,947 on 2026-10-10."},{"name":"source.taxonomy.species_in_source","type":"integer","nullable":false,"desc":"Every node ranked `species` in the dump — root's `species_in_taxonomy`."},{"name":"timeline[].year","type":"integer","nullable":false,"desc":"The year of `seq_rel_date`."},{"name":"timeline[].assemblies","type":"integer","nullable":false,"desc":"GenBank's `latest` assemblies released that year — per year, not cumulative."}]},{"id":"list-gene-constraint","verb":"GET","path":"/v1/genome/constraint","summary":"Gene constraint, one gnomAD release at a time","returns":"array","params":[{"name":"release","in":"query","type":"string","required":false,"desc":"The gnomAD release to serve, as the bucket names it — `4.1`. Omit it for the newest served (`default_release` on `/v1/genome/constraint/releases`). A release not served is a `404` naming the ones that are. A walk never mixes releases: the cursor carries the one page 1 was served from.","example":"4.1"},{"name":"symbol","in":"query","type":"string","required":false,"desc":"Gene symbols, comma-separated or the parameter repeated — `A1BG,XPO6`. Matched without regard to case; each row carries gnomAD's own spelling. At most 100. A symbol with no MANE Select or canonical transcript in the release is simply absent from the page.","example":"A1BG,XPO6"},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `loeuf` (default), `pli`, `oe_lof` or `gene`, each optionally prefixed with `-` to reverse. A gene with no value for the field sorts LAST in either direction, and ties break on `gene` in the same direction. #151 proposed `sort=loeuf\u0026order=asc`; here that is `order=loeuf`, and `?sort=` is a `400`.","example":"loeuf"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25. A whole release is about 19,600 genes — **about 200 requests at `limit=100`, each one metered** — so budget for that, or ask for the genes you need with `symbol`.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. It carries the release and the read the walk is over, so a release landing mid-walk does not change the rows you get; if the same release is read again mid-walk, the next page is a `400` asking for a new walk, never a page of the new read."}],"fields":[{"name":"gene","type":"string","nullable":false,"desc":"The gene symbol, as gnomAD writes it: `A1BG`, `C10orf105`. The key: one row per gene per release."},{"name":"transcript","type":"string","nullable":false,"desc":"The transcript the metrics are read from: the gene's MANE Select transcript where it has one, else its canonical one, and within either the row gnomAD writes last — the Ensembl `ENST…` id where there is one, otherwise a RefSeq `NM_`/`XM_` id (100 genes of 4.1.2)."},{"name":"mane_select","type":"boolean","nullable":false,"desc":"Whether `transcript` is the gene's MANE Select transcript. `false` means it is the canonical transcript of a gene with no MANE Select row in this release."},{"name":"loeuf","type":"number","nullable":true,"desc":"LOEUF, gnomAD's `lof.oe_ci.upper`: the upper bound of the 90% interval on the ratio of observed to expected loss-of-function variants. **Lower means the gene tolerates less predicted loss-of-function variation than expected in gnomAD's reference population. It is not a measure of how important the gene is.** Four significant digits, as JavaScript's `toPrecision(4)` gives them. A few genes with under one expected loss-of-function variant have a LOEUF above 10 — 29 in 4.1.2, CLDN17 at 42.21 — and are served as gnomAD states them: an interval over almost nothing is wide. Null, with `oe_lof`, `pli`, `lof_observed` and `lof_expected`, where gnomAD states no loss-of-function metrics for the transcript (1,570 genes of 4.1.2)."},{"name":"oe_lof","type":"number","nullable":true,"desc":"`lof.oe`: observed over expected loss-of-function variants. Four significant digits."},{"name":"pli","type":"number","nullable":true,"desc":"`lof.pLI`: the probability of loss-of-function intolerance, 0 to 1. Four significant digits with the exponent kept: `1.771e-16`."},{"name":"oe_mis","type":"number","nullable":true,"desc":"`mis.oe`: observed over expected missense variants. Four significant digits. Null on every gene of a release whose table lacks the column, which `meta.columns_absent` then names."},{"name":"oe_syn","type":"number","nullable":true,"desc":"`syn.oe`: observed over expected synonymous variants. Optional like `oe_mis`."},{"name":"lof_observed","type":"integer","nullable":true,"desc":"`lof.obs`: loss-of-function variants observed. A count, never rounded."},{"name":"lof_expected","type":"number","nullable":true,"desc":"`lof.exp`: loss-of-function variants expected. Four significant digits."},{"name":"constraint_decile","type":"integer","nullable":true,"desc":"`lof.oe_ci.upper_bin_decile`: the gene's LOEUF decile, 0 (most constrained) to 9. Never rounded. Optional like `oe_mis`."},{"name":"release","type":"string","nullable":false,"desc":"The gnomAD release these numbers come from, `4.1.2` — the same as `meta.release`."}]},{"id":"get-gene-constraint-releases","verb":"GET","path":"/v1/genome/constraint/releases","summary":"Which gnomAD releases are listed, and which are served","returns":"object","params":[],"fields":[{"name":"default_release","type":"string","nullable":true,"desc":"What `GET /v1/genome/constraint` serves without `?release=`: the newest release, as a version, that is served. Null until one is."},{"name":"releases","type":"array","nullable":false,"desc":"Every release gnomAD's public bucket lists a constraint table for, oldest first — discovered from the bucket's own listing every morning, never named in code. See `releases[].*`."},{"name":"licence","type":"string","nullable":false,"desc":"`CC0-1.0`: gnomAD releases its data under the Creative Commons Zero dedication."},{"name":"terms_url","type":"string","nullable":false,"desc":"gnomAD's terms of use: CC0, attribution requested, and a request that integrators keep \"gnomAD\" out of the names of their tools."},{"name":"releases[].release","type":"string","nullable":false,"desc":"The version the bucket's prefix spells: `4.0`, `4.1`, `4.1.1`, `4.1.2`."},{"name":"releases[].served","type":"boolean","nullable":false,"desc":"Whether `GET /v1/genome/constraint?release=` answers for it: a whole read of its table has landed. A listed release is not served when its header lacks a column the gene rule needs — 4.0's has no `canonical` — or before its first read lands."},{"name":"releases[].columns_missing","type":"array of string","nullable":true,"desc":"The required columns the release's header lacks, from the newest check of the object listed now: `[\"canonical\"]` for 4.0, `[]` when it has them all, null when no run has checked this object yet."},{"name":"releases[].listed","type":"object","nullable":false,"desc":"The object the bucket lists for the release today: `url`, `size`, and Cloud Storage's `generation` and `md5Hash`. See `releases[].listed.*`."},{"name":"releases[].listed_updated","type":"timestamp","nullable":true,"desc":"When gnomAD last wrote that object, as the bucket says."},{"name":"releases[].checked_at","type":"timestamp","nullable":false,"desc":"When a run last found that object in the bucket's listing."},{"name":"releases[].served_from","type":"object","nullable":true,"desc":"The object the served read was made from, verified byte for byte against its size and MD5 as it streamed — the same as `listed` unless gnomAD replaced the object and the re-read has not landed. Null when not served. The same shape as the `served_from` of the gnomad row of `/v1/genome/releases`."},{"name":"releases[].read","type":"object","nullable":true,"desc":"The served read: `fetched_at`, `parser_version`, `format`, `columns`, `columns_absent`, `transcripts`, `genes`, `mane_genes` and `loeuf_above_10`. Null when not served. See `releases[].read.*`."},{"name":"releases[].attribution","type":"string","nullable":false,"desc":"The line gnomAD asks to travel with its data, for this release: \"Constraint metrics from gnomAD v4.1.2 (Broad Institute), released under CC0.\""},{"name":"releases[].listed.url","type":"string","nullable":false,"desc":"The object's address in the public bucket."},{"name":"releases[].listed.size","type":"integer","nullable":false,"desc":"Its size in bytes: 574,538,527 for 4.1.2's table."},{"name":"releases[].listed.generation","type":"string","nullable":false,"desc":"Cloud Storage's generation for the object, a **string** as the bucket sends it. It changes when gnomAD replaces the object and at no other time; a changed one is read again."},{"name":"releases[].listed.md5Hash","type":"string","nullable":false,"desc":"The object's MD5, base64, as Cloud Storage spells the field."},{"name":"releases[].served_from.url","type":"string","nullable":false,"desc":"The object's address in the public bucket."},{"name":"releases[].served_from.size","type":"integer","nullable":false,"desc":"Its size in bytes: 574,538,527 for 4.1.2's table."},{"name":"releases[].served_from.generation","type":"string","nullable":false,"desc":"Cloud Storage's generation for the object, a **string** as the bucket sends it. It changes when gnomAD replaces the object and at no other time; a changed one is read again."},{"name":"releases[].served_from.md5Hash","type":"string","nullable":false,"desc":"The object's MD5, base64, as Cloud Storage spells the field."},{"name":"releases[].read.fetched_at","type":"timestamp","nullable":false,"desc":"When the scraper read the file. Every gene served for the release came from this read."},{"name":"releases[].read.parser_version","type":"integer","nullable":false,"desc":"The reduction rules that read it. A change to them that would read the same file into different rows moves it, and every release is read again."},{"name":"releases[].read.format","type":"string","nullable":false,"desc":"`tsv` (4.1) or `tsv.bgz` (4.1.1 on)."},{"name":"releases[].read.columns","type":"integer","nullable":false,"desc":"How many columns the header had: 55 for 4.1, 113 for 4.1.2, in a different order — every column is found by its name."},{"name":"releases[].read.columns_absent","type":"array of string","nullable":false,"desc":"The optional columns the header lacked, null on every gene of the release. `[]` for every release served today."},{"name":"releases[].read.transcripts","type":"integer","nullable":false,"desc":"The rows the table held, one per transcript: 221,898 for 4.1.2."},{"name":"releases[].read.genes","type":"integer","nullable":false,"desc":"The genes they reduced to: 19,608 for 4.1.2, 18,182 for 4.1."},{"name":"releases[].read.mane_genes","type":"integer","nullable":false,"desc":"How many of those genes are served from a MANE Select transcript."},{"name":"releases[].read.loeuf_above_10","type":"integer","nullable":false,"desc":"How many genes have a LOEUF above 10: 29 in 4.1.2, 0 in 4.1."}]},{"id":"get-gene-constraint","verb":"GET","path":"/v1/genome/constraint/{symbol}","summary":"One gene's constraint in one gnomAD release","returns":"object","params":[{"name":"symbol","in":"path","type":"string","required":true,"desc":"The gene symbol, matched without regard to case: `A1BG`. A `404` when the release has no MANE Select or canonical transcript for it, naming the release it looked in.","example":"A1BG"},{"name":"release","in":"query","type":"string","required":false,"desc":"The gnomAD release to read, as on the collection. Omit it for the newest served.","example":"4.1"}],"fields":[{"name":"gene","type":"string","nullable":false,"desc":"The gene symbol, as gnomAD writes it: `A1BG`, `C10orf105`. The key: one row per gene per release."},{"name":"transcript","type":"string","nullable":false,"desc":"The transcript the metrics are read from: the gene's MANE Select transcript where it has one, else its canonical one, and within either the row gnomAD writes last — the Ensembl `ENST…` id where there is one, otherwise a RefSeq `NM_`/`XM_` id (100 genes of 4.1.2)."},{"name":"mane_select","type":"boolean","nullable":false,"desc":"Whether `transcript` is the gene's MANE Select transcript. `false` means it is the canonical transcript of a gene with no MANE Select row in this release."},{"name":"loeuf","type":"number","nullable":true,"desc":"LOEUF, gnomAD's `lof.oe_ci.upper`: the upper bound of the 90% interval on the ratio of observed to expected loss-of-function variants. **Lower means the gene tolerates less predicted loss-of-function variation than expected in gnomAD's reference population. It is not a measure of how important the gene is.** Four significant digits, as JavaScript's `toPrecision(4)` gives them. A few genes with under one expected loss-of-function variant have a LOEUF above 10 — 29 in 4.1.2, CLDN17 at 42.21 — and are served as gnomAD states them: an interval over almost nothing is wide. Null, with `oe_lof`, `pli`, `lof_observed` and `lof_expected`, where gnomAD states no loss-of-function metrics for the transcript (1,570 genes of 4.1.2)."},{"name":"oe_lof","type":"number","nullable":true,"desc":"`lof.oe`: observed over expected loss-of-function variants. Four significant digits."},{"name":"pli","type":"number","nullable":true,"desc":"`lof.pLI`: the probability of loss-of-function intolerance, 0 to 1. Four significant digits with the exponent kept: `1.771e-16`."},{"name":"oe_mis","type":"number","nullable":true,"desc":"`mis.oe`: observed over expected missense variants. Four significant digits. Null on every gene of a release whose table lacks the column, which `meta.columns_absent` then names."},{"name":"oe_syn","type":"number","nullable":true,"desc":"`syn.oe`: observed over expected synonymous variants. Optional like `oe_mis`."},{"name":"lof_observed","type":"integer","nullable":true,"desc":"`lof.obs`: loss-of-function variants observed. A count, never rounded."},{"name":"lof_expected","type":"number","nullable":true,"desc":"`lof.exp`: loss-of-function variants expected. Four significant digits."},{"name":"constraint_decile","type":"integer","nullable":true,"desc":"`lof.oe_ci.upper_bin_decile`: the gene's LOEUF decile, 0 (most constrained) to 9. Never rounded. Optional like `oe_mis`."},{"name":"release","type":"string","nullable":false,"desc":"The gnomAD release these numbers come from, `4.1.2` — the same as `meta.release`."}]},{"id":"get-gwas-overview","verb":"GET","path":"/v1/genome/gwas/overview","summary":"The GWAS Catalog's totals, years and p-value bands","returns":"object","params":[],"fields":[{"name":"source","type":"object","nullable":false,"desc":"The parse these figures were read from and the attribution to carry with them — the same block on every GWAS document, and in `meta.source` on every GWAS answer. See `source.*`."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`gwas_catalog`, the catalog's key in the genome registry: `/v1/genome/releases/gwas_catalog` says which release EBI's stats endpoint names today, checked every morning."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`GWAS Catalog`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`EMBL-EBI`."},{"name":"source.url","type":"string","nullable":false,"desc":"The zip the figures were read from: the release's **dated** directory, `https://ftp.ebi.ac.uk/pub/databases/gwas/releases/2026/10/05/gwas-catalog-associations_ontology-annotated-full.zip`, which EBI never rewrites — `releases/latest/` only when no dated directory held the release yet."},{"name":"source.release_version","type":"string","nullable":false,"desc":"The release, as EBI's stats endpoint dates it: `2026-10-03`. Part of the citation."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When the zip was **first** read by this parser, ISO 8601 to the millisecond. A later read of the same bytes by the same parser leaves it standing, so the documents and their `ETag` change only when the release does — send the `ETag` back as `If-None-Match` and the answer is a `304`, which is not billed."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The counting rules that produced these figures. It moves when a change to them would read the same zip differently, and the release is then read again."},{"name":"source.note","type":"string","nullable":false,"desc":"The sentence every document carries, verbatim: a GWAS association is a statistical relationship in a study population and does not establish that the variant causes the trait. **If you republish these figures, carry it.**"},{"name":"source.release_published","type":"date","nullable":true,"desc":"The dated directory the zip was read from, `2026-10-05`: the day EBI published the release, which is not the release's own date. `null` when it was read from `releases/latest/`."},{"name":"source.parse_id","type":"string","nullable":false,"desc":"This parse: the first sixteen hex digits of `checksum_sha256` and the parser, `c3b18bcde213728a-p1`. The same bytes read by the same parser are the same parse."},{"name":"source.checksum_sha256","type":"string","nullable":false,"desc":"The sha256 of the zip as served: `sha256sum` over the same file gives the same string."},{"name":"source.content_length","type":"integer","nullable":false,"desc":"The zip's size in bytes: 72,826,184 for release 2026-10-03."},{"name":"source.etag","type":"string","nullable":true,"desc":"The `ETag` header EBI sent with the zip, verbatim."},{"name":"source.last_modified","type":"string","nullable":true,"desc":"The `Last-Modified` header EBI sent with the zip, **verbatim** — an RFC 7231 date, never normalised."},{"name":"source.member","type":"object","nullable":false,"desc":"The zip's one member as its local header declares it: the file that was inflated, its CRC-32 and both sizes, each checked as it was read. See `member.*`."},{"name":"source.associations_expected","type":"integer","nullable":false,"desc":"The association rows EBI's stats endpoint said the release holds."},{"name":"source.associations_parsed","type":"integer","nullable":false,"desc":"The rows read from the file. **Equal to `associations_expected` on every release served**: a file that counts any other number is refused before anything is published, and the previous release stays served."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with any figure republished from these reads: `NHGRI-EBI GWAS Catalog, release 2026-10-03, provided under the EMBL-EBI Terms of Use; please cite the GWAS Catalog (Cerezo et al., Nucleic Acids Research 53(D1):D998–D1005, 2025)`. EMBL-EBI expects attribution in publications, services and products."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"The EMBL-EBI Terms of Use, under which \"the NHGRI-EBI GWAS Catalog and all its contents are available\": EMBL-EBI \"places no additional restrictions on the use or redistribution of the data ... other than those provided by the original data owners\"."},{"name":"source.citation","type":"string","nullable":false,"desc":"The paper the catalog asks to be cited, as its citation page writes it."},{"name":"source.citation_url","type":"string","nullable":false,"desc":"The catalog's citation page."},{"name":"source.member.name","type":"string","nullable":false,"desc":"`gwas-catalog-download-associations-alt-full.tsv`."},{"name":"source.member.crc32","type":"string","nullable":false,"desc":"The member's CRC-32 in hex, `92792d40`, checked against the bytes inflated."},{"name":"source.member.compressed_bytes","type":"integer","nullable":false,"desc":"The member's deflated size."},{"name":"source.member.bytes","type":"integer","nullable":false,"desc":"Its inflated size: 745,002,769 for release 2026-10-03."},{"name":"totals","type":"object","nullable":false,"desc":"#152's seven totals. See `totals.*`."},{"name":"associations_by_year","type":"array","nullable":false,"desc":"Association rows per year of the study's `DATE`, **ascending**, from 2005 to the release's own year. A row dated outside that range is in no year."},{"name":"studies_by_year","type":"object","nullable":false,"desc":"Study accessions per year, as an **object keyed by the year as a string** — `{\"2005\": 2, \"2006\": 5, …}` — the shape the file this replaces carried. Each accession counts once, in the earliest year any of its rows is dated (one study's rows share one date, so on the 2026-10-03 release that is simply its year)."},{"name":"p_value_bands","type":"array","nullable":false,"desc":"Association rows per p-value band, **always all five, strongest first**: `≥1e-100`, `1e-100 – 1e-50`, `1e-50 – 1e-20`, `1e-20 – 5e-8` and `weaker than 5e-8`, the labels byte for byte — U+2265 and the U+2013 en dash included — because they are the contract a chart keys on. **The edges are on `PVALUE_MLOG` at 100, 50, 20 and 8**, the pipeline these figures replace: the fourth band ends at p = 1e-8, not at the 5e-8 its label names, so an association with p between 5e-8 and 1e-8 — genome-wide significant — is counted `weaker than 5e-8` (89,036 rows of release 2026-10-03). The labels and the edges are kept together so a chart built on the old file shows the same bars."},{"name":"p_value_missing","type":"integer","nullable":false,"desc":"Rows with no `PVALUE_MLOG`, in no band: one in release 2026-10-03. The bands and this add up to `totals.associations`."},{"name":"trait_pairing","type":"object","nullable":false,"desc":"How each row's `MAPPED_TRAIT` labels were paired with its `MAPPED_TRAIT_URI` terms, counted in rows: the provenance of every trait name. See `trait_pairing.*`."},{"name":"totals.associations","type":"integer","nullable":false,"desc":"Association rows in the release, header excluded — `associations_parsed`, which is the stats endpoint's count."},{"name":"totals.associations_with_mapped_trait","type":"integer","nullable":false,"desc":"Rows with a `MAPPED_TRAIT`."},{"name":"totals.studies","type":"integer","nullable":false,"desc":"Distinct `STUDY ACCESSION`s. **Not** the stats endpoint's `studies`, which counts something else and is smaller; the two are not meant to agree."},{"name":"totals.publications","type":"integer","nullable":false,"desc":"Distinct `PUBMEDID`s."},{"name":"totals.distinct_variants","type":"integer","nullable":false,"desc":"Distinct `SNPS` values that start `rs`. A haplotype's `rs1; rs2` and an interaction's `rs1 x rs2` are one value each. **Not** the stats endpoint's `snps`."},{"name":"totals.distinct_mapped_traits","type":"integer","nullable":false,"desc":"Distinct trait labels after each row's labels were paired with its terms: the traits `/v1/genome/gwas/traits` serves, every one."},{"name":"totals.mapped_genes","type":"integer","nullable":false,"desc":"Distinct `MAPPED_GENE` symbols as written: the genes `/v1/genome/gwas/genes` serves."},{"name":"associations_by_year[].year","type":"integer","nullable":false,"desc":"A year, 2005 or later."},{"name":"associations_by_year[].associations","type":"integer","nullable":false,"desc":"Association rows dated in it."},{"name":"p_value_bands[].band","type":"string","nullable":false,"desc":"One of the five labels, byte for byte."},{"name":"p_value_bands[].count","type":"integer","nullable":false,"desc":"Association rows in the band; `0` for a band with none."},{"name":"trait_pairing.single_term","type":"integer","nullable":false,"desc":"Rows naming one term: the whole label is that term's."},{"name":"trait_pairing.paired_by_index","type":"integer","nullable":false,"desc":"Rows naming n terms whose label splits on commas into exactly n parts: label i is term i's."},{"name":"trait_pairing.resolved_from_terms","type":"integer","nullable":false,"desc":"Rows whose label holds a comma of its own beside another term, so it does not split into n parts, and every term on it is named elsewhere: each term takes the label it is paired with there."},{"name":"trait_pairing.split_by_known_labels","type":"integer","nullable":false,"desc":"Such rows with a term named nowhere else, where exactly one way of cutting the label agrees with the labels known."},{"name":"trait_pairing.kept_unsplit","type":"integer","nullable":false,"desc":"Such rows that cut more than one way: the label is kept whole, as one trait with no term, rather than guessed."},{"name":"trait_pairing.without_term","type":"integer","nullable":false,"desc":"Rows with a `MAPPED_TRAIT` and no `MAPPED_TRAIT_URI`."},{"name":"trait_pairing.without_mapped_trait","type":"integer","nullable":false,"desc":"Rows with no `MAPPED_TRAIT` at all, credited to no trait."},{"name":"trait_pairing.traits_without_term","type":"integer","nullable":false,"desc":"Traits served with `efo_id` `null`."}]},{"id":"get-gwas-chromosomes","verb":"GET","path":"/v1/genome/gwas/chromosomes","summary":"The GWAS Catalog's associations and variants per chromosome","returns":"object","params":[],"fields":[{"name":"source","type":"object","nullable":false,"desc":"The parse these figures were read from and the attribution to carry with them — the same block on every GWAS document, and in `meta.source` on every GWAS answer. See `source.*`."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`gwas_catalog`, the catalog's key in the genome registry: `/v1/genome/releases/gwas_catalog` says which release EBI's stats endpoint names today, checked every morning."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`GWAS Catalog`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`EMBL-EBI`."},{"name":"source.url","type":"string","nullable":false,"desc":"The zip the figures were read from: the release's **dated** directory, `https://ftp.ebi.ac.uk/pub/databases/gwas/releases/2026/10/05/gwas-catalog-associations_ontology-annotated-full.zip`, which EBI never rewrites — `releases/latest/` only when no dated directory held the release yet."},{"name":"source.release_version","type":"string","nullable":false,"desc":"The release, as EBI's stats endpoint dates it: `2026-10-03`. Part of the citation."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When the zip was **first** read by this parser, ISO 8601 to the millisecond. A later read of the same bytes by the same parser leaves it standing, so the documents and their `ETag` change only when the release does — send the `ETag` back as `If-None-Match` and the answer is a `304`, which is not billed."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The counting rules that produced these figures. It moves when a change to them would read the same zip differently, and the release is then read again."},{"name":"source.note","type":"string","nullable":false,"desc":"The sentence every document carries, verbatim: a GWAS association is a statistical relationship in a study population and does not establish that the variant causes the trait. **If you republish these figures, carry it.**"},{"name":"source.release_published","type":"date","nullable":true,"desc":"The dated directory the zip was read from, `2026-10-05`: the day EBI published the release, which is not the release's own date. `null` when it was read from `releases/latest/`."},{"name":"source.parse_id","type":"string","nullable":false,"desc":"This parse: the first sixteen hex digits of `checksum_sha256` and the parser, `c3b18bcde213728a-p1`. The same bytes read by the same parser are the same parse."},{"name":"source.checksum_sha256","type":"string","nullable":false,"desc":"The sha256 of the zip as served: `sha256sum` over the same file gives the same string."},{"name":"source.content_length","type":"integer","nullable":false,"desc":"The zip's size in bytes: 72,826,184 for release 2026-10-03."},{"name":"source.etag","type":"string","nullable":true,"desc":"The `ETag` header EBI sent with the zip, verbatim."},{"name":"source.last_modified","type":"string","nullable":true,"desc":"The `Last-Modified` header EBI sent with the zip, **verbatim** — an RFC 7231 date, never normalised."},{"name":"source.member","type":"object","nullable":false,"desc":"The zip's one member as its local header declares it: the file that was inflated, its CRC-32 and both sizes, each checked as it was read. See `member.*`."},{"name":"source.associations_expected","type":"integer","nullable":false,"desc":"The association rows EBI's stats endpoint said the release holds."},{"name":"source.associations_parsed","type":"integer","nullable":false,"desc":"The rows read from the file. **Equal to `associations_expected` on every release served**: a file that counts any other number is refused before anything is published, and the previous release stays served."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with any figure republished from these reads: `NHGRI-EBI GWAS Catalog, release 2026-10-03, provided under the EMBL-EBI Terms of Use; please cite the GWAS Catalog (Cerezo et al., Nucleic Acids Research 53(D1):D998–D1005, 2025)`. EMBL-EBI expects attribution in publications, services and products."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"The EMBL-EBI Terms of Use, under which \"the NHGRI-EBI GWAS Catalog and all its contents are available\": EMBL-EBI \"places no additional restrictions on the use or redistribution of the data ... other than those provided by the original data owners\"."},{"name":"source.citation","type":"string","nullable":false,"desc":"The paper the catalog asks to be cited, as its citation page writes it."},{"name":"source.citation_url","type":"string","nullable":false,"desc":"The catalog's citation page."},{"name":"source.member.name","type":"string","nullable":false,"desc":"`gwas-catalog-download-associations-alt-full.tsv`."},{"name":"source.member.crc32","type":"string","nullable":false,"desc":"The member's CRC-32 in hex, `92792d40`, checked against the bytes inflated."},{"name":"source.member.compressed_bytes","type":"integer","nullable":false,"desc":"The member's deflated size."},{"name":"source.member.bytes","type":"integer","nullable":false,"desc":"Its inflated size: 745,002,769 for release 2026-10-03."},{"name":"note","type":"string","nullable":false,"desc":"The note the file this replaces carried, **generated for each release** with its own counts: `CHR_ID carries multi-locus values on haplotype and SNP-interaction rows (\"5;5;5\", \"6 x 6\"). Those 5,238 rows are split and credited once per distinct chromosome; 0 rows yielded no recognisable chromosome and are excluded.`"},{"name":"chromosomes","type":"array","nullable":false,"desc":"One entry per chromosome a row names, `1`–`22`, `X`, `Y`, `MT`, in that order and **only those present** — `MT` is absent from release 2026-10-03, so 24 entries; do not assume a length. See `chromosomes[].*`."},{"name":"chromosome_rows","type":"object","nullable":false,"desc":"The rows behind the note, counted, and the rows with no `CHR_ID` at all — 207,434 in release 2026-10-03, which is why the chromosomes' associations do not add up to `totals.associations`. See `chromosome_rows.*`."},{"name":"chromosomes[].chromosome","type":"string","nullable":false,"desc":"`1`–`22`, `X`, `Y` or `MT`."},{"name":"chromosomes[].associations","type":"integer","nullable":false,"desc":"Rows whose `CHR_ID` names the chromosome. `CHR_ID` is split on `;` and on ` x ` — a haplotype row carries `8;8`, an interaction row `2 x 2` — and a row is credited once per distinct chromosome it names."},{"name":"chromosomes[].variants","type":"integer","nullable":false,"desc":"Distinct `rs`-prefixed `SNPS` values on those rows: the global filter, applied per chromosome. **Distinct per chromosome**, so they do not add up to `totals.distinct_variants` — a variant on rows of two chromosomes counts on both."},{"name":"chromosome_rows.multi_locus","type":"integer","nullable":false,"desc":"Rows whose `CHR_ID` split into more than one part."},{"name":"chromosome_rows.unrecognised","type":"integer","nullable":false,"desc":"Rows whose `CHR_ID` named no chromosome of the 25: excluded."},{"name":"chromosome_rows.without_chr_id","type":"integer","nullable":false,"desc":"Rows with an empty `CHR_ID`: on no chromosome."}]},{"id":"list-gwas-traits","verb":"GET","path":"/v1/genome/gwas/traits","summary":"Every trait the GWAS Catalog maps, by associations, studies or variants","returns":"array","params":[{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25. **Every trait is served** — 16,861 in release 2026-10-03 — so the top of any of the three orders is one request: `?order=-studies\u0026limit=15` is the top fifteen by studies.","example":"15"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's `meta.next`. Do not construct one. It carries the parse the walk started on, so a page asked for after the next release has been served is a `400` saying so — start the walk again — never that release's rows appended to the pages you hold."},{"name":"order","in":"query","type":"string","required":false,"desc":"`-associations`, `-studies` or `-variants`, most first, or the same without the `-` for fewest first. Default `-associations`. A tie goes to the slug, ascending, so the order is total and stable between releases. Changing it mid-walk is a `400`.","example":"-studies"}],"fields":[{"name":"slug","type":"string","nullable":false,"desc":"The trait's id on `/v1/genome/gwas/traits/{id}`: the label lower-cased, accents dropped and every run of anything but a–z and 0–9 one hyphen — `body-height`. Where two labels slug alike, each takes its term after it — so **a slug holds within a release, not across them**: a release that adds a label slugging like a served one moves the served trait to its term-suffixed slug, and the plain one answers `404`. Store `efo_id` where it is not `null`: `/traits/{id}` takes it too, and it does not move."},{"name":"trait","type":"string","nullable":false,"desc":"One `MAPPED_TRAIT` label, **paired with its term by index**: a label holding a comma of its own, `osteoarthritis, knee`, is one trait, never the two the pipeline this replaces made of it."},{"name":"efo_id","type":"string","nullable":true,"desc":"The label's ontology term, the last segment of `efo_uri`: `EFO_0004574`, but also `OBA_…`, `MONDO_…`, `HP_…`, `GO_…` — a plain string. **No two traits share one**: a label takes the term it is paired with most often, a tie going to the smaller URI, and never an id another label holds, under any URI. `null` for a label kept whole with no term."},{"name":"efo_uri","type":"string","nullable":true,"desc":"The term's URI, as `MAPPED_TRAIT_URI` writes it."},{"name":"associations","type":"integer","nullable":false,"desc":"Association rows naming the trait, in any position of the row's labels."},{"name":"studies","type":"integer","nullable":false,"desc":"Distinct study accessions among those rows."},{"name":"variants","type":"integer","nullable":false,"desc":"Distinct `rs`-prefixed `SNPS` values among those rows."},{"name":"genes","type":"integer","nullable":false,"desc":"Distinct `MAPPED_GENE` symbols among those rows."}]},{"id":"get-gwas-trait","verb":"GET","path":"/v1/genome/gwas/traits/{id}","summary":"One GWAS Catalog trait and its top variants and genes","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"The trait's `slug`, `body-height`, or its ontology id, `OBA_VT0001253` — no two traits share either. A trait the served release does not map is a `404`.","example":"body-height"}],"fields":[{"name":"source","type":"object","nullable":false,"desc":"The parse these figures were read from and the attribution to carry with them — the same block on every GWAS document, and in `meta.source` on every GWAS answer. See `source.*`."},{"name":"source.source_id","type":"string","nullable":false,"desc":"`gwas_catalog`, the catalog's key in the genome registry: `/v1/genome/releases/gwas_catalog` says which release EBI's stats endpoint names today, checked every morning."},{"name":"source.source_name","type":"string","nullable":false,"desc":"`GWAS Catalog`."},{"name":"source.authority","type":"string","nullable":false,"desc":"`EMBL-EBI`."},{"name":"source.url","type":"string","nullable":false,"desc":"The zip the figures were read from: the release's **dated** directory, `https://ftp.ebi.ac.uk/pub/databases/gwas/releases/2026/10/05/gwas-catalog-associations_ontology-annotated-full.zip`, which EBI never rewrites — `releases/latest/` only when no dated directory held the release yet."},{"name":"source.release_version","type":"string","nullable":false,"desc":"The release, as EBI's stats endpoint dates it: `2026-10-03`. Part of the citation."},{"name":"source.retrieved_at","type":"timestamp","nullable":false,"desc":"When the zip was **first** read by this parser, ISO 8601 to the millisecond. A later read of the same bytes by the same parser leaves it standing, so the documents and their `ETag` change only when the release does — send the `ETag` back as `If-None-Match` and the answer is a `304`, which is not billed."},{"name":"source.parser_version","type":"integer","nullable":false,"desc":"The counting rules that produced these figures. It moves when a change to them would read the same zip differently, and the release is then read again."},{"name":"source.note","type":"string","nullable":false,"desc":"The sentence every document carries, verbatim: a GWAS association is a statistical relationship in a study population and does not establish that the variant causes the trait. **If you republish these figures, carry it.**"},{"name":"source.release_published","type":"date","nullable":true,"desc":"The dated directory the zip was read from, `2026-10-05`: the day EBI published the release, which is not the release's own date. `null` when it was read from `releases/latest/`."},{"name":"source.parse_id","type":"string","nullable":false,"desc":"This parse: the first sixteen hex digits of `checksum_sha256` and the parser, `c3b18bcde213728a-p1`. The same bytes read by the same parser are the same parse."},{"name":"source.checksum_sha256","type":"string","nullable":false,"desc":"The sha256 of the zip as served: `sha256sum` over the same file gives the same string."},{"name":"source.content_length","type":"integer","nullable":false,"desc":"The zip's size in bytes: 72,826,184 for release 2026-10-03."},{"name":"source.etag","type":"string","nullable":true,"desc":"The `ETag` header EBI sent with the zip, verbatim."},{"name":"source.last_modified","type":"string","nullable":true,"desc":"The `Last-Modified` header EBI sent with the zip, **verbatim** — an RFC 7231 date, never normalised."},{"name":"source.member","type":"object","nullable":false,"desc":"The zip's one member as its local header declares it: the file that was inflated, its CRC-32 and both sizes, each checked as it was read. See `member.*`."},{"name":"source.associations_expected","type":"integer","nullable":false,"desc":"The association rows EBI's stats endpoint said the release holds."},{"name":"source.associations_parsed","type":"integer","nullable":false,"desc":"The rows read from the file. **Equal to `associations_expected` on every release served**: a file that counts any other number is refused before anything is published, and the previous release stays served."},{"name":"source.attribution","type":"string","nullable":false,"desc":"The line to carry with any figure republished from these reads: `NHGRI-EBI GWAS Catalog, release 2026-10-03, provided under the EMBL-EBI Terms of Use; please cite the GWAS Catalog (Cerezo et al., Nucleic Acids Research 53(D1):D998–D1005, 2025)`. EMBL-EBI expects attribution in publications, services and products."},{"name":"source.terms_url","type":"string","nullable":false,"desc":"The EMBL-EBI Terms of Use, under which \"the NHGRI-EBI GWAS Catalog and all its contents are available\": EMBL-EBI \"places no additional restrictions on the use or redistribution of the data ... other than those provided by the original data owners\"."},{"name":"source.citation","type":"string","nullable":false,"desc":"The paper the catalog asks to be cited, as its citation page writes it."},{"name":"source.citation_url","type":"string","nullable":false,"desc":"The catalog's citation page."},{"name":"source.member.name","type":"string","nullable":false,"desc":"`gwas-catalog-download-associations-alt-full.tsv`."},{"name":"source.member.crc32","type":"string","nullable":false,"desc":"The member's CRC-32 in hex, `92792d40`, checked against the bytes inflated."},{"name":"source.member.compressed_bytes","type":"integer","nullable":false,"desc":"The member's deflated size."},{"name":"source.member.bytes","type":"integer","nullable":false,"desc":"Its inflated size: 745,002,769 for release 2026-10-03."},{"name":"slug","type":"string","nullable":false,"desc":"The trait's id on `/v1/genome/gwas/traits/{id}`: the label lower-cased, accents dropped and every run of anything but a–z and 0–9 one hyphen — `body-height`. Where two labels slug alike, each takes its term after it — so **a slug holds within a release, not across them**: a release that adds a label slugging like a served one moves the served trait to its term-suffixed slug, and the plain one answers `404`. Store `efo_id` where it is not `null`: `/traits/{id}` takes it too, and it does not move."},{"name":"trait","type":"string","nullable":false,"desc":"One `MAPPED_TRAIT` label, **paired with its term by index**: a label holding a comma of its own, `osteoarthritis, knee`, is one trait, never the two the pipeline this replaces made of it."},{"name":"efo_id","type":"string","nullable":true,"desc":"The label's ontology term, the last segment of `efo_uri`: `EFO_0004574`, but also `OBA_…`, `MONDO_…`, `HP_…`, `GO_…` — a plain string. **No two traits share one**: a label takes the term it is paired with most often, a tie going to the smaller URI, and never an id another label holds, under any URI. `null` for a label kept whole with no term."},{"name":"efo_uri","type":"string","nullable":true,"desc":"The term's URI, as `MAPPED_TRAIT_URI` writes it."},{"name":"associations","type":"integer","nullable":false,"desc":"Association rows naming the trait, in any position of the row's labels."},{"name":"studies","type":"integer","nullable":false,"desc":"Distinct study accessions among those rows."},{"name":"variants","type":"integer","nullable":false,"desc":"Distinct `rs`-prefixed `SNPS` values among those rows."},{"name":"genes","type":"integer","nullable":false,"desc":"Distinct `MAPPED_GENE` symbols among those rows."},{"name":"top_variants","type":"array","nullable":false,"desc":"The trait's forty most-associated variants, most first, a tie in name order — every variant it has when it has fewer, so no trait carries `variants` beside an empty list. Counted for the trait in **any** position of a row's labels, as the totals are. See `top_variants[].*`."},{"name":"top_genes","type":"array","nullable":false,"desc":"The trait's forty most-associated mapped genes, the same way. See `top_genes[].*`."},{"name":"top_variants[].rs_id","type":"string","nullable":false,"desc":"A `SNPS` value that starts `rs` — usually one rsID, a haplotype's `rs1; rs2` as one value."},{"name":"top_variants[].associations","type":"integer","nullable":false,"desc":"Rows naming both the trait and the variant."},{"name":"top_genes[].gene","type":"string","nullable":false,"desc":"A `MAPPED_GENE` symbol as written."},{"name":"top_genes[].associations","type":"integer","nullable":false,"desc":"Rows naming both the trait and the gene."}]},{"id":"list-gwas-genes","verb":"GET","path":"/v1/genome/gwas/genes","summary":"Every gene the GWAS Catalog maps, by associations","returns":"array","max_page_size":30000,"params":[{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–30000. Defaults to 25. **Every gene is served, and the whole list fits in one page**: 23,426 genes in release 2026-10-03, about a thousand more a year, so `limit=30000` returns all of them in one request for years yet. Follow `meta.next` anyway — it is `null` when the page is the whole list, and a client that stops at page 1 without reading it would one day stop short — or ask for the genes you need with `symbols`.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's `meta.next`. Do not construct one. It carries the parse the walk started on, as the traits' does."},{"name":"symbols","in":"query","type":"string","required":false,"desc":"Up to 100 symbols, comma-separated, **exactly as `MAPPED_GENE` writes them**: `FADS1,FADS2`. Answers every one in one response, in the order sent, and names the ones the release maps no row for in `meta.not_found` — `[]` when it has them all. Takes no `limit` or `cursor`: `?symbols=FADS1,FADS2,APOE`."}],"fields":[{"name":"gene","type":"string","nullable":false,"desc":"A `MAPPED_GENE` symbol **exactly as written** — no case folding, no alias resolution, no mapping to HGNC — because the join to NCBI's gene symbols is by exact string. A value that is empty, `NR` or an intergenic interval (`GENE1 - GENE2`) credits no gene: the variant sits between them, not in either. Anything else is split on commas and semicolons, and an interaction pair, `GPR39 x GPR39`, stays one string."},{"name":"associations","type":"integer","nullable":false,"desc":"Rows crediting the gene, once per row. The order of the collection: most first, a tie in symbol order."}]},{"id":"list-genes","verb":"GET","path":"/v1/genome/genes","summary":"List NCBI Gene's genes, one release at a time","returns":"array","params":[{"name":"tax_id","in":"query","type":"integer","required":false,"desc":"The taxon, an NCBI Taxonomy id: `9606` (Homo sapiens) when absent. NCBI's human file also holds `63221` and `741158`, the Neanderthal and the Denisovan, whose mitochondrial genes share their symbols with the human ones. A taxon the served release does not hold is a `400` naming those it does.","example":"9606"},{"name":"gene_id","in":"query","type":"string","required":false,"desc":"GeneIDs, comma-separated or repeated, bare or as the `id` a row carries: `3045,ncbi_gene:100187828`. At most 100; a 101st is a `400`, never a truncation.","example":"3045,100187828"},{"name":"symbol","in":"query","type":"string","required":false,"desc":"Symbols, comma-separated or repeated, matched whole and without regard to case. **Every gene of the taxon that bears one is returned** — `HBD` answers two. At most 100.","example":"HBD,A1BG"},{"name":"chromosome","in":"query","type":"string","required":false,"desc":"One chromosome: `1`–`22`, `X`, `Y`, `MT` or `Un`. A gene NCBI places on `X|Y` — the pseudoautosomal regions — matches both `X` and `Y`.","example":"21"},{"name":"gene_type","in":"query","type":"string","required":false,"desc":"One `type_of_gene`: `protein-coding`, `ncRNA`, `pseudo`, `biological-region`, `tRNA`, `rRNA`, `snRNA`, `snoRNA`, `scRNA`, `other` or `unknown` — and NCBI's `transposon` and `miscRNA`, which no gene has today. Anything else is a `400`.","example":"protein-coding"},{"name":"q","in":"query","type":"string","required":false,"desc":"The start of a symbol or of any synonym, without regard to case: `HBB` answers HBB, HBBP1 and every gene with a synonym that begins HBB. Up to 64 characters; `%` and `_` are literal.","example":"HBB"},{"name":"updated_since","in":"query","type":"timestamp","required":false,"desc":"Genes whose content became served after this instant, RFC 3339 or a date — a daily follower's few dozen. A withdrawn gene is not among them: the snapshot's `meta.source.withdrawn` lists the GeneIDs the served release removed.","example":"2026-10-09T00:00:00Z"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. **Valid only in the release the walk began in**: this API keeps no old release, so a page asked after the daily release has replaced it is a `400` telling you to start again, never a page of one release joined to another."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`ncbi_gene:\u003cGeneID\u003e` — `ncbi_gene:1` is A1BG. Pass it to `GET /v1/genome/genes/{id}`."},{"name":"source","type":"string","nullable":false,"desc":"`ncbi_gene`: the scraper, and its row on `/v1/sources`."},{"name":"ncbi_gene_id","type":"integer","nullable":false,"desc":"NCBI's GeneID: **the identity**, unique across every taxon. Key on this, never on `symbol` — 17 human symbols name more than one gene."},{"name":"symbol","type":"string","nullable":false,"desc":"NCBI's symbol for the gene. **Not an identity**: HBD is GeneID 3045 (hemoglobin subunit delta) and GeneID 100187828 (a phenotype with no chromosome), and COX1 is a human, a Neanderthal and a Denisovan gene. Never null and never `-`."},{"name":"name","type":"string","nullable":true,"desc":"The nomenclature authority's full name (`Full_name_from_nomenclature_authority`), `alpha-1-B glycoprotein`. Null for a gene no authority has named."},{"name":"description","type":"string","nullable":true,"desc":"gene_info's own `description` column, kept apart from `name`: set on almost every gene, including those `name` is null for. Fall back from one to the other yourself if you want one field."},{"name":"chromosome","type":"string","nullable":true,"desc":"As NCBI writes it: `1`–`22`, `X`, `Y`, `MT`, `Un`, or `X|Y` for a gene of the pseudoautosomal regions. Null where NCBI places the gene on none."},{"name":"map_location","type":"string","nullable":true,"desc":"The cytogenetic band, `19q13.43`."},{"name":"gene_type","type":"string","nullable":true,"desc":"`type_of_gene`: `protein-coding`, `ncRNA`, `pseudo`, `biological-region`, `tRNA`, `rRNA`, `snRNA`, `snoRNA`, `scRNA`, `other` or `unknown` — NCBI's own vocabulary (entrezgene.asn)."},{"name":"synonyms","type":"array of string","nullable":false,"desc":"Every unofficial symbol NCBI lists, in its order and **never truncated** — 31 is the most one gene has. `[]` where NCBI lists none."},{"name":"ensembl_id","type":"string","nullable":true,"desc":"The first `Ensembl:` cross-reference, bare: `ENSG00000121410`. A gene with two keeps the second in `db_xrefs`."},{"name":"hgnc_id","type":"string","nullable":true,"desc":"HGNC's identifier, `HGNC:5` — gene_info writes it `HGNC:HGNC:5`, because the database's name is part of HGNC's own id."},{"name":"tax_id","type":"integer","nullable":false,"desc":"The organism's NCBI Taxonomy id: `9606` (Homo sapiens), or `63221` and `741158` for the Neanderthal and Denisovan mitochondrial genes NCBI's human file also carries."},{"name":"locus_tag","type":"string","nullable":true,"desc":"`LocusTag`; null on every human gene today."},{"name":"db_xrefs","type":"array of string","nullable":false,"desc":"Every cross-reference, verbatim and in NCBI's order, `database:value`: `MIM:138670`, `HGNC:HGNC:5`, `Ensembl:ENSG00000121410`."},{"name":"nomenclature_symbol","type":"string","nullable":true,"desc":"`Symbol_from_nomenclature_authority`."},{"name":"nomenclature_status","type":"string","nullable":true,"desc":"`O` for official, `I` for interim; null where no authority has named the gene."},{"name":"other_designations","type":"array of string","nullable":false,"desc":"Alternate descriptions NCBI has assigned the gene, in its order."},{"name":"modification_date","type":"date","nullable":false,"desc":"The day NCBI last changed the record."},{"name":"feature_types","type":"array of string","nullable":false,"desc":"`Feature_type`: annotated features as `type:class`, `regulatory:enhancer_blocking_element`."},{"name":"updated_in_release","type":"string","nullable":false,"desc":"The release that first served this gene **as it is now**: its content last changed in that release, whatever release is served today."},{"name":"updated_at","type":"timestamp","nullable":false,"desc":"When `updated_in_release` became served. `?updated_since=` compares this, so a daily follower asks for a few dozen genes, not 194,000."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored this content."}]},{"id":"get-gene-snapshot","verb":"GET","path":"/v1/genome/genes/snapshot","summary":"Every gene of one taxon in one request","returns":"array","params":[{"name":"tax_id","in":"query","type":"integer","required":false,"desc":"The taxon, an NCBI Taxonomy id: `9606` (Homo sapiens) when absent. NCBI's human file also holds `63221` and `741158`, the Neanderthal and the Denisovan, whose mitochondrial genes share their symbols with the human ones. A taxon the served release does not hold is a `400` naming those it does.","example":"9606"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`ncbi_gene:\u003cGeneID\u003e` — `ncbi_gene:1` is A1BG. Pass it to `GET /v1/genome/genes/{id}`."},{"name":"source","type":"string","nullable":false,"desc":"`ncbi_gene`: the scraper, and its row on `/v1/sources`."},{"name":"ncbi_gene_id","type":"integer","nullable":false,"desc":"NCBI's GeneID: **the identity**, unique across every taxon. Key on this, never on `symbol` — 17 human symbols name more than one gene."},{"name":"symbol","type":"string","nullable":false,"desc":"NCBI's symbol for the gene. **Not an identity**: HBD is GeneID 3045 (hemoglobin subunit delta) and GeneID 100187828 (a phenotype with no chromosome), and COX1 is a human, a Neanderthal and a Denisovan gene. Never null and never `-`."},{"name":"name","type":"string","nullable":true,"desc":"The nomenclature authority's full name (`Full_name_from_nomenclature_authority`), `alpha-1-B glycoprotein`. Null for a gene no authority has named."},{"name":"description","type":"string","nullable":true,"desc":"gene_info's own `description` column, kept apart from `name`: set on almost every gene, including those `name` is null for. Fall back from one to the other yourself if you want one field."},{"name":"chromosome","type":"string","nullable":true,"desc":"As NCBI writes it: `1`–`22`, `X`, `Y`, `MT`, `Un`, or `X|Y` for a gene of the pseudoautosomal regions. Null where NCBI places the gene on none."},{"name":"map_location","type":"string","nullable":true,"desc":"The cytogenetic band, `19q13.43`."},{"name":"gene_type","type":"string","nullable":true,"desc":"`type_of_gene`: `protein-coding`, `ncRNA`, `pseudo`, `biological-region`, `tRNA`, `rRNA`, `snRNA`, `snoRNA`, `scRNA`, `other` or `unknown` — NCBI's own vocabulary (entrezgene.asn)."},{"name":"synonyms","type":"array of string","nullable":false,"desc":"Every unofficial symbol NCBI lists, in its order and **never truncated** — 31 is the most one gene has. `[]` where NCBI lists none."},{"name":"ensembl_id","type":"string","nullable":true,"desc":"The first `Ensembl:` cross-reference, bare: `ENSG00000121410`. A gene with two keeps the second in `db_xrefs`."},{"name":"hgnc_id","type":"string","nullable":true,"desc":"HGNC's identifier, `HGNC:5` — gene_info writes it `HGNC:HGNC:5`, because the database's name is part of HGNC's own id."},{"name":"tax_id","type":"integer","nullable":false,"desc":"The organism's NCBI Taxonomy id: `9606` (Homo sapiens), or `63221` and `741158` for the Neanderthal and Denisovan mitochondrial genes NCBI's human file also carries."},{"name":"locus_tag","type":"string","nullable":true,"desc":"`LocusTag`; null on every human gene today."},{"name":"db_xrefs","type":"array of string","nullable":false,"desc":"Every cross-reference, verbatim and in NCBI's order, `database:value`: `MIM:138670`, `HGNC:HGNC:5`, `Ensembl:ENSG00000121410`."},{"name":"nomenclature_symbol","type":"string","nullable":true,"desc":"`Symbol_from_nomenclature_authority`."},{"name":"nomenclature_status","type":"string","nullable":true,"desc":"`O` for official, `I` for interim; null where no authority has named the gene."},{"name":"other_designations","type":"array of string","nullable":false,"desc":"Alternate descriptions NCBI has assigned the gene, in its order."},{"name":"modification_date","type":"date","nullable":false,"desc":"The day NCBI last changed the record."},{"name":"feature_types","type":"array of string","nullable":false,"desc":"`Feature_type`: annotated features as `type:class`, `regulatory:enhancer_blocking_element`."},{"name":"updated_in_release","type":"string","nullable":false,"desc":"The release that first served this gene **as it is now**: its content last changed in that release, whatever release is served today."},{"name":"updated_at","type":"timestamp","nullable":false,"desc":"When `updated_in_release` became served. `?updated_since=` compares this, so a daily follower asks for a few dozen genes, not 194,000."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored this content."}]},{"id":"get-gene","verb":"GET","path":"/v1/genome/genes/{id}","summary":"Retrieve one gene, by GeneID or by its symbol","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"`ncbi_gene:\u003cGeneID\u003e`, or a symbol. A symbol answers the gene only when exactly one gene of the taxon bears it; when several do it is a `400` naming every candidate's id — `HBD` names two — and **never a pick**. A symbol no gene bears is a `404`; a GeneID withdrawn at somebody's request is a `410`.","example":"ncbi_gene:1"},{"name":"tax_id","in":"query","type":"integer","required":false,"desc":"The taxon a symbol is looked up in, `9606` when absent: `COX1` is a human gene, and with `tax_id=741158` the Denisovan one. A GeneID is unique across taxa and needs none; given one, the gene must be of it.","example":"9606"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`ncbi_gene:\u003cGeneID\u003e` — `ncbi_gene:1` is A1BG. Pass it to `GET /v1/genome/genes/{id}`."},{"name":"source","type":"string","nullable":false,"desc":"`ncbi_gene`: the scraper, and its row on `/v1/sources`."},{"name":"ncbi_gene_id","type":"integer","nullable":false,"desc":"NCBI's GeneID: **the identity**, unique across every taxon. Key on this, never on `symbol` — 17 human symbols name more than one gene."},{"name":"symbol","type":"string","nullable":false,"desc":"NCBI's symbol for the gene. **Not an identity**: HBD is GeneID 3045 (hemoglobin subunit delta) and GeneID 100187828 (a phenotype with no chromosome), and COX1 is a human, a Neanderthal and a Denisovan gene. Never null and never `-`."},{"name":"name","type":"string","nullable":true,"desc":"The nomenclature authority's full name (`Full_name_from_nomenclature_authority`), `alpha-1-B glycoprotein`. Null for a gene no authority has named."},{"name":"description","type":"string","nullable":true,"desc":"gene_info's own `description` column, kept apart from `name`: set on almost every gene, including those `name` is null for. Fall back from one to the other yourself if you want one field."},{"name":"chromosome","type":"string","nullable":true,"desc":"As NCBI writes it: `1`–`22`, `X`, `Y`, `MT`, `Un`, or `X|Y` for a gene of the pseudoautosomal regions. Null where NCBI places the gene on none."},{"name":"map_location","type":"string","nullable":true,"desc":"The cytogenetic band, `19q13.43`."},{"name":"gene_type","type":"string","nullable":true,"desc":"`type_of_gene`: `protein-coding`, `ncRNA`, `pseudo`, `biological-region`, `tRNA`, `rRNA`, `snRNA`, `snoRNA`, `scRNA`, `other` or `unknown` — NCBI's own vocabulary (entrezgene.asn)."},{"name":"synonyms","type":"array of string","nullable":false,"desc":"Every unofficial symbol NCBI lists, in its order and **never truncated** — 31 is the most one gene has. `[]` where NCBI lists none."},{"name":"ensembl_id","type":"string","nullable":true,"desc":"The first `Ensembl:` cross-reference, bare: `ENSG00000121410`. A gene with two keeps the second in `db_xrefs`."},{"name":"hgnc_id","type":"string","nullable":true,"desc":"HGNC's identifier, `HGNC:5` — gene_info writes it `HGNC:HGNC:5`, because the database's name is part of HGNC's own id."},{"name":"tax_id","type":"integer","nullable":false,"desc":"The organism's NCBI Taxonomy id: `9606` (Homo sapiens), or `63221` and `741158` for the Neanderthal and Denisovan mitochondrial genes NCBI's human file also carries."},{"name":"locus_tag","type":"string","nullable":true,"desc":"`LocusTag`; null on every human gene today."},{"name":"db_xrefs","type":"array of string","nullable":false,"desc":"Every cross-reference, verbatim and in NCBI's order, `database:value`: `MIM:138670`, `HGNC:HGNC:5`, `Ensembl:ENSG00000121410`."},{"name":"nomenclature_symbol","type":"string","nullable":true,"desc":"`Symbol_from_nomenclature_authority`."},{"name":"nomenclature_status","type":"string","nullable":true,"desc":"`O` for official, `I` for interim; null where no authority has named the gene."},{"name":"other_designations","type":"array of string","nullable":false,"desc":"Alternate descriptions NCBI has assigned the gene, in its order."},{"name":"modification_date","type":"date","nullable":false,"desc":"The day NCBI last changed the record."},{"name":"feature_types","type":"array of string","nullable":false,"desc":"`Feature_type`: annotated features as `type:class`, `regulatory:enhancer_blocking_element`."},{"name":"updated_in_release","type":"string","nullable":false,"desc":"The release that first served this gene **as it is now**: its content last changed in that release, whatever release is served today."},{"name":"updated_at","type":"timestamp","nullable":false,"desc":"When `updated_in_release` became served. `?updated_since=` compares this, so a daily follower asks for a few dozen genes, not 194,000."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored this content."}]}]},{"slug":"reachability","name":"Reachability API","status":"retired","operations":[{"id":"list-reachability","verb":"GET","path":"/v1/reachability","summary":"List watched targets and how each one last answered","returns":"array","params":[{"name":"watchlist","in":"query","type":"string","required":false,"desc":"The named set of targets to read — `middle-corridor` is the first one. Open rather than a closed set, because registering a watchlist is a row rather than a release, so an unknown value answers an empty page rather than a 400.","example":"middle-corridor"},{"name":"vantage","in":"query","type":"string","required":false,"desc":"Where the probe was made from, e.g. `hetzner-nbg1`. Narrowing on it is how you compare two vantages' answers for the same target — which is the comparison this collection exists to make routine.","example":"hetzner-nbg1"},{"name":"reachable","in":"query","type":"boolean","required":false,"desc":"`true` for targets whose last probe got a 200, `false` for the rest. Omit it for both. **`reachable=false` is not a list of sites that are down** — pair it with `failure_class` before drawing that conclusion.","example":"false"},{"name":"failure_class","in":"query","type":"string","required":false,"desc":"One of `http_error`, `blocked_by_bot_protection`, `tls_error`, `dns_error`, `timeout`, `connection_error`, `robots_disallowed`. Closed, unlike `watchlist` and `vantage`, because this one is our own judgement and a typo in it would mean you believed you had filtered. `?failure_class=blocked_by_bot_protection` is how you ask which of these are our problem rather than theirs.","example":"blocked_by_bot_protection"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `ingested_at`, each optionally prefixed with `-` to reverse. Default is `-ingested_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-ingested_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`\u003csource\u003e:\u003ctarget_id\u003e` — the prober and the target it asked. Stable for the life of the target, and what `/v1/reachability/{id}` takes."},{"name":"source","type":"string","nullable":false,"desc":"Which prober made the request. **This is the one collection here where `source` is softon and not an upstream publisher**: a row is a record of a request this platform made, so the source is the client that made it. A second prober at a second vantage is a second source, and a second row for the same target."},{"name":"target_id","type":"string","nullable":false,"desc":"The stable key for one watched URL. Chosen when the target is registered and never derived from the URL, so a target that changes host keeps the history in the three fields at the bottom of this table."},{"name":"url","type":"string","nullable":false,"desc":"The URL that was requested, exactly as registered."},{"name":"watchlist","type":"string","nullable":false,"desc":"The named set of targets this one belongs to, and the value `?watchlist=` takes. `middle-corridor` is the first one. Open rather than a closed set — a new watchlist is a row, not a release — so an unknown value answers an empty page rather than a 400."},{"name":"vantage","type":"string","nullable":false,"desc":"**Where the request was made from**, and the field that stops this row being read as a verdict on the site. `hetzner-nbg1` is a datacenter address in Nuremberg, which is this platform's ingestion box.\n\nThe answer genuinely depends on where you ask from: the same URL has been measured answering `403` to a datacenter address and `200` to a residential one on the same day. A reachability figure quoted without the vantage attached is a measurement of a pair reported as a property of one half of it — which is also why the prober is carried as `source`, so that a second client probing the same target is a second row rather than a silent redefinition of this one."},{"name":"reachable","type":"boolean","nullable":false,"desc":"Whether the last probe got an HTTP **200**. Strictly 200: a `204` or a `206` reads as false here, deliberately, because that is what the pipeline this replaces means by the word.\n\n**`false` is not a claim that the site is down.** Two of the six targets served today sit behind a bot-protection edge whose answer is decided by the request rather than by the site: measured with one client, one user agent, one address and one minute, both answered `403` on every HTTP/2 sample and `200` on every HTTP/1.1 one, and one of them is not stable even within a single client.\n\nSo read `failure_class` before turning this into a count, and `consecutive_failures` before turning one row into a finding. A single `blocked_by_bot_protection` is noise; sixty-one consecutive ones are a fact about a relationship with that site."},{"name":"http_status","type":"integer","nullable":true,"desc":"The final status after redirects. Null when no HTTP response was obtained at all: a name that did not resolve, a handshake that was refused, a timeout, or a target whose robots.txt means we did not ask. Always present as a key, null where inapplicable."},{"name":"final_url","type":"string","nullable":true,"desc":"Where the request ended up after redirects. Null when there was no response."},{"name":"redirected","type":"boolean","nullable":false,"desc":"Whether the request moved before answering. False when there was no response."},{"name":"failure_class","type":"string","nullable":true,"desc":"Why it was not a 200, from a closed set, and **null exactly when `reachable` is true** — the database refuses either half without the other, so there is no such row as one that failed without saying why.\n\n`http_error` — the host answered, with something that is not 200. `blocked_by_bot_protection` — the host answered 403 with a bot-protection challenge page. **Our client failed a check; the site is up.** It is separate from `http_error` because it is a fact about the request rather than about the target, and separate from an outage because collapsing the two publishes a finding about a site that is serving fine. `tls_error` — the certificate or the handshake was refused; a browser would refuse it too. `dns_error` — the name did not resolve. `timeout` — nothing came back inside the budget. `connection_error` — the connection failed or was reset below HTTP. `robots_disallowed` — **we did not ask.** The target's robots.txt refuses this crawler for this path, so `http_status` is null because no request was made, not because one failed."},{"name":"error","type":"string","nullable":true,"desc":"A short plain sentence, at most 200 characters, or null. `\"TLS certificate expired\"`, not a stack trace.\n\n**Treat it as user-facing copy**, because on the page this dataset was built for it is rendered verbatim to the public. The pipeline it replaces writes `str(exception)` into the equivalent field, which is how a visitor to a transport-analysis page came to read a urllib3 connection-pool repr complete with a CPython source-file line number.\n\nNull rather than truncated when a sentence cannot be vouched for: a shortened exception repr is still an exception repr, and the meaning is in `failure_class`, which is never null on a failing row."},{"name":"response_ms","type":"integer","nullable":true,"desc":"How long the request took, in milliseconds. Null when nothing came back. One measurement from one vantage on a shared box — useful for noticing a target getting slower, not a latency SLA."},{"name":"robots_allowed","type":"boolean","nullable":false,"desc":"What the target's own `robots.txt` says about this crawler fetching this path. Nothing in the pipeline this replaces checks robots at all.\n\nFails **open** when robots.txt cannot be fetched — a 404 or a timeout there is not a refusal. When it is false no request is made, and `failure_class` is `robots_disallowed`."},{"name":"checked_at","type":"timestamp","nullable":false,"desc":"When the probe ran, on the prober's clock. **Date a finding by this**, not by `ingested_at`: the pipeline this replaces stamps its rows with the date of the build that read them, so a row could claim it was checked on a day nothing was."},{"name":"last_reachable_at","type":"timestamp","nullable":true,"desc":"The last probe that got a 200. Null for a target that has never answered one."},{"name":"first_failed_at","type":"timestamp","nullable":true,"desc":"When the current run of failures began. Null while the target is reachable, and reset the moment it answers 200 again."},{"name":"consecutive_failures","type":"integer","nullable":false,"desc":"How many probes in a row have not got a 200; 0 while the target is reachable.\n\nThis and the two fields above are the value this collection has that a one-shot check cannot: \"this target has been refusing us for sixty-one straight probes\" is an editorial signal, and \"it refused us once, today\" is not. They are accumulated where the row is stored, so they survive a redeploy and the prober never has to remember anything.\n\nCounts **probes**, not days — the same number at the daily cadence this runs at, and named for the fact so that changing the cadence cannot silently redefine it. All three reset if `vantage` changes, because a counter spanning two vantages answers neither question."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the observation. Differs from `checked_at` by the time the batch spent in flight; compare the two to see whether probe runs are landing at all."}]},{"id":"get-reachability","verb":"GET","path":"/v1/reachability/{id}","summary":"Retrieve one watched target","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"The target id from a list response, `\u003csource\u003e:\u003ctarget_id\u003e`. Stable for the life of the target: the id survives the target changing host, which is what keeps its failure history attached to it.","example":"softonbot:az-transit-gov"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`\u003csource\u003e:\u003ctarget_id\u003e` — the prober and the target it asked. Stable for the life of the target, and what `/v1/reachability/{id}` takes."},{"name":"source","type":"string","nullable":false,"desc":"Which prober made the request. **This is the one collection here where `source` is softon and not an upstream publisher**: a row is a record of a request this platform made, so the source is the client that made it. A second prober at a second vantage is a second source, and a second row for the same target."},{"name":"target_id","type":"string","nullable":false,"desc":"The stable key for one watched URL. Chosen when the target is registered and never derived from the URL, so a target that changes host keeps the history in the three fields at the bottom of this table."},{"name":"url","type":"string","nullable":false,"desc":"The URL that was requested, exactly as registered."},{"name":"watchlist","type":"string","nullable":false,"desc":"The named set of targets this one belongs to, and the value `?watchlist=` takes. `middle-corridor` is the first one. Open rather than a closed set — a new watchlist is a row, not a release — so an unknown value answers an empty page rather than a 400."},{"name":"vantage","type":"string","nullable":false,"desc":"**Where the request was made from**, and the field that stops this row being read as a verdict on the site. `hetzner-nbg1` is a datacenter address in Nuremberg, which is this platform's ingestion box.\n\nThe answer genuinely depends on where you ask from: the same URL has been measured answering `403` to a datacenter address and `200` to a residential one on the same day. A reachability figure quoted without the vantage attached is a measurement of a pair reported as a property of one half of it — which is also why the prober is carried as `source`, so that a second client probing the same target is a second row rather than a silent redefinition of this one."},{"name":"reachable","type":"boolean","nullable":false,"desc":"Whether the last probe got an HTTP **200**. Strictly 200: a `204` or a `206` reads as false here, deliberately, because that is what the pipeline this replaces means by the word.\n\n**`false` is not a claim that the site is down.** Two of the six targets served today sit behind a bot-protection edge whose answer is decided by the request rather than by the site: measured with one client, one user agent, one address and one minute, both answered `403` on every HTTP/2 sample and `200` on every HTTP/1.1 one, and one of them is not stable even within a single client.\n\nSo read `failure_class` before turning this into a count, and `consecutive_failures` before turning one row into a finding. A single `blocked_by_bot_protection` is noise; sixty-one consecutive ones are a fact about a relationship with that site."},{"name":"http_status","type":"integer","nullable":true,"desc":"The final status after redirects. Null when no HTTP response was obtained at all: a name that did not resolve, a handshake that was refused, a timeout, or a target whose robots.txt means we did not ask. Always present as a key, null where inapplicable."},{"name":"final_url","type":"string","nullable":true,"desc":"Where the request ended up after redirects. Null when there was no response."},{"name":"redirected","type":"boolean","nullable":false,"desc":"Whether the request moved before answering. False when there was no response."},{"name":"failure_class","type":"string","nullable":true,"desc":"Why it was not a 200, from a closed set, and **null exactly when `reachable` is true** — the database refuses either half without the other, so there is no such row as one that failed without saying why.\n\n`http_error` — the host answered, with something that is not 200. `blocked_by_bot_protection` — the host answered 403 with a bot-protection challenge page. **Our client failed a check; the site is up.** It is separate from `http_error` because it is a fact about the request rather than about the target, and separate from an outage because collapsing the two publishes a finding about a site that is serving fine. `tls_error` — the certificate or the handshake was refused; a browser would refuse it too. `dns_error` — the name did not resolve. `timeout` — nothing came back inside the budget. `connection_error` — the connection failed or was reset below HTTP. `robots_disallowed` — **we did not ask.** The target's robots.txt refuses this crawler for this path, so `http_status` is null because no request was made, not because one failed."},{"name":"error","type":"string","nullable":true,"desc":"A short plain sentence, at most 200 characters, or null. `\"TLS certificate expired\"`, not a stack trace.\n\n**Treat it as user-facing copy**, because on the page this dataset was built for it is rendered verbatim to the public. The pipeline it replaces writes `str(exception)` into the equivalent field, which is how a visitor to a transport-analysis page came to read a urllib3 connection-pool repr complete with a CPython source-file line number.\n\nNull rather than truncated when a sentence cannot be vouched for: a shortened exception repr is still an exception repr, and the meaning is in `failure_class`, which is never null on a failing row."},{"name":"response_ms","type":"integer","nullable":true,"desc":"How long the request took, in milliseconds. Null when nothing came back. One measurement from one vantage on a shared box — useful for noticing a target getting slower, not a latency SLA."},{"name":"robots_allowed","type":"boolean","nullable":false,"desc":"What the target's own `robots.txt` says about this crawler fetching this path. Nothing in the pipeline this replaces checks robots at all.\n\nFails **open** when robots.txt cannot be fetched — a 404 or a timeout there is not a refusal. When it is false no request is made, and `failure_class` is `robots_disallowed`."},{"name":"checked_at","type":"timestamp","nullable":false,"desc":"When the probe ran, on the prober's clock. **Date a finding by this**, not by `ingested_at`: the pipeline this replaces stamps its rows with the date of the build that read them, so a row could claim it was checked on a day nothing was."},{"name":"last_reachable_at","type":"timestamp","nullable":true,"desc":"The last probe that got a 200. Null for a target that has never answered one."},{"name":"first_failed_at","type":"timestamp","nullable":true,"desc":"When the current run of failures began. Null while the target is reachable, and reset the moment it answers 200 again."},{"name":"consecutive_failures","type":"integer","nullable":false,"desc":"How many probes in a row have not got a 200; 0 while the target is reachable.\n\nThis and the two fields above are the value this collection has that a one-shot check cannot: \"this target has been refusing us for sixty-one straight probes\" is an editorial signal, and \"it refused us once, today\" is not. They are accumulated where the row is stored, so they survive a redeploy and the prober never has to remember anything.\n\nCounts **probes**, not days — the same number at the daily cadence this runs at, and named for the fact so that changing the cadence cannot silently redefine it. All three reset if `vantage` changes, because a counter spanning two vantages answers neither question."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the observation. Differs from `checked_at` by the time the batch spent in flight; compare the two to see whether probe runs are landing at all."}]}]},{"slug":"cyber","name":"Cyber Threat Data API","status":"retired","operations":[{"id":"list-kev","verb":"GET","path":"/v1/cyber/kev","summary":"List known exploited vulnerabilities","returns":"array","max_page_size":3000,"params":[{"name":"catalog_version","in":"query","type":"string","required":false,"desc":"Serve one published catalog exactly, e.g. `2026.10.04`. Omit it and you get the CURRENT catalog. Read it off page 1 and pass it back to pin a multi-page walk to one snapshot. An unknown value answers an empty page rather than a 400 — the labels are CISA's and we do not assert a format for them.","example":"2026.10.04"},{"name":"date","in":"query","type":"date","required":false,"desc":"The catalog as it stood on this day: the newest one released on or before it. This is how you ask what a `due_date` said at the time of an audit. Mutually exclusive with `catalog_version` — sending both is a 400, because they can name different snapshots and guessing which you meant is not an answer worth giving on a dataset that carries deadlines."},{"name":"cve","in":"query","type":"string","required":false,"desc":"CVE ids, comma-separated — `CVE-2025-39964,CVE-2002-0367`. Case is normalised, and a repeated `?cve=` is unioned with the list rather than overwriting it. This is the filter for \"are any of my inventory's CVEs being exploited\".","example":"CVE-2026-59310"},{"name":"vendor","in":"query","type":"string","required":false,"desc":"The vendor or project, exactly as the row carries it — `Microsoft`, `Linux`. Case-insensitive but not a substring match: `Apache` does NOT return `Apache Tomcat` rows. An unknown value answers an empty page rather than a 400, because this vocabulary is CISA's and grows whenever they catalogue a new publisher.","example":"Broadcom"},{"name":"ransomware","in":"query","type":"string","required":false,"desc":"`known` or `unknown`, CISA's own two values. Closed, unlike `vendor`, because a typo here would mean you believed you had filtered. **`unknown` means CISA has no evidence, not that there is none.**","example":"known"},{"name":"added_from","in":"query","type":"date","required":false,"desc":"Earliest `date_added`, inclusive. The sync watermark: it is a fact about the catalogue rather than about when we stored the row.","example":"2026-09-01"},{"name":"added_to","in":"query","type":"date","required":false,"desc":"Latest `date_added`, inclusive."},{"name":"due_from","in":"query","type":"date","required":false,"desc":"Earliest `due_date`, inclusive."},{"name":"due_to","in":"query","type":"date","required":false,"desc":"Latest `due_date`, inclusive. `?due_to=\u003ctoday\u003e` is everything past its BOD deadline.","example":"2026-09-20"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–3000. Defaults to 25. **The whole current catalog fits in one page**: it listed 1,739 vulnerabilities on 2026-10-08, and CISA added 245 in 2025 and 255 in 2026 to that day, so `limit=3000` returns all of it in one request until about 2030. Follow `meta.next` anyway — it is `null` when the page is the whole catalog, and a client that stops at page 1 without reading it would one day stop short.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `date_released`, each optionally prefixed with `-` to reverse. Default is `-date_released`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-date_released"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003ccatalog version\u003e:\u003cCVE id\u003e\" — `cisa_kev:2026.10.04:CVE-2025-39964`. Opaque: treat the whole string as the id rather than parsing the parts out of it. **It names a SNAPSHOT of a CVE, not the CVE** — the same CVE has a different id in every catalog version that lists it, so an id you stored keeps resolving to the `due_date` you saw rather than silently moving to a newer one."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered this catalog. `cisa_kev` is CISA's own published feed."},{"name":"catalog_version","type":"string","nullable":false,"desc":"CISA's own version label for the published document — `2026.10.04`. Treat it as opaque rather than as a date: it is the publisher's rendering, and `date_released` is the fact. **Pass it back as `?catalog_version=` to pin a walk to one snapshot.**"},{"name":"date_released","type":"timestamp","nullable":false,"desc":"When that catalog was published. A real instant with a time of day in it, not a date — the measured release was 19:00:05 UTC."},{"name":"catalog_count","type":"integer","nullable":false,"desc":"CISA's own count of the records in that document, **verbatim and never reconciled** with the number of rows served. It has agreed on every fetch measured; if it ever does not, that disagreement is a fact about the upstream document and replacing it with our arithmetic would delete the only evidence of it."},{"name":"cve_id","type":"string","nullable":false,"desc":"The CVE identifier, `CVE-2025-39964`. **The year in it is the year the CVE was assigned, not the year CISA catalogued it** — `CVE-2002-0367` was added to this catalog in 2022. `date_added` is the other fact and neither is derivable from the other."},{"name":"vendor_project","type":"string","nullable":true,"desc":"The upstream's own label for the vendor or project — `Microsoft`, `Linux`, `Cisco`. Free text rather than a taxonomy this platform maintains, so `?vendor=` is a case-insensitive EXACT match: `Apache` and `Apache Tomcat` are two entries here and this API will not collapse them for you."},{"name":"product","type":"string","nullable":true,"desc":"The upstream's own label for the affected product."},{"name":"vulnerability_name","type":"string","nullable":true,"desc":"CISA's short title for the vulnerability."},{"name":"date_added","type":"date","nullable":true,"desc":"The day CISA added this CVE to the catalog. **This is the watermark to sync on** — \"what has been catalogued since Tuesday\" is a question about the catalogue, where `ingested_at` is a question about us. `?added_from=` takes it."},{"name":"due_date","type":"date","nullable":true,"desc":"The Binding Operational Directive deadline for `required_action`. **`?due_to=\u003ctoday\u003e` is how you ask what is past it.**"},{"name":"short_description","type":"string","nullable":true,"desc":"CISA's own summary of the vulnerability."},{"name":"required_action","type":"string","nullable":true,"desc":"The BOD instruction, verbatim and at length. It usually names the directive it comes from and points at `notes` for the URL. Together with `due_date` this is what makes a row a compliance instrument rather than a vulnerability description — and what makes a stale copy of one harmful rather than merely old."},{"name":"known_ransomware_campaign_use","type":"string","nullable":false,"desc":"`Known` or `Unknown`, **CISA's own value and not a boolean**. `Unknown` is the agency saying it has no evidence of ransomware use — not that there is none — so publishing it as `false` would put our coercion out under their name. 360 of 1,716 were `Known` on the measured catalog. `?ransomware=known` takes the same vocabulary, lower-cased."},{"name":"forensic_triage","type":"string","nullable":false,"desc":"`Yes` or `No`, CISA's own value. This one genuinely is a boolean and is still carried as published, so this collection has one rule rather than two."},{"name":"notes","type":"string","nullable":true,"desc":"CISA's free-text note, usually carrying the vendor advisory URL and the BOD reference."},{"name":"cwes","type":"array of string","nullable":true,"desc":"The upstream's CWE identifiers, `[\"CWE-362\"]`. **An empty array and `null` mean different things and both occur**: `[]` is \"CISA classified this and named no weakness\" (175 of 1,716 on the measured catalog) and `null` is \"CISA did not carry the field\". They are not collapsed."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored this snapshot. A whole catalog arrives in one batch, so every row of one `catalog_version` shares this to the microsecond — it dates the snapshot, not the record."}]},{"id":"get-kev","verb":"GET","path":"/v1/cyber/kev/{id}","summary":"Retrieve one catalogued vulnerability","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"The id from a list response, `\u003csource\u003e:\u003ccatalog version\u003e:\u003cCVE id\u003e`. It names a snapshot, so it is stable forever: a published catalog version is never revised, and this keeps resolving to the `due_date` you saw. **The one path that does not default to the current catalog** — an id carries its own.","example":"cisa_kev:2026.10.04:CVE-2026-53362"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003ccatalog version\u003e:\u003cCVE id\u003e\" — `cisa_kev:2026.10.04:CVE-2025-39964`. Opaque: treat the whole string as the id rather than parsing the parts out of it. **It names a SNAPSHOT of a CVE, not the CVE** — the same CVE has a different id in every catalog version that lists it, so an id you stored keeps resolving to the `due_date` you saw rather than silently moving to a newer one."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered this catalog. `cisa_kev` is CISA's own published feed."},{"name":"catalog_version","type":"string","nullable":false,"desc":"CISA's own version label for the published document — `2026.10.04`. Treat it as opaque rather than as a date: it is the publisher's rendering, and `date_released` is the fact. **Pass it back as `?catalog_version=` to pin a walk to one snapshot.**"},{"name":"date_released","type":"timestamp","nullable":false,"desc":"When that catalog was published. A real instant with a time of day in it, not a date — the measured release was 19:00:05 UTC."},{"name":"catalog_count","type":"integer","nullable":false,"desc":"CISA's own count of the records in that document, **verbatim and never reconciled** with the number of rows served. It has agreed on every fetch measured; if it ever does not, that disagreement is a fact about the upstream document and replacing it with our arithmetic would delete the only evidence of it."},{"name":"cve_id","type":"string","nullable":false,"desc":"The CVE identifier, `CVE-2025-39964`. **The year in it is the year the CVE was assigned, not the year CISA catalogued it** — `CVE-2002-0367` was added to this catalog in 2022. `date_added` is the other fact and neither is derivable from the other."},{"name":"vendor_project","type":"string","nullable":true,"desc":"The upstream's own label for the vendor or project — `Microsoft`, `Linux`, `Cisco`. Free text rather than a taxonomy this platform maintains, so `?vendor=` is a case-insensitive EXACT match: `Apache` and `Apache Tomcat` are two entries here and this API will not collapse them for you."},{"name":"product","type":"string","nullable":true,"desc":"The upstream's own label for the affected product."},{"name":"vulnerability_name","type":"string","nullable":true,"desc":"CISA's short title for the vulnerability."},{"name":"date_added","type":"date","nullable":true,"desc":"The day CISA added this CVE to the catalog. **This is the watermark to sync on** — \"what has been catalogued since Tuesday\" is a question about the catalogue, where `ingested_at` is a question about us. `?added_from=` takes it."},{"name":"due_date","type":"date","nullable":true,"desc":"The Binding Operational Directive deadline for `required_action`. **`?due_to=\u003ctoday\u003e` is how you ask what is past it.**"},{"name":"short_description","type":"string","nullable":true,"desc":"CISA's own summary of the vulnerability."},{"name":"required_action","type":"string","nullable":true,"desc":"The BOD instruction, verbatim and at length. It usually names the directive it comes from and points at `notes` for the URL. Together with `due_date` this is what makes a row a compliance instrument rather than a vulnerability description — and what makes a stale copy of one harmful rather than merely old."},{"name":"known_ransomware_campaign_use","type":"string","nullable":false,"desc":"`Known` or `Unknown`, **CISA's own value and not a boolean**. `Unknown` is the agency saying it has no evidence of ransomware use — not that there is none — so publishing it as `false` would put our coercion out under their name. 360 of 1,716 were `Known` on the measured catalog. `?ransomware=known` takes the same vocabulary, lower-cased."},{"name":"forensic_triage","type":"string","nullable":false,"desc":"`Yes` or `No`, CISA's own value. This one genuinely is a boolean and is still carried as published, so this collection has one rule rather than two."},{"name":"notes","type":"string","nullable":true,"desc":"CISA's free-text note, usually carrying the vendor advisory URL and the BOD reference."},{"name":"cwes","type":"array of string","nullable":true,"desc":"The upstream's CWE identifiers, `[\"CWE-362\"]`. **An empty array and `null` mean different things and both occur**: `[]` is \"CISA classified this and named no weakness\" (175 of 1,716 on the measured catalog) and `null` is \"CISA did not carry the field\". They are not collapsed."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored this snapshot. A whole catalog arrives in one batch, so every row of one `catalog_version` shares this to the microsecond — it dates the snapshot, not the record."}]},{"id":"get-kev-years","verb":"GET","path":"/v1/cyber/kev/years","summary":"The CVE-id years one catalog lists","returns":"object","params":[{"name":"catalog_version","in":"query","type":"string","required":false,"desc":"Answer for one published catalog exactly, e.g. `2026.10.04`. Omit it and you get the CURRENT catalog. An unknown value is a `404`, because there is no catalog to describe.","example":"2026.10.04"},{"name":"date","in":"query","type":"date","required":false,"desc":"Answer for the catalog in force on this day: the newest one released on or before it. Mutually exclusive with `catalog_version` — sending both is a `400`, as on `GET /v1/cyber/kev`."}],"fields":[{"name":"catalog_version","type":"string","nullable":false,"desc":"The catalog the years were read from — the current one unless `?catalog_version=` or `?date=` named another, resolved exactly as `GET /v1/cyber/kev` resolves it."},{"name":"min_year","type":"integer","nullable":false,"desc":"The earliest year in `years`."},{"name":"max_year","type":"integer","nullable":false,"desc":"The latest year in `years`."},{"name":"years","type":"array of integer","nullable":false,"desc":"Every year a CVE id in that catalog carries, ascending — **a set, not a range: its gaps are data.** No CVE assigned in 2003 is in the 2026.10.04 catalog, so 2003 is absent between 2002 and 2004. Read off the id with `^CVE-(\\d{4})-`, so it is the year the CVE was assigned, not the year CISA catalogued it — `CVE-2002-0367` was added in 2022 — and a long sequence number such as `CVE-2026-102490` changes nothing."}]},{"id":"get-kev-snapshot","verb":"GET","path":"/v1/cyber/kev/snapshot","summary":"Which catalog is being served, and where it was read","returns":"object","params":[{"name":"catalog_version","in":"query","type":"string","required":false,"desc":"Answer for one published catalog exactly, e.g. `2026.10.04`. Omit it and you get the CURRENT catalog. An unknown value is a `404`, because there is no catalog to describe.","example":"2026.10.04"},{"name":"date","in":"query","type":"date","required":false,"desc":"Answer for the catalog in force on this day: the newest one released on or before it. Mutually exclusive with `catalog_version` — sending both is a `400`, as on `GET /v1/cyber/kev`."}],"fields":[{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `cisa_kev`."},{"name":"catalog_version","type":"string","nullable":false,"desc":"CISA's own version label for the document — the current catalog unless `?catalog_version=` or `?date=` named another."},{"name":"date_released","type":"timestamp","nullable":false,"desc":"When CISA published that document — a real instant."},{"name":"catalog_count","type":"integer","nullable":false,"desc":"CISA's own `count` for the document, verbatim and never reconciled."},{"name":"listed","type":"integer","nullable":false,"desc":"How many vulnerabilities the document actually carried — the length of its array, counted by us when we read it."},{"name":"held","type":"integer","nullable":false,"desc":"How many of that version's rows `GET /v1/cyber/kev?catalog_version=` serves. Equal to `listed` unless a record has been withdrawn, which is our action rather than CISA's arithmetic, or lacks a value the collection requires — `knownRansomwareCampaignUse` or `forensicTriage` absent, null or empty — which is stored and not served."},{"name":"count_matches","type":"boolean","nullable":false,"desc":"`catalog_count == listed`: whether CISA's count agrees with the array it shipped. It always has. **`false` is a fact about the upstream document**, and both numbers are served so you can see by how much."},{"name":"upstream_url","type":"string","nullable":false,"desc":"The URL the served bytes were read from: CISA's feed, or **CISA's own copy on GitHub** (`raw.githubusercontent.com/cisagov/kev-data`) when the feed does not answer us. Redirects are refused, so this is where the bytes came from rather than where we asked."},{"name":"checksum_sha256","type":"string","nullable":false,"desc":"`sha256` of those bytes **as served, before any parsing**, so your `sha256sum` of a download of `upstream_url` is the same string while the catalog has not moved — not a digest of a re-serialisation of ours."},{"name":"licence","type":"string","nullable":false,"desc":"`public_domain_us_government`: a work of the United States Government (17 U.S.C. 105), which CISA distributes under **CC0 1.0** (https://www.cisa.gov/sites/default/files/licenses/kev/license.txt). Nothing to pass on. CISA's terms add that using the data does not authorise the CISA logo or the DHS seal, nor make it an endorsement by CISA or DHS, and that a third-party link in a record's `notes` is under that site's own terms."},{"name":"attribution","type":"string","nullable":false,"desc":"`Cybersecurity and Infrastructure Security Agency (CISA)`, for a source line."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When this platform first fetched and stored this catalog version. It never moves."},{"name":"last_checked_at","type":"timestamp","nullable":false,"desc":"The newest run that read this catalog version, **whether or not it had changed** — so a catalog that has not moved and a collector that has not looked are told apart. The run fires daily at 02:00 UTC; a value much older than a day is a collector that has stopped reading, not a quiet catalog."}]},{"id":"list-epss","verb":"GET","path":"/v1/cyber/epss","summary":"Score a list of CVEs","returns":"array","params":[{"name":"cve","in":"query","type":"string","required":true,"desc":"**Required.** Up to 100 CVE ids, comma-separated — `CVE-2021-44228,CVE-2014-0160` — or the parameter repeated, which means the same thing. Case is normalised. A 101st id is a `400` naming the count rather than a silent truncation, because the ids that were dropped would come back in `meta.not_found`, which means \"FIRST publishes no score for this CVE\" and would be false about them.\n\nThere is no way to page the whole score set here and that is deliberate: it is over 377,000 rows re-scored daily, and `GET /v1/cyber/epss/snapshot` hands you the URL of the one file FIRST publishes it in.","example":"CVE-2021-44228,CVE-2026-00000"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003cscore date\u003e:\u003cCVE id\u003e\" — `epss:2026-09-20:CVE-2021-44228`. Opaque: treat the whole string as the id. **There is no `GET /v1/cyber/epss/{id}`, and that is deliberate** — a superseded score is pruned, so an id you stored last week names a row that is gone, and a by-id route would be a promise of permanence this dataset does not make. Ask for the CVE instead: `?cve=` always answers with the score in force."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered this score. `epss` is FIRST's own published file."},{"name":"score_date","type":"date","nullable":false,"desc":"The day FIRST produced this score — the as-of, and half the id. **Read it per row rather than assuming one value for the response.** A daily snapshot lands as nineteen batches, so a run that failed part-way leaves some rows on the previous day, and each row says which."},{"name":"score_at","type":"timestamp","nullable":true,"desc":"The instant FIRST's file states for its scores — `2026-10-06T12:00:23Z` — kept whole and served in UTC, as FIRST writes it. `score_date` is its day and stays what identifies the row. It belongs to the file, so it is read from the receipt the daily run posts after its last score: **null when the run that read the score's file stopped part-way** — it posts no receipt — or the score was stored before this field existed, until the next whole nightly run re-scores it, which FIRST's daily file does for every CVE it still scores; and if FIRST ever prints a stamp with no time zone: the day is still known then, and an instant guessed into UTC would not be FIRST's. **A CVE FIRST has stopped scoring is never re-scored**: it keeps its last score, under that score's own `score_date`, so a null here on such a row stays null."},{"name":"model_version","type":"string","nullable":false,"desc":"The trained model that produced the score, `v2026.06.15`. On every row because **scores are not comparable across a retrain** — FIRST re-scales, so a raw score from two different model versions is two different scales. `percentile` is the field to compare across days."},{"name":"cve_id","type":"string","nullable":false,"desc":"The CVE identifier, `CVE-2021-44228`, upper-case as published."},{"name":"epss","type":"number","nullable":false,"desc":"**The probability this CVE will be exploited in the wild in the next 30 days, in `[0, 1]`.** Not a percentage: `0.00042` is four in ten thousand, and multiplying by 100 somewhere and forgetting further down is how a backlog gets sorted by a number a hundred times too large. Served at the precision FIRST published — up to five decimals, unrounded, never padded, so `0.5` and `0.00047` both occur.\n\nIt is **not a severity and not CVSS**, and it says nothing about impact. `/v1/cyber/kev` is the complementary fact: evidence that exploitation has already happened."},{"name":"percentile","type":"number","nullable":false,"desc":"Where this score ranks against every other scored CVE, in `[0, 1]`. **This is the field to compare across days**, because it is a rank within its own model run where the raw score moves when the model is retrained. `0.0` and `1.0` both occur."},{"name":"upstream_url","type":"string","nullable":false,"desc":"The dated file these numbers were read out of, after the redirect chain — not the `-current` pointer, which names no day. Download it and you have FIRST's own copy of this snapshot. **FIRST keeps every day's file at a URL of this shape**, which is why this platform serves only the current score per CVE rather than mirroring the archive."},{"name":"checksum_sha256","type":"string","nullable":false,"desc":"`sha256` of that file's **gzipped bytes as served**, so your `sha256sum` of the download and this value are the same string. A digest of a decompressed copy would be a statement about someone's gunzip instead."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the score. **Not the as-of** — `score_date` is — and the two differ by the time between FIRST publishing and the daily run reading it."}]},{"id":"get-epss-snapshot","verb":"GET","path":"/v1/cyber/epss/snapshot","summary":"Which score set is being served","returns":"object","unmetered":true,"params":[],"fields":[{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `epss`."},{"name":"score_date","type":"date","nullable":false,"desc":"The day of the newest score set held. After a clean run every row shares it; after a run that failed mid-batch some rows are older, so this is **the newest day we hold** and each row's own `score_date` stays the authority for that row."},{"name":"score_at","type":"timestamp","nullable":true,"desc":"The instant FIRST's file states for that day's scores, in UTC — `2026-10-06T12:00:23Z`. From the same receipt as `file_bytes` and `row_count`, so **null** in the same cases, and for a stamp with no time zone."},{"name":"model_version","type":"string","nullable":false,"desc":"The model that produced that day's scores, `v2026.06.15`."},{"name":"upstream_url","type":"string","nullable":false,"desc":"The dated file, resolved past the redirect chain. **This is the address of the bulk data**: if you want every score rather than a hundred, fetch this from FIRST once a day rather than paging an API."},{"name":"checksum_sha256","type":"string","nullable":false,"desc":"`sha256` of that file's gzipped bytes as served. Verify that what we served is what FIRST published."},{"name":"file_bytes","type":"integer","nullable":true,"desc":"How many bytes that file was **as served** — the gzipped bytes `checksum_sha256` is over, so the size of your download of `upstream_url` is this number. Read from the receipt the daily run posts after its last score, and only when that receipt names this day AND this checksum. **null when the run stopped part-way** — it posts no receipt, and some rows then still carry the previous day — or the day was stored before receipts existed. Never a size measured from another file."},{"name":"row_count","type":"integer","nullable":true,"desc":"How many scores that file listed: the size of the scored CVE corpus, 383,485 on 2026-10-06. From the same receipt as `file_bytes`, and **null** in the same cases. It counts the file; after a clean run it is also the number of rows served at `score_date`, and `stale` is what says whether the run was clean."},{"name":"stale","type":"boolean","nullable":false,"desc":"**`true` when a daily file was missed or landed only in part.** It is measured from the newest day that landed **whole** — every score stored and served, then the receipt — and not from `score_date`: a run that stops part-way moves some rows to the new day, leaves the rest on the old one, and counts as a missed day. The flag turns `true` when that day is older than the day before today in UTC, counted from 03:00 UTC. The run reads FIRST's file at 02:00 UTC, so after a good run the newest whole day is yesterday's, all day — which is why the flag does not flip at midnight. `true` means our run failed or stopped part-way, or FIRST had not yet published: the scores served are then all or partly the prior file's, and **each row's own `score_date` says which day it is for**."},{"name":"licence","type":"string","nullable":false,"desc":"SPDX identifier, `CC-BY-4.0`. **This is an attribution obligation that travels with the data**, not a note about our terms: if you publish these scores, reproduce `attribution`. FIRST's own EPSS FAQ (https://www.first.org/epss/faq) says the scores are free to use, with no registration, and that attribution is requested when EPSS data is used in publications or products."},{"name":"attribution","type":"string","nullable":false,"desc":"The string to reproduce, FIRST's own and carried verbatim. An attribution somebody rewrote is not the attribution that was asked for."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored that snapshot."}]},{"id":"lookup-nvd","verb":"GET","path":"/v1/cyber/nvd","summary":"Look up NVD's record of a list of CVEs","returns":"array","params":[{"name":"cve","in":"query","type":"string","required":true,"desc":"**Required.** Up to 100 CVE ids, comma-separated — `CVE-2026-63077,CVE-2025-1976` — or the parameter repeated, which means the same thing. Case is normalised; anything that is not a CVE id is a `400` naming it, and a 101st id is a `400` naming the count rather than a silent truncation.\n\n**What is held is NVD's record of every CVE in CISA's KEV catalog**, read every night — not NVD's whole corpus. An id in `meta.not_found` is one this platform does not hold: outside the catalog, or a catalog CVE NVD has no record of (`GET /v1/cyber/nvd/partitions` names those). **It never means NVD has no record of the CVE.** `data` holds the records found, in the order their ids were sent.","example":"CVE-2026-63077,CVE-2026-00000"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`\u003csource\u003e:\u003cCVE id\u003e`, `nvd:CVE-2026-63077`. Opaque. There is no `GET /v1/cyber/nvd/{id}`: one CVE is a lookup of one, `?cve=CVE-2026-63077`."},{"name":"source","type":"string","nullable":false,"desc":"The scraper that read it, `nvd`: NVD's CVE API."},{"name":"cve_id","type":"string","nullable":false,"desc":"NVD's `cve.id`, upper-case as published."},{"name":"published","type":"string","nullable":false,"desc":"NVD's `cve.published`, **exactly as NVD prints it** — `2026-07-27T17:16:38.830`, UTC with no zone written. A string, not a parsed instant: the first ten characters are the date."},{"name":"last_modified","type":"string","nullable":false,"desc":"NVD's `cve.lastModified`, printed the same way. **This moves whenever NVD revises the record**, and the nightly run reads every CVE again, so a revision NVD makes to an old CVE reaches this field within a day."},{"name":"vuln_status","type":"string","nullable":true,"desc":"NVD's `vulnStatus` — `Analyzed`, `Modified`, `Undergoing Analysis`, `Rejected`, … A **Rejected** CVE is served as Rejected, never dropped: deciding what to do with it is yours."},{"name":"description","type":"string","nullable":true,"desc":"The first English description NVD holds; null when there is none."},{"name":"cvss_version","type":"string","nullable":true,"desc":"The selected metric's `cvssData.version`: `4.0`, `3.1`, `3.0` or `2.0`. The selection is **the first entry of the first non-empty family of `cvssMetricV40`, `cvssMetricV31`, `cvssMetricV30`, `cvssMetricV2`** — whoever published it; `cvss_source` and `cvss_type` say whose it is. null when NVD holds no CVSS metric (a Rejected CVE carries none)."},{"name":"cvss_confidence","type":"string","nullable":true,"desc":"**Ours, not NVD's**: `legacy` when the selected family is v2, `primary` when it is a modern one, null when there is none. `primary` means a modern CVSS version — **not** NVD's `type: Primary`, which is `cvss_type`."},{"name":"cvss_base_score","type":"number","nullable":true,"desc":"The selected metric's `cvssData.baseScore`, as NVD wrote it."},{"name":"cvss_severity","type":"string","nullable":true,"desc":"The selected metric's `cvssData.baseSeverity` — `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`. CVSS v2 keeps its severity outside `cvssData`, so this is **null for a legacy selection**, as #169's contract reads it."},{"name":"cvss_vector","type":"string","nullable":true,"desc":"The selected metric's `cvssData.vectorString`, e.g. `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H`."},{"name":"cvss_attack_vector","type":"string","nullable":true,"desc":"`cvssData.attackVector`, **NVD's raw vocabulary, never normalised**: `NETWORK`, `ADJACENT_NETWORK` (CVSS 3.x), `ADJACENT` (CVSS 4.0), `LOCAL`, `PHYSICAL`. Folding 4.0's spelling into 3.x's is your decision to make. **null for a legacy (v2) selection** — ours, the legacy rule: v2's `accessVector` vocabulary is never remapped onto these names."},{"name":"cvss_attack_complexity","type":"string","nullable":true,"desc":"`cvssData.attackComplexity`, raw; null for a legacy selection."},{"name":"cvss_privileges_required","type":"string","nullable":true,"desc":"`cvssData.privilegesRequired`, raw; null for a legacy selection."},{"name":"cvss_user_interaction","type":"string","nullable":true,"desc":"`cvssData.userInteraction`, raw — CVSS 4.0 adds `PASSIVE` and `ACTIVE` to 3.x's `NONE` and `REQUIRED`; null for a legacy selection."},{"name":"cvss_source","type":"string","nullable":true,"desc":"Who published the selected metric — `nvd@nist.gov`, or a CNA's address or UUID. For CVE-2026-63077 it is `cve@jetbrains.com`, because NVD lists the CNA's metric first."},{"name":"cvss_type","type":"string","nullable":true,"desc":"NVD's `type` for the selected metric: `Primary` (NVD's own assessment) or `Secondary` (a CNA's or ADP's). **About 45% of the catalog's CVEs select a Secondary metric** under the first-entry rule; this field is what lets you prefer NVD's own on purpose."},{"name":"metric_keys_present","type":"array of string","nullable":false,"desc":"Every metric family NVD holds an entry in, selected or not, sorted — `cvssMetricV2`, `cvssMetricV31`, `ssvcV203` (CISA's SSVC decision, on every catalog CVE since 2026), …. A family the selection does not read is listed here rather than ignored in silence."},{"name":"cwes_raw","type":"array of string","nullable":false,"desc":"Every English weakness value, in NVD's order, **placeholders included verbatim**: `NVD-CWE-noinfo` and `NVD-CWE-Other` arrive as NVD wrote them (379 of the 1,739 catalog CVEs on 2026-10-09). A CWE two sources both list is listed twice. `[]` when NVD lists none."},{"name":"references","type":"array of string","nullable":false,"desc":"Every reference url, in NVD's order. Unbounded — one catalog CVE lists 341. `[]` when NVD lists none."},{"name":"cisa_exploit_add","type":"string","nullable":true,"desc":"NVD's `cisaExploitAdd`, the day CISA added the CVE to KEV, as NVD carries it."},{"name":"cisa_action_due","type":"string","nullable":true,"desc":"NVD's `cisaActionDue`, the KEV remediation deadline, verbatim."},{"name":"cisa_required_action","type":"string","nullable":true,"desc":"NVD's `cisaRequiredAction`, verbatim."},{"name":"cisa_vulnerability_name","type":"string","nullable":true,"desc":"NVD's `cisaVulnerabilityName`, verbatim."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"**The as-of to quote**: the newest read of NVD that returned exactly this record. It moves on every nightly read that finds the record unchanged, so a value more than a day or two old is a record the nightly run could not read — `GET /v1/cyber/nvd/partitions` counts those per year."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored or confirmed the record."}]},{"id":"get-nvd-partitions","verb":"GET","path":"/v1/cyber/nvd/partitions","summary":"Where the NVD records came from, year by year, and the terms","returns":"object","params":[],"fields":[{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `nvd`."},{"name":"scope","type":"string","nullable":false,"desc":"`kev`: what is held is NVD's record of every CVE in CISA's Known Exploited Vulnerabilities catalog, **not NVD's whole corpus**. A CVE outside it is not held, and that is all a miss on `GET /v1/cyber/nvd` says about it."},{"name":"run","type":"string","nullable":false,"desc":"The newest run's name: the instant it began reading NVD, `20261009T020003Z`."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"That instant, as a timestamp."},{"name":"read","type":"integer","nullable":false,"desc":"How many CVEs that run read."},{"name":"missing","type":"integer","nullable":false,"desc":"How many of those it could not store as read — a record this platform refused. They serve an earlier read, and the next run fails once saying so. 0 on a clean run."},{"name":"flagged","type":"integer","nullable":true,"desc":"NVD's own count of the CVEs it flags as in KEV (`?hasKev`) — the paged read's total."},{"name":"catalog_version","type":"string","nullable":true,"desc":"CISA's catalog version as that run read it — every CVE it lists that NVD had not flagged yet was read by its id. null when the catalog could not be read."},{"name":"catalog_count","type":"integer","nullable":true,"desc":"How many CVEs that catalog lists; null with `catalog_version`."},{"name":"not_in_nvd","type":"array of string","nullable":false,"desc":"Every catalog CVE **NVD holds no record of**, by name. Not held and not served — a lookup names them in `meta.not_found`. `[]` on an ordinary night."},{"name":"unread","type":"integer","nullable":true,"desc":"How many CVEs that run owed a read by id and did not make — capped per run, or stopped by NVD refusing — so their records serve an older read. 0 on a clean run."},{"name":"unknown_metric_keys","type":"array of string","nullable":false,"desc":"Metric families present on a record read that the selection does not know — a family NVD added that nobody has decided about yet. `[]` today."},{"name":"held","type":"integer","nullable":false,"desc":"The CVEs served: the partitions' `record_count`, summed."},{"name":"unconfirmed","type":"integer","nullable":false,"desc":"Records the newest run did not confirm, summed over the partitions: **0 means every record served was read by the newest run.**"},{"name":"licence","type":"string","nullable":false,"desc":"`public_domain_us_government`: NVD is a work of the United States Government, in the public domain in the US (17 U.S.C. 105). Nothing to pass on."},{"name":"attribution","type":"string","nullable":false,"desc":"`National Institute of Standards and Technology (NIST NVD)`, for a source line. The records are NVD's verbatim except `cvss_confidence` and the legacy nulling, which are this platform's and documented as such."},{"name":"notice","type":"string","nullable":false,"desc":"`This product uses the NVD API but is not endorsed or certified by the NVD.` — the sentence NVD's API terms (https://nvd.nist.gov/developers/terms-of-use) ask a service built on the API to display prominently. If you build on these records, display it."},{"name":"partitions","type":"array","nullable":false,"desc":"One per CVE-id year held, oldest year first — see the `partitions[].` rows."},{"name":"partitions[].year","type":"integer","nullable":false,"desc":"The year in the CVE id — `2002` for `CVE-2002-0367`, whenever it was published."},{"name":"partitions[].record_count","type":"integer","nullable":false,"desc":"The CVEs of that year held and served."},{"name":"partitions[].max_last_modified","type":"string","nullable":false,"desc":"The newest `last_modified` NVD printed on any of them, verbatim — how far NVD's revisions of the year reach."},{"name":"partitions[].fetched_at","type":"timestamp","nullable":false,"desc":"The **oldest** read still served for the year: every record of it was read from NVD at or after this instant. On a clean night it is the newest run's."},{"name":"partitions[].unconfirmed","type":"integer","nullable":false,"desc":"How many of the year's records the newest run did not confirm — not read, read and refused, or no longer returned — and so serve an older read. **0 is the year refreshed whole; anything else is a partition that failed to refresh, reported** rather than skipped."},{"name":"partitions[].content_sha256","type":"string","nullable":false,"desc":"A fingerprint of the year's records **as this platform holds them**: sha256 over the lines `\u003cCVE id\u003e:\u003crecord digest\u003e`, in byte order of the id, joined by a newline. It changes exactly when a record of the year changes. **Not a checksum of any file NVD publishes** — this platform reads NVD's API, not its year feeds — and not recomputable from the responses, which do not carry the per-record digests."},{"name":"partitions[].source_url","type":"string","nullable":true,"desc":"The request the newest run read the held set from: `https://services.nvd.nist.gov/rest/json/cves/2.0?hasKev`."}]},{"id":"lookup-cvelist","verb":"GET","path":"/v1/cyber/cvelist","summary":"Stored CVE Program records with exact bytes and provenance","returns":"array","params":[{"name":"cve","in":"query","type":"string","required":true,"desc":"Up to 100 CVE IDs, comma-separated or repeated. Every requested ID is answered in order. Responses are capped at 16 MiB; split a large batch into smaller requests.","example":"CVE-2024-21182"}],"fields":[{"name":"cve_id","type":"string","nullable":false,"desc":"Caller CVE identifier, normalized to uppercase."},{"name":"source","type":"string","nullable":false,"desc":"cvelist, the ingest source."},{"name":"status","type":"string","nullable":false,"desc":"found, not_found (a stored upstream 404), failed or unfetched. Unfetched also covers a suppressed record; it makes no claim about CVE.org."},{"name":"found","type":"boolean","nullable":false,"desc":"Whether this platform holds a usable record, including a last-good record after a failed refresh."},{"name":"record","type":"object","nullable":true,"desc":"The entire upstream CVE JSON 5 object, including all CNA and ADP containers and unknown fields. Original UTF-8 bytes, including whitespace, are retained inside this value."},{"name":"record_sha256","type":"string","nullable":true,"desc":"SHA-256 of the original upstream UTF-8 bytes, before any JSON normalization."},{"name":"commit_sha","type":"string","nullable":true,"desc":"Full GitHub commit SHA the file was fetched from. Null for the live CVE Services fallback, whose content is not attributed to a Git commit."},{"name":"source_url","type":"string","nullable":true,"desc":"Actual source URL, pinned to the commit for GitHub records."},{"name":"retrieved_at","type":"datetime","nullable":true,"desc":"UTC time these exact source bytes were retrieved. An unchanged check or failed refresh keeps the original retrieval provenance."},{"name":"error","type":"string","nullable":true,"desc":"Named latest acquisition failure. A transport error remains distinct from an upstream 404; a failed refresh can coexist with a last-good record."},{"name":"institution","type":"string","nullable":false,"desc":"CVE.org / MITRE."},{"name":"copyright","type":"string","nullable":false,"desc":"MITRE copyright designation, required on redistributed copies."},{"name":"licence","type":"string","nullable":false,"desc":"The full CVE usage grant, reproduced with every result. This source is not CC0."},{"name":"terms_url","type":"string","nullable":false,"desc":"Official CVE Program Terms of Use."}]}]},{"slug":"climate","name":"Climate Data API","status":"retired","operations":[{"id":"list-cckp-climatology","verb":"GET","path":"/v1/climate/cckp/climatology","summary":"List World Bank CCKP climatologies","returns":"array","params":[{"name":"geo","in":"query","type":"string","required":false,"desc":"ISO 3166-1 alpha-3, case-insensitive. `AZE` is the one stored.","example":"AZE"},{"name":"collection","in":"query","type":"string","required":false,"desc":"`cru-x0.5` or `era5-x0.25`.","example":"cru-x0.5"},{"name":"variable","in":"query","type":"string","required":false,"desc":"`tas` (temperature) or `pr` (precipitation).","example":"tas"},{"name":"aggregation","in":"query","type":"string","required":false,"desc":"`annual` or `monthly`.","example":"monthly"},{"name":"period","in":"query","type":"string","required":false,"desc":"`1991-2020`.\n\n**Every filter here is closed, `source` included**: a value outside the stored set is a `400` that lists the stored values, not an empty page. An empty page for `?variable=tasmax` would read as \"CCKP has no maximum temperature for Azerbaijan\" — and `tasmax` is a real CCKP code, just not a stored series. `/v1/climate/cckp/vocabulary` lists every code.\n\n**One value per filter, not a list.** A filter given twice is a `400` as well: read for its first value, `?variable=tas\u0026variable=pr` would answer the `tas` rows and drop `pr` without a word. Leave a filter out to get every stored value; the eight series fit on one page.","example":"1991-2020"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `ingested_at`, each optionally prefixed with `-` to reverse. Default is `-ingested_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-ingested_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, `\u003csource\u003e:\u003cgeo\u003e:\u003cresolved query\u003e` — `cckp:AZE:cru-x0.5_climatology_tas_climatology_monthly_1991-2020_mean_historical_cru_ts4.07_mean`. Opaque, and long because it IS the query: an id in a log says what was asked."},{"name":"source","type":"string","nullable":false,"desc":"The scraper that read it, `cckp`."},{"name":"geo","type":"string","nullable":false,"desc":"The ISO 3166-1 alpha-3 country the query was asked for. `AZE` is the one stored."},{"name":"collection","type":"string","nullable":false,"desc":"The dataset CCKP computed the values from: `cru-x0.5` (CRU TS gridded station observations, 0.5°) or `era5-x0.25` (the ERA5 reanalysis, 0.25°). Two independent estimates of the same climate; they differ, and both are served."},{"name":"variable","type":"string","nullable":false,"desc":"`tas`, mean near-surface air temperature, or `pr`, precipitation."},{"name":"aggregation","type":"string","nullable":false,"desc":"`annual` (one value) or `monthly` (twelve)."},{"name":"period","type":"string","nullable":false,"desc":"The span averaged over, `1991-2020`."},{"name":"percentile","type":"string","nullable":false,"desc":"`mean`. Observational collections take no other — an ensemble would be `median` — and getting this one code wrong is what returned nothing for the pipeline this replaces."},{"name":"scenario","type":"string","nullable":false,"desc":"`historical`. A projection would carry an SSP here."},{"name":"resolved_query","type":"string","nullable":false,"desc":"The eleven-code query CCKP was asked, verbatim. **The audit trail**: put it in `upstream_url`'s shape and you see what we saw. `GET /v1/climate/cckp/vocabulary` explains every code in it."},{"name":"upstream_url","type":"string","nullable":false,"desc":"The URL that was read, query and geo included."},{"name":"unit","type":"string","nullable":false,"desc":"`C` (degrees Celsius) for `tas`, `mm` for `pr`. A monthly `pr` value is that month's total and an annual one the year's: CRU's twelve monthly values sum to its annual value. CCKP's answer states no unit; these are the portal's."},{"name":"values","type":"array","nullable":false,"desc":"The points the query last returned, in calendar order: `month` (1–12 on a monthly climatology, null on an annual one), `value` (in `unit`, unrounded) and `upstream_key` (CCKP's own label, `YYYY-MM`, where YYYY is the period's FIRST year — `1991-07` is the ANNUAL value of 1991–2020, not July 1991).\n\n**`[]` with a `reason` is not \"no climate data\".** It is CCKP answering this query with nothing, which it does for any code one token wrong. Read `reason`.\n\nThe values are the LAST ones the query returned: a later answer without values leaves them standing and moves `reason` — a 1991–2020 climatology does not change. `retrieved_at` dates them."},{"name":"upstream_status","type":"string","nullable":true,"desc":"CCKP's `metadata.status` for the LATEST answer, verbatim — `success` or `error`."},{"name":"upstream_messages","type":"array","nullable":false,"desc":"That answer's message array, verbatim — where CCKP explains an error, e.g. `[\"Number of parameters mismatch. Needed: 11. Given: 3\"]`. `[]` on a success."},{"name":"reason","type":"string","nullable":true,"desc":"Null when the latest answer had values. Otherwise one of two, and **neither is a statement about the country's climate**:\n\n`upstream_returned_no_rows` — CCKP answered `success` with no data. It does that for EVERY well-formed query it holds nothing under — a wrong code, a code from the wrong family, a country it does not know — and cannot say which: a query-construction error. `upstream_error` — CCKP answered with a status other than `success`; `upstream_messages` says why."},{"name":"retrieved_at","type":"timestamp","nullable":true,"desc":"When `values` were read: the last answer that had values. Null for a query that has never returned any, and never advanced by an answer without them. Compare it with `ingested_at`, the latest answer of any kind."},{"name":"licence","type":"string","nullable":false,"desc":"SPDX identifier, `CC-BY-4.0`. An attribution obligation that travels with the data: if you publish these values, reproduce `attribution`."},{"name":"attribution","type":"string","nullable":false,"desc":"`World Bank Group`, the attribution the portal asks for."},{"name":"attribution_url","type":"string","nullable":false,"desc":"The Climate Change Knowledge Portal, `https://climateknowledgeportal.worldbank.org/`."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When the latest answer was stored — this month's run, values or not. Same name and meaning as on every other collection here."}]},{"id":"get-cckp-vocabulary","verb":"GET","path":"/v1/climate/cckp/vocabulary","summary":"CCKP's query grammar and codes","returns":"object","params":[],"fields":[{"name":"grammar","type":"string","nullable":false,"desc":"The eleven slots in the order a query joins them with `_`: `{collection}_{type}_{variable}_{product}_{aggregation}_{period}_{percentile}_{scenario}_{model}_{model_calculation}_{statistic}`."},{"name":"segments","type":"integer","nullable":false,"desc":"How many codes a query carries: 11. CCKP's own error names the number."},{"name":"upstream_format","type":"string","nullable":false,"desc":"The format CCKP prints for any malformed query, verbatim. It names TWELVE codes — `{grid-code}` is the one no query that returns values sends — while the same answer says eleven are needed."},{"name":"url_template","type":"string","nullable":false,"desc":"Where a query is read: `https://cckpapi.worldbank.org/cckp/v1/{query}/{geo}?_format=json`. No key."},{"name":"slots","type":"array","nullable":false,"desc":"The eleven positions — `position`, `name` and `codes`, each code with its `meaning` — and the codes each has been seen taking in a query that returned values."},{"name":"rules","type":"array","nullable":false,"desc":"The constraints between slots that decide whether a well-formed query returns values, as sentences. The percentile and model rules are the whole of what made CCKP look empty."},{"name":"combinations","type":"array","nullable":false,"desc":"Queries run against CCKP and what each returned: `query`, `geo`, `value_count` (0 is the 75-byte empty success), `stored` (whether `/v1/climate/cckp/climatology` serves it), `observed_on` and a `note`. The ones that returned nothing are here on purpose: each shows a rule failing."},{"name":"licence","type":"string","nullable":false,"desc":"SPDX identifier, `CC-BY-4.0`."},{"name":"attribution","type":"string","nullable":false,"desc":"`World Bank Group`."},{"name":"attribution_url","type":"string","nullable":false,"desc":"`https://climateknowledgeportal.worldbank.org/`."}]},{"id":"get-climate-districts","verb":"GET","path":"/v1/climate/districts","summary":"Azerbaijan's 78 districts, the boundary set climate totals are summed over","returns":"object","params":[{"name":"boundary_set","in":"query","type":"string","required":false,"desc":"The set to serve. Omitted, the current one, `az-adm2-gb-9469f09-simplified-naftalan-osm-3812595v20` — the only one built today. A set's name names one geometry forever, so naming it pins what you hold. ONE name: a comma list or a repeated `?boundary_set=` is a `400`, and an unknown name a `404` that lists the sets served.","example":"az-adm2-gb-9469f09-simplified-naftalan-osm-3812595v20"}],"fields":[{"name":"boundary_set","type":"string","nullable":false,"desc":"The set's name, `az-adm2-gb-9469f09-simplified-naftalan-osm-3812595v20`: its inputs and their versions, spelled out. **A name names one geometry, forever** — this platform refuses a set re-drawn under a name it holds, so a new outline is a new name served beside this one. Every climate total summed over the set carries the same name."},{"name":"source","type":"string","nullable":false,"desc":"The scraper that built it, `districts`."},{"name":"country","type":"string","nullable":false,"desc":"ISO 3166-1 alpha-3, `AZE`."},{"name":"district_count","type":"integer","nullable":false,"desc":"How many districts the set holds: 78. Always the number of features in `feature_collection` — a set is stored whole or not at all."},{"name":"geometry_sha256","type":"string","nullable":false,"desc":"sha256 over every district's `code`, `name`, `shape_id` and `geometry`, in the set's order, as canonical JSON — sorted keys, no whitespace, `json.dumps(..., sort_keys=True, separators=(\",\", \":\"))` in Python over those four keys of each feature. `2f781726…ad33` for this set. What the one-name-one-geometry rule above is enforced on."},{"name":"area_method","type":"string","nullable":true,"desc":"How `area_km2` is measured: geodesic on the WGS84 ellipsoid (Karney's algorithm, as geographiclib and PROJ's geod run it), exterior rings less holes, to 0.001 km²."},{"name":"inputs","type":"array","nullable":false,"desc":"The two pinned public inputs, each with its URL, its version, its size and sha256 where it is a file, and its `licence` and `attribution`: geoBoundaries gbOpen AZE ADM2, the simplified geometry, at release commit `9469f09` (CC BY-SA 3.0), and OpenStreetMap relation 3812595 at version 20 with its way and node versions (ODbL 1.0). With `operations`, enough to rebuild the set and check it against `geometry_sha256`."},{"name":"operations","type":"array","nullable":false,"desc":"What was done to the inputs, one sentence each, in order: the stray second `Lankaran District` feature dropped, the code rule, Naftalan's polygon replaced by OpenStreetMap's, Naftalan's hole moved in Goranboy, and the area method."},{"name":"built_at","type":"timestamp","nullable":false,"desc":"When the set was built from its pinned inputs and stored. The monthly run rebuilds the set and checks it against its digest every time, and sends it again only when this platform does not already hold it — so this moves only when the set is posted again."},{"name":"licence","type":"string","nullable":false,"desc":"The set's terms as an SPDX expression, from its inputs: `CC-BY-SA-3.0 AND ODbL-1.0`. **Share-alike applies to the outlines**: if you publish them, or a map made from them, publish it under the same terms. Each feature carries the terms of its own outline, which is narrower."},{"name":"attribution","type":"string","nullable":false,"desc":"The credit lines the inputs require, joined with `; ` — geoBoundaries' and OpenStreetMap's. Reproduce it with the outlines."},{"name":"feature_collection","type":"object","nullable":false,"desc":"The set as a GeoJSON FeatureCollection (RFC 7946), one Feature per district **in the set's order**, which is #130's own order and the order the districts are burned onto a raster in: where two outlines both claim a pixel's centre, the earlier one has it. Each feature's `id` is its `code`, its `geometry` a Polygon or MultiPolygon in WGS 84 longitude/latitude at the inputs' own precision, every exterior counter-clockwise and every hole clockwise. About 300 KB. See `feature_collection.features[].properties.*`.\n\n**A public rebuild, not any consumer's own file.** #130 asked for its consumer's `districts.geojson`, which is not public; this is the set the owner chose instead (2026-10-03): geoBoundaries less one stray feature, Naftalan from OpenStreetMap — geoBoundaries draws it about 6 km south of the town, on empty land — and the enclave's hole moved in Goranboy to match. Summed over it, GHSL's built-up surface lands on every number #130 prints, but no byte parity with that file is claimed: a district drawn differently there would show as a value that moves."},{"name":"feature_collection.type","type":"string","nullable":false,"desc":"`FeatureCollection`."},{"name":"feature_collection.features","type":"array","nullable":false,"desc":"One Feature per district, in the set's order."},{"name":"feature_collection.features[].type","type":"string","nullable":false,"desc":"`Feature`."},{"name":"feature_collection.features[].id","type":"string","nullable":false,"desc":"The district's `code`."},{"name":"feature_collection.features[].properties","type":"object","nullable":false,"desc":"See `feature_collection.features[].properties.*`."},{"name":"feature_collection.features[].geometry","type":"object","nullable":false,"desc":"A GeoJSON Polygon or MultiPolygon, as the set was built: no coordinate rounded, reprojected or simplified beyond what the input itself is."},{"name":"feature_collection.features[].properties.code","type":"string","nullable":false,"desc":"The slug every climate total over the set is keyed by — `khachmaz-district`, `baku-city`: geoBoundaries' `shapeName` lower-cased, every run of other characters one hyphen. Exactly #130's 78 codes."},{"name":"feature_collection.features[].properties.name","type":"string","nullable":false,"desc":"geoBoundaries' `shapeName`, `Khachmaz District`."},{"name":"feature_collection.features[].properties.shape_id","type":"string","nullable":true,"desc":"geoBoundaries' `shapeID`, `54616110B89497242950609`."},{"name":"feature_collection.features[].properties.area_km2","type":"number","nullable":false,"desc":"Geodesic area in km², to 0.001: exterior rings less holes. 85,725.930 km² over the 78 — the simplified outlines' area, not an official figure."},{"name":"feature_collection.features[].properties.drawn_from","type":"string","nullable":false,"desc":"Whose rings these are: `geoboundaries` for 76 districts, `openstreetmap` for `naftalan-city`, and `geoboundaries+openstreetmap` for `goranboy-district` — geoBoundaries' exterior with OpenStreetMap's Naftalan ring as its hole."},{"name":"feature_collection.features[].properties.licence","type":"string","nullable":false,"desc":"SPDX expression for this outline: `CC-BY-SA-3.0`, `ODbL-1.0`, or both joined with `AND` for Goranboy."},{"name":"feature_collection.features[].properties.attribution","type":"string","nullable":false,"desc":"The credit line this outline requires, verbatim."}]},{"id":"get-ghsl-builtup-districts","verb":"GET","path":"/v1/climate/ghsl/builtup/districts","summary":"GHSL built-up surface per district, 2010 and 2020","returns":"object","params":[{"name":"country","in":"query","type":"string","required":false,"desc":"`AZ` or `AZE`, case-insensitive — #130's own `?country=az`. Azerbaijan is the one country covered, and any other is a `400` rather than Azerbaijan's numbers under its name.","example":"az"},{"name":"epochs","in":"query","type":"string","required":false,"desc":"Comma-separated years, ascending in the answer whatever the order asked: `2010,2020`, both, by default. Each must be an epoch the release holds; another is a `400` naming them, never an answer without it.","example":"2010,2020"},{"name":"release","in":"query","type":"string","required":false,"desc":"The GHSL release, `R2023A` by default — the only global GHS-BUILT-S release JRC publishes, checked monthly. A newer release is served BESIDE it, never over it: every value changes between releases, and a request that names none keeps answering R2023A. An unknown release is a `404` that lists what is served.","example":"R2023A"},{"name":"boundary_set","in":"query","type":"string","required":false,"desc":"The district set the totals were summed over — the current one, `az-adm2-gb-9469f09-simplified-naftalan-osm-3812595v20`, by default. `GET /v1/climate/districts` serves its outlines.\n\n**One value per parameter.** A parameter given twice is a `400`: read for its first value, it would answer part of the question without saying so.","example":"az-adm2-gb-9469f09-simplified-naftalan-osm-3812595v20"}],"fields":[{"name":"source","type":"string","nullable":false,"desc":"`European Commission / JRC Global Human Settlement Layer (GHSL), GHS-BUILT-S R2023A` — the contract's own string, verbatim. The credit line to display is `meta.attribution`."},{"name":"resolution_arcsec","type":"number","nullable":false,"desc":"`3`: the resolution of the GHSL tiles the totals were read from. Not the analysis grid's, which is 15 — `meta.grid`."},{"name":"epochs","type":"array of integer","nullable":false,"desc":"The epochs in the answer, ascending, **as integers**: `[2010, 2020]`. The keys of `built_up_km2_by_district` are the same years as STRINGS — two types for one concept, kept exactly as #130's consumer reads them."},{"name":"units","type":"string","nullable":false,"desc":"`km^2 built-up surface`."},{"name":"built_up_km2_by_district","type":"object","nullable":false,"desc":"Epoch (a STRING key, `\"2010\"`) to district code to km² of built-up surface, **every one of the set's 78 districts under every epoch, never null**, in the set's order. A district's value is the built-up surface of every 15-arc-second grid pixel whose CENTRE falls inside it — so the 78 sum to a little less than the grid (722.19 km² of 1,356.45 in 2010), which is correct: pixels on no district belong to none.\n\nExact to the square metre: GHSL's uint16 square metres are summed as integers, 5 × 5 tile pixels to a grid pixel, and divided by 1e6 once, so every value has at most six decimals. National totals 722.187192 (2010) and 848.616301 (2020); `baku-city` 162.226699 and 197.708859.\n\n**Exact over the mosaic #130's consumer built, not an area-weighted split.** The four tiles are mosaicked as rasterio.merge does (`meta.method`, version `1`). GHSL's pixel centres sit on the 3-arc-second lattice the grid's edges lie on, so a tile pixel centred on a grid edge straddles two grid pixels; the mosaic moves every tile pixel half a pixel north-west, and that one counts wholly in ONE of them rather than being shared. The convention is what reproduces #130's numbers. Measured over the same tiles, an exact area-weighted split would move `shusha-city` by about 15%, `shusha-district` and `jabrayil-district` by about 7%, nine of the 78 districts by more than 1%, and each national total by 0.02%.\n\n**Built-up surface is not built-up growth's cause, and not night light.** GHSL's epochs are 2010 and 2020 and its resolution 3 arc-seconds; a comparison with another series at other years or another grid is descriptive, never pixel-matched or causal."}]},{"id":"get-ghsl-builtup-zones","verb":"GET","path":"/v1/climate/ghsl/builtup/zones","summary":"GHSL built-up surface inside a named set of zones, such as Baku's eight wedges","returns":"object","params":[{"name":"set","in":"query","type":"string","required":true,"desc":"**Required.** The zone set: `baku-sectors`, eight compass wedges of 45° within 45 km of Baku's centre (49.8362 E, 40.3656 N), the first straddling north. ONE name: a comma list or a repeated `?set=` is a `400`, and an unknown name a `404` that lists the sets computed.","example":"baku-sectors"},{"name":"epochs","in":"query","type":"string","required":false,"desc":"Comma-separated years, `2010,2020` by default. Each must be an epoch the release holds.","example":"2010,2020"},{"name":"release","in":"query","type":"string","required":false,"desc":"The GHSL release, `R2023A` by default. An unknown one is a `404` that lists what is served.","example":"R2023A"}],"fields":[{"name":"zone_set","type":"string","nullable":false,"desc":"The set's name, `baku-sectors`."},{"name":"definition","type":"object","nullable":false,"desc":"How the zones are drawn, as the computation recorded it. For `baku-sectors`: `centre` (49.8362 E, 40.3656 N), `radius_km` 45, `sectors` 8 of `sector_width_deg` 45 from `first_sector_from_deg` -22.5, the `distance` rule — equirectangular about the centre, 111.32 km per degree of latitude and 111.32 × cos(40.3656°) per degree of longitude — and `membership`: a grid pixel is in a wedge when its CENTRE is, land, sea and every district alike. The wedges are not districts and cannot be rebuilt from district totals at any accuracy; Baku City is one polygon."},{"name":"epochs","type":"array of integer","nullable":false,"desc":"The epochs in the answer, ascending, as integers."},{"name":"units","type":"string","nullable":false,"desc":"`km^2 built-up surface`."},{"name":"zones","type":"array","nullable":false,"desc":"Every zone of the set, in its order — N, NE, E, SE, S, SW, W, NW. See `zones[].*`."},{"name":"total_km2","type":"object","nullable":false,"desc":"The zones summed, per epoch (STRING keys): 194.295769 km² in 2010 and 237.336392 in 2020 — #130's 194.296 and 237.336. Summed in whole square metres, so exact over the mosaic `meta.method` describes: each tile pixel counted wholly in one grid pixel, as rasterio.merge places it."},{"name":"dominant_builtup_expansion_sector","type":"string","nullable":true,"desc":"The zone whose built-up surface grew most between the first and the last epoch in `epochs` — `NE`, by 15.954 km² — compared in whole square metres, the first in the set's order winning a tie. Null when one epoch is asked for: there is no growth to compare."},{"name":"zones[].zone_id","type":"string","nullable":false,"desc":"The zone's name within its set: `N`, `NE`, … `NW`."},{"name":"zones[].from_deg","type":"number","nullable":true,"desc":"Where a compass sector starts, in degrees clockwise from north. The first straddles north, so `N` runs from -22.5 to 22.5. Null for a zone that is not a sector."},{"name":"zones[].to_deg","type":"number","nullable":true,"desc":"Where it ends, the same way. Null for a zone that is not a sector."},{"name":"zones[].builtup_km2","type":"object","nullable":false,"desc":"Epoch (a STRING key) to km² of built-up surface inside the zone: `NE` is 65.261626 in 2010 and 81.216106 in 2020."}]}]},{"slug":"geo","name":"Geo Data API","status":"retired","operations":[{"id":"get-country-set","verb":"GET","path":"/v1/geo/countries","summary":"One set of country outlines, as a GeoJSON FeatureCollection","returns":"object","params":[{"name":"set","in":"query","type":"string","required":true,"desc":"**Required.** The named set of countries to serve. One today: `caucasus-caspian` — Russia, Georgia, Azerbaijan, Turkey, Armenia, Iran and Turkmenistan, the countries around the Caspian, in that order. ONE name: a comma list or a repeated `?set=` is a `400` rather than half an answer, and an unknown name is a `404` that lists the sets served.","example":"caucasus-caspian"}],"fields":[{"name":"set","type":"string","nullable":false,"desc":"The name of the FeatureCollection served: the set you asked for on `/v1/geo/countries` (`caucasus-caspian`), the sea's name on `/v1/geo/marine/{slug}` (`caspian-sea`). Lower-case words joined by hyphens."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper built the document. `naturalearth` is the rebuild from Natural Earth's own release files, and the only source on this dataset today."},{"name":"release","type":"string","nullable":false,"desc":"The Natural Earth release the document was built from — `v5.1.2`. **A tag, never a branch**: the build pins the tag and the size and sha256 of each upstream file, so a new Natural Earth release reaches this API only as a deliberate rebuild, never because upstream moved. v5.1.2 (2022-05-13) is the newest Natural Earth has published. Adopting a newer release moves it even when the document comes out of that release byte for byte the same: `sha256` and `rebuilt_at` then stay as they were, and the `ETag` moves with `release`."},{"name":"upstream_file","type":"string","nullable":false,"desc":"The Natural Earth file it was cut from, at that release — `ne_10m_admin_0_countries.geojson` or `ne_10m_geography_marine_polys.geojson`, in the `geojson/` directory of github.com/nvkelso/natural-earth-vector. 1:10m, Natural Earth's most detailed scale."},{"name":"sha256","type":"string","nullable":false,"desc":"`sha256` of the FeatureCollection **as built**: Python's `json.dumps` with its default `, ` and `: ` separators and ASCII escapes, no trailing newline. That is not the compact form this response carries — `json.dumps(json.loads(feature_collection))` in Python gives back the built bytes exactly, so this is checkable. At v5.1.2 it is `50b0cc03…8dfb` (1,131,431 bytes) for `caucasus-caspian` and `cb341003…b888` (9,851 bytes) for `caspian-sea`. Not the `ETag`, which is over what this response actually carries."},{"name":"rebuilt_at","type":"timestamp","nullable":false,"desc":"When these bytes were built and stored. **It moves only when the bytes do**: a re-run of the same release re-derives identical bytes and leaves it alone, so the `ETag` over this response does not change under you and a conditional `GET` keeps answering `304`."},{"name":"licence","type":"string","nullable":false,"desc":"`public-domain`. Natural Earth's own terms: every version of its vector data is in the public domain, for any use, commercial included, with no permission needed."},{"name":"attribution","type":"string","nullable":false,"desc":"`Made with Natural Earth.` — Natural Earth's own short citation, verbatim. Crediting is requested and **not required**, so unlike `attribution` on `/v1/cyber/epss/snapshot` this is a credit line you may reproduce rather than a condition you have to."},{"name":"feature_collection","type":"object","nullable":false,"desc":"The GeoJSON FeatureCollection (RFC 7946): WGS 84 longitude/latitude at Natural Earth's own precision, **not simplified, clipped, reprojected or rounded** — Russia keeps all 214 of its parts, Kamchatka and Kaliningrad included, because clipping at a box changes the answer at its edge.\n\nOn `/v1/geo/countries` each feature's `properties` is exactly `{name, iso_a3}`, in the order the release lists them — Russia, Georgia, Azerbaijan, Turkey, Armenia, Iran, Turkmenistan — and **that order is part of the answer**: take the first feature containing a point and a point on a shared border belongs to whichever comes first. `name` is FROZEN: features are chosen by ISO 3166 alpha-3 code and named from a fixed table, so `Turkey` stays `Turkey` when Natural Earth renames it.\n\nOn `/v1/geo/marine/{slug}` it holds one feature, Natural Earth's own, verbatim: every property — `wikidataid` is `Q5484` for the Caspian, and is how it is found — and its own `bbox`."}]},{"id":"get-marine-area","verb":"GET","path":"/v1/geo/marine/{slug}","summary":"One sea's outline, as a GeoJSON FeatureCollection","returns":"object","params":[{"name":"slug","in":"path","type":"string","required":true,"desc":"The sea, lower-case and hyphenated. One today: `caspian-sea`. An unknown one is a `404` that lists what is served.","example":"caspian-sea"}],"fields":[{"name":"set","type":"string","nullable":false,"desc":"The name of the FeatureCollection served: the set you asked for on `/v1/geo/countries` (`caucasus-caspian`), the sea's name on `/v1/geo/marine/{slug}` (`caspian-sea`). Lower-case words joined by hyphens."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper built the document. `naturalearth` is the rebuild from Natural Earth's own release files, and the only source on this dataset today."},{"name":"release","type":"string","nullable":false,"desc":"The Natural Earth release the document was built from — `v5.1.2`. **A tag, never a branch**: the build pins the tag and the size and sha256 of each upstream file, so a new Natural Earth release reaches this API only as a deliberate rebuild, never because upstream moved. v5.1.2 (2022-05-13) is the newest Natural Earth has published. Adopting a newer release moves it even when the document comes out of that release byte for byte the same: `sha256` and `rebuilt_at` then stay as they were, and the `ETag` moves with `release`."},{"name":"upstream_file","type":"string","nullable":false,"desc":"The Natural Earth file it was cut from, at that release — `ne_10m_admin_0_countries.geojson` or `ne_10m_geography_marine_polys.geojson`, in the `geojson/` directory of github.com/nvkelso/natural-earth-vector. 1:10m, Natural Earth's most detailed scale."},{"name":"sha256","type":"string","nullable":false,"desc":"`sha256` of the FeatureCollection **as built**: Python's `json.dumps` with its default `, ` and `: ` separators and ASCII escapes, no trailing newline. That is not the compact form this response carries — `json.dumps(json.loads(feature_collection))` in Python gives back the built bytes exactly, so this is checkable. At v5.1.2 it is `50b0cc03…8dfb` (1,131,431 bytes) for `caucasus-caspian` and `cb341003…b888` (9,851 bytes) for `caspian-sea`. Not the `ETag`, which is over what this response actually carries."},{"name":"rebuilt_at","type":"timestamp","nullable":false,"desc":"When these bytes were built and stored. **It moves only when the bytes do**: a re-run of the same release re-derives identical bytes and leaves it alone, so the `ETag` over this response does not change under you and a conditional `GET` keeps answering `304`."},{"name":"licence","type":"string","nullable":false,"desc":"`public-domain`. Natural Earth's own terms: every version of its vector data is in the public domain, for any use, commercial included, with no permission needed."},{"name":"attribution","type":"string","nullable":false,"desc":"`Made with Natural Earth.` — Natural Earth's own short citation, verbatim. Crediting is requested and **not required**, so unlike `attribution` on `/v1/cyber/epss/snapshot` this is a credit line you may reproduce rather than a condition you have to."},{"name":"feature_collection","type":"object","nullable":false,"desc":"The GeoJSON FeatureCollection (RFC 7946): WGS 84 longitude/latitude at Natural Earth's own precision, **not simplified, clipped, reprojected or rounded** — Russia keeps all 214 of its parts, Kamchatka and Kaliningrad included, because clipping at a box changes the answer at its edge.\n\nOn `/v1/geo/countries` each feature's `properties` is exactly `{name, iso_a3}`, in the order the release lists them — Russia, Georgia, Azerbaijan, Turkey, Armenia, Iran, Turkmenistan — and **that order is part of the answer**: take the first feature containing a point and a point on a shared border belongs to whichever comes first. `name` is FROZEN: features are chosen by ISO 3166 alpha-3 code and named from a fixed table, so `Turkey` stays `Turkey` when Natural Earth renames it.\n\nOn `/v1/geo/marine/{slug}` it holds one feature, Natural Earth's own, verbatim: every property — `wikidataid` is `Q5484` for the Caspian, and is how it is found — and its own `bbox`."}]},{"id":"list-places","verb":"GET","path":"/v1/geo/places","summary":"List stored places of one collection","returns":"array","params":[{"name":"collection","in":"query","type":"string","required":true,"desc":"One collection: azerbaijan, world-cities or world-countries. Required for lists and lookups.","example":"azerbaijan"},{"name":"kind","in":"query","type":"string","required":false,"desc":"Comma-separated place kinds; at most 20. Empty means all kinds.","example":"locality"},{"name":"source","in":"query","type":"string","required":false,"desc":"manual or the collection builder, openstreetmap or geonames.","example":"manual"},{"name":"bbox","in":"query","type":"string","required":false,"desc":"minLon,minLat,maxLon,maxLat; keeps points inside this WGS84 box. Crossing the antimeridian is not supported.","example":"49,40,51,41"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Page size, 1–100; default 25.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Pass meta.next unchanged. A cursor is bound to its collection and filters."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Source, collection and source record ID, separated by colons."},{"name":"source","type":"string","nullable":false,"desc":"openstreetmap, geonames or manual. Manual pins take precedence in lookups."},{"name":"collection","type":"string","nullable":false,"desc":"azerbaijan, world-cities or world-countries."},{"name":"kind","type":"string","nullable":false,"desc":"Settlement, administrative area, station or country kind."},{"name":"name","type":"string","nullable":false,"desc":"Primary upstream name, unchanged."},{"name":"name_az","type":"string","nullable":true,"desc":"Azerbaijani name, when present."},{"name":"name_en","type":"string","nullable":true,"desc":"English name, when present."},{"name":"names","type":"array\u003cstring\u003e","nullable":false,"desc":"All upstream names and aliases, without truncation."},{"name":"lat","type":"number","nullable":true,"desc":"WGS84 latitude at upstream precision; null exactly when unresolved."},{"name":"lon","type":"number","nullable":true,"desc":"WGS84 longitude at upstream precision; null exactly when unresolved."},{"name":"confidence","type":"string","nullable":false,"desc":"ok, approximate or unresolved. Manual coordinates retain their approximate flag."},{"name":"display_name","type":"string","nullable":true,"desc":"Place with its administrative context."},{"name":"area","type":"string","nullable":true,"desc":"First administrative area or city."},{"name":"subarea","type":"string","nullable":true,"desc":"City district, when known."},{"name":"country_code","type":"string","nullable":true,"desc":"ISO country code."},{"name":"country","type":"string","nullable":true,"desc":"Country name."},{"name":"population","type":"integer","nullable":true,"desc":"Upstream population, when available."},{"name":"wikidata","type":"string","nullable":true,"desc":"Upstream Wikidata identifier."},{"name":"osm_type","type":"string","nullable":true,"desc":"node, way or relation for an OSM record."},{"name":"osm_id","type":"integer","nullable":true,"desc":"OpenStreetMap object ID."},{"name":"geonames_id","type":"integer","nullable":true,"desc":"GeoNames identifier."},{"name":"feature_code","type":"string","nullable":true,"desc":"GeoNames feature code."},{"name":"details","type":"object","nullable":false,"desc":"Additional source facts; an empty object when absent."},{"name":"licence","type":"string","nullable":false,"desc":"Collection licence: ODbL-1.0 or CC-BY-4.0. Carry this when redistributing."},{"name":"attribution","type":"string","nullable":false,"desc":"Required collection credit, including manual pins within the collection."},{"name":"ingested_at","type":"datetime","nullable":false,"desc":"UTC instant this content was stored."},{"name":"first_seen_at","type":"datetime","nullable":false,"desc":"UTC instant this identity was first stored."}]},{"id":"lookup-places","verb":"GET","path":"/v1/geo/places/lookup","summary":"Resolve caller names from a stored collection","returns":"array","params":[{"name":"collection","in":"query","type":"string","required":true,"desc":"One collection: azerbaijan, world-cities or world-countries. Required for lists and lookups.","example":"azerbaijan"},{"name":"key","in":"query","type":"string","required":true,"desc":"Repeated caller names, at most 100 distinct keys of 200 characters each. An optional qualifier after | matches country, ISO code, area or city district. No comma splitting. Each key is answered, including unresolved names in meta.not_found.","example":"Sea Breeze"},{"name":"viewbox","in":"query","type":"string","required":false,"desc":"minLon,minLat,maxLon,maxLat. Prefer points inside this box without excluding other matches.","example":"49,40,51,41"},{"name":"bounded","in":"query","type":"boolean","required":false,"desc":"Require a point inside viewbox. A viewbox is required when true.","example":"false"},{"name":"prefer","in":"query","type":"string","required":false,"desc":"Ordered comma-separated place kinds; at most 20. Manual pins rank first, then viewbox, kind preference, primary name, upstream rank, population and ID.","example":"village,town"}],"fields":[{"name":"key","type":"string","nullable":false,"desc":"Caller key unchanged, including its qualifier."},{"name":"collection","type":"string","nullable":false,"desc":"The requested collection."},{"name":"id","type":"string","nullable":true,"desc":"Best place identity; null when unresolved."},{"name":"source","type":"string","nullable":true,"desc":"Best place source; null when unresolved."},{"name":"name","type":"string","nullable":true,"desc":"Best place primary name."},{"name":"kind","type":"string","nullable":true,"desc":"Best place kind."},{"name":"confidence","type":"string","nullable":false,"desc":"ok, approximate or unresolved."},{"name":"candidates","type":"integer","nullable":false,"desc":"Number of candidates after qualifier and bounded-viewbox filtering."},{"name":"lat","type":"number","nullable":true,"desc":"Latitude; null exactly when unresolved."},{"name":"lon","type":"number","nullable":true,"desc":"Longitude; null exactly when unresolved."},{"name":"display_name","type":"string","nullable":true,"desc":"Best place and its context."},{"name":"matched_name","type":"string","nullable":true,"desc":"Upstream name or alias that matched the caller key."},{"name":"area","type":"string","nullable":true,"desc":"Best place administrative area."},{"name":"subarea","type":"string","nullable":true,"desc":"Best place city district."},{"name":"country_code","type":"string","nullable":true,"desc":"Best place ISO code."},{"name":"country","type":"string","nullable":true,"desc":"Best place country name."},{"name":"population","type":"integer","nullable":true,"desc":"Best place upstream population."},{"name":"wikidata","type":"string","nullable":true,"desc":"Best place Wikidata identifier."},{"name":"osm_type","type":"string","nullable":true,"desc":"Best place OSM object type."},{"name":"osm_id","type":"integer","nullable":true,"desc":"Best place OSM object ID."},{"name":"geonames_id","type":"integer","nullable":true,"desc":"Best place GeoNames ID."},{"name":"licence","type":"string","nullable":false,"desc":"Requested collection licence, even when unresolved."},{"name":"attribution","type":"string","nullable":false,"desc":"Required collection credit, even when unresolved."},{"name":"ingested_at","type":"datetime","nullable":true,"desc":"Best place content storage time; null when unresolved."}]},{"id":"get-places-snapshot","verb":"GET","path":"/v1/geo/places/snapshot","summary":"Every place collection, its build and licence","returns":"array","params":[],"fields":[{"name":"collection","type":"string","nullable":false,"desc":"Published collection name."},{"name":"title","type":"string","nullable":false,"desc":"Description of the collection coverage."},{"name":"source","type":"string","nullable":false,"desc":"Builder: openstreetmap or geonames."},{"name":"licence","type":"string","nullable":false,"desc":"ODbL-1.0 or CC-BY-4.0."},{"name":"licence_url","type":"string","nullable":false,"desc":"Full licence text."},{"name":"attribution","type":"string","nullable":false,"desc":"Required collection credit."},{"name":"places","type":"integer","nullable":false,"desc":"Current retrievable places, including manual pins."},{"name":"pinned","type":"integer","nullable":false,"desc":"Current manual pins."},{"name":"build","type":"string","nullable":true,"desc":"Newest builder receipt ID; null before the first build."},{"name":"inputs","type":"array\u003cobject\u003e","nullable":false,"desc":"Input files, URLs, sizes and digests for that build. Empty array before a build."},{"name":"data_as_of","type":"datetime","nullable":true,"desc":"Upstream date when available; null for undated local input."},{"name":"built_at","type":"datetime","nullable":true,"desc":"UTC time the builder ran."},{"name":"build_places","type":"integer","nullable":true,"desc":"Places named by the builder receipt, excluding manual pins."},{"name":"posted","type":"integer","nullable":true,"desc":"Places this rebuild posted."},{"name":"withdrawn","type":"integer","nullable":true,"desc":"Places this source withdrew."},{"name":"held_known","type":"boolean","nullable":true,"desc":"Whether the builder could read its held membership."},{"name":"held_places","type":"integer","nullable":true,"desc":"Stored membership after applying the build."},{"name":"verified","type":"boolean","nullable":true,"desc":"Whether stored membership matches the receipt digest."},{"name":"ingested_at","type":"datetime","nullable":true,"desc":"UTC instant the receipt landed."}]}]},{"slug":"seismic","name":"Seismic Data API","status":"retired","operations":[{"id":"list-faults","verb":"GET","path":"/v1/seismic/faults","summary":"List active fault traces","returns":"array","params":[{"name":"bbox","in":"query","type":"string","required":false,"desc":"`minLon,minLat,maxLon,maxLat`, in WGS84 degrees. A trace is in the box when **any one of its vertices** is, inclusive at both ends, and it is then served **whole** — never clipped, so a returned trace can run well outside the box. A trace that crosses the box between two vertices outside it is not in it. Omit it for the whole world. Refused with a `400`, never answered with an empty page, when it is not four numbers in range or a minimum exceeds its maximum — including a box that crosses the antimeridian, which is two requests here. Until the first GEM file has landed on a deployment, every request here answers `503` rather than an empty box.","example":"40,35,55,45"},{"name":"format","in":"query","type":"string","required":false,"desc":"`records` (the default): each row a flat object, `geometry` a bare GeoJSON geometry. `geojson`: each row a GeoJSON `Feature` — `id` and `geometry` at the top, every other field under `properties`. Either way `data` is an array of rows, so a FeatureCollection is `{\"type\": \"FeatureCollection\", \"features\": data}` over the pages you fetched."},{"name":"count","in":"query","type":"boolean","required":false,"desc":"Adds `meta.total`: how many traces the box matches across every page. **Use it to check you have the whole set** — the Caspian box `40,35,55,45` matches 348, which is four pages at `limit=100`.\n\n**One value per parameter.** `bbox`, `format`, `count` and `source` given twice are each a `400`, never read for the first value: `?format=records\u0026format=geojson` would otherwise answer in one shape and drop the other unread.","example":"true"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"`gem_active_faults`, the one source this collection is fed by, in any case. Any other value is a `400` naming it, rather than an empty page that would read as \"no active faults\": there is no other source here to select. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`sha1(\"\u003ccatalog_id\u003e|\u003cname\u003e|\u003clon\u003e,\u003clat\u003e\")`, lower-case hex, over the trace's FIRST vertex at four decimals (Python's `.4f`: round-half-even on the double, minus sign kept), with the literal `None` for a missing catalog_id or name — `99e4f1663c87d75b881c015b0e7fe0a328061530` is `ME_TRCS001|None|40.5481,39.4141`. **Softon derives this; it is not GEM's id**, and it is the id the consumers of this dataset already key on, so it is bare, with no `\u003csource\u003e:` prefix. GEM's own `catalog_id` repeats and is published separately.\n\nOne exception in the whole file: where two traces share a recipe key — one fork in GEM's East African catalog — each is hashed with `|1` and `|2` appended and neither carries the plain id. `id_key` is always the exact string that was hashed."},{"name":"id_key","type":"string","nullable":false,"desc":"The string `id` is the sha1 of, so the recipe can be checked on every row: `ME_TRCS001|None|40.5481,39.4141`."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `gem_active_faults` — what `?source=` and `GET /v1/sources` use. Not `data_source`."},{"name":"catalog_id","type":"string","nullable":true,"desc":"GEM's own id for the trace. **Not unique** — 107 values repeat across the file, `ME_IR2s3s1` and `ME_IRCS016` inside the Caspian box — which is why `id` exists."},{"name":"catalog_name","type":"string","nullable":true,"desc":"The regional catalog the trace comes from: `EMME` or `SHARE` for every trace in the Caspian box, `UCERF3`, `GEM Faulted Earth` and others elsewhere."},{"name":"name","type":"string","nullable":true,"desc":"The fault's name, where GEM gives one. Most traces have none — none of the 348 in the Caspian box does — and an empty string is published as null."},{"name":"fault_type","type":"string","nullable":true,"desc":"`catalog_name` again. Published because one consumer's interface declares it, filled from catalog_name exactly as that consumer's own pipeline did; it is not meant to diverge."},{"name":"source_reference","type":"string","nullable":true,"desc":"`catalog_name` a third time, for the same consumer and the same reason as `fault_type`."},{"name":"data_source","type":"string","nullable":false,"desc":"The dataset code, `GEM_GAF_DB`, on every row."},{"name":"slip_type","type":"string","nullable":true,"desc":"GEM's string: `Reverse`, `Dextral`, `Sinistral-Reverse`, `Spreading_Ridge`, ..."},{"name":"slip_rate_mm_yr","type":"number","nullable":true,"desc":"The net slip rate, in millimetres per year — GEM's PREFERRED value, parsed out of its `(preferred,low,high)` string: the text between the opening parenthesis and the first comma, as a number. Null when that text is empty, is not a number, or reads as NaN, and when the string has no opening parenthesis at all (`None`, `\"\"`). The parse is softon's; GEM's own string is in `upstream_properties`."},{"name":"slip_rate_mm_yr_low","type":"number","nullable":true,"desc":"The lower bound from the same string — its second part, by the same rule. Null where GEM gives none, which on most traces it does not."},{"name":"slip_rate_mm_yr_high","type":"number","nullable":true,"desc":"The upper bound from the same string — its third part, by the same rule."},{"name":"dip_deg","type":"number","nullable":true,"desc":"The average dip, in degrees — GEM's PREFERRED value, parsed out of its `(preferred,low,high)` string: the text between the opening parenthesis and the first comma, as a number. Null when that text is empty, is not a number, or reads as NaN, and when the string has no opening parenthesis at all (`None`, `\"\"`). The parse is softon's; GEM's own string is in `upstream_properties`."},{"name":"dip_deg_low","type":"number","nullable":true,"desc":"The lower bound from the same string — its second part, by the same rule. Null where GEM gives none, which on most traces it does not."},{"name":"dip_deg_high","type":"number","nullable":true,"desc":"The upper bound from the same string — its third part, by the same rule."},{"name":"rake_deg","type":"number","nullable":true,"desc":"The average rake, in degrees — GEM's PREFERRED value, parsed out of its `(preferred,low,high)` string: the text between the opening parenthesis and the first comma, as a number. Null when that text is empty, is not a number, or reads as NaN, and when the string has no opening parenthesis at all (`None`, `\"\"`). The parse is softon's; GEM's own string is in `upstream_properties`."},{"name":"rake_deg_low","type":"number","nullable":true,"desc":"The lower bound from the same string — its second part, by the same rule. Null where GEM gives none, which on most traces it does not."},{"name":"rake_deg_high","type":"number","nullable":true,"desc":"The upper bound from the same string — its third part, by the same rule."},{"name":"dip_dir","type":"string","nullable":true,"desc":"The dip direction as GEM's catalog writes it — a compass point on some (`NE`), a bearing on others (`270`). Null on most traces, all 348 in the Caspian box included."},{"name":"upper_seis_depth_km","type":"number","nullable":true,"desc":"The upper seismogenic depth, in kilometres — GEM's PREFERRED value, parsed out of its `(preferred,low,high)` string: the text between the opening parenthesis and the first comma, as a number. Null when that text is empty, is not a number, or reads as NaN, and when the string has no opening parenthesis at all (`None`, `\"\"`). The parse is softon's; GEM's own string is in `upstream_properties`."},{"name":"upper_seis_depth_km_low","type":"number","nullable":true,"desc":"The lower bound from the same string — its second part, by the same rule. Null where GEM gives none, which on most traces it does not."},{"name":"upper_seis_depth_km_high","type":"number","nullable":true,"desc":"The upper bound from the same string — its third part, by the same rule."},{"name":"lower_seis_depth_km","type":"number","nullable":true,"desc":"The lower seismogenic depth, in kilometres — GEM's PREFERRED value, parsed out of its `(preferred,low,high)` string: the text between the opening parenthesis and the first comma, as a number. Null when that text is empty, is not a number, or reads as NaN, and when the string has no opening parenthesis at all (`None`, `\"\"`). The parse is softon's; GEM's own string is in `upstream_properties`."},{"name":"lower_seis_depth_km_low","type":"number","nullable":true,"desc":"The lower bound from the same string — its second part, by the same rule. Null where GEM gives none, which on most traces it does not."},{"name":"lower_seis_depth_km_high","type":"number","nullable":true,"desc":"The upper bound from the same string — its third part, by the same rule."},{"name":"upstream_properties","type":"object","nullable":true,"desc":"GEM's property bag for the trace, every key and value as published: the tuple strings the parsed numbers came from, and the attributes no field above carries — `activity_confidence`, `strike_slip_rate`, `reference`, `notes` and more, varying by catalog."},{"name":"version","type":"string","nullable":true,"desc":"The commit of GEM's repository this trace was read from."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored the trace. It moves only when the trace is re-posted — the first run, and a run after the pinned commit moves — so it is **not** when the file was last checked; the snapshot's `retrieved_at` is that."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the trace. It never moves, which is why the collection is walked in its order."},{"name":"geometry","type":"object","nullable":false,"desc":"The trace: a GeoJSON `LineString` in WGS84 longitude-then-latitude, a third coordinate where GEM gives one. **Never clipped** to the box that selected it and **never rounded** — the coordinates carry GEM's own digits, up to fifteen decimals. A bare geometry, not a Feature; `format=geojson` is the Feature shape."}]},{"id":"get-faults-snapshot","verb":"GET","path":"/v1/seismic/faults/snapshot","summary":"Which GEM file is being served, and its licence","returns":"object","params":[],"fields":[{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `gem_active_faults`."},{"name":"source_code","type":"string","nullable":false,"desc":"The dataset code, `GEM_GAF_DB` — the same value every row carries as `data_source`."},{"name":"provider","type":"string","nullable":false,"desc":"`GEM Foundation`."},{"name":"version","type":"string","nullable":false,"desc":"The commit of GEM's repository the file is pinned to. A commit rather than a tag: the repository's one release tag, `2019.0`, predates three commits that changed this file. **Under a commit pin the data cannot change**; a new version is a deliberate change on this side, and it shows up here."},{"name":"upstream_url","type":"string","nullable":false,"desc":"The commit-pinned raw URL the file is fetched from."},{"name":"upstream_sha256","type":"string","nullable":false,"desc":"`sha256` of the file's bytes. Every run checks the download against it before parsing anything, and refuses it on a mismatch."},{"name":"upstream_bytes","type":"integer","nullable":false,"desc":"The file's size in bytes."},{"name":"feature_count","type":"integer","nullable":false,"desc":"Traces in GEM's file: 16,195 at the pinned commit."},{"name":"held_count","type":"integer","nullable":false,"desc":"Traces this API serves for the source. Equal to `feature_count` when every trace in the file has landed."},{"name":"count_matches","type":"boolean","nullable":false,"desc":"`held_count` and `feature_count` agreeing."},{"name":"licence","type":"string","nullable":false,"desc":"SPDX identifier, `CC-BY-SA-4.0`. **Share-alike**: if you redistribute these traces, or a dataset you built from them, it goes out under the same licence, with `attribution` reproduced. The `id` and the parsed numeric fields are softon's adaptations of GEM's data, and they are covered too."},{"name":"attribution","type":"string","nullable":false,"desc":"The string to reproduce: `GEM Global Active Faults Database (GEM Foundation), CC-BY-SA 4.0`."},{"name":"citation","type":"string","nullable":false,"desc":"The citation the GEM Foundation asks for: `Styron, R. and Pagani, M. (2020). The GEM Global Active Faults Database. Earthquake Spectra.`"},{"name":"retrieved_at","type":"timestamp","nullable":false,"desc":"When a run last fetched the file and verified its `sha256`. The answer to \"how recently did softon check\", which no row's `ingested_at` is."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored that run's receipt."}]},{"id":"get-fault","verb":"GET","path":"/v1/seismic/faults/{id}","summary":"Retrieve one fault trace","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"The trace's sha1 id, bare — no `\u003csource\u003e:` prefix. Stable for as long as GEM's file carries the trace with the same catalog_id, name and first vertex. Until the first GEM file has landed on a deployment, an id answers `503`, not a `404` that would say the trace never existed.","example":"99e4f1663c87d75b881c015b0e7fe0a328061530"},{"name":"format","in":"query","type":"string","required":false,"desc":"`records` (the default) or `geojson`, as on the collection, and likewise a `400` when given twice."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`sha1(\"\u003ccatalog_id\u003e|\u003cname\u003e|\u003clon\u003e,\u003clat\u003e\")`, lower-case hex, over the trace's FIRST vertex at four decimals (Python's `.4f`: round-half-even on the double, minus sign kept), with the literal `None` for a missing catalog_id or name — `99e4f1663c87d75b881c015b0e7fe0a328061530` is `ME_TRCS001|None|40.5481,39.4141`. **Softon derives this; it is not GEM's id**, and it is the id the consumers of this dataset already key on, so it is bare, with no `\u003csource\u003e:` prefix. GEM's own `catalog_id` repeats and is published separately.\n\nOne exception in the whole file: where two traces share a recipe key — one fork in GEM's East African catalog — each is hashed with `|1` and `|2` appended and neither carries the plain id. `id_key` is always the exact string that was hashed."},{"name":"id_key","type":"string","nullable":false,"desc":"The string `id` is the sha1 of, so the recipe can be checked on every row: `ME_TRCS001|None|40.5481,39.4141`."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `gem_active_faults` — what `?source=` and `GET /v1/sources` use. Not `data_source`."},{"name":"catalog_id","type":"string","nullable":true,"desc":"GEM's own id for the trace. **Not unique** — 107 values repeat across the file, `ME_IR2s3s1` and `ME_IRCS016` inside the Caspian box — which is why `id` exists."},{"name":"catalog_name","type":"string","nullable":true,"desc":"The regional catalog the trace comes from: `EMME` or `SHARE` for every trace in the Caspian box, `UCERF3`, `GEM Faulted Earth` and others elsewhere."},{"name":"name","type":"string","nullable":true,"desc":"The fault's name, where GEM gives one. Most traces have none — none of the 348 in the Caspian box does — and an empty string is published as null."},{"name":"fault_type","type":"string","nullable":true,"desc":"`catalog_name` again. Published because one consumer's interface declares it, filled from catalog_name exactly as that consumer's own pipeline did; it is not meant to diverge."},{"name":"source_reference","type":"string","nullable":true,"desc":"`catalog_name` a third time, for the same consumer and the same reason as `fault_type`."},{"name":"data_source","type":"string","nullable":false,"desc":"The dataset code, `GEM_GAF_DB`, on every row."},{"name":"slip_type","type":"string","nullable":true,"desc":"GEM's string: `Reverse`, `Dextral`, `Sinistral-Reverse`, `Spreading_Ridge`, ..."},{"name":"slip_rate_mm_yr","type":"number","nullable":true,"desc":"The net slip rate, in millimetres per year — GEM's PREFERRED value, parsed out of its `(preferred,low,high)` string: the text between the opening parenthesis and the first comma, as a number. Null when that text is empty, is not a number, or reads as NaN, and when the string has no opening parenthesis at all (`None`, `\"\"`). The parse is softon's; GEM's own string is in `upstream_properties`."},{"name":"slip_rate_mm_yr_low","type":"number","nullable":true,"desc":"The lower bound from the same string — its second part, by the same rule. Null where GEM gives none, which on most traces it does not."},{"name":"slip_rate_mm_yr_high","type":"number","nullable":true,"desc":"The upper bound from the same string — its third part, by the same rule."},{"name":"dip_deg","type":"number","nullable":true,"desc":"The average dip, in degrees — GEM's PREFERRED value, parsed out of its `(preferred,low,high)` string: the text between the opening parenthesis and the first comma, as a number. Null when that text is empty, is not a number, or reads as NaN, and when the string has no opening parenthesis at all (`None`, `\"\"`). The parse is softon's; GEM's own string is in `upstream_properties`."},{"name":"dip_deg_low","type":"number","nullable":true,"desc":"The lower bound from the same string — its second part, by the same rule. Null where GEM gives none, which on most traces it does not."},{"name":"dip_deg_high","type":"number","nullable":true,"desc":"The upper bound from the same string — its third part, by the same rule."},{"name":"rake_deg","type":"number","nullable":true,"desc":"The average rake, in degrees — GEM's PREFERRED value, parsed out of its `(preferred,low,high)` string: the text between the opening parenthesis and the first comma, as a number. Null when that text is empty, is not a number, or reads as NaN, and when the string has no opening parenthesis at all (`None`, `\"\"`). The parse is softon's; GEM's own string is in `upstream_properties`."},{"name":"rake_deg_low","type":"number","nullable":true,"desc":"The lower bound from the same string — its second part, by the same rule. Null where GEM gives none, which on most traces it does not."},{"name":"rake_deg_high","type":"number","nullable":true,"desc":"The upper bound from the same string — its third part, by the same rule."},{"name":"dip_dir","type":"string","nullable":true,"desc":"The dip direction as GEM's catalog writes it — a compass point on some (`NE`), a bearing on others (`270`). Null on most traces, all 348 in the Caspian box included."},{"name":"upper_seis_depth_km","type":"number","nullable":true,"desc":"The upper seismogenic depth, in kilometres — GEM's PREFERRED value, parsed out of its `(preferred,low,high)` string: the text between the opening parenthesis and the first comma, as a number. Null when that text is empty, is not a number, or reads as NaN, and when the string has no opening parenthesis at all (`None`, `\"\"`). The parse is softon's; GEM's own string is in `upstream_properties`."},{"name":"upper_seis_depth_km_low","type":"number","nullable":true,"desc":"The lower bound from the same string — its second part, by the same rule. Null where GEM gives none, which on most traces it does not."},{"name":"upper_seis_depth_km_high","type":"number","nullable":true,"desc":"The upper bound from the same string — its third part, by the same rule."},{"name":"lower_seis_depth_km","type":"number","nullable":true,"desc":"The lower seismogenic depth, in kilometres — GEM's PREFERRED value, parsed out of its `(preferred,low,high)` string: the text between the opening parenthesis and the first comma, as a number. Null when that text is empty, is not a number, or reads as NaN, and when the string has no opening parenthesis at all (`None`, `\"\"`). The parse is softon's; GEM's own string is in `upstream_properties`."},{"name":"lower_seis_depth_km_low","type":"number","nullable":true,"desc":"The lower bound from the same string — its second part, by the same rule. Null where GEM gives none, which on most traces it does not."},{"name":"lower_seis_depth_km_high","type":"number","nullable":true,"desc":"The upper bound from the same string — its third part, by the same rule."},{"name":"upstream_properties","type":"object","nullable":true,"desc":"GEM's property bag for the trace, every key and value as published: the tuple strings the parsed numbers came from, and the attributes no field above carries — `activity_confidence`, `strike_slip_rate`, `reference`, `notes` and more, varying by catalog."},{"name":"version","type":"string","nullable":true,"desc":"The commit of GEM's repository this trace was read from."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored the trace. It moves only when the trace is re-posted — the first run, and a run after the pinned commit moves — so it is **not** when the file was last checked; the snapshot's `retrieved_at` is that."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the trace. It never moves, which is why the collection is walked in its order."},{"name":"geometry","type":"object","nullable":false,"desc":"The trace: a GeoJSON `LineString` in WGS84 longitude-then-latitude, a third coordinate where GEM gives one. **Never clipped** to the box that selected it and **never rounded** — the coordinates carry GEM's own digits, up to fifteen decimals. A bare geometry, not a Feature; `format=geojson` is the Feature shape."}]},{"id":"list-quakes","verb":"GET","path":"/v1/seismic/quakes","summary":"List earthquakes, or what ComCat changed since a moment","returns":"array","params":[{"name":"updated_since","in":"query","type":"timestamp","required":false,"desc":"**The sync cursor.** Events ComCat changed STRICTLY AFTER this instant — on `source_updated_at`, never origin time — so a 1990 earthquake revised yesterday comes back for yesterday's instant. RFC 3339 or a date. Hand back the newest `source_updated_at` you hold, **less an overlap of an hour or two**: ComCat can index a revision after events with later timestamps, and the mirror polls ComCat at minute 40 of every hour but 09 UTC. With it and no `order`, the walk is `updated_asc`: `updated_since=2026-10-01T00:00:00Z` is every change since that instant, oldest first."},{"name":"bbox","in":"query","type":"string","required":false,"desc":"`minLon,minLat,maxLon,maxLat`, WGS84, inclusive — GeoJSON's order, as on `/v1/seismic/faults`. Or send the bounds by name instead. **It must lie inside a mirrored window** (lat 35..45, lon 40..55 today, listed on the snapshot): a box that reaches outside is a `400` naming the windows, never an empty page that reads as \"no earthquakes there\". Omit it for the whole window.","example":"40,35,55,45"},{"name":"min_lat","in":"query","type":"number","required":false,"desc":"The box by name, as #121 writes it: `min_lat`, `max_lat`, `min_lon`, `max_lon`, any of them; a bound left out is the window's. Not together with `bbox`."},{"name":"max_lat","in":"query","type":"number","required":false,"desc":"See `min_lat`."},{"name":"min_lon","in":"query","type":"number","required":false,"desc":"See `min_lat`."},{"name":"max_lon","in":"query","type":"number","required":false,"desc":"See `min_lat`."},{"name":"start","in":"query","type":"timestamp","required":false,"desc":"Events whose `origin_time` is at or after this instant. RFC 3339 or a date. The range is half-open: `[start, end)`.","example":"1990-01-01"},{"name":"end","in":"query","type":"timestamp","required":false,"desc":"Events whose `origin_time` is BEFORE this instant — `start=1990-01-01\u0026end=1991-01-01` is 1990, and 1991-01-01T00:00:00.000Z is not in it.","example":"1991-01-01"},{"name":"event_type","in":"query","type":"string","required":false,"desc":"ComCat's `type`, exactly: `earthquake`, `quarry blast`, `explosion`, ... The mirror holds every type, so an event ComCat reclassifies is seen changing type; a sync that filters on one type will not see an event leave it."},{"name":"min_magnitude","in":"query","type":"number","required":false,"desc":"Events of at least this `magnitude`. Events with no magnitude are left out when it is sent."},{"name":"include_deleted","in":"query","type":"boolean","required":false,"desc":"`true` adds the events ComCat has deleted, with `status` `deleted` — the way a sync hears of a deletion. Left out by default, as ComCat's own service leaves them out."},{"name":"include","in":"query","type":"string","required":false,"desc":"`detail` adds each event's detail document as read, as `detail`. One value."},{"name":"count","in":"query","type":"boolean","required":false,"desc":"Adds `meta.total`, how many events the filter matches across every page.\n\n**One value per parameter.** Any parameter given twice is a `400`, never read for its first value."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. Valid only for the query and order that produced it."},{"name":"order","in":"query","type":"string","required":false,"desc":"`time_desc` (the default: newest earthquakes first), `time_asc`, `updated_asc` (the default with `updated_since`: oldest change first) or `updated_desc`, each with `id` as the tiebreak, so a page cut anywhere resumes exactly. An event ComCat revises during an `updated_asc` walk moves to its end and is met again there: that is a new version, and it is how the walk tells you."},{"name":"source","in":"query","type":"string","required":false,"desc":"`usgs_comcat`, the one source this collection is fed by, in any case. Any other value is a `400` naming it. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"ComCat's event id, bare — `us7000tfrw`, `iscgem768929` — with no `\u003csource\u003e:` prefix: the id ComCat and the consumers of this dataset already key on. The same value as `source_event_id` and `preferred_event_id`."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `usgs_comcat`."},{"name":"source_event_id","type":"string","nullable":false,"desc":"`feature.id`, verbatim."},{"name":"preferred_event_id","type":"string","nullable":false,"desc":"`feature.id` again: ComCat's preferred id is the one a feature is served under. When ComCat merges two events, the merged one keeps the preferred id and the other appears in `associated_ids`."},{"name":"origin_time","type":"timestamp","nullable":false,"desc":"`properties.time`, when the earthquake happened, from epoch milliseconds. RFC 3339 in UTC with exactly three fractional digits and a literal `Z` — `1990-12-28T04:03:53.650Z` — on every row, so a parser that reads three digits reads them all."},{"name":"source_updated_at","type":"timestamp","nullable":false,"desc":"`properties.updated`, when ComCat last changed the event. **The sync cursor**: a revision to an event of any age moves it, which is what `updated_since` reads. RFC 3339 in UTC with exactly three fractional digits and a literal `Z` — `1990-12-28T04:03:53.650Z` — on every row, so a parser that reads three digits reads them all."},{"name":"magnitude","type":"number","nullable":true,"desc":"`properties.mag`."},{"name":"magnitude_type","type":"string","nullable":true,"desc":"`properties.magType` — `mb`, `mw`, `mww`, `ml`, ..."},{"name":"depth_km","type":"number","nullable":true,"desc":"`geometry.coordinates[2]`, in **kilometres** as ComCat gives it — no conversion."},{"name":"longitude","type":"number","nullable":false,"desc":"`geometry.coordinates[0]`. GeoJSON writes longitude FIRST; this field is named so nobody has to remember that."},{"name":"latitude","type":"number","nullable":false,"desc":"`geometry.coordinates[1]`."},{"name":"place","type":"string","nullable":true,"desc":"`properties.place`, ComCat's English free text — `15 km SSW of F?man, Iran`, its own `?` included. Null on most deleted events."},{"name":"event_type","type":"string","nullable":false,"desc":"`properties.type` — `earthquake`, or another of ComCat's types such as `quarry blast` or `explosion` — and `earthquake` when ComCat gives none."},{"name":"status","type":"string","nullable":true,"desc":"`properties.status` as ComCat writes it: `reviewed`, `automatic`, `deleted`, ..."},{"name":"review_status","type":"string","nullable":false,"desc":"`status` folded to one of three: `reviewed`, `automatic`, or `unknown` for anything else, `deleted` included."},{"name":"significance","type":"integer","nullable":true,"desc":"`properties.sig`, ComCat's 0-1000+ significance."},{"name":"felt_reports","type":"integer","nullable":true,"desc":"`properties.felt`, \"Did You Feel It?\" responses."},{"name":"cdi","type":"number","nullable":true,"desc":"`properties.cdi`, the community decimal intensity."},{"name":"mmi","type":"number","nullable":true,"desc":"`properties.mmi`, ShakeMap's instrumental intensity."},{"name":"alert_level","type":"string","nullable":true,"desc":"`properties.alert`, the PAGER level: `green`, `yellow`, `orange`, `red`, or null."},{"name":"tsunami","type":"boolean","nullable":true,"desc":"`properties.tsunami`, **0 or 1 upstream**, as a boolean; null when ComCat gives neither. It flags events in oceanic regions large enough to check, not a tsunami."},{"name":"network_code","type":"string","nullable":true,"desc":"`properties.net`, the network of the preferred origin: `us`, `iscgem`, ..."},{"name":"event_code","type":"string","nullable":true,"desc":"`properties.code`, the network's own code for the event."},{"name":"associated_ids","type":"array of string","nullable":false,"desc":"`properties.ids` — **comma-wrapped** upstream, `,usp0004jvv,iscgem348922,` — split with the empty segments dropped. Every id ComCat associates with the event, its own included."},{"name":"source_networks","type":"array of string","nullable":false,"desc":"`properties.sources`, split the same way."},{"name":"product_types","type":"array of string","nullable":false,"desc":"`properties.types`, split the same way: `origin`, `phase-data`, `shakemap`, ... `[]` where ComCat gives none. Always the summary's: with `?include=detail` the detail document's product types are `detail.product_types`, beside this field and never written over it."},{"name":"station_count","type":"integer","nullable":true,"desc":"`properties.nst`."},{"name":"nearest_station_distance_deg","type":"number","nullable":true,"desc":"`properties.dmin`, in **degrees**, not kilometres."},{"name":"rms_seconds","type":"number","nullable":true,"desc":"`properties.rms`."},{"name":"azimuthal_gap_deg","type":"number","nullable":true,"desc":"`properties.gap`."},{"name":"detail_url","type":"string","nullable":true,"desc":"`properties.detail`, exactly as ComCat gives it. The scraper follows it and never builds one."},{"name":"public_url","type":"string","nullable":true,"desc":"`properties.url`, the event's page on earthquake.usgs.gov."},{"name":"licence","type":"string","nullable":true,"desc":"The terms this event comes under, by the network that contributed it: `public-domain` for USGS's own (`us`, `atlas`), `CC-BY-SA-3.0` for the ISC-GEM catalogue (`iscgem`, `iscgemsup`) — attribution and share-alike apply. Null for a network not yet reviewed. `/v1/seismic/quakes/snapshot` carries the attribution each asks for."},{"name":"detail_wanted","type":"boolean","nullable":false,"desc":"This version of the event meets the enrichment rule — magnitude 4 or more, a PAGER alert, or felt reports — so its detail document is read once per version, newest change first: within hours of a change, and over about a day for the ~3,000 a first load owes. `?include=detail` serves what was read."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored the event, in the same fixed shape. It moves when ComCat changes the event or its detail is read — **not** a sync cursor: use `source_updated_at`."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the event."},{"name":"raw","type":"object","nullable":false,"desc":"**The feature's `properties` and `geometry`, byte for byte as ComCat wrote them**, as `{\"properties\":{...},\"geometry\":{...}}`: every key ComCat publishes, `title` and `tz` included, with ComCat's own spelling of every number. Every field above is read out of it."},{"name":"detail","type":"object","nullable":true,"desc":"**Only with `?include=detail`.** The event's detail document as the scraper read it, through the feature's own detail URL, once per version of the event: null when none has been read — for an event that is not `detail_wanted`, or one still waiting its turn: a run reads a few hundred, newest change first, so a revision's detail follows within hours and a first load's ~3,000 take about a day. See `detail.*`."},{"name":"detail.status","type":"string","nullable":false,"desc":"`ok`, or what the event's detail URL answered instead: `not_found` (404), `deleted` (409) or `unreadable` (an answer that was not a detail document)."},{"name":"detail.fetched_at","type":"timestamp","nullable":false,"desc":"When the scraper read it."},{"name":"detail.updated_at","type":"timestamp","nullable":true,"desc":"The detail document's own `updated`: the version of the event it describes."},{"name":"detail.current","type":"boolean","nullable":false,"desc":"`updated_at` is the event's `source_updated_at` or later. False when ComCat has revised the event since and the new version's detail is still to be read."},{"name":"detail.product_types","type":"array of string","nullable":false,"desc":"The keys of the document's `products`, in ComCat's order — what the consumer this replaces wrote over the summary's `product_types`."},{"name":"detail.horizontal_error_km","type":"number","nullable":true,"desc":"`products.origin[0].properties[\"horizontal-error\"]`, a string upstream."},{"name":"detail.depth_error_km","type":"number","nullable":true,"desc":"`[\"depth-error\"]`, likewise. Absent on many events."},{"name":"detail.magnitude_error","type":"number","nullable":true,"desc":"`[\"magnitude-error\"]`."},{"name":"detail.magnitude_station_count","type":"integer","nullable":true,"desc":"`[\"magnitude-num-stations-used\"]`."},{"name":"detail.location_source","type":"string","nullable":true,"desc":"`[\"location-source\"]`."},{"name":"detail.magnitude_source","type":"string","nullable":true,"desc":"`[\"magnitude-source\"]`."},{"name":"detail.origin_properties","type":"object","nullable":true,"desc":"`products.origin[0].properties` as ComCat wrote it, for every field the six above do not carry. Null when the document has no origin product."}]},{"id":"get-quakes-snapshot","verb":"GET","path":"/v1/seismic/quakes/snapshot","summary":"Where the earthquakes come from, under what terms, and the last poll","returns":"object","params":[],"fields":[{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `usgs_comcat`."},{"name":"source_name","type":"string","nullable":false,"desc":"`ANSS Comprehensive Earthquake Catalog`."},{"name":"provider","type":"string","nullable":false,"desc":"`U.S. Geological Survey`."},{"name":"api_base_url","type":"string","nullable":false,"desc":"`https://earthquake.usgs.gov/fdsnws/event/1/query`."},{"name":"source_version","type":"string","nullable":false,"desc":"`fdsnws-event-1`."},{"name":"attribution_text","type":"string","nullable":false,"desc":"`Source: U.S. Geological Survey`."},{"name":"citation_text","type":"string","nullable":false,"desc":"`U.S. Geological Survey, Earthquake Hazards Program, ANSS Comprehensive Earthquake Catalog (ComCat)`."},{"name":"license_name","type":"string","nullable":false,"desc":"`Public Domain (U.S. Government Work)` — the terms of USGS's own events. ComCat carries other networks' too: see `network_licences` and each event's `licence`."},{"name":"network_licences","type":"array","nullable":false,"desc":"The terms by contributing network: `network_codes`, `licence`, the `attribution` to reproduce and the `terms_url` they come from. The ISC-GEM catalogue's events are CC BY-SA 3.0, which binds whoever redistributes them to attribute and to share alike."},{"name":"windows","type":"array","nullable":false,"desc":"The boxes mirrored, each `name`, `min_lat`, `max_lat`, `min_lon`, `max_lon`, inclusive — as the newest complete read recorded them. A box outside them is a `400` on the collection."},{"name":"last_poll_at","type":"timestamp","nullable":true,"desc":"The newest successful poll — what every quake response carries as `meta.last_poll_at`."},{"name":"last_full_read_at","type":"timestamp","nullable":true,"desc":"The newest successful read of the whole windows: once a day, at 03:40 UTC."},{"name":"last_poll","type":"object","nullable":false,"desc":"The newest run whatever it found, a failure included: `status` (`ok` or `failed`), `error`, `mode` (`full` or `incremental`), `polled_at`, `updated_after` (the watermark an incremental poll sent), `api_version`, `request_urls` (the queries, every one carrying `starttime`), `feature_count`, `posted_count`, `detail_pending_count` and `withdrawn_count`."},{"name":"event_count","type":"integer","nullable":false,"desc":"Events served, deleted ones included."},{"name":"deleted_count","type":"integer","nullable":false,"desc":"Of those, the ones ComCat marks deleted, which a default walk leaves out."}]},{"id":"get-quake","verb":"GET","path":"/v1/seismic/quakes/{id}","summary":"Retrieve one earthquake, with its detail","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"ComCat's event id, bare — no `\u003csource\u003e:` prefix. An id ComCat has merged into another event answers that event: its `id` is the one to hold from then on, and `associated_ids` names the one you asked for. A deleted event is served, with `status` `deleted`. Until the first complete read has landed, an id answers `503`, not a `404` that would say the event never existed.","example":"usp0004jvv"},{"name":"include","in":"query","type":"string","required":false,"desc":"`detail` adds the event's detail document as read — the six origin-product fields of #121 and the product types — as `detail`, null when none has been read. One value."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"ComCat's event id, bare — `us7000tfrw`, `iscgem768929` — with no `\u003csource\u003e:` prefix: the id ComCat and the consumers of this dataset already key on. The same value as `source_event_id` and `preferred_event_id`."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `usgs_comcat`."},{"name":"source_event_id","type":"string","nullable":false,"desc":"`feature.id`, verbatim."},{"name":"preferred_event_id","type":"string","nullable":false,"desc":"`feature.id` again: ComCat's preferred id is the one a feature is served under. When ComCat merges two events, the merged one keeps the preferred id and the other appears in `associated_ids`."},{"name":"origin_time","type":"timestamp","nullable":false,"desc":"`properties.time`, when the earthquake happened, from epoch milliseconds. RFC 3339 in UTC with exactly three fractional digits and a literal `Z` — `1990-12-28T04:03:53.650Z` — on every row, so a parser that reads three digits reads them all."},{"name":"source_updated_at","type":"timestamp","nullable":false,"desc":"`properties.updated`, when ComCat last changed the event. **The sync cursor**: a revision to an event of any age moves it, which is what `updated_since` reads. RFC 3339 in UTC with exactly three fractional digits and a literal `Z` — `1990-12-28T04:03:53.650Z` — on every row, so a parser that reads three digits reads them all."},{"name":"magnitude","type":"number","nullable":true,"desc":"`properties.mag`."},{"name":"magnitude_type","type":"string","nullable":true,"desc":"`properties.magType` — `mb`, `mw`, `mww`, `ml`, ..."},{"name":"depth_km","type":"number","nullable":true,"desc":"`geometry.coordinates[2]`, in **kilometres** as ComCat gives it — no conversion."},{"name":"longitude","type":"number","nullable":false,"desc":"`geometry.coordinates[0]`. GeoJSON writes longitude FIRST; this field is named so nobody has to remember that."},{"name":"latitude","type":"number","nullable":false,"desc":"`geometry.coordinates[1]`."},{"name":"place","type":"string","nullable":true,"desc":"`properties.place`, ComCat's English free text — `15 km SSW of F?man, Iran`, its own `?` included. Null on most deleted events."},{"name":"event_type","type":"string","nullable":false,"desc":"`properties.type` — `earthquake`, or another of ComCat's types such as `quarry blast` or `explosion` — and `earthquake` when ComCat gives none."},{"name":"status","type":"string","nullable":true,"desc":"`properties.status` as ComCat writes it: `reviewed`, `automatic`, `deleted`, ..."},{"name":"review_status","type":"string","nullable":false,"desc":"`status` folded to one of three: `reviewed`, `automatic`, or `unknown` for anything else, `deleted` included."},{"name":"significance","type":"integer","nullable":true,"desc":"`properties.sig`, ComCat's 0-1000+ significance."},{"name":"felt_reports","type":"integer","nullable":true,"desc":"`properties.felt`, \"Did You Feel It?\" responses."},{"name":"cdi","type":"number","nullable":true,"desc":"`properties.cdi`, the community decimal intensity."},{"name":"mmi","type":"number","nullable":true,"desc":"`properties.mmi`, ShakeMap's instrumental intensity."},{"name":"alert_level","type":"string","nullable":true,"desc":"`properties.alert`, the PAGER level: `green`, `yellow`, `orange`, `red`, or null."},{"name":"tsunami","type":"boolean","nullable":true,"desc":"`properties.tsunami`, **0 or 1 upstream**, as a boolean; null when ComCat gives neither. It flags events in oceanic regions large enough to check, not a tsunami."},{"name":"network_code","type":"string","nullable":true,"desc":"`properties.net`, the network of the preferred origin: `us`, `iscgem`, ..."},{"name":"event_code","type":"string","nullable":true,"desc":"`properties.code`, the network's own code for the event."},{"name":"associated_ids","type":"array of string","nullable":false,"desc":"`properties.ids` — **comma-wrapped** upstream, `,usp0004jvv,iscgem348922,` — split with the empty segments dropped. Every id ComCat associates with the event, its own included."},{"name":"source_networks","type":"array of string","nullable":false,"desc":"`properties.sources`, split the same way."},{"name":"product_types","type":"array of string","nullable":false,"desc":"`properties.types`, split the same way: `origin`, `phase-data`, `shakemap`, ... `[]` where ComCat gives none. Always the summary's: with `?include=detail` the detail document's product types are `detail.product_types`, beside this field and never written over it."},{"name":"station_count","type":"integer","nullable":true,"desc":"`properties.nst`."},{"name":"nearest_station_distance_deg","type":"number","nullable":true,"desc":"`properties.dmin`, in **degrees**, not kilometres."},{"name":"rms_seconds","type":"number","nullable":true,"desc":"`properties.rms`."},{"name":"azimuthal_gap_deg","type":"number","nullable":true,"desc":"`properties.gap`."},{"name":"detail_url","type":"string","nullable":true,"desc":"`properties.detail`, exactly as ComCat gives it. The scraper follows it and never builds one."},{"name":"public_url","type":"string","nullable":true,"desc":"`properties.url`, the event's page on earthquake.usgs.gov."},{"name":"licence","type":"string","nullable":true,"desc":"The terms this event comes under, by the network that contributed it: `public-domain` for USGS's own (`us`, `atlas`), `CC-BY-SA-3.0` for the ISC-GEM catalogue (`iscgem`, `iscgemsup`) — attribution and share-alike apply. Null for a network not yet reviewed. `/v1/seismic/quakes/snapshot` carries the attribution each asks for."},{"name":"detail_wanted","type":"boolean","nullable":false,"desc":"This version of the event meets the enrichment rule — magnitude 4 or more, a PAGER alert, or felt reports — so its detail document is read once per version, newest change first: within hours of a change, and over about a day for the ~3,000 a first load owes. `?include=detail` serves what was read."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored the event, in the same fixed shape. It moves when ComCat changes the event or its detail is read — **not** a sync cursor: use `source_updated_at`."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the event."},{"name":"raw","type":"object","nullable":false,"desc":"**The feature's `properties` and `geometry`, byte for byte as ComCat wrote them**, as `{\"properties\":{...},\"geometry\":{...}}`: every key ComCat publishes, `title` and `tz` included, with ComCat's own spelling of every number. Every field above is read out of it."},{"name":"detail","type":"object","nullable":true,"desc":"**Only with `?include=detail`.** The event's detail document as the scraper read it, through the feature's own detail URL, once per version of the event: null when none has been read — for an event that is not `detail_wanted`, or one still waiting its turn: a run reads a few hundred, newest change first, so a revision's detail follows within hours and a first load's ~3,000 take about a day. See `detail.*`."},{"name":"detail.status","type":"string","nullable":false,"desc":"`ok`, or what the event's detail URL answered instead: `not_found` (404), `deleted` (409) or `unreadable` (an answer that was not a detail document)."},{"name":"detail.fetched_at","type":"timestamp","nullable":false,"desc":"When the scraper read it."},{"name":"detail.updated_at","type":"timestamp","nullable":true,"desc":"The detail document's own `updated`: the version of the event it describes."},{"name":"detail.current","type":"boolean","nullable":false,"desc":"`updated_at` is the event's `source_updated_at` or later. False when ComCat has revised the event since and the new version's detail is still to be read."},{"name":"detail.product_types","type":"array of string","nullable":false,"desc":"The keys of the document's `products`, in ComCat's order — what the consumer this replaces wrote over the summary's `product_types`."},{"name":"detail.horizontal_error_km","type":"number","nullable":true,"desc":"`products.origin[0].properties[\"horizontal-error\"]`, a string upstream."},{"name":"detail.depth_error_km","type":"number","nullable":true,"desc":"`[\"depth-error\"]`, likewise. Absent on many events."},{"name":"detail.magnitude_error","type":"number","nullable":true,"desc":"`[\"magnitude-error\"]`."},{"name":"detail.magnitude_station_count","type":"integer","nullable":true,"desc":"`[\"magnitude-num-stations-used\"]`."},{"name":"detail.location_source","type":"string","nullable":true,"desc":"`[\"location-source\"]`."},{"name":"detail.magnitude_source","type":"string","nullable":true,"desc":"`[\"magnitude-source\"]`."},{"name":"detail.origin_properties","type":"object","nullable":true,"desc":"`products.origin[0].properties` as ComCat wrote it, for every field the six above do not carry. Null when the document has no origin product."}]}]},{"slug":"labour","name":"Labour Market Data API","status":"retired","operations":[{"id":"list-ilostat-employment","verb":"GET","path":"/v1/labour/ilostat/employment","summary":"List modelled employment by sex and occupation","returns":"array","params":[{"name":"country","in":"query","type":"string","required":false,"desc":"Area codes, comma-separated or repeated -- `AZE,TUR`. Case is normalised. An unknown code is a `400`, because `GET /v1/labour/ilostat/areas` publishes the set; a code that is listed and has no figures answers an empty page. Naming an aggregate such as `X01` needs `include_aggregates=true`. Until the first ILOSTAT load has landed on a deployment there is no codelist to check a code against, and every request to the three collections answers `503` rather than an empty page or a `400` calling a real code unknown.","example":"AZE"},{"name":"sex","in":"query","type":"string","required":false,"desc":"`all`, `male` or `female`, comma-separated or repeated. Omit it for all three.","example":"all"},{"name":"isco","in":"query","type":"string","required":false,"desc":"ISCO-08 codes, bare and comma-separated -- `96,TOTAL`. Any of the thirteen CL_OCU_ISCO08 codes is accepted; `0`, `6`, `9` and `X` answer an empty page, because this dataflow publishes no figures under them.","example":"96"},{"name":"include_aggregates","in":"query","type":"boolean","required":false,"desc":"`true` adds ILO's world, regional and income aggregates -- `X01` is World. Excluded by default, so that summing or ranking rows never counts an aggregate as a country.\n\n**One value.** Given twice it is a `400`, never read for the first value: `?include_aggregates=true\u0026include_aggregates=false` would otherwise answer with aggregates and drop the second half unread. `country`, `sex` and `isco` are sets, and repeating one of them is a union."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. **A walk is pinned to the ILO load it started in**: every page after the first is read from the load page 1 came from, which the cursor names. If ILO publishes a new load and it lands between two of your pages, the next page is a `400` that says so -- never an empty last page, and never the new load's rows again under new ids. Start the walk again without a cursor; `GET /v1/labour/ilostat/snapshot` says which load is served."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `ingested_at`, each optionally prefixed with `-` to reverse. Default is `ingested_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-ingested_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"`ilostat`, the one source these collections are fed by, in any case. Any other value is a `400` naming it, and so is `source` given twice, rather than an empty page that would read as \"ILO publishes nothing for this\". `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003cLAST_UPDATE\u003e:\u003carea\u003e:\u003csex\u003e:\u003ccode\u003e:\u003cyear\u003e\" -- `ilostat:20260304T064858:AZE:all:96:2025`. Opaque: treat the whole string as the id. It names one SNAPSHOT of a figure, so a reload by ILO is a new id and an id never changes what it says."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered the row. `ilostat` is ILO's SDMX API."},{"name":"country_code","type":"string","nullable":false,"desc":"ILO's REF_AREA: ISO 3166 alpha-3 for a country (`AZE`), an X-code for one of ILO's world, regional and income aggregates (`X01` is World). **Read `is_aggregate`, not the code**: `XA1` is an aggregate that the `^X[0-9]` pattern misses."},{"name":"country_name","type":"string","nullable":false,"desc":"ILO's English name for the area, from its CL_AREA codelist. English only."},{"name":"is_aggregate","type":"boolean","nullable":false,"desc":"True for ILO's world, regional and income aggregates -- 87 of the 276 areas with data. Read from ILO's own metadata: an area ILO groups, or assigns no ILO region of its own. Aggregates are excluded unless you pass `include_aggregates=true`, so that summing or ranking rows never counts World as a country."},{"name":"isco08_code","type":"string","nullable":false,"desc":"The ISCO-08 occupation code, bare: `1` .. `8`, `96` or `TOTAL`. **This dataflow publishes no 0, 6 or 9.** `96` is major groups 6 and 9 combined -- skilled agricultural and elementary occupations -- and for every area, sex and year the eight groups sum to `TOTAL`. A consumer that expects 0 to 9 and drops `96` loses 40% of world employment, 43% of Azerbaijan's."},{"name":"isco_level","type":"integer","nullable":true,"desc":"`1` on a major group and on `96`, which is two of them. Null on `TOTAL`, which is every group summed rather than a group at any level. **`TOTAL` rows are in the default answer**, so a table of your own whose level column is NOT NULL takes the groups with `isco=1,2,3,4,5,7,8,96` and keeps `TOTAL` apart, as the denominator."},{"name":"sex","type":"string","nullable":false,"desc":"`all`, `male` or `female` -- ILO's SEX_T, SEX_M and SEX_F."},{"name":"year","type":"integer","nullable":false,"desc":"The year this figure is for."},{"name":"reference_year","type":"integer","nullable":false,"desc":"The newest year this AREA's rows are at in the snapshot. **Show it beside every country in a comparison**: it is 2025 for 184 countries and 2021 to 2023 for five, so a ranking of latest figures mixes years. Within one area every row is at this year today; the scraper refuses a snapshot whose groups do not add up to `TOTAL` for an area, sex and year, so a country cannot be split across two partial years."},{"name":"employment_thousands","type":"number","nullable":false,"desc":"Employed persons, **in thousands**, exactly as ILO publishes it -- never scaled: `4902.445` is 4,902,445 people. The scraper refuses an answer whose unit is anything else."},{"name":"obs_status","type":"string","nullable":true,"desc":"ILO's own observation status code, or null where it gives none. `A` (adjusted) is the one value in the current snapshot, on Sudan's and Ukraine's groups."},{"name":"source_version","type":"string","nullable":false,"desc":"The modelled-estimates round, read off the name ILO gives the dataflow: `Nov. 2025`. Derived on every run rather than typed, so it moves the day ILO publishes a new round."},{"name":"source_last_update","type":"string","nullable":false,"desc":"ILO's own LAST_UPDATE for the load this row came in, `2026-03-04T06:48:58`: an ISO 8601 local date-time with **no offset, because ILO states none**. It is the snapshot key -- every row of one load carries it, and it begins the row's `id`. It moves when ILO reloads a round, which it did on 4 March 2026 while the round's name stayed `Nov. 2025`."},{"name":"licence","type":"string","nullable":false,"desc":"SPDX identifier, `CC-BY-4.0`. ILO licenses its databases and datasets, with their referential metadata, under CC BY 4.0 since 3 May 2023. **An attribution obligation that travels with the data**: if you publish these figures, reproduce `attribution`."},{"name":"attribution","type":"string","nullable":false,"desc":"The string to reproduce: `Source: ILOSTAT, ILO modelled estimates, Nov. 2025`. It names the round, so it changes with `source_version`."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the snapshot -- the `fetched_at` of the pipeline this replaces. Every row of one snapshot shares it to the microsecond, so it dates the snapshot rather than the row, and it does not move when an unchanged snapshot is re-read."}]},{"id":"list-ilostat-areas","verb":"GET","path":"/v1/labour/ilostat/areas","summary":"List ILO's reference areas","returns":"array","params":[{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. **A walk is pinned to the ILO load it started in**: every page after the first is read from the load page 1 came from, which the cursor names. If ILO publishes a new load and it lands between two of your pages, the next page is a `400` that says so -- never an empty last page, and never the new load's rows again under new ids. Start the walk again without a cursor; `GET /v1/labour/ilostat/snapshot` says which load is served."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `ingested_at`, each optionally prefixed with `-` to reverse. Default is `ingested_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-ingested_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"`ilostat`, the one source these collections are fed by, in any case. Any other value is a `400` naming it, and so is `source` given twice, rather than an empty page that would read as \"ILO publishes nothing for this\". `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003cLAST_UPDATE\u003e:\u003ccode\u003e\" -- `ilostat:20260304T064858:AZE`. Opaque."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered the codelist. `ilostat`."},{"name":"code","type":"string","nullable":false,"desc":"The area code, as employment rows carry it in `country_code`."},{"name":"name_en","type":"string","nullable":true,"desc":"ILO's English name. English only, by design."},{"name":"is_aggregate","type":"boolean","nullable":false,"desc":"True when ILO groups the area or assigns it no ILO region of its own. **Both are read because each alone is wrong somewhere**: `XA1` (World: Upper-middle income excluding China) is an aggregate the `^X[0-9]` pattern misses, and `X94` (Arab States: Low income) is one that ILO's own GROUP annotation marks as not a group."},{"name":"source_version","type":"string","nullable":false,"desc":"The modelled-estimates round, read off the name ILO gives the dataflow: `Nov. 2025`. Derived on every run rather than typed, so it moves the day ILO publishes a new round."},{"name":"source_last_update","type":"string","nullable":false,"desc":"ILO's own LAST_UPDATE for the load this row came in, `2026-03-04T06:48:58`: an ISO 8601 local date-time with **no offset, because ILO states none**. It is the snapshot key -- every row of one load carries it, and it begins the row's `id`. It moves when ILO reloads a round, which it did on 4 March 2026 while the round's name stayed `Nov. 2025`."},{"name":"licence","type":"string","nullable":false,"desc":"SPDX identifier, `CC-BY-4.0`. ILO licenses its databases and datasets, with their referential metadata, under CC BY 4.0 since 3 May 2023. **An attribution obligation that travels with the data**: if you publish these figures, reproduce `attribution`."},{"name":"attribution","type":"string","nullable":false,"desc":"The string to reproduce: `Source: ILOSTAT, ILO modelled estimates, Nov. 2025`. It names the round, so it changes with `source_version`."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the snapshot -- the `fetched_at` of the pipeline this replaces. Every row of one snapshot shares it to the microsecond, so it dates the snapshot rather than the row, and it does not move when an unchanged snapshot is re-read."}]},{"id":"list-ilostat-isco-major-groups","verb":"GET","path":"/v1/labour/ilostat/isco-major-groups","summary":"List the ISCO-08 occupation codes","returns":"array","params":[{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. **A walk is pinned to the ILO load it started in**: every page after the first is read from the load page 1 came from, which the cursor names. If ILO publishes a new load and it lands between two of your pages, the next page is a `400` that says so -- never an empty last page, and never the new load's rows again under new ids. Start the walk again without a cursor; `GET /v1/labour/ilostat/snapshot` says which load is served."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `ingested_at`, each optionally prefixed with `-` to reverse. Default is `ingested_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-ingested_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"`ilostat`, the one source these collections are fed by, in any case. Any other value is a `400` naming it, and so is `source` given twice, rather than an empty page that would read as \"ILO publishes nothing for this\". `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003cLAST_UPDATE\u003e:\u003ccode\u003e\" -- `ilostat:20260304T064858:96`. Opaque."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered the codelist. `ilostat`."},{"name":"code","type":"string","nullable":false,"desc":"The bare code, as employment rows carry it in `isco08_code`: `TOTAL`, `0` .. `9`, `96` or `X`."},{"name":"upstream_code","type":"string","nullable":false,"desc":"ILO's own: `OCU_ISCO08_96`."},{"name":"name_en","type":"string","nullable":true,"desc":"ILO's English label, numbered as ILO numbers it: `96. Elementary occupations and skilled agricultural, forestry and fishery workers`."},{"name":"is_total","type":"boolean","nullable":false,"desc":"True on `TOTAL` alone."},{"name":"includes","type":"array of string","nullable":true,"desc":"The major groups a combined code holds -- `[\"6\", \"9\"]` on `96` -- read off ILO's own labels. Null on every other code."},{"name":"sort_order","type":"integer","nullable":true,"desc":"ILO's own ordering annotation, which puts `96` between `9` and `0`."},{"name":"published","type":"boolean","nullable":false,"desc":"Whether the served snapshot's employment rows carry this code at all. **False on `0`, `6`, `9` and `X`**: the modelled dataflow does not publish them, and asking for one answers an empty page rather than a 400, because the code is real."},{"name":"source_version","type":"string","nullable":false,"desc":"The modelled-estimates round, read off the name ILO gives the dataflow: `Nov. 2025`. Derived on every run rather than typed, so it moves the day ILO publishes a new round."},{"name":"source_last_update","type":"string","nullable":false,"desc":"ILO's own LAST_UPDATE for the load this row came in, `2026-03-04T06:48:58`: an ISO 8601 local date-time with **no offset, because ILO states none**. It is the snapshot key -- every row of one load carries it, and it begins the row's `id`. It moves when ILO reloads a round, which it did on 4 March 2026 while the round's name stayed `Nov. 2025`."},{"name":"licence","type":"string","nullable":false,"desc":"SPDX identifier, `CC-BY-4.0`. ILO licenses its databases and datasets, with their referential metadata, under CC BY 4.0 since 3 May 2023. **An attribution obligation that travels with the data**: if you publish these figures, reproduce `attribution`."},{"name":"attribution","type":"string","nullable":false,"desc":"The string to reproduce: `Source: ILOSTAT, ILO modelled estimates, Nov. 2025`. It names the round, so it changes with `source_version`."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the snapshot -- the `fetched_at` of the pipeline this replaces. Every row of one snapshot shares it to the microsecond, so it dates the snapshot rather than the row, and it does not move when an unchanged snapshot is re-read."}]},{"id":"get-ilostat-snapshot","verb":"GET","path":"/v1/labour/ilostat/snapshot","summary":"Which ILOSTAT snapshot is being served","returns":"object","params":[],"fields":[{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `ilostat`."},{"name":"dataflow","type":"string","nullable":true,"desc":"ILO's identifier for the dataflow: `ILO:DF_EMP_2EMP_SEX_OCU_NB(1.0)`."},{"name":"dataflow_name","type":"string","nullable":true,"desc":"ILO's English name for it: `Employment by sex and occupation -- ILO modelled estimates, Nov. 2025`. `source_version` and `attribution` are read from it."},{"name":"source_version","type":"string","nullable":false,"desc":"The modelled-estimates round being served: `Nov. 2025`."},{"name":"source_last_update","type":"string","nullable":false,"desc":"ILO's LAST_UPDATE for the load being served, local time with no offset -- the snapshot key every row carries."},{"name":"upstream_url","type":"string","nullable":true,"desc":"The SDMX request the snapshot was read from: OCU as a wildcard, all three sexes, the latest observation of every series, as CSV."},{"name":"checksum_sha256","type":"string","nullable":true,"desc":"`sha256` of the CSV bytes that request answered with. The data endpoint carries no version of its own, so this is how to tell two reads of it apart."},{"name":"employment_rows","type":"integer","nullable":false,"desc":"Employment rows in the snapshot, aggregates included -- 7,452 when this shipped."},{"name":"areas_reported","type":"integer","nullable":false,"desc":"Areas with figures in the snapshot -- 276, of which 87 are aggregates."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the snapshot."},{"name":"last_checked_at","type":"timestamp","nullable":true,"desc":"When this platform last asked ILO, whatever came back. Weekly. A run that finds its snapshot already held stores nothing new and moves only this."},{"name":"last_check_outcome","type":"string","nullable":true,"desc":"`ok` or `failed`."},{"name":"last_check_detail","type":"string","nullable":true,"desc":"What that run found, in a sentence -- including, when it failed, why: an empty answer, ILO's `No data is found`, or occupation groups that did not add up to `TOTAL`."},{"name":"stale","type":"boolean","nullable":false,"desc":"True when the newest check failed, or when no check has succeeded for nine days. **The snapshot served is the last good one either way** -- this says how recently it was confirmed against ILO, never that the figures changed."},{"name":"licence","type":"string","nullable":false,"desc":"SPDX identifier, `CC-BY-4.0`."},{"name":"attribution","type":"string","nullable":false,"desc":"The string to reproduce if you publish these figures, naming the round: `Source: ILOSTAT, ILO modelled estimates, Nov. 2025`."}]},{"id":"list-oews-occupations","verb":"GET","path":"/v1/labour/oews/occupations","summary":"List U.S. employment and wages by occupation","returns":"array","params":[{"name":"year","in":"query","type":"integer","required":false,"desc":"A May reference year -- `2025`. Omit it for the newest year served. Answers the rows of that year's file alone, so every measure is from the one year it states. A year that is not served is a `400` naming the ones that are -- `GET /v1/labour/oews/years` lists them -- and so is `year` given twice. Until the first OEWS year has landed on a deployment every OEWS route answers `503` rather than an empty page.","example":"2025"},{"name":"o_group","in":"query","type":"string","required":false,"desc":"BLS's levels, comma-separated or repeated -- `detailed`, `broad`, `minor`, `major`, `total` -- or `all` for every one. **Omit it for `detailed` alone**: one row per SOC code, 830 for May 2025. Any other value is a `400`.","example":"detailed"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. **A walk stays in the file it started in**: every page after the first is read from the year and the load page 1 came from, which the cursor names. A new year released, or a corrected file for the year you are walking, is met by your NEXT walk; this one finishes as it began, every row once. A full walk of a year's detailed occupations is nine requests at `limit=100`."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"`oews`, the one source this collection is fed by, in any case. Any other value is a `400` naming it, and so is `source` given twice, rather than an empty page that would read as \"BLS publishes nothing\". `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003cyear\u003e:\u003cload\u003e:\u003csoc_code\u003e:\u003co_group\u003e\" -- `oews:2025:852250997cefv1:15-1252:detailed`. Opaque: treat the whole string as the id. The load is the workbook's digest and the parser version, so a corrected file from BLS is a new id and an id never changes what it says."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered the row. `oews` is BLS's national OEWS file."},{"name":"soc_code","type":"string","nullable":false,"desc":"The 6-digit code with its hyphen, `15-1252`, exactly as BLS writes it -- **the join key**. A SOC 2018 code, or, where OEWS publishes a combination of detailed occupations, the OEWS-specific code BLS gives it. Every year served is coded this way, which is why May 2021 is the first: May 2019 and May 2020 mixed SOC 2010 and SOC 2018 codes."},{"name":"soc_title","type":"string","nullable":false,"desc":"BLS's title for the occupation: `Software Developers`."},{"name":"area_type","type":"string","nullable":true,"desc":"`national` on every row: the national table is the only one served."},{"name":"area_code","type":"string","nullable":true,"desc":"The file's own AREA code, **`99`**, with `area_name` its AREA_TITLE, `U.S.`. Not the `0000000` area segment of a BLS timeseries id, which is the same national cut spelled for the timeseries API: this collection is read from BLS's national table, and serves that table's codes rather than inventing another's. Map `99` once if your own table is keyed on a different literal."},{"name":"area_name","type":"string","nullable":true,"desc":"The file's AREA_TITLE, `U.S.`."},{"name":"industry_code","type":"string","nullable":true,"desc":"The file's NAICS, `000000`: all industries. `industry_name` is its title, `Cross-industry`."},{"name":"year","type":"integer","nullable":false,"desc":"The May reference year. **All of a row's measures are from this one year**: a year is one file, served whole or not at all."},{"name":"reference_period","type":"string","nullable":false,"desc":"`\u003cyear\u003e-05`, `2025-05`: OEWS estimates are as of May of the reference year."},{"name":"employment","type":"integer","nullable":true,"desc":"Employment in the occupation, the U.S. total across all industries, rounded by BLS to the nearest ten. **Excludes the self-employed**, as every OEWS figure does. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"mean_wage_annual","type":"integer","nullable":true,"desc":"Mean annual wage, whole U.S. dollars. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"median_wage_annual","type":"integer","nullable":true,"desc":"Median annual wage, whole U.S. dollars. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct10_wage_annual","type":"integer","nullable":true,"desc":"10th percentile annual wage, whole U.S. dollars. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct25_wage_annual","type":"integer","nullable":true,"desc":"25th percentile annual wage, whole U.S. dollars. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct75_wage_annual","type":"integer","nullable":true,"desc":"75th percentile annual wage, whole U.S. dollars. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct90_wage_annual","type":"integer","nullable":true,"desc":"90th percentile annual wage, whole U.S. dollars. **The measure BLS top-codes most**: 72 rows of May 2024 carry `#` here, a wage at or above $239,200 a year, and May 2025 carries none. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"industry_name","type":"string","nullable":true,"desc":"The file's NAICS_TITLE, `Cross-industry`."},{"name":"o_group","type":"string","nullable":false,"desc":"BLS's level for the code: `total` (`00-0000`, all occupations), `major`, `minor`, `broad` or `detailed`. In BLS's words, `detailed` is \"the most detailed data available\": a SOC detailed occupation, or a broad occupation or an OEWS-specific combination where OEWS no longer publishes the detail. So **seven codes appear twice**, once as `broad` and once as `detailed` with identical figures -- `13-1020` Buyers and Purchasing Agents among them. A default walk returns `detailed` rows only, one per code."},{"name":"mean_wage_hourly","type":"number","nullable":true,"desc":"Mean hourly wage, U.S. dollars to the cent. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"median_wage_hourly","type":"number","nullable":true,"desc":"Median hourly wage, U.S. dollars to the cent. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct10_wage_hourly","type":"number","nullable":true,"desc":"10th percentile hourly wage, U.S. dollars to the cent. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct25_wage_hourly","type":"number","nullable":true,"desc":"25th percentile hourly wage, U.S. dollars to the cent. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct75_wage_hourly","type":"number","nullable":true,"desc":"75th percentile hourly wage, U.S. dollars to the cent. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct90_wage_hourly","type":"number","nullable":true,"desc":"90th percentile hourly wage, U.S. dollars to the cent. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"employment_prse","type":"number","nullable":true,"desc":"BLS's percent relative standard error of `employment`, to the tenth: `0.6` is 0.6%. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"mean_wage_prse","type":"number","nullable":true,"desc":"BLS's percent relative standard error of `mean_wage_annual` and `mean_wage_hourly`, to the tenth. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"annual_only","type":"boolean","nullable":false,"desc":"BLS's ANNUAL flag: only annual wages are published -- 58 detailed occupations in May 2025, teachers and pilots among them -- so the six hourly measures are null with `*`."},{"name":"hourly_only","type":"boolean","nullable":false,"desc":"BLS's HOURLY flag: only hourly wages are published -- five performers' occupations, `27-2011` Actors among them -- so the six annual measures are null with `*`."},{"name":"markers","type":"object","nullable":false,"desc":"BLS's marker behind every null measure, keyed by field: `{\"mean_wage_annual\": \"*\", ...}`, and `{}` when no measure of the row is withheld. `*` is a wage estimate BLS does not publish, `**` an employment estimate it does not publish, and `#` a wage at or above the year's top-code -- $115.00 an hour or $239,200 a year since May 2022, $100.00 or $208,000 for May 2021; `GET /v1/labour/oews/years` carries each year's own legend. **A null with `#` behind it is a high wage, not a missing one**: read the marker before you treat the null as absent. Every null measure has a marker and every marker sits on a null; a row that broke that is refused rather than stored."},{"name":"load_key","type":"string","nullable":false,"desc":"The file the row was read from: the first twelve hex digits of the workbook's sha256, then the parser version -- `852250997cefv1`. It is the same on every row of a year, and `GET /v1/labour/oews/years` names the load served for each year."},{"name":"licence","type":"string","nullable":false,"desc":"`public_domain_us_government`: a work of the United States Government. BLS asks to be cited as the source -- reproduce `citation`."},{"name":"citation","type":"string","nullable":false,"desc":"`U.S. Bureau of Labor Statistics, Occupational Employment and Wage Statistics.`"},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the row -- the key a walk is ordered by. Every row of one year's file shares it, so a walk is in SOC code order, and it never moves when an unchanged file is read again."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored a change to the row. A file never changes under its own load, so in practice it equals `first_seen_at`."}]},{"id":"get-oews-occupation","verb":"GET","path":"/v1/labour/oews/occupations/{soc_code}","summary":"Retrieve one occupation's employment and wages","returns":"object","params":[{"name":"soc_code","in":"path","type":"string","required":true,"desc":"A 6-digit code with its hyphen -- `15-1252`. Answers the code's row at its most detailed level: a code BLS lists as both broad and detailed, with identical figures, answers the detailed row. **A code the year does not hold is a `404`**, never an empty `200`; one without its hyphen is a `400`.","example":"15-1252"},{"name":"year","in":"query","type":"integer","required":false,"desc":"A May reference year -- `2025`. Omit it for the newest year served. Answers the code from that year's file alone, so every measure is from the one year it states. A year that is not served is a `400` naming the ones that are -- `GET /v1/labour/oews/years` lists them -- and so is `year` given twice. Until the first OEWS year has landed on a deployment every OEWS route answers `503` rather than an empty page.","example":"2025"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003cyear\u003e:\u003cload\u003e:\u003csoc_code\u003e:\u003co_group\u003e\" -- `oews:2025:852250997cefv1:15-1252:detailed`. Opaque: treat the whole string as the id. The load is the workbook's digest and the parser version, so a corrected file from BLS is a new id and an id never changes what it says."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered the row. `oews` is BLS's national OEWS file."},{"name":"soc_code","type":"string","nullable":false,"desc":"The 6-digit code with its hyphen, `15-1252`, exactly as BLS writes it -- **the join key**. A SOC 2018 code, or, where OEWS publishes a combination of detailed occupations, the OEWS-specific code BLS gives it. Every year served is coded this way, which is why May 2021 is the first: May 2019 and May 2020 mixed SOC 2010 and SOC 2018 codes."},{"name":"soc_title","type":"string","nullable":false,"desc":"BLS's title for the occupation: `Software Developers`."},{"name":"area_type","type":"string","nullable":true,"desc":"`national` on every row: the national table is the only one served."},{"name":"area_code","type":"string","nullable":true,"desc":"The file's own AREA code, **`99`**, with `area_name` its AREA_TITLE, `U.S.`. Not the `0000000` area segment of a BLS timeseries id, which is the same national cut spelled for the timeseries API: this collection is read from BLS's national table, and serves that table's codes rather than inventing another's. Map `99` once if your own table is keyed on a different literal."},{"name":"area_name","type":"string","nullable":true,"desc":"The file's AREA_TITLE, `U.S.`."},{"name":"industry_code","type":"string","nullable":true,"desc":"The file's NAICS, `000000`: all industries. `industry_name` is its title, `Cross-industry`."},{"name":"year","type":"integer","nullable":false,"desc":"The May reference year. **All of a row's measures are from this one year**: a year is one file, served whole or not at all."},{"name":"reference_period","type":"string","nullable":false,"desc":"`\u003cyear\u003e-05`, `2025-05`: OEWS estimates are as of May of the reference year."},{"name":"employment","type":"integer","nullable":true,"desc":"Employment in the occupation, the U.S. total across all industries, rounded by BLS to the nearest ten. **Excludes the self-employed**, as every OEWS figure does. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"mean_wage_annual","type":"integer","nullable":true,"desc":"Mean annual wage, whole U.S. dollars. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"median_wage_annual","type":"integer","nullable":true,"desc":"Median annual wage, whole U.S. dollars. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct10_wage_annual","type":"integer","nullable":true,"desc":"10th percentile annual wage, whole U.S. dollars. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct25_wage_annual","type":"integer","nullable":true,"desc":"25th percentile annual wage, whole U.S. dollars. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct75_wage_annual","type":"integer","nullable":true,"desc":"75th percentile annual wage, whole U.S. dollars. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct90_wage_annual","type":"integer","nullable":true,"desc":"90th percentile annual wage, whole U.S. dollars. **The measure BLS top-codes most**: 72 rows of May 2024 carry `#` here, a wage at or above $239,200 a year, and May 2025 carries none. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"industry_name","type":"string","nullable":true,"desc":"The file's NAICS_TITLE, `Cross-industry`."},{"name":"o_group","type":"string","nullable":false,"desc":"BLS's level for the code: `total` (`00-0000`, all occupations), `major`, `minor`, `broad` or `detailed`. In BLS's words, `detailed` is \"the most detailed data available\": a SOC detailed occupation, or a broad occupation or an OEWS-specific combination where OEWS no longer publishes the detail. So **seven codes appear twice**, once as `broad` and once as `detailed` with identical figures -- `13-1020` Buyers and Purchasing Agents among them. A default walk returns `detailed` rows only, one per code."},{"name":"mean_wage_hourly","type":"number","nullable":true,"desc":"Mean hourly wage, U.S. dollars to the cent. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"median_wage_hourly","type":"number","nullable":true,"desc":"Median hourly wage, U.S. dollars to the cent. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct10_wage_hourly","type":"number","nullable":true,"desc":"10th percentile hourly wage, U.S. dollars to the cent. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct25_wage_hourly","type":"number","nullable":true,"desc":"25th percentile hourly wage, U.S. dollars to the cent. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct75_wage_hourly","type":"number","nullable":true,"desc":"75th percentile hourly wage, U.S. dollars to the cent. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"pct90_wage_hourly","type":"number","nullable":true,"desc":"90th percentile hourly wage, U.S. dollars to the cent. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"employment_prse","type":"number","nullable":true,"desc":"BLS's percent relative standard error of `employment`, to the tenth: `0.6` is 0.6%. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"mean_wage_prse","type":"number","nullable":true,"desc":"BLS's percent relative standard error of `mean_wage_annual` and `mean_wage_hourly`, to the tenth. **Null where BLS printed a marker** -- `markers` says which -- and never `\"*\"`, `\"**\"` or `\"#\"`."},{"name":"annual_only","type":"boolean","nullable":false,"desc":"BLS's ANNUAL flag: only annual wages are published -- 58 detailed occupations in May 2025, teachers and pilots among them -- so the six hourly measures are null with `*`."},{"name":"hourly_only","type":"boolean","nullable":false,"desc":"BLS's HOURLY flag: only hourly wages are published -- five performers' occupations, `27-2011` Actors among them -- so the six annual measures are null with `*`."},{"name":"markers","type":"object","nullable":false,"desc":"BLS's marker behind every null measure, keyed by field: `{\"mean_wage_annual\": \"*\", ...}`, and `{}` when no measure of the row is withheld. `*` is a wage estimate BLS does not publish, `**` an employment estimate it does not publish, and `#` a wage at or above the year's top-code -- $115.00 an hour or $239,200 a year since May 2022, $100.00 or $208,000 for May 2021; `GET /v1/labour/oews/years` carries each year's own legend. **A null with `#` behind it is a high wage, not a missing one**: read the marker before you treat the null as absent. Every null measure has a marker and every marker sits on a null; a row that broke that is refused rather than stored."},{"name":"load_key","type":"string","nullable":false,"desc":"The file the row was read from: the first twelve hex digits of the workbook's sha256, then the parser version -- `852250997cefv1`. It is the same on every row of a year, and `GET /v1/labour/oews/years` names the load served for each year."},{"name":"licence","type":"string","nullable":false,"desc":"`public_domain_us_government`: a work of the United States Government. BLS asks to be cited as the source -- reproduce `citation`."},{"name":"citation","type":"string","nullable":false,"desc":"`U.S. Bureau of Labor Statistics, Occupational Employment and Wage Statistics.`"},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the row -- the key a walk is ordered by. Every row of one year's file shares it, so a walk is in SOC code order, and it never moves when an unchanged file is read again."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored a change to the row. A file never changes under its own load, so in practice it equals `first_seen_at`."}]},{"id":"list-oews-years","verb":"GET","path":"/v1/labour/oews/years","summary":"The OEWS years served, and when BLS released each","returns":"array","unmetered":true,"params":[],"fields":[{"name":"year","type":"integer","nullable":false,"desc":"The May reference year."},{"name":"reference_period","type":"string","nullable":false,"desc":"`\u003cyear\u003e-05`."},{"name":"released_at","type":"string","nullable":false,"desc":"The day BLS released the year's estimates, `YYYY-MM-DD`, read from BLS's own OEWS release feed: `2026-05-15` for May 2025, `2025-04-02` for May 2024. **Not the file's Last-Modified**, which BLS moved for every year at once when it re-uploaded them all on 2026-05-15 -- that one is `file_last_modified`."},{"name":"soc_count","type":"integer","nullable":false,"desc":"Detailed occupations the year holds -- 830 for May 2025 -- which is what a default walk of `/v1/labour/oews/occupations?year=` returns, one row per code."},{"name":"occupation_rows","type":"integer","nullable":false,"desc":"Rows at every level, the total included: 1,401 for May 2025."},{"name":"rows_by_group","type":"object","nullable":false,"desc":"`occupation_rows` by level: `{\"total\": 1, \"major\": 22, \"minor\": 94, \"broad\": 454, \"detailed\": 830}` for May 2025."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `oews`."},{"name":"load_key","type":"string","nullable":false,"desc":"The load served for the year: the workbook's digest, then the parser version. Every row of the year carries it. **When it changes, BLS published a corrected file** (or this platform read the same one with a fixed parser) and the year's rows have new ids."},{"name":"estimates_title","type":"string","nullable":true,"desc":"The workbook's own title: `May 2025 OEWS Estimates`."},{"name":"marker_notes","type":"object","nullable":false,"desc":"The year's own legend for BLS's markers, verbatim from the file -- `{\"*\": \"indicates that a wage estimate is not available\", \"#\": \"indicates a wage equal to or greater than $115.00 per hour or $239,200 per year\", ...}`. Per year, because the top-code moved: May 2021's is $100.00 or $208,000."},{"name":"release_url","type":"string","nullable":true,"desc":"BLS's news release for the year, from its release feed."},{"name":"upstream_url","type":"string","nullable":false,"desc":"The zip the rows were read from: `https://www.bls.gov/oes/special-requests/oesm25nat.zip`."},{"name":"archive_sha256","type":"string","nullable":false,"desc":"`sha256` of the zip as BLS served it."},{"name":"upstream_bytes","type":"integer","nullable":true,"desc":"The zip's size as served: 279,525 bytes for May 2025."},{"name":"workbook_sha256","type":"string","nullable":false,"desc":"`sha256` of the workbook inside the zip, which is what a load is keyed on: a zip re-compressed around the same workbook is the same load."},{"name":"file_last_modified","type":"timestamp","nullable":true,"desc":"The zip's Last-Modified as BLS served it."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When a run last read the file this load is. It moves, with `etag` and `file_last_modified`, on a week BLS serves the same workbook again under new validators, when nothing served has changed."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the load -- the file served for the year -- so it moves only when the load served does. Watch `load_key` rather than either timestamp: it is what changes with the data."},{"name":"last_confirmed_at","type":"timestamp","nullable":true,"desc":"The newest run that confirmed the year's file: found it unchanged, read it again and found the same workbook, or posted it. Weekly."},{"name":"stale","type":"boolean","nullable":false,"desc":"True when no run has confirmed the year for nine days. **The year served is its last good file either way** -- this says how recently that file was confirmed against BLS, never that the numbers changed."},{"name":"last_checked_at","type":"timestamp","nullable":true,"desc":"The newest run of the scraper, whatever it found."},{"name":"last_check_outcome","type":"string","nullable":true,"desc":"`ok` or `failed`."},{"name":"last_check_detail","type":"string","nullable":true,"desc":"What that run found, in a sentence -- including, when it failed, why."},{"name":"licence","type":"string","nullable":false,"desc":"`public_domain_us_government`."},{"name":"citation","type":"string","nullable":false,"desc":"The citation BLS asks for: `U.S. Bureau of Labor Statistics, Occupational Employment and Wage Statistics.`"}]},{"id":"get-onet-release","verb":"GET","path":"/v1/labour/onet/release","summary":"Which O*NET release is being served, and its licence","returns":"object","params":[{"name":"version","in":"query","type":"string","required":false,"desc":"The release to read, dotted -- `31.0`. Omit it for the newest. Two releases are served, the newest and the one before it; `GET /v1/labour/onet/release` lists them in `versions_held`, and any other is a `404` naming them. Every response says which release it was read from in `meta.version`. One value.","example":"31.0"}],"fields":[{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `onet`."},{"name":"version","type":"string","nullable":false,"desc":"The release, dotted as the O*NET database names it: `31.0`. **Read from the O*NET database page every week, never configured** -- a new release is found the week it is published and served once it has landed whole."},{"name":"released_on","type":"string","nullable":false,"desc":"The release month, `2026-08`. O*NET names a month and no day, and none is invented."},{"name":"release_label","type":"string","nullable":false,"desc":"The release's own label: `August 2026 Release`."},{"name":"database_title","type":"string","nullable":false,"desc":"`O*NET 31.0 Database`, the first line of its Read Me."},{"name":"source_url","type":"string","nullable":false,"desc":"The O*NET database page, which the release is read from."},{"name":"download_url","type":"string","nullable":false,"desc":"The zip the release was read from: the CSV build."},{"name":"download_sha256","type":"string","nullable":false,"desc":"`sha256` of the zip's bytes as they were read."},{"name":"download_bytes","type":"integer","nullable":false,"desc":"The zip's size as it was read: 16,237,378 for 31.0."},{"name":"dictionary_url","type":"string","nullable":true,"desc":"The O*NET data dictionary for this release."},{"name":"provider","type":"string","nullable":false,"desc":"Who publishes the database."},{"name":"license_name","type":"string","nullable":false,"desc":"`CC BY 4.0`."},{"name":"licence","type":"string","nullable":false,"desc":"SPDX identifier, `CC-BY-4.0`."},{"name":"license_url","type":"string","nullable":false,"desc":"The licence's own page."},{"name":"citation_text","type":"string","nullable":false,"desc":"The credit the O*NET database licence asks for, for this version, with this platform named as having modified the data. **CC BY 4.0 binds whoever redistributes the data: display it with anything built on it.**"},{"name":"modifications","type":"string","nullable":false,"desc":"What this platform changed from the published database, which CC BY 4.0 asks a redistributor to indicate."},{"name":"counts","type":"object","nullable":false,"desc":"What the release holds. **A release is served only when exactly these rows are held** -- see `counts.*`."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When the run that read the zip began to read it."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When the read now served became whole on this platform -- the moment the release started being served."},{"name":"versions_held","type":"array of string","nullable":false,"desc":"Every release served, newest first: this one and the one before it. Any of them can be named in `?version=` on every O*NET route."},{"name":"last_checked_at","type":"timestamp","nullable":true,"desc":"When this platform last read the O*NET database page, whatever it found. Weekly."},{"name":"last_check_outcome","type":"string","nullable":true,"desc":"`ok` or `failed`."},{"name":"last_check_detail","type":"string","nullable":true,"desc":"What that run found, in a sentence -- including, when it failed, why."},{"name":"newest_listed","type":"string","nullable":true,"desc":"The release the O*NET database page named at that check. Newer than `version` means a release is published and not yet served -- the check that read it failed, and `last_check_detail` says why."},{"name":"stale","type":"boolean","nullable":false,"desc":"True when the newest check failed, or when no check has succeeded for nine days. **The release served is the last good one either way** -- this says how recently it was confirmed against O*NET, never that its rows changed."},{"name":"counts.occupations","type":"integer","nullable":false,"desc":"Occupations -- 1,016 in 31.0."},{"name":"counts.tasks","type":"integer","nullable":false,"desc":"Task statements -- 18,838 in 31.0."},{"name":"counts.descriptors","type":"integer","nullable":false,"desc":"Importance ratings -- 146,551 in 31.0."},{"name":"counts.descriptors_by_type","type":"object","nullable":false,"desc":"The ratings by descriptor type: `skill`, `ability`, `knowledge` and `work_activity`, which sum to `descriptors`."},{"name":"counts.elements","type":"integer","nullable":false,"desc":"Content-model elements rated -- 161 in 31.0, 35 of them skills."},{"name":"counts.occupations_without_job_zone","type":"integer","nullable":false,"desc":"Occupations whose `job_zone` is null."},{"name":"counts.tasks_without_type","type":"integer","nullable":false,"desc":"Tasks whose `task_type` is null."},{"name":"counts.recommend_suppress","type":"integer","nullable":false,"desc":"Ratings O*NET recommends suppressing."},{"name":"counts.descriptors_by_type.skill","type":"integer","nullable":false,"desc":"Skill ratings -- 31,850 in 31.0."},{"name":"counts.descriptors_by_type.ability","type":"integer","nullable":false,"desc":"Ability ratings -- 47,320 in 31.0."},{"name":"counts.descriptors_by_type.knowledge","type":"integer","nullable":false,"desc":"Knowledge ratings -- 30,030 in 31.0."},{"name":"counts.descriptors_by_type.work_activity","type":"integer","nullable":false,"desc":"Work activity ratings -- 37,351 in 31.0."}]},{"id":"list-onet-occupations","verb":"GET","path":"/v1/labour/onet/occupations","summary":"List the O*NET occupations of a release","returns":"array","params":[{"name":"version","in":"query","type":"string","required":false,"desc":"The release to read, dotted -- `31.0`. Omit it for the newest. Two releases are served, the newest and the one before it; `GET /v1/labour/onet/release` lists them in `versions_held`, and any other is a `404` naming them. Every response says which release it was read from in `meta.version`. One value.","example":"31.0"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25. A release is 1,016 occupations: 11 pages at `limit=100`. **Each page is metered**, so check `GET /v1/labour/onet/release` first and sync only when `version` has moved.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. It carries the release the walk began in, so a release landing mid-walk does not change the rows you get: the walk finishes in the release it started. If that release is read again or pruned mid-walk, the next page is a `400` asking for a new walk -- never an empty page that would read as the end."}],"fields":[{"name":"onet_soc_code","type":"string","nullable":false,"desc":"The O*NET-SOC code, **with its `.NN` suffix**: `15-1252.00`, never `15-1252`. The BLS SOC code is the part before the dot; split it yourself."},{"name":"title","type":"string","nullable":false,"desc":"The title the O*NET database gives, verbatim. Never null."},{"name":"description","type":"string","nullable":false,"desc":"The full description from the O*NET database, verbatim -- the whole prose, not a summary. Never null."},{"name":"job_zone","type":"integer","nullable":true,"desc":"1 to 5, from the O*NET Job Zones file. **null where the O*NET database assigns none** -- 93 of 31.0's 1,016 occupations, the residual \"All Other\" categories among them -- and never 0."}]},{"id":"get-onet-occupation","verb":"GET","path":"/v1/labour/onet/occupations/{onet_soc_code}","summary":"Retrieve one O*NET occupation","returns":"object","params":[{"name":"onet_soc_code","in":"path","type":"string","required":true,"desc":"An O*NET-SOC code with its `.NN` suffix -- `15-1252.00`. A code shaped otherwise is a `400` (a bare BLS SOC code such as `15-1252` is named as one); a code the release does not list is a `404`, and an occupation withdrawn by hand a `410`. Until the first release has landed on a deployment, every O*NET route but `/release` answers `503`.","example":"15-1252.00"},{"name":"version","in":"query","type":"string","required":false,"desc":"The release to read, dotted -- `31.0`. Omit it for the newest. Two releases are served, the newest and the one before it; `GET /v1/labour/onet/release` lists them in `versions_held`, and any other is a `404` naming them. Every response says which release it was read from in `meta.version`. One value.","example":"31.0"}],"fields":[{"name":"onet_soc_code","type":"string","nullable":false,"desc":"The O*NET-SOC code, **with its `.NN` suffix**: `15-1252.00`, never `15-1252`. The BLS SOC code is the part before the dot; split it yourself."},{"name":"title","type":"string","nullable":false,"desc":"The title the O*NET database gives, verbatim. Never null."},{"name":"description","type":"string","nullable":false,"desc":"The full description from the O*NET database, verbatim -- the whole prose, not a summary. Never null."},{"name":"job_zone","type":"integer","nullable":true,"desc":"1 to 5, from the O*NET Job Zones file. **null where the O*NET database assigns none** -- 93 of 31.0's 1,016 occupations, the residual \"All Other\" categories among them -- and never 0."}]},{"id":"list-onet-occupation-tasks","verb":"GET","path":"/v1/labour/onet/occupations/{onet_soc_code}/tasks","summary":"List one occupation's task statements","returns":"array","params":[{"name":"onet_soc_code","in":"path","type":"string","required":true,"desc":"An O*NET-SOC code with its `.NN` suffix -- `15-1252.00`. A code shaped otherwise is a `400` (a bare BLS SOC code such as `15-1252` is named as one); a code the release does not list is a `404`, and an occupation withdrawn by hand a `410`. Until the first release has landed on a deployment, every O*NET route but `/release` answers `503`.","example":"15-1252.00"},{"name":"version","in":"query","type":"string","required":false,"desc":"The release to read, dotted -- `31.0`. Omit it for the newest. Two releases are served, the newest and the one before it; `GET /v1/labour/onet/release` lists them in `versions_held`, and any other is a `404` naming them. Every response says which release it was read from in `meta.version`. One value.","example":"31.0"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25. An occupation has at most a few dozen tasks; an occupation with none answers an empty page, which is then true. **Each page is metered**, so check `GET /v1/labour/onet/release` first and sync only when `version` has moved.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. It carries the release the walk began in, so a release landing mid-walk does not change the rows you get: the walk finishes in the release it started. If that release is read again or pruned mid-walk, the next page is a `400` asking for a new walk -- never an empty page that would read as the end."}],"fields":[{"name":"onet_soc_code","type":"string","nullable":false,"desc":"The occupation, with its `.NN` suffix."},{"name":"onet_task_id","type":"integer","nullable":false,"desc":"The O*NET Task ID: an integer, unique with `onet_soc_code`."},{"name":"task_text","type":"string","nullable":false,"desc":"The task statement, verbatim."},{"name":"task_type","type":"string","nullable":true,"desc":"`Core`, `Supplemental` or **null** -- null where the database gives no type (418 of 31.0's 18,838 tasks; the text build spells it `n/a`), and for any value outside the two. Never another string."}]},{"id":"list-onet-occupation-descriptors","verb":"GET","path":"/v1/labour/onet/occupations/{onet_soc_code}/descriptors","summary":"List one occupation's importance ratings","returns":"array","params":[{"name":"onet_soc_code","in":"path","type":"string","required":true,"desc":"An O*NET-SOC code with its `.NN` suffix -- `15-1252.00`. A code shaped otherwise is a `400` (a bare BLS SOC code such as `15-1252` is named as one); a code the release does not list is a `404`, and an occupation withdrawn by hand a `410`. Until the first release has landed on a deployment, every O*NET route but `/release` answers `503`.","example":"15-1252.00"},{"name":"type","in":"query","type":"string","required":false,"desc":"One descriptor type: `skill`, `ability`, `knowledge` or `work_activity`. Omit it for all four. Anything else is a `400`. One value.","example":"skill"},{"name":"version","in":"query","type":"string","required":false,"desc":"The release to read, dotted -- `31.0`. Omit it for the newest. Two releases are served, the newest and the one before it; `GET /v1/labour/onet/release` lists them in `versions_held`, and any other is a `404` naming them. Every response says which release it was read from in `meta.version`. One value.","example":"31.0"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25. An occupation rated in all four files has 161 ratings: two pages at `limit=100`. **Each page is metered**, so check `GET /v1/labour/onet/release` first and sync only when `version` has moved.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. It carries the release the walk began in, so a release landing mid-walk does not change the rows you get: the walk finishes in the release it started. If that release is read again or pruned mid-walk, the next page is a `400` asking for a new walk -- never an empty page that would read as the end."}],"fields":[{"name":"onet_soc_code","type":"string","nullable":false,"desc":"The occupation, with its `.NN` suffix."},{"name":"descriptor_type","type":"string","nullable":false,"desc":"`skill`, `ability`, `knowledge` or `work_activity` -- exactly those four. `skill` is the O*NET Essential Skills and Transferable Skills files together: the 35 elements its Skills file held until 31.0 split it in two. `onet_file` says which."},{"name":"element_id","type":"string","nullable":false,"desc":"The O*NET content-model id -- `2.A.1.b` -- the join key across occupations."},{"name":"element_name","type":"string","nullable":false,"desc":"The element's name in the O*NET content model, `Active Listening`. One name per element in a release."},{"name":"scale_id","type":"string","nullable":false,"desc":"Always `IM`, importance: the only scale served, and the one `data_value`'s rescale is for. The O*NET level ratings (`LV`, 0-7) are not served."},{"name":"data_value","type":"number","nullable":false,"desc":"Importance on 0-100, **one decimal**: `round((raw - 1) / 4 * 100, 1)` computed in Python over a float, so 3.75 is `68.8`, 3.0 is `50.0` and a tie rounds half to even on the binary value -- 3.25 is `56.2`, where decimal half-up says 56.3. Computed once, by the same expression a consumer computing its own uses, so the digits match theirs."},{"name":"data_value_raw","type":"number","nullable":false,"desc":"The importance rating, 1 to 5, as the O*NET database publishes it."},{"name":"recommend_suppress","type":"boolean","nullable":true,"desc":"The O*NET Recommend Suppress flag: true where the O*NET database advises against relying on the rating, false where it does not, **null where the database leaves it empty**."},{"name":"onet_file","type":"string","nullable":false,"desc":"The O*NET file the rating comes from: `Essential Skills`, `Transferable Skills`, `Abilities`, `Knowledge` or `Work Activities`."}]},{"id":"list-onet-tasks","verb":"GET","path":"/v1/labour/onet/tasks","summary":"List every O*NET task statement of a release","returns":"array","params":[{"name":"version","in":"query","type":"string","required":false,"desc":"The release to read, dotted -- `31.0`. Omit it for the newest. Two releases are served, the newest and the one before it; `GET /v1/labour/onet/release` lists them in `versions_held`, and any other is a `404` naming them. Every response says which release it was read from in `meta.version`. One value.","example":"31.0"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25. A release is 18,838 tasks: 189 pages at `limit=100`. **Each page is metered**, so check `GET /v1/labour/onet/release` first and sync only when `version` has moved.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. It carries the release the walk began in, so a release landing mid-walk does not change the rows you get: the walk finishes in the release it started. If that release is read again or pruned mid-walk, the next page is a `400` asking for a new walk -- never an empty page that would read as the end."}],"fields":[{"name":"onet_soc_code","type":"string","nullable":false,"desc":"The occupation, with its `.NN` suffix."},{"name":"onet_task_id","type":"integer","nullable":false,"desc":"The O*NET Task ID: an integer, unique with `onet_soc_code`."},{"name":"task_text","type":"string","nullable":false,"desc":"The task statement, verbatim."},{"name":"task_type","type":"string","nullable":true,"desc":"`Core`, `Supplemental` or **null** -- null where the database gives no type (418 of 31.0's 18,838 tasks; the text build spells it `n/a`), and for any value outside the two. Never another string."}]},{"id":"list-onet-descriptors","verb":"GET","path":"/v1/labour/onet/descriptors","summary":"List every O*NET importance rating of a release","returns":"array","params":[{"name":"type","in":"query","type":"string","required":false,"desc":"One descriptor type: `skill`, `ability`, `knowledge` or `work_activity`. Omit it for all four. Anything else is a `400`. One value.","example":"skill"},{"name":"version","in":"query","type":"string","required":false,"desc":"The release to read, dotted -- `31.0`. Omit it for the newest. Two releases are served, the newest and the one before it; `GET /v1/labour/onet/release` lists them in `versions_held`, and any other is a `404` naming them. Every response says which release it was read from in `meta.version`. One value.","example":"31.0"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25. A release is 146,551 ratings: 1,466 pages at `limit=100`, or 319 for `type=skill`. **Each page is metered**, so check `GET /v1/labour/onet/release` first and sync only when `version` has moved.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. It carries the release the walk began in, so a release landing mid-walk does not change the rows you get: the walk finishes in the release it started. If that release is read again or pruned mid-walk, the next page is a `400` asking for a new walk -- never an empty page that would read as the end."}],"fields":[{"name":"onet_soc_code","type":"string","nullable":false,"desc":"The occupation, with its `.NN` suffix."},{"name":"descriptor_type","type":"string","nullable":false,"desc":"`skill`, `ability`, `knowledge` or `work_activity` -- exactly those four. `skill` is the O*NET Essential Skills and Transferable Skills files together: the 35 elements its Skills file held until 31.0 split it in two. `onet_file` says which."},{"name":"element_id","type":"string","nullable":false,"desc":"The O*NET content-model id -- `2.A.1.b` -- the join key across occupations."},{"name":"element_name","type":"string","nullable":false,"desc":"The element's name in the O*NET content model, `Active Listening`. One name per element in a release."},{"name":"scale_id","type":"string","nullable":false,"desc":"Always `IM`, importance: the only scale served, and the one `data_value`'s rescale is for. The O*NET level ratings (`LV`, 0-7) are not served."},{"name":"data_value","type":"number","nullable":false,"desc":"Importance on 0-100, **one decimal**: `round((raw - 1) / 4 * 100, 1)` computed in Python over a float, so 3.75 is `68.8`, 3.0 is `50.0` and a tie rounds half to even on the binary value -- 3.25 is `56.2`, where decimal half-up says 56.3. Computed once, by the same expression a consumer computing its own uses, so the digits match theirs."},{"name":"data_value_raw","type":"number","nullable":false,"desc":"The importance rating, 1 to 5, as the O*NET database publishes it."},{"name":"recommend_suppress","type":"boolean","nullable":true,"desc":"The O*NET Recommend Suppress flag: true where the O*NET database advises against relying on the rating, false where it does not, **null where the database leaves it empty**."},{"name":"onet_file","type":"string","nullable":false,"desc":"The O*NET file the rating comes from: `Essential Skills`, `Transferable Skills`, `Abilities`, `Knowledge` or `Work Activities`."}]}]},{"slug":"worldbank","name":"World Bank Indicators API","status":"retired","operations":[{"id":"list-wb-countries","verb":"GET","path":"/v1/worldbank/countries","summary":"List the World Bank's economies","returns":"array","params":[{"name":"countries","in":"query","type":"string","required":false,"desc":"ISO3 codes, comma-separated — `AZE,USA` — or the parameter repeated, which means the same thing. Case is normalised. A two-letter code is a `400`: `iso2` is not accepted here. So is a code the World Bank's list does not hold, and an aggregate named without `include_aggregates=true` — each named in the `400` rather than answered with an empty page.","example":"AZE,USA"},{"name":"region","in":"query","type":"string","required":false,"desc":"A World Bank region id — `ECS`, `SSF`, `LCN` — case-insensitive and exact. The World Bank's vocabulary: an unknown id answers an empty page rather than a 400. One value: given twice it is a `400`, not the first of the two.","example":"ECS"},{"name":"income_level","in":"query","type":"string","required":false,"desc":"A World Bank income group id — `HIC`, `UMC`, `LMC`, `LIC`. One value, like `region`.","example":"UMC"},{"name":"include_aggregates","in":"query","type":"boolean","required":false,"desc":"`true` adds the aggregates — 79 on 2026-10-09: `WLD`, `EUU`, `HIC` — to the 217 economies. **Off by default**: an aggregate is a sum of the other rows, and a consumer treating it as a country double counts.","example":"true"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to the one source the World Bank collections are fed by, `wdi`, case-insensitive. Any other stem is a `400` naming it rather than an empty page; `GET /v1/sources` lists every stem. One value."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003ciso3\u003e\" — `wdi:AZE`. Opaque: treat the whole string as the id."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered the list. `wdi` reads the World Bank Indicators API."},{"name":"iso3","type":"string","nullable":false,"desc":"The World Bank's id: ISO 3166 alpha-3 for an economy — and the World Bank's own `XKX` for Kosovo and `CHI` for the Channel Islands — and an ISO3-SHAPED code for an aggregate. **The shape of this code does not tell a country from an aggregate**: `WLD`, `EUU` and `ARB` look exactly like `AZE`. `is_aggregate` does. **This is the key to join on.**"},{"name":"iso2","type":"string","nullable":true,"desc":"The World Bank's `iso2Code`, **byte for byte**. Other datasets join on it, so it is passed through rather than normalised — and it is not always ISO 3166: the World aggregate's is `1W`."},{"name":"name","type":"string","nullable":true,"desc":"The World Bank's name for the entry, trimmed — `Azerbaijan`, `Arab World`."},{"name":"region","type":"string","nullable":true,"desc":"The World Bank's region, `Europe \u0026 Central Asia`. **Trimmed**: upstream sends `Latin America \u0026 Caribbean ` and `Sub-Saharan Africa ` with a trailing space, and a groupBy on the untrimmed value splits one region in two. `Aggregates` on every aggregate."},{"name":"region_id","type":"string","nullable":true,"desc":"The region's id, `ECS`. `NA` on every aggregate and on no economy — **which is the aggregate rule**. `?region=` takes this."},{"name":"admin_region","type":"string","nullable":true,"desc":"The World Bank's administrative region, given only to low- and middle-income economies — `Europe \u0026 Central Asia (excluding high income)`. Null on high-income economies and on aggregates."},{"name":"admin_region_id","type":"string","nullable":true,"desc":"The administrative region's id, `ECA`."},{"name":"income_level","type":"string","nullable":true,"desc":"The World Bank's income group, `Upper middle income`. **Revised by the World Bank every 1 July**; `Aggregates` on every aggregate."},{"name":"income_level_id","type":"string","nullable":true,"desc":"The income group's id — `HIC`, `UMC`, `LMC`, `LIC`. `?income_level=` takes this."},{"name":"lending_type","type":"string","nullable":true,"desc":"Which World Bank lending the economy is eligible for, `IBRD`. `Aggregates` on every aggregate, as the World Bank files it, with `lending_type_id` null."},{"name":"lending_type_id","type":"string","nullable":true,"desc":"The lending type's id, `IBD`. Null on every aggregate."},{"name":"capital_city","type":"string","nullable":true,"desc":"The capital, as the World Bank names it. Null on aggregates."},{"name":"longitude","type":"number","nullable":true,"desc":"The capital's longitude, as published. Null on every aggregate and on six economies."},{"name":"latitude","type":"number","nullable":true,"desc":"The capital's latitude, as published."},{"name":"is_aggregate","type":"boolean","nullable":false,"desc":"**True for a World Bank aggregate** — `World`, `Euro area`, `High income` — and false for one of the 217 economies. It is the upstream's own `region.id` being `NA`, not a guess from the code. Aggregates are served only under `?include_aggregates=true`."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the entry. The key the list is walked on: it does not move when a refresh re-stores the list."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored the entry."}]},{"id":"get-wdi-context","verb":"GET","path":"/v1/worldbank/context","summary":"Each country's newest value of each indicator, keyed by ISO3","returns":"array","params":[{"name":"iso3","in":"query","type":"string","required":false,"desc":"ISO 3166-1 alpha-3 codes, comma-separated — `AZE,USA` — or the parameter repeated; case is normalised, at most 100. **Every code named is answered, in the order named, in one response**: an economy with its values, and a real ISO code the World Bank does not report — `REU`, `VAT`, `TKL`, `TWN` — as an empty record with `wdi_economy` false, never an error. A code that is no country at all — `ZZZ` — is a `400` naming it, and so is an aggregate — `WLD`, `EUU`, `HIC` — whose series are at `GET /v1/worldbank/indicators/{code}`. The World Bank's own `XKX` (Kosovo) and `CHI` (Channel Islands) are answered as the economies they are. Leave it out to walk every economy instead, a page at a time.","example":"AZE"},{"name":"indicators","in":"query","type":"string","required":false,"desc":"Which indicators each record carries: fields — `gdp_per_capita_usd` — or WDI codes — `NY.GDP.PCAP.CD` — comma-separated or repeated, at most 50. Left out, the default ten. `GET /v1/worldbank/indicators` lists every served code with its field; a name that is not served is a `400` naming the fields that are, and one this deployment holds no series of yet is a `503` naming it."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Records per page of the walk over every economy, 1–100. Defaults to 25. A `400` beside `iso3`, which answers every code at once."},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from the walk's previous meta.next. Do not construct one. A `400` beside `iso3`."}],"fields":[{"name":"iso3","type":"string","nullable":false,"desc":"The code asked for, upper-cased: ISO 3166-1 alpha-3, or the World Bank's own `XKX` for Kosovo and `CHI` for the Channel Islands. **The key**: join on it, never on a name."},{"name":"name","type":"string","nullable":true,"desc":"The World Bank's name for the economy, `United States` — for display only, never a key, because no two name lists agree. Null when `wdi_economy` is false."},{"name":"wdi_economy","type":"boolean","nullable":false,"desc":"True when the World Bank's country list holds the code as one of its economies. **False for a real ISO code it does not report** — Réunion, the Holy See, Tokelau, Taiwan — whose record carries no indicator at all: the World Bank publishes nothing for it, which is an answer, not an error."},{"name":"population","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes the total population (`SP.POP.TOTL`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** Persons."},{"name":"population_growth_pct","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes population growth (`SP.POP.GROW`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** Annual %."},{"name":"gdp_per_capita_usd","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes GDP per capita (`NY.GDP.PCAP.CD`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** Current US$."},{"name":"unemployment_pct","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes unemployment (`SL.UEM.TOTL.ZS`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** % of the total labour force, the ILO's modelled estimate."},{"name":"labor_force_total","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes the total labour force (`SL.TLF.TOTL.IN`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** Persons."},{"name":"tertiary_enrollment_pct","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes gross tertiary enrolment (`SE.TER.ENRR`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** A gross ratio, %: it exceeds 100 where enrolment outnumbers the age group — Greece's 2023 value is 165.11 — and is never clamped. Its year often trails the others'."},{"name":"working_age_share_pct","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes the population aged 15–64 (`SP.POP.1564.TO.ZS`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** % of the total population."},{"name":"urban_population_share_pct","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes the urban population (`SP.URB.TOTL.IN.ZS`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** % of the total population."},{"name":"remittances_received_pct_gdp","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes personal remittances received (`BX.TRF.PWKR.DT.GD.ZS`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** % of GDP."},{"name":"remittances_received_usd","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes personal remittances received (`BX.TRF.PWKR.CD.DT`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** Current US$."},{"name":"rd_expenditure_pct_gdp","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes research and development expenditure (`GB.XPD.RSDV.GD.ZS`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** % of GDP. Outside the default set: present only when `indicators` names it."},{"name":"gdp_usd","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes GDP (`NY.GDP.MKTP.CD`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** Current US$. Outside the default set: present only when `indicators` names it."},{"name":"researchers_per_million","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes researchers in R\u0026D (`SP.POP.SCIE.RD.P6`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** Per million people. Outside the default set: present only when `indicators` names it."},{"name":"hightech_exports_usd","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes high-technology exports (`TX.VAL.TECH.CD`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** Current US$. Outside the default set: present only when `indicators` names it."},{"name":"hightech_exports_pct_manuf","type":"object","nullable":false,"desc":"`{value, year}`: the newest year the World Bank publishes high-technology exports (`TX.VAL.TECH.MF.ZS`) for the country, and that value — a number, unrounded, an integer where the World Bank's is one. **Absent — not null and not zero — where it publishes none.** % of manufactured exports. Outside the default set: present only when `indicators` names it."}]},{"id":"list-wdi-indicators","verb":"GET","path":"/v1/worldbank/indicators","summary":"The served WDI codes: field, unit, release and deprecation","returns":"array","params":[{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."}],"fields":[{"name":"code","type":"string","nullable":false,"desc":"The WDI code, `NY.GDP.PCAP.CD` — what `GET /v1/worldbank/indicators/{code}` takes, and what `GET /v1/worldbank/context?indicators=` takes beside the field."},{"name":"field","type":"string","nullable":true,"desc":"The key `GET /v1/worldbank/context` serves the code's `{value, year}` under, `gdp_per_capita_usd`. Null for a code served by its series route only."},{"name":"name","type":"string","nullable":true,"desc":"WDI's own name for the indicator, `GDP per capita (current US$)`, as its metadata last gave it. Null until the first run has described the code."},{"name":"unit","type":"string","nullable":true,"desc":"The unit this platform states for the code — `current US$`, `% of GDP`, `persons` — read off WDI's own name, because the World Bank's unit field is empty for every served code."},{"name":"context_default","type":"boolean","nullable":false,"desc":"True for the ten codes `GET /v1/worldbank/context` answers when `indicators` is left out."},{"name":"deprecated","type":"boolean","nullable":false,"desc":"**True when the World Bank has moved the code out of WDI** — filed under source 57, the WDI Database Archives, which is how it retires one. The World Bank's own statement, as the daily run last read it, never this platform's opinion. A deprecated code's held series stays served, dated by `release`, and is no longer refreshed."},{"name":"wb_source_id","type":"string","nullable":true,"desc":"The World Bank source the code is filed under: `2`, World Development Indicators, or `57` for a retired one. Null until the first run has described the code."},{"name":"wb_source_name","type":"string","nullable":true,"desc":"That source's name, `World Development Indicators`."},{"name":"source_organization","type":"string","nullable":true,"desc":"Who produces the figures the World Bank republishes, as the World Bank credits them — the UN Population Division, the ILO."},{"name":"source_note","type":"string","nullable":true,"desc":"WDI's definition of the indicator."},{"name":"release","type":"date","nullable":true,"desc":"WDI's own `lastupdated` for the series held, `2026-10-08`. Null before a series is held — a code added to the catalogue is fetched the night after."},{"name":"retrieved_at","type":"timestamp","nullable":true,"desc":"When this platform fetched and stored that series."},{"name":"licence","type":"string","nullable":true,"desc":"The licence the series' own metadata stated when it was fetched, verbatim: `CC BY-4.0`, the World Bank's spelling of CC BY 4.0 and the only one a held series can have. Null before a series is held."},{"name":"licence_url","type":"string","nullable":true,"desc":"The licence link the series' metadata gave beside it."},{"name":"added_at","type":"timestamp","nullable":false,"desc":"When the code joined this catalogue. The key the catalogue is walked on, then the code."}]},{"id":"list-wdi-observations","verb":"GET","path":"/v1/worldbank/indicators/{code}","summary":"One indicator's series, every economy and year","returns":"array","params":[{"name":"code","in":"path","type":"string","required":true,"desc":"A served WDI code — `SP.POP.TOTL`, `NY.GDP.PCAP.CD`. A code that is not served is a `404` naming the ones that are. `GET /v1/worldbank/snapshot` counts them.","example":"SP.POP.TOTL"},{"name":"countries","in":"query","type":"string","required":false,"desc":"ISO3 codes, comma-separated or repeated. **Naming an aggregate without `include_aggregates=true` is a `400`** rather than an empty answer, which would read as \"no data for the World\" — and for an aggregate the World Bank publishes no values of this code for, the `400` says that instead. A code the World Bank's list does not hold is a `400` naming it.","example":"AZE,USA"},{"name":"from","in":"query","type":"integer","required":false,"desc":"Earliest year, inclusive. One value.","example":"2020"},{"name":"to","in":"query","type":"integer","required":false,"desc":"Latest year, inclusive. One value.","example":"2025"},{"name":"include_aggregates","in":"query","type":"boolean","required":false,"desc":"`true` adds the values of every aggregate that has them for this code: the ones the World Bank publishes under an ISO3-shaped code, and the five income groups — `HIC`, `LIC`, `LMC`, `UMC`, `INX` — it publishes with none, served under their ISO3. 48 on `SP.POP.TOTL` on 2026-10-10: 44 of the first kind, `FCV` (fragile and conflict-affected situations) the newest, and four income groups, Not classified (`INX`) having no values. Off by default.","example":"true"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to the one source the World Bank collections are fed by, `wdi`, case-insensitive. Any other stem is a `400` naming it rather than an empty page; `GET /v1/sources` lists every stem. One value."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, \"\u003csource\u003e:\u003cindicator\u003e:\u003ciso3\u003e:\u003cyear\u003e\" — `wdi:SP.POP.TOTL:AZE:2025`. Opaque: treat the whole string as the id."},{"name":"source","type":"string","nullable":false,"desc":"Which scraper delivered the series. `wdi` reads the World Bank Indicators API."},{"name":"indicator","type":"string","nullable":false,"desc":"The WDI code, `SP.POP.TOTL` — the one in the path."},{"name":"iso3","type":"string","nullable":false,"desc":"The economy, by the World Bank's id — the indicator answer's `countryiso3code`, never its `country.id`, which is a two-letter code. An aggregate's id when `is_aggregate` is true: the five income groups, which the World Bank publishes with no ISO3 at all, come back under the id its country list gives them (`HIC`), reached through their iso2Code (`XD`). **The key to join on**, not `country`."},{"name":"country","type":"string","nullable":true,"desc":"The label the World Bank's indicator answer carried, `United States`. For display; `iso3` is the identity."},{"name":"year","type":"integer","nullable":false,"desc":"The year, as a number. Upstream sends a string."},{"name":"value","type":"number","nullable":false,"desc":"The value the World Bank published, **unrounded and unscaled**: integers stay integers (`341784857`), a gross enrolment ratio above 100 is not clamped. **A year with no value has no row** — never a zero, which would claim the country published one."},{"name":"source_last_updated","type":"date","nullable":true,"desc":"WDI's own `lastupdated` for the release this value came from. **Read it per row**: a new release that fails the short-read guard is refused and the series it would have replaced stays served, dated by this. `GET /v1/worldbank/snapshot` says which release is current."},{"name":"is_aggregate","type":"boolean","nullable":false,"desc":"True for a World Bank aggregate's value — served only under `?include_aggregates=true`. From the country list's `region.id`, by join."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the value. The key the series is walked on, because it does not move when a release re-stores a series — so a walk paging across a release is never cut short."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored the value: the last refresh of its series, which every value of that series shares."}]},{"id":"get-wb-snapshot","verb":"GET","path":"/v1/worldbank/snapshot","summary":"Which WDI release is being served, and the licence","returns":"object","params":[],"fields":[{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `wdi`."},{"name":"source_name","type":"string","nullable":false,"desc":"`World Bank World Development Indicators`."},{"name":"source_url","type":"string","nullable":false,"desc":"The World Bank's catalogue page for WDI."},{"name":"source_last_updated","type":"date","nullable":false,"desc":"WDI's own `lastupdated`: the newest the daily poll has read — the release the World Bank says is current. Compare a row's `source_last_updated` with it to know whether that row is current. **It never moves back**: the poll is served from a cache that can answer with the release before for most of a day, and that answer is not taken for a release."},{"name":"last_checked_at","type":"timestamp","nullable":false,"desc":"When the poll last read it. Daily: a value days old is a poll that has stopped, whatever `source_last_updated` says."},{"name":"fetched_at","type":"timestamp","nullable":true,"desc":"When this platform last stored a series — the last refresh. WDI moves a few times a year, so this is ordinarily weeks old. Null before the first."},{"name":"economies","type":"integer","nullable":false,"desc":"Economies in the country list: 217."},{"name":"aggregates","type":"integer","nullable":false,"desc":"Aggregates in the country list: 79 on 2026-10-09, the newest being FCV, the World Bank's fragile and conflict-affected situations. Not every one carries values in every code: 48 at most in any one code served on that day — 44 under an ISO3-shaped code and four of the five income groups; Not classified has none."},{"name":"indicators","type":"integer","nullable":false,"desc":"WDI codes served."},{"name":"observations","type":"integer","nullable":false,"desc":"Values held across every served code, the aggregates' included."},{"name":"indicators_behind","type":"array of string","nullable":false,"desc":"The codes whose held series is from an older release than `source_last_updated` — a code whose own `per_page=1` probe still names the release held, which the World Bank's cache can answer for days after a release and which the code is read on the first run past; a release refused by the short-read guard; or a code not yet fetched. **`[]` when everything is current**, never omitted. A code the World Bank has retired is not listed: it is no longer fetched, and `GET /v1/worldbank/indicators` marks it `deprecated`."},{"name":"licence","type":"string","nullable":false,"desc":"SPDX identifier, `CC-BY-4.0`. **An attribution obligation that travels with the data**: if you publish these numbers, reproduce `attribution`. It is the licence of every series served, because a series is stored only when its own metadata says CC BY 4.0 — `indicator_licences` says so per code."},{"name":"licence_url","type":"string","nullable":false,"desc":"The World Bank's licence page, which carries the added terms."},{"name":"licence_terms","type":"string","nullable":false,"desc":"What the World Bank adds to CC BY 4.0: a dispute goes to non-binding mediation, then to arbitration under the UNCITRAL rules. Not stock CC BY, which is why it is said."},{"name":"attribution","type":"string","nullable":false,"desc":"The line to reproduce if you publish these numbers."},{"name":"indicator_licences","type":"array of object","nullable":false,"desc":"Every served code's licence, as the World Bank's own metadata stated it for the series held: `{code, licence, licence_url, release}`, `licence` verbatim — `CC BY-4.0` is the World Bank's spelling of CC BY 4.0. **WDI's licence is not every series' licence**: military spending is under SIPRI's terms and CO2 per capita under CC BY-NC 4.0, so a series is fetched only after its metadata says `CC BY-4.0` on that run and is never stored otherwise. `licence` is null for a code with no series held — not fetched yet, or refused for its licence. `[]` before any code is served, never omitted."}]}]},{"slug":"nobel","name":"Laureates and Prizes API","status":"retired","operations":[{"id":"list-nobel-laureates","verb":"GET","path":"/v1/nobel/laureates","summary":"List laureates","returns":"array","params":[{"name":"year","in":"query","type":"integer","required":false,"desc":"One award year, `1903`. Laureates holding a prize awarded that year. Not with `year_from` or `year_to`: sending both is a `400`, because they can name different years.","example":"1947"},{"name":"year_from","in":"query","type":"integer","required":false,"desc":"Earliest award year, inclusive."},{"name":"year_to","in":"query","type":"integer","required":false,"desc":"Latest award year, inclusive."},{"name":"category","in":"query","type":"string","required":false,"desc":"`Chemistry`, `Economic Sciences`, `Literature`, `Peace`, `Physics` or `Physiology or Medicine`, in any case. Laureates holding a prize in it. **A closed set**: anything else is a `400`, because an empty page here would read as \"never awarded\".","example":"Peace"},{"name":"country_now_qid","in":"query","type":"string","required":false,"desc":"A present-day country's Wikidata Q-id — `Q159` — for the laureates born, or founded, in what is that country today. A Q-id rather than a name because names vary between records; `/v1/nobel/countries` lists every Q-id with the labels it goes by.","example":"Q30"},{"name":"shape","in":"query","type":"string","required":false,"desc":"`upstream` answers each record as the Nobel Prize API served it — every key, every language, the links — in place of the derived row, for checking this API against the upstream or for a field the row does not carry. **The same JSON values, not the same bytes**: key order and whitespace are not kept, and `\u0026`, `\u003c` and `\u003e` arrive escaped as JSON allows."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `-first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to one source stem. This dataset has one, `nobel_api`, and any other value is a `400` rather than an empty page.","example":"nobel_api"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"The Nobel Prize API's own laureate id, as a string — `6` is Marie Curie. **No `\u003csource\u003e:` prefix**: the id is the upstream's and unique on its own. Pass it to `/v1/nobel/laureates/{id}`, or as `laureate_id` to awards and affiliations."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `nobel_api`."},{"name":"type","type":"string","nullable":false,"desc":"`person` or `organization`. An organisation is a record with an `orgName` upstream — the International Committee of the Red Cross, the American Friends Service Committee."},{"name":"name","type":"string","nullable":true,"desc":"`knownName`, else `orgName`, else `fullName`, in English."},{"name":"full_name","type":"string","nullable":true,"desc":"`fullName`, else `orgName`, in English."},{"name":"gender","type":"string","nullable":true,"desc":"The upstream's own value, `male` or `female`. **Null for every organisation, and never inferred** from a name."},{"name":"birth_date","type":"string","nullable":true,"desc":"`birth.date`, else an organisation's founding date, **exactly as published**: `YYYY-MM-DD` with `00` where the upstream does not know the month or day (`1917-00-00`). Never coerced to the 1st, so do not parse it as a date."},{"name":"birth_year","type":"integer","nullable":true,"desc":"The year of `birth.date` alone — so it is **null for every organisation**, even one whose `birth_date` carries a founding date. Read the year off `birth_date` if you want founding years too."},{"name":"death_date","type":"string","nullable":true,"desc":"`death.date`, as published, `00` and all."},{"name":"death_year","type":"integer","nullable":true,"desc":"The year of `death_date`."},{"name":"birth_place","type":"object","nullable":true,"desc":"Where the laureate was born — or an organisation founded — or null when the upstream records no place. `city` and `country` are the labels recorded **at the time**, so `country` is a historical name: `USSR`, `Prussia`, `Austria-Hungary`. `city_now`, `country_now` and `country_now_qid` are the Foundation's own present-day mapping and that country's Wikidata Q-id; `continent`, and `latitude` and `longitude` of the present-day city as numbers, complete it. **The two countries are never collapsed**: Mikhail Gorbachev's `country` is `USSR` and his `country_now` is `Russia`. Only the present-day country has a Q-id, because it is the only one the upstream gives one for."},{"name":"wikidata_id","type":"string","nullable":true,"desc":"The laureate's Wikidata item as the upstream gives it — `Q7186` for Marie Curie — and the key to join anything else on. **Nothing from Wikidata is merged into this row**: every field here is the Nobel Prize API's."},{"name":"wikipedia_url","type":"string","nullable":true,"desc":"The English Wikipedia article the upstream links."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When this platform fetched, from the Nobel Prize API, the content this row now carries. **It moves only when that content changes** — a corrected date, a second prize — so it is the field to cache on, and the age to show a reader."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When that content was stored here."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the record at all. It never moves, which is why it is the sort key: the catalogue as first stored shares one, and a laureate announced later arrives with a newer one, at the head of the default order."}]},{"id":"get-nobel-laureate","verb":"GET","path":"/v1/nobel/laureates/{id}","summary":"Retrieve one laureate","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"The laureate's `id`, the Nobel Prize API's own: `6` is Marie Curie. **No `\u003csource\u003e:` prefix on this dataset.**","example":"6"},{"name":"shape","in":"query","type":"string","required":false,"desc":"`upstream` answers each record as the Nobel Prize API served it — every key, every language, the links — in place of the derived row, for checking this API against the upstream or for a field the row does not carry. **The same JSON values, not the same bytes**: key order and whitespace are not kept, and `\u0026`, `\u003c` and `\u003e` arrive escaped as JSON allows."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"The Nobel Prize API's own laureate id, as a string — `6` is Marie Curie. **No `\u003csource\u003e:` prefix**: the id is the upstream's and unique on its own. Pass it to `/v1/nobel/laureates/{id}`, or as `laureate_id` to awards and affiliations."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `nobel_api`."},{"name":"type","type":"string","nullable":false,"desc":"`person` or `organization`. An organisation is a record with an `orgName` upstream — the International Committee of the Red Cross, the American Friends Service Committee."},{"name":"name","type":"string","nullable":true,"desc":"`knownName`, else `orgName`, else `fullName`, in English."},{"name":"full_name","type":"string","nullable":true,"desc":"`fullName`, else `orgName`, in English."},{"name":"gender","type":"string","nullable":true,"desc":"The upstream's own value, `male` or `female`. **Null for every organisation, and never inferred** from a name."},{"name":"birth_date","type":"string","nullable":true,"desc":"`birth.date`, else an organisation's founding date, **exactly as published**: `YYYY-MM-DD` with `00` where the upstream does not know the month or day (`1917-00-00`). Never coerced to the 1st, so do not parse it as a date."},{"name":"birth_year","type":"integer","nullable":true,"desc":"The year of `birth.date` alone — so it is **null for every organisation**, even one whose `birth_date` carries a founding date. Read the year off `birth_date` if you want founding years too."},{"name":"death_date","type":"string","nullable":true,"desc":"`death.date`, as published, `00` and all."},{"name":"death_year","type":"integer","nullable":true,"desc":"The year of `death_date`."},{"name":"birth_place","type":"object","nullable":true,"desc":"Where the laureate was born — or an organisation founded — or null when the upstream records no place. `city` and `country` are the labels recorded **at the time**, so `country` is a historical name: `USSR`, `Prussia`, `Austria-Hungary`. `city_now`, `country_now` and `country_now_qid` are the Foundation's own present-day mapping and that country's Wikidata Q-id; `continent`, and `latitude` and `longitude` of the present-day city as numbers, complete it. **The two countries are never collapsed**: Mikhail Gorbachev's `country` is `USSR` and his `country_now` is `Russia`. Only the present-day country has a Q-id, because it is the only one the upstream gives one for."},{"name":"wikidata_id","type":"string","nullable":true,"desc":"The laureate's Wikidata item as the upstream gives it — `Q7186` for Marie Curie — and the key to join anything else on. **Nothing from Wikidata is merged into this row**: every field here is the Nobel Prize API's."},{"name":"wikipedia_url","type":"string","nullable":true,"desc":"The English Wikipedia article the upstream links."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When this platform fetched, from the Nobel Prize API, the content this row now carries. **It moves only when that content changes** — a corrected date, a second prize — so it is the field to cache on, and the age to show a reader."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When that content was stored here."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the record at all. It never moves, which is why it is the sort key: the catalogue as first stored shares one, and a laureate announced later arrives with a newer one, at the head of the default order."}]},{"id":"list-nobel-prizes","verb":"GET","path":"/v1/nobel/prizes","summary":"List prize slots, awarded or not","returns":"array","params":[{"name":"year","in":"query","type":"integer","required":false,"desc":"One award year, `1903`. Every slot of that year — five, or six from 1969. Not with `year_from` or `year_to`: sending both is a `400`, because they can name different years.","example":"1903"},{"name":"year_from","in":"query","type":"integer","required":false,"desc":"Earliest award year, inclusive."},{"name":"year_to","in":"query","type":"integer","required":false,"desc":"Latest award year, inclusive."},{"name":"category","in":"query","type":"string","required":false,"desc":"`Chemistry`, `Economic Sciences`, `Literature`, `Peace`, `Physics` or `Physiology or Medicine`, in any case. Every slot in it. **A closed set**: anything else is a `400`, because an empty page here would read as \"never awarded\".","example":"Physics"},{"name":"awarded","in":"query","type":"boolean","required":false,"desc":"`true` for the slots a prize was awarded in, `false` for the ones it was not. Either leaves out a slot not announced yet, which is neither. Omit it for the whole grid.","example":"true"},{"name":"shape","in":"query","type":"string","required":false,"desc":"`upstream` answers each record as the Nobel Prize API served it — every key, every language, the links — in place of the derived row, for checking this API against the upstream or for a field the row does not carry. **The same JSON values, not the same bytes**: key order and whitespace are not kept, and `\u0026`, `\u003c` and `\u003e` arrive escaped as JSON allows."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `-first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to one source stem. This dataset has one, `nobel_api`, and any other value is a `400` rather than an empty page.","example":"nobel_api"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`\u003cyear\u003e-\u003ccategory\u003e` — `1972-Peace`. One per slot. A category with spaces is percent-encoded in a path: `/v1/nobel/prizes/1911-Physiology%20or%20Medicine`."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `nobel_api`."},{"name":"year","type":"integer","nullable":false,"desc":"The award year."},{"name":"category","type":"string","nullable":false,"desc":"One of `Chemistry`, `Economic Sciences`, `Literature`, `Peace`, `Physics` and `Physiology or Medicine`. Economic Sciences has slots from 1969."},{"name":"category_full","type":"string","nullable":true,"desc":"The prize's full title, in English."},{"name":"date_awarded","type":"string","nullable":true,"desc":"The upstream's `dateAwarded`. **Not a test of whether the prize was awarded**: seven of the slots nobody won carry one, and a slot not announced yet carries the day it will be. Read `awarded`."},{"name":"prize_amount","type":"integer","nullable":true,"desc":"The prize money in SEK, nominal."},{"name":"prize_amount_adjusted","type":"integer","nullable":true,"desc":"The Nobel Foundation's own inflation-adjusted amount in SEK, passed through as published and never recomputed."},{"name":"laureate_count","type":"integer","nullable":false,"desc":"How many laureates the slot names; `0` for a slot where no prize was awarded, and for one whose prize has not been announced yet."},{"name":"awarded","type":"boolean","nullable":true,"desc":"`true` when the slot names laureates. `false` when it names none and its `top_motivation` says why — **a `false` slot is a row, not missing data**: 49 slots through 2025 were never awarded, every category in 1940, 1941 and 1942 among them, and they are in this collection because they are part of the grid. **`null` when the prize has not been announced yet**: the upstream lists a year's six slots as its announcements begin, each dated for its announcement day, so for about a week each October a slot can name nobody without being a gap. Test `awarded === false` for the gaps, never a falsy value."},{"name":"top_motivation","type":"string","nullable":true,"desc":"The upstream's `topMotivation`, in English. Every slot with no prize carries it, and it is the field that says what happened to the money; a few awarded slots carry one too. A slot not announced yet carries none."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When this platform fetched, from the Nobel Prize API, the content this row now carries. **It moves only when that content changes** — a corrected date, a second prize — so it is the field to cache on, and the age to show a reader."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When that content was stored here."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the record at all. It never moves, which is why it is the sort key: the catalogue as first stored shares one, and a laureate announced later arrives with a newer one, at the head of the default order."}]},{"id":"get-nobel-prize","verb":"GET","path":"/v1/nobel/prizes/{id}","summary":"Retrieve one prize slot","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"`\u003cyear\u003e-\u003ccategory\u003e`, the slot's `id`: `1972-Peace`. The category is matched in any case; percent-encode its spaces.","example":"1972-Peace"},{"name":"shape","in":"query","type":"string","required":false,"desc":"`upstream` answers each record as the Nobel Prize API served it — every key, every language, the links — in place of the derived row, for checking this API against the upstream or for a field the row does not carry. **The same JSON values, not the same bytes**: key order and whitespace are not kept, and `\u0026`, `\u003c` and `\u003e` arrive escaped as JSON allows."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`\u003cyear\u003e-\u003ccategory\u003e` — `1972-Peace`. One per slot. A category with spaces is percent-encoded in a path: `/v1/nobel/prizes/1911-Physiology%20or%20Medicine`."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `nobel_api`."},{"name":"year","type":"integer","nullable":false,"desc":"The award year."},{"name":"category","type":"string","nullable":false,"desc":"One of `Chemistry`, `Economic Sciences`, `Literature`, `Peace`, `Physics` and `Physiology or Medicine`. Economic Sciences has slots from 1969."},{"name":"category_full","type":"string","nullable":true,"desc":"The prize's full title, in English."},{"name":"date_awarded","type":"string","nullable":true,"desc":"The upstream's `dateAwarded`. **Not a test of whether the prize was awarded**: seven of the slots nobody won carry one, and a slot not announced yet carries the day it will be. Read `awarded`."},{"name":"prize_amount","type":"integer","nullable":true,"desc":"The prize money in SEK, nominal."},{"name":"prize_amount_adjusted","type":"integer","nullable":true,"desc":"The Nobel Foundation's own inflation-adjusted amount in SEK, passed through as published and never recomputed."},{"name":"laureate_count","type":"integer","nullable":false,"desc":"How many laureates the slot names; `0` for a slot where no prize was awarded, and for one whose prize has not been announced yet."},{"name":"awarded","type":"boolean","nullable":true,"desc":"`true` when the slot names laureates. `false` when it names none and its `top_motivation` says why — **a `false` slot is a row, not missing data**: 49 slots through 2025 were never awarded, every category in 1940, 1941 and 1942 among them, and they are in this collection because they are part of the grid. **`null` when the prize has not been announced yet**: the upstream lists a year's six slots as its announcements begin, each dated for its announcement day, so for about a week each October a slot can name nobody without being a gap. Test `awarded === false` for the gaps, never a falsy value."},{"name":"top_motivation","type":"string","nullable":true,"desc":"The upstream's `topMotivation`, in English. Every slot with no prize carries it, and it is the field that says what happened to the money; a few awarded slots carry one too. A slot not announced yet carries none."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When this platform fetched, from the Nobel Prize API, the content this row now carries. **It moves only when that content changes** — a corrected date, a second prize — so it is the field to cache on, and the age to show a reader."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When that content was stored here."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the record at all. It never moves, which is why it is the sort key: the catalogue as first stored shares one, and a laureate announced later arrives with a newer one, at the head of the default order."}]},{"id":"list-nobel-awards","verb":"GET","path":"/v1/nobel/awards","summary":"List awards, one per laureate per prize","returns":"array","params":[{"name":"year","in":"query","type":"integer","required":false,"desc":"One award year, `1903`. Awards of that year. Not with `year_from` or `year_to`: sending both is a `400`, because they can name different years.","example":"1979"},{"name":"year_from","in":"query","type":"integer","required":false,"desc":"Earliest award year, inclusive."},{"name":"year_to","in":"query","type":"integer","required":false,"desc":"Latest award year, inclusive."},{"name":"category","in":"query","type":"string","required":false,"desc":"`Chemistry`, `Economic Sciences`, `Literature`, `Peace`, `Physics` or `Physiology or Medicine`, in any case. Awards in it. **A closed set**: anything else is a `400`, because an empty page here would read as \"never awarded\".","example":"Physics"},{"name":"laureate_id","in":"query","type":"string","required":false,"desc":"One laureate's `id` — `6`. Every prize one laureate holds — two rows for Marie Curie. One id, not a list.","example":"114"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `-first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to one source stem. This dataset has one, `nobel_api`, and any other value is a `400` rather than an empty page.","example":"nobel_api"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`\u003claureate id\u003e-\u003cyear\u003e-\u003ccategory\u003e` — `6-1903-Physics`. Unique, because a laureate holds a slot at most once. Pass it to `/v1/nobel/awards/{id}`."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `nobel_api`."},{"name":"laureate_id","type":"string","nullable":false,"desc":"The laureate's `id`."},{"name":"year","type":"integer","nullable":false,"desc":"The award year."},{"name":"category","type":"string","nullable":false,"desc":"The category, as on the prize slot."},{"name":"portion","type":"string","nullable":true,"desc":"The laureate's share as the upstream publishes it: `1`, `1/2`, `1/3` or `1/4`."},{"name":"prize_share","type":"number","nullable":true,"desc":"`portion` as a number at full precision — `1/3` is `0.3333333333333333`; rounding is yours to do. **Every awarded slot's shares sum to 1** within 0.02. Sum this for prize equivalents; count rows for laureates — the two answer different questions."},{"name":"sort_order","type":"integer","nullable":true,"desc":"The laureate's position in the slot's own list."},{"name":"motivation","type":"string","nullable":true,"desc":"Why the prize was awarded, in English."},{"name":"prize_status","type":"string","nullable":true,"desc":"The upstream's `prizeStatus`: `received` on all but three awards, which are `declined` or `restricted`."},{"name":"affiliation_count","type":"integer","nullable":false,"desc":"How many institutions the award names. **0 for every Literature award** and for all but 4 Peace awards — see `/v1/nobel/affiliations`."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When this platform fetched, from the Nobel Prize API, the content this row now carries. **It moves only when that content changes** — a corrected date, a second prize — so it is the field to cache on, and the age to show a reader."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When that content was stored here."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the record at all. It never moves, which is why it is the sort key: the catalogue as first stored shares one, and a laureate announced later arrives with a newer one, at the head of the default order."}]},{"id":"get-nobel-award","verb":"GET","path":"/v1/nobel/awards/{id}","summary":"Retrieve one award","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"`\u003claureate id\u003e-\u003cyear\u003e-\u003ccategory\u003e`, the award's `id`: `6-1911-Chemistry`.","example":"6-1911-Chemistry"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`\u003claureate id\u003e-\u003cyear\u003e-\u003ccategory\u003e` — `6-1903-Physics`. Unique, because a laureate holds a slot at most once. Pass it to `/v1/nobel/awards/{id}`."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `nobel_api`."},{"name":"laureate_id","type":"string","nullable":false,"desc":"The laureate's `id`."},{"name":"year","type":"integer","nullable":false,"desc":"The award year."},{"name":"category","type":"string","nullable":false,"desc":"The category, as on the prize slot."},{"name":"portion","type":"string","nullable":true,"desc":"The laureate's share as the upstream publishes it: `1`, `1/2`, `1/3` or `1/4`."},{"name":"prize_share","type":"number","nullable":true,"desc":"`portion` as a number at full precision — `1/3` is `0.3333333333333333`; rounding is yours to do. **Every awarded slot's shares sum to 1** within 0.02. Sum this for prize equivalents; count rows for laureates — the two answer different questions."},{"name":"sort_order","type":"integer","nullable":true,"desc":"The laureate's position in the slot's own list."},{"name":"motivation","type":"string","nullable":true,"desc":"Why the prize was awarded, in English."},{"name":"prize_status","type":"string","nullable":true,"desc":"The upstream's `prizeStatus`: `received` on all but three awards, which are `declined` or `restricted`."},{"name":"affiliation_count","type":"integer","nullable":false,"desc":"How many institutions the award names. **0 for every Literature award** and for all but 4 Peace awards — see `/v1/nobel/affiliations`."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When this platform fetched, from the Nobel Prize API, the content this row now carries. **It moves only when that content changes** — a corrected date, a second prize — so it is the field to cache on, and the age to show a reader."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When that content was stored here."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the record at all. It never moves, which is why it is the sort key: the catalogue as first stored shares one, and a laureate announced later arrives with a newer one, at the head of the default order."}]},{"id":"list-nobel-affiliations","verb":"GET","path":"/v1/nobel/affiliations","summary":"List the institutions awards were made at","returns":"array","params":[{"name":"year","in":"query","type":"integer","required":false,"desc":"One award year, `1903`. Affiliations of that year's awards. Not with `year_from` or `year_to`: sending both is a `400`, because they can name different years.","example":"1979"},{"name":"year_from","in":"query","type":"integer","required":false,"desc":"Earliest award year, inclusive."},{"name":"year_to","in":"query","type":"integer","required":false,"desc":"Latest award year, inclusive."},{"name":"category","in":"query","type":"string","required":false,"desc":"`Chemistry`, `Economic Sciences`, `Literature`, `Peace`, `Physics` or `Physiology or Medicine`, in any case. Affiliations of its awards. **A closed set**: anything else is a `400`, because an empty page here would read as \"never awarded\".","example":"Physics"},{"name":"laureate_id","in":"query","type":"string","required":false,"desc":"One laureate's `id` — `6`. Every institution one laureate's awards name. One id, not a list.","example":"114"},{"name":"country_now_qid","in":"query","type":"string","required":false,"desc":"A present-day country's Wikidata Q-id — `Q183` — for the institutions located in what is that country today.","example":"Q183"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `-first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to one source stem. This dataset has one, `nobel_api`, and any other value is a `400` rather than an empty page.","example":"nobel_api"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`\u003caward id\u003e-\u003cindex\u003e`, the index the institution's 0-based position in the upstream's list — `114-1979-Physics-1`. A position rather than an identity, which is why there is no route that takes it."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `nobel_api`."},{"name":"laureate_prize_id","type":"string","nullable":false,"desc":"The `id` of the award this institution belongs to."},{"name":"laureate_id","type":"string","nullable":false,"desc":"The laureate's `id`."},{"name":"year","type":"integer","nullable":false,"desc":"The award year."},{"name":"category","type":"string","nullable":false,"desc":"The category."},{"name":"name","type":"string","nullable":true,"desc":"The institution as it was named at award time, in English."},{"name":"name_now","type":"string","nullable":true,"desc":"Its present-day name. **Both are kept**: `Kaiser-Wilhelm-Institut` and `Max-Planck-Institut` are one lineage and two strings."},{"name":"city","type":"string","nullable":true,"desc":"The city, as recorded at the time."},{"name":"country","type":"string","nullable":true,"desc":"The country, as recorded at the time — a historical label."},{"name":"city_now","type":"string","nullable":true,"desc":"The city as it is named today."},{"name":"country_now","type":"string","nullable":true,"desc":"The country the city is in today."},{"name":"country_now_qid","type":"string","nullable":true,"desc":"That country's Wikidata Q-id."},{"name":"share_weight","type":"number","nullable":true,"desc":"The award's `prize_share` divided by its `affiliation_count`, so a laureate listed at two institutions counts **once in total** across them. Sum this, not `prize_share`, when ranking institutions."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When this platform fetched, from the Nobel Prize API, the content this row now carries. **It moves only when that content changes** — a corrected date, a second prize — so it is the field to cache on, and the age to show a reader."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When that content was stored here."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored the record at all. It never moves, which is why it is the sort key: the catalogue as first stored shares one, and a laureate announced later arrives with a newer one, at the head of the default order."}]},{"id":"list-nobel-countries","verb":"GET","path":"/v1/nobel/countries","summary":"List present-day countries, with the names they were recorded under","returns":"array","params":[{"name":"historical_label","in":"query","type":"string","required":false,"desc":"A country as the data recorded it at the time — `USSR`, `Prussia`, `Austria-Hungary` — for every present-day country a place recorded under it is in today. Case-insensitive and exact: `Russia` does not match `Russian Empire`. An unknown label answers an empty page.","example":"Russian Empire"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `-first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"-first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to one source stem. This dataset has one, `nobel_api`, and any other value is a `400` rather than an empty page.","example":"nobel_api"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"The present-day country's Wikidata Q-id — `Q159`. The same value as `qid`; every collection's rows carry an `id`."},{"name":"qid","type":"string","nullable":false,"desc":"The present-day country's Wikidata Q-id, `Q159` for Russia. **This is the key to join on, never the name**: labels vary between records."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `nobel_api`."},{"name":"name_now","type":"string","nullable":true,"desc":"The label the data uses most for the country today, ties broken alphabetically. It is the upstream's own spelling — `USA`, not `United States`."},{"name":"labels_now","type":"array of string","nullable":false,"desc":"Every present-day label the data uses for this Q-id, sorted. Q145 is both `United Kingdom` and `Scotland`."},{"name":"historical_labels","type":"array of string","nullable":false,"desc":"Every country label the data recorded **at the time** for a place now inside this country, sorted. A historical state is listed on every present-day country it covered — `USSR` is on Russia's row and on Belarus's — and never mapped to a successor of our choosing. `[]` when nothing was recorded under another name."},{"name":"birth_places","type":"integer","nullable":false,"desc":"Laureates born — or, for an organisation, founded — in this country as it is today."},{"name":"affiliations","type":"integer","nullable":false,"desc":"Affiliation rows located in it today."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"The newest `fetched_at` among the rows counted."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"The newest `ingested_at` among the rows counted."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"The oldest `first_seen_at` among the rows counted: when the country first appeared here. The sort key."}]},{"id":"get-nobel-country","verb":"GET","path":"/v1/nobel/countries/{id}","summary":"Retrieve one present-day country","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"The country's Wikidata Q-id, in either case: `Q159`.","example":"Q159"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"The present-day country's Wikidata Q-id — `Q159`. The same value as `qid`; every collection's rows carry an `id`."},{"name":"qid","type":"string","nullable":false,"desc":"The present-day country's Wikidata Q-id, `Q159` for Russia. **This is the key to join on, never the name**: labels vary between records."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `nobel_api`."},{"name":"name_now","type":"string","nullable":true,"desc":"The label the data uses most for the country today, ties broken alphabetically. It is the upstream's own spelling — `USA`, not `United States`."},{"name":"labels_now","type":"array of string","nullable":false,"desc":"Every present-day label the data uses for this Q-id, sorted. Q145 is both `United Kingdom` and `Scotland`."},{"name":"historical_labels","type":"array of string","nullable":false,"desc":"Every country label the data recorded **at the time** for a place now inside this country, sorted. A historical state is listed on every present-day country it covered — `USSR` is on Russia's row and on Belarus's — and never mapped to a successor of our choosing. `[]` when nothing was recorded under another name."},{"name":"birth_places","type":"integer","nullable":false,"desc":"Laureates born — or, for an organisation, founded — in this country as it is today."},{"name":"affiliations","type":"integer","nullable":false,"desc":"Affiliation rows located in it today."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"The newest `fetched_at` among the rows counted."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"The newest `ingested_at` among the rows counted."},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"The oldest `first_seen_at` among the rows counted: when the country first appeared here. The sort key."}]},{"id":"get-nobel-snapshot","verb":"GET","path":"/v1/nobel/snapshot","summary":"Which catalogue is served, since when, and under what terms","returns":"object","params":[],"fields":[{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `nobel_api`."},{"name":"source_url","type":"string","nullable":true,"desc":"The API root the run walked, `https://api.nobelprize.org/2.1`."},{"name":"corpus_sha256","type":"string","nullable":false,"desc":"Identifies this version of the whole catalogue: a hash over every record's content. It changes when any record changes, appears or disappears — compare it with the one you hold to know whether anything moved."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"The **first** fetch that saw this version of the catalogue. It stays put while the version is current."},{"name":"last_checked_at","type":"timestamp","nullable":false,"desc":"The **newest** fetch that confirmed it, changed or not. A `last_checked_at` from today and a `fetched_at` from August is a catalogue that has not moved since August; a `last_checked_at` days old is a run that has not happened."},{"name":"terms","type":"string","nullable":true,"desc":"The terms-of-use URL the Nobel Prize API stated on that fetch, verbatim. **Never a remembered constant**: if the URL moves upstream, this moves with it, and it is null if a fetch arrived without one. The terms put the data under CC0 and encourage citing the API as the source."},{"name":"license","type":"string","nullable":true,"desc":"The licence URL the API stated on that fetch, verbatim."},{"name":"disclaimer","type":"string","nullable":true,"desc":"The disclaimer URL the API stated on that fetch, verbatim."},{"name":"coverage","type":"object","nullable":false,"desc":"`from` and `to`: the first and last award year of the prize slots served, derived from the data and never set by hand. Both null when nothing is served."},{"name":"upstream","type":"object","nullable":true,"desc":"What the Nobel Prize API said about itself on that fetch: `laureates` and `prizes`, its own `meta.count` for each catalogue, and `prizes_awarded`, `prizes_not_awarded` and `prizes_pending`, the split of its slots the run measured — `prizes_pending` being the slots not announced yet, `0` outside October. Each is null if the fetch did not say."},{"name":"served","type":"object","nullable":false,"desc":"What this API answers with now: `laureates`, `prizes`, `prizes_awarded`, `prizes_not_awarded`, `prizes_pending`, `awards`, `affiliations` and `countries`, each the count a walk of that collection returns. The three prize counts are the slots served with `awarded` true, false and null, so they add up to `prizes`. Published beside `upstream` and reconciled with nothing, so a disagreement is something you can see."},{"name":"upstream_cache_age_seconds","type":"integer","nullable":true,"desc":"How old, in seconds, the oldest copy the upstream's CDN served that fetch was. It serves well past its own stated lifetime, so the content can predate `fetched_at` by this much. Null when no page said."}]}]},{"slug":"bibliometrics","name":"Bibliometrics API","status":"retired","operations":[{"id":"list-bibliometrics-country-years","verb":"GET","path":"/v1/bibliometrics/country-years","summary":"Works and citations per country and year","returns":"array","max_page_size":5000,"params":[{"name":"snapshot","in":"query","type":"date","required":false,"desc":"The OpenAlex release to serve, by its date — `2026-09-23`. Omit it and you get the newest complete snapshot. The newest four are kept; one not served is a `404` naming the ones that are. A walk never mixes snapshots either way: the cursor carries the one page 1 was served from, and the read of it.","example":"2026-09-23"},{"name":"country","in":"query","type":"string","required":false,"desc":"ISO 3166-1 alpha-2 codes, comma-separated or the parameter repeated — `AZ,TR,GE`. Case is normalised. A code that is not two letters is a `400` (and `UK` says `GB`); a well-formed code with no institutions answers an empty page.","example":"AZ"},{"name":"year","in":"query","type":"integer","required":false,"desc":"One year, four digits. Mutually exclusive with `year_from`/`year_to`, which are how several years are asked for: each year parameter takes one value, and a repeated one is a `400` rather than an answer for its first.","example":"2025"},{"name":"year_from","in":"query","type":"integer","required":false,"desc":"Earliest year, inclusive.","example":"2015"},{"name":"year_to","in":"query","type":"integer","required":false,"desc":"Latest year, inclusive. `year_from` after `year_to` is a `400`."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–5000. Defaults to 25. A whole snapshot is about 3,900 rows — 3,907 on the 2026-09-23 release — so **`limit=5000` returns all of it in one request**, and will until about 2031: a year of counts adds about 231 rows. Follow `meta.next` anyway — it is `null` when the page is the whole snapshot.","example":"5000"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. It carries the snapshot the walk is over, so a release landing mid-walk does not change the rows you get; if that same release is read again mid-walk, the next page is a `400` asking for a new walk, never a page of the new read."}],"fields":[{"name":"country_code","type":"string","nullable":false,"desc":"ISO 3166-1 alpha-2, upper-case, **exactly as OpenAlex emits it** — no aliasing and no country list applied, so territories the World Bank does not track (`TW`, `XK`, dependencies) are present. Join on it character for character."},{"name":"year","type":"integer","nullable":false,"desc":"Calendar year. The 2026-09-23 release runs from 2010 — OpenAlex's `counts_by_year` covers roughly the last fifteen years — to the year after the read: the **current, partial year is served, not suppressed**, and so is OpenAlex's \"forthcoming\" year. Years outside 1990 through the read's year + 1 are junk in the upstream (10000, 9999 and 2025002 all occur); they are left out of every sum and counted in the snapshot's `dropped_out_of_range`."},{"name":"works_count","type":"integer","nullable":false,"desc":"Scholarly works OpenAlex attributes to this country's institutions in this year, **whole counting**: a work counts in full for the country of every institution it is attributed to, so a three-country collaboration counts once in each and the rows do not add up to a world total. Not corrected for mis-affiliated records — see the snapshot's `suspect_institutions`."},{"name":"cited_by_count","type":"integer","nullable":false,"desc":"Citations to those works, summed the same way **from the same observations as `works_count`** — so `cited_by_count / works_count` is a like-for-like ratio within a row, and never mixes two years. Low for the latest years because citations take years to accrue."}]},{"id":"get-bibliometrics-snapshot","verb":"GET","path":"/v1/bibliometrics/snapshot","summary":"Which OpenAlex release the rows were summed from","returns":"object","params":[{"name":"snapshot","in":"query","type":"date","required":false,"desc":"Describe this release instead of the newest. Not served is a `404` naming the ones that are.","example":"2026-09-23"}],"fields":[{"name":"snapshot_id","type":"date","nullable":false,"desc":"The OpenAlex release these rows were summed from, the date on its manifest, `2026-09-23`. What `?snapshot=` takes on both routes. OpenAlex releases its public snapshot quarterly, on the second Wednesday of January, April, July and October."},{"name":"source","type":"string","nullable":false,"desc":"The dataset label, `OpenAlex — /institutions, aggregated by country_code`. **Not a scraper stem**, unlike `source` elsewhere in this API: this is #162's header-block field, by its name."},{"name":"source_url","type":"string","nullable":false,"desc":"Where the records were read: the institutions prefix of OpenAlex's public snapshot, `https://openalex.s3.amazonaws.com/data/jsonl/institutions/` — the same records the `/institutions` API serves, one quarterly release at a time."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When the read that produced these rows completed. A weekly check that finds the release unchanged does not move it — `last_checked_at` moves instead — so it always dates the data you are being served."},{"name":"institution_count","type":"integer","nullable":false,"desc":"Institutions **read**, the ones with no `country_code` included — not institutions that contributed a row. `institutions_without_country` is the difference."},{"name":"row_count","type":"integer","nullable":false,"desc":"Country-year rows in this snapshot. **Every one of them is served**: a snapshot missing any row is not served at all, and the previous one stays current."},{"name":"year_range","type":"array of integer","nullable":false,"desc":"`[min, max]` over the rows' years. Measured, not promised: 1990 is a sanity floor, and the 2026-09-23 release starts at 2010."},{"name":"countries","type":"integer","nullable":false,"desc":"Distinct `country_code` values in the rows."},{"name":"attribution","type":"string","nullable":false,"desc":"The counting method, `whole`. **Not a credit line**, unlike `attribution` elsewhere in this API: every work counts in full for each institution's own country. If fractional counting is ever offered it will be a separately named field, never a change to this one."},{"name":"license","type":"string","nullable":false,"desc":"`CC0`: OpenAlex dedicates its data to the public domain, so there is no attribution obligation to pass on. Spelled as #162's contract spells the field."},{"name":"dropped_out_of_range","type":"integer","nullable":false,"desc":"`counts_by_year` observations left out of every sum because their year fell outside `year_floor`..`year_ceiling` — junk years in the upstream metadata, counted rather than silently dropped. Observations of institutions with no country are not counted here: they were never candidates for a row."},{"name":"institutions_without_country","type":"integer","nullable":false,"desc":"Institutions read that carry no `country_code`, and so contributed no row."},{"name":"year_floor","type":"integer","nullable":false,"desc":"The lowest year a row may have: 1990."},{"name":"year_ceiling","type":"integer","nullable":false,"desc":"The highest year a row may have: the year after the read."},{"name":"last_checked_at","type":"timestamp","nullable":false,"desc":"When a run last confirmed this release is still OpenAlex's current one. Runs are weekly; a value more than a week or two old means the checks have stopped, not that the data has."},{"name":"manifest_url","type":"string","nullable":false,"desc":"The manifest of the release read. Fetch it and compare `manifest_sha256` to know whether OpenAlex has published since."},{"name":"manifest_sha256","type":"string","nullable":false,"desc":"`sha256` of that manifest's bytes as read."},{"name":"suspect_rule","type":"object","nullable":true,"desc":"The rule `suspect_institutions` was drawn up under: `baseline_from`, `baseline_to`, `ratio`, `min_works`, `min_baseline_years`."},{"name":"suspect_institutions","type":"array of object","nullable":false,"desc":"Institution-years whose `works_count` is at least `min_works` and more than `ratio` times the median of that institution's own works over the baseline years — the shape of a mis-affiliated record. Each carries `openalex_id`, `display_name`, `country_code`, `year`, `works_count`, `baseline_median_works`, `ratio` and `share_of_country_year`, its weight in the row it lands in. **Additive and diagnostic: nothing listed is subtracted from any row.** On the 2026-09-23 release the National Institute for Fusion Science carries 10.0M, 18.9M and 12.2M works for 2024-2026 against a few hundred a year before — nearly all of Japan's rows for those years. `[]` when nothing met the rule."}]}]},{"slug":"films","name":"Film Corpus API","status":"retired","operations":[{"id":"list-films","verb":"GET","path":"/v1/films","summary":"List the films of the Azerbaijani film corpus","returns":"array","params":[{"name":"corpus_version","in":"query","type":"string","required":false,"desc":"Serve one weekly rebuild of the corpus, e.g. `20261007T082744Z`. Omit it and you get the NEWEST complete one. A walk never needs it to stay consistent — the cursor already keeps every page in the rebuild page 1 came from — so this is for reading an older rebuild on purpose. One value; one that names no complete rebuild is a `400`. Until the first rebuild has completed on a deployment, every request here answers `503` rather than an empty page, which would read as \"no films\".","example":"20261007T082744Z"},{"name":"discovery_rule","in":"query","type":"string","required":false,"desc":"Films found by ANY of these rules, comma-separated or repeated: `country_az`, `country_azssr`, `studio_az`, `language_az`, `ussr_location`, `ussr_director`. Any other name is a `400`.","example":"country_az,language_az"},{"name":"imdb_id","in":"query","type":"string","required":false,"desc":"IMDb title ids, comma-separated or repeated — `tt2731144`. The filter for \"which of my titles does Wikidata place in this corpus\". Case is normalised; anything that is not `tt` and digits is a `400`.","example":"tt0499565"},{"name":"tmdb_id","in":"query","type":"string","required":false,"desc":"TMDb movie ids, comma-separated or repeated — `1474359`. Digits only.","example":"190292"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one. **It keeps the walk in the rebuild it began in**, so a sweep that spans a new weekly rebuild neither skips nor repeats a film."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, `\u003csource\u003e:\u003cQID\u003e` — `wikidata:Q58354`. Pass it to `/v1/films/{id}`. Stable for the life of the Wikidata item: a QID is never reused or renumbered."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `wikidata`, and the first half of `id`."},{"name":"qid","type":"string","nullable":false,"desc":"The Wikidata item id, `Q58354`."},{"name":"discovery_rules","type":"array of string","nullable":false,"desc":"Every rule that found this film, from the closed set `country_az`, `country_azssr`, `studio_az`, `language_az`, `ussr_location`, `ussr_director`, in that order. **A published contract: the names never change**, because they are what decides what counts as an Azerbaijani film. Most of the Soviet-era Baku studio's output is recorded with country of origin Soviet Union, so the corpus is six queries rather than one:\n\n`country_az` — country of origin Azerbaijan. `country_azssr` — country of origin the Azerbaijan SSR. `studio_az` — a production company whose country is either. `language_az` — original language or language of work Azerbaijani. `ussr_location` — a Soviet film set or filmed in either. `ussr_director` — a Soviet film directed by a citizen of either.\n\nA film found only by `ussr_location` or `ussr_director` is the weak evidence; whether to count it is your call, and this field is what makes it one."},{"name":"publication","type":"string","nullable":true,"desc":"The earliest P577 (publication date), as the raw literal Wikidata stores — `1929-01-01T00:00:00Z` for a film known only by its year, so read the year off the leading digits rather than trusting the month and day. Not reformatted: a leading `-` on a malformed statement is kept. Null where none is recorded."},{"name":"duration","type":"number","nullable":true,"desc":"Runtime in **minutes**: P2047, the longest on record, converted where it was recorded in hours or seconds. Null where there is none, or where the statement's unit is not a unit of time."},{"name":"duration_unit","type":"string","nullable":true,"desc":"The unit P2047 was recorded in, as a QID — `Q7727` (minute) on every statement measured; `Q25235` is an hour and `Q11574` a second. Null with `duration` when there is no runtime."},{"name":"imdb_id","type":"string","nullable":true,"desc":"P345, the IMDb title id, exactly as Wikidata records it — `tt2731144`. Where the item records more than one, the first in text order is served and the others are not. From Wikidata under CC0: IMDb's own datasets are licensed for non-commercial use only, and this is the id without them. `?imdb_id=` filters on it, so it matches the one served."},{"name":"tmdb_id","type":"string","nullable":true,"desc":"P4947, the TMDb movie id, verbatim — `1474359`. A string, because it is an identifier. Where the item records more than one, the first in text order is served (`1000` before `99`) and the others are not. From Wikidata under CC0, not from TMDb. `?tmdb_id=` filters on it, so it matches the one served."},{"name":"image","type":"string","nullable":true,"desc":"P18 as Wikidata's query service returns it: a Wikimedia Commons `Special:FilePath` URL whose last path segment is the percent-encoded file name. Where the item records more than one image, the first of those URLs in text order is served and the others are not. **The file name is Wikidata's data; the image is a Commons file with its own licence** — check it there before you display it. Nothing here fetches it."},{"name":"original_title","type":"string","nullable":true,"desc":"P1476 (title). Where several are recorded, the Azerbaijani one, else the first by language tag; the others are still here, as labels and aliases."},{"name":"labels","type":"object","nullable":false,"desc":"The item's label in `az`, `en`, `ru`, `tr`, `az-latn` and `az-cyrl` — all six keys always, null where Wikidata has none in that language."},{"name":"alt_labels","type":"array of object","nullable":false,"desc":"Every alias in those six languages, as `{lang, title}`, by language and then by text. `[]` when there are none."},{"name":"types","type":"array of object","nullable":false,"desc":"P31 (instance of), each `{qid, az, en}`, ordered by QID number. Key on `qid`: the labels are Wikidata's and can be edited. A statement recorded as \"unknown value\" is not an item and is left out of this list and the four below rather than invented into one."},{"name":"genres","type":"array of object","nullable":false,"desc":"P136 (genre), in the shape `types` has."},{"name":"countries","type":"array of object","nullable":false,"desc":"P495 (country of origin), in the shape `types` has."},{"name":"languages","type":"array of object","nullable":false,"desc":"P364 (original language) and P407 (language of work) together, in the shape `types` has."},{"name":"companies","type":"array of object","nullable":false,"desc":"P272 (production company), in the shape `types` has."},{"name":"credits","type":"object","nullable":false,"desc":"Six roles: `director` (P57), `writer` (P58), `cast` (P161), `producer` (P162), `cinematographer` (P344) and `composer` (P86). Each is always present and `[]` when nobody is credited, and each person is `{qid, name_az, name_en}`, ordered by QID number. **Cast is not capped**: every credited person is served."},{"name":"corpus_version","type":"string","nullable":false,"desc":"Which weekly rebuild of the corpus this row is from, `20261007T082744Z` — the instant it began reading Wikidata, written as a name. Pass it as `?corpus_version=` to read that rebuild again."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When that rebuild began querying Wikidata: the upstream time, never the time this API served the row. Every fact on the row was read at or after it, within one run. **This is the one to quote.**"},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the rebuild. Every film of one rebuild shares it."},{"name":"labels.az","type":"string","nullable":true,"desc":"The label in Azerbaijani."},{"name":"labels.en","type":"string","nullable":true,"desc":"The label in English."},{"name":"labels.ru","type":"string","nullable":true,"desc":"The label in Russian."},{"name":"labels.tr","type":"string","nullable":true,"desc":"The label in Turkish."},{"name":"labels.az-latn","type":"string","nullable":true,"desc":"The label in Azerbaijani, tagged as Latin script."},{"name":"labels.az-cyrl","type":"string","nullable":true,"desc":"The label in Azerbaijani, tagged as Cyrillic script."},{"name":"alt_labels[].lang","type":"string","nullable":false,"desc":"One of the six language tags `labels` carries."},{"name":"alt_labels[].title","type":"string","nullable":false,"desc":"The alias, as Wikidata records it."},{"name":"types[].qid","type":"string","nullable":false,"desc":"The Wikidata item. The stable key."},{"name":"types[].az","type":"string","nullable":true,"desc":"Its Azerbaijani label, or null where it has none."},{"name":"types[].en","type":"string","nullable":true,"desc":"Its English label, or null where it has none."},{"name":"credits.director","type":"array of object","nullable":false,"desc":"Everyone credited by P57, in the shape `credits.director[]` has."},{"name":"credits.writer","type":"array of object","nullable":false,"desc":"Everyone credited by P58, in the shape `credits.director[]` has."},{"name":"credits.cast","type":"array of object","nullable":false,"desc":"Everyone credited by P161, in the shape `credits.director[]` has."},{"name":"credits.producer","type":"array of object","nullable":false,"desc":"Everyone credited by P162, in the shape `credits.director[]` has."},{"name":"credits.cinematographer","type":"array of object","nullable":false,"desc":"Everyone credited by P344, in the shape `credits.director[]` has."},{"name":"credits.composer","type":"array of object","nullable":false,"desc":"Everyone credited by P86, in the shape `credits.director[]` has."},{"name":"credits.director[].qid","type":"string","nullable":false,"desc":"The person's Wikidata item. The stable key."},{"name":"credits.director[].name_az","type":"string","nullable":true,"desc":"Their Azerbaijani label, or null where they have none."},{"name":"credits.director[].name_en","type":"string","nullable":true,"desc":"Their English label, or null where they have none."}]},{"id":"get-film-snapshot","verb":"GET","path":"/v1/films/snapshot","summary":"Which rebuild of the corpus is being served","returns":"object","params":[{"name":"corpus_version","in":"query","type":"string","required":false,"desc":"Describe this rebuild rather than the newest. One that names no complete rebuild is a `404`.","example":"20261007T082744Z"}],"fields":[{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `wikidata`."},{"name":"corpus_version","type":"string","nullable":false,"desc":"The rebuild, `20261007T082744Z`. The value `?corpus_version=` takes on every film route."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When the rebuild began querying Wikidata."},{"name":"films","type":"integer","nullable":false,"desc":"How many films the rebuild found."},{"name":"served","type":"integer","nullable":false,"desc":"How many of them `GET /v1/films?corpus_version=` serves: `films`, less any film withdrawn by hand since. A full walk of the rebuild arrives at this number."},{"name":"by_discovery_rule","type":"object","nullable":false,"desc":"How many films each of the six rules found. A film found by several rules counts under each, so these do not sum to `films`."},{"name":"endpoint","type":"string","nullable":true,"desc":"The SPARQL endpoint the rebuild read, as its run recorded it."},{"name":"licence","type":"string","nullable":false,"desc":"The SPDX identifier, `CC0-1.0`. Wikidata's structured data is public domain, so you may store, transform and resell it."},{"name":"attribution","type":"string","nullable":false,"desc":"`Wikidata (SPARQL)` — the credit this corpus's consumers already publish. CC0 asks for none, and printing it keeps that line true."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the rebuild."},{"name":"by_discovery_rule.country_az","type":"integer","nullable":false,"desc":"Films the `country_az` rule found."},{"name":"by_discovery_rule.country_azssr","type":"integer","nullable":false,"desc":"Films the `country_azssr` rule found."},{"name":"by_discovery_rule.studio_az","type":"integer","nullable":false,"desc":"Films the `studio_az` rule found."},{"name":"by_discovery_rule.language_az","type":"integer","nullable":false,"desc":"Films the `language_az` rule found."},{"name":"by_discovery_rule.ussr_location","type":"integer","nullable":false,"desc":"Films the `ussr_location` rule found."},{"name":"by_discovery_rule.ussr_director","type":"integer","nullable":false,"desc":"Films the `ussr_director` rule found."}]},{"id":"get-film","verb":"GET","path":"/v1/films/{id}","summary":"Retrieve one film","returns":"object","params":[{"name":"id","in":"path","type":"string","required":true,"desc":"The id from a list response, `\u003csource\u003e:\u003cQID\u003e`. Resolved in the newest complete rebuild: a film that rebuild does not hold is a `404` naming it, because a film absent from the corpus was found by none of the six rules that week. Before the first rebuild has completed on a deployment, a well-formed id answers `503`, not a `404` saying it never existed.","example":"wikidata:Q58354"},{"name":"corpus_version","in":"query","type":"string","required":false,"desc":"Read the film as this rebuild held it.","example":"20261007T082744Z"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, `\u003csource\u003e:\u003cQID\u003e` — `wikidata:Q58354`. Pass it to `/v1/films/{id}`. Stable for the life of the Wikidata item: a QID is never reused or renumbered."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `wikidata`, and the first half of `id`."},{"name":"qid","type":"string","nullable":false,"desc":"The Wikidata item id, `Q58354`."},{"name":"discovery_rules","type":"array of string","nullable":false,"desc":"Every rule that found this film, from the closed set `country_az`, `country_azssr`, `studio_az`, `language_az`, `ussr_location`, `ussr_director`, in that order. **A published contract: the names never change**, because they are what decides what counts as an Azerbaijani film. Most of the Soviet-era Baku studio's output is recorded with country of origin Soviet Union, so the corpus is six queries rather than one:\n\n`country_az` — country of origin Azerbaijan. `country_azssr` — country of origin the Azerbaijan SSR. `studio_az` — a production company whose country is either. `language_az` — original language or language of work Azerbaijani. `ussr_location` — a Soviet film set or filmed in either. `ussr_director` — a Soviet film directed by a citizen of either.\n\nA film found only by `ussr_location` or `ussr_director` is the weak evidence; whether to count it is your call, and this field is what makes it one."},{"name":"publication","type":"string","nullable":true,"desc":"The earliest P577 (publication date), as the raw literal Wikidata stores — `1929-01-01T00:00:00Z` for a film known only by its year, so read the year off the leading digits rather than trusting the month and day. Not reformatted: a leading `-` on a malformed statement is kept. Null where none is recorded."},{"name":"duration","type":"number","nullable":true,"desc":"Runtime in **minutes**: P2047, the longest on record, converted where it was recorded in hours or seconds. Null where there is none, or where the statement's unit is not a unit of time."},{"name":"duration_unit","type":"string","nullable":true,"desc":"The unit P2047 was recorded in, as a QID — `Q7727` (minute) on every statement measured; `Q25235` is an hour and `Q11574` a second. Null with `duration` when there is no runtime."},{"name":"imdb_id","type":"string","nullable":true,"desc":"P345, the IMDb title id, exactly as Wikidata records it — `tt2731144`. Where the item records more than one, the first in text order is served and the others are not. From Wikidata under CC0: IMDb's own datasets are licensed for non-commercial use only, and this is the id without them. `?imdb_id=` filters on it, so it matches the one served."},{"name":"tmdb_id","type":"string","nullable":true,"desc":"P4947, the TMDb movie id, verbatim — `1474359`. A string, because it is an identifier. Where the item records more than one, the first in text order is served (`1000` before `99`) and the others are not. From Wikidata under CC0, not from TMDb. `?tmdb_id=` filters on it, so it matches the one served."},{"name":"image","type":"string","nullable":true,"desc":"P18 as Wikidata's query service returns it: a Wikimedia Commons `Special:FilePath` URL whose last path segment is the percent-encoded file name. Where the item records more than one image, the first of those URLs in text order is served and the others are not. **The file name is Wikidata's data; the image is a Commons file with its own licence** — check it there before you display it. Nothing here fetches it."},{"name":"original_title","type":"string","nullable":true,"desc":"P1476 (title). Where several are recorded, the Azerbaijani one, else the first by language tag; the others are still here, as labels and aliases."},{"name":"labels","type":"object","nullable":false,"desc":"The item's label in `az`, `en`, `ru`, `tr`, `az-latn` and `az-cyrl` — all six keys always, null where Wikidata has none in that language."},{"name":"alt_labels","type":"array of object","nullable":false,"desc":"Every alias in those six languages, as `{lang, title}`, by language and then by text. `[]` when there are none."},{"name":"types","type":"array of object","nullable":false,"desc":"P31 (instance of), each `{qid, az, en}`, ordered by QID number. Key on `qid`: the labels are Wikidata's and can be edited. A statement recorded as \"unknown value\" is not an item and is left out of this list and the four below rather than invented into one."},{"name":"genres","type":"array of object","nullable":false,"desc":"P136 (genre), in the shape `types` has."},{"name":"countries","type":"array of object","nullable":false,"desc":"P495 (country of origin), in the shape `types` has."},{"name":"languages","type":"array of object","nullable":false,"desc":"P364 (original language) and P407 (language of work) together, in the shape `types` has."},{"name":"companies","type":"array of object","nullable":false,"desc":"P272 (production company), in the shape `types` has."},{"name":"credits","type":"object","nullable":false,"desc":"Six roles: `director` (P57), `writer` (P58), `cast` (P161), `producer` (P162), `cinematographer` (P344) and `composer` (P86). Each is always present and `[]` when nobody is credited, and each person is `{qid, name_az, name_en}`, ordered by QID number. **Cast is not capped**: every credited person is served."},{"name":"corpus_version","type":"string","nullable":false,"desc":"Which weekly rebuild of the corpus this row is from, `20261007T082744Z` — the instant it began reading Wikidata, written as a name. Pass it as `?corpus_version=` to read that rebuild again."},{"name":"fetched_at","type":"timestamp","nullable":false,"desc":"When that rebuild began querying Wikidata: the upstream time, never the time this API served the row. Every fact on the row was read at or after it, within one run. **This is the one to quote.**"},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the rebuild. Every film of one rebuild shares it."},{"name":"labels.az","type":"string","nullable":true,"desc":"The label in Azerbaijani."},{"name":"labels.en","type":"string","nullable":true,"desc":"The label in English."},{"name":"labels.ru","type":"string","nullable":true,"desc":"The label in Russian."},{"name":"labels.tr","type":"string","nullable":true,"desc":"The label in Turkish."},{"name":"labels.az-latn","type":"string","nullable":true,"desc":"The label in Azerbaijani, tagged as Latin script."},{"name":"labels.az-cyrl","type":"string","nullable":true,"desc":"The label in Azerbaijani, tagged as Cyrillic script."},{"name":"alt_labels[].lang","type":"string","nullable":false,"desc":"One of the six language tags `labels` carries."},{"name":"alt_labels[].title","type":"string","nullable":false,"desc":"The alias, as Wikidata records it."},{"name":"types[].qid","type":"string","nullable":false,"desc":"The Wikidata item. The stable key."},{"name":"types[].az","type":"string","nullable":true,"desc":"Its Azerbaijani label, or null where it has none."},{"name":"types[].en","type":"string","nullable":true,"desc":"Its English label, or null where it has none."},{"name":"credits.director","type":"array of object","nullable":false,"desc":"Everyone credited by P57, in the shape `credits.director[]` has."},{"name":"credits.writer","type":"array of object","nullable":false,"desc":"Everyone credited by P58, in the shape `credits.director[]` has."},{"name":"credits.cast","type":"array of object","nullable":false,"desc":"Everyone credited by P161, in the shape `credits.director[]` has."},{"name":"credits.producer","type":"array of object","nullable":false,"desc":"Everyone credited by P162, in the shape `credits.director[]` has."},{"name":"credits.cinematographer","type":"array of object","nullable":false,"desc":"Everyone credited by P344, in the shape `credits.director[]` has."},{"name":"credits.composer","type":"array of object","nullable":false,"desc":"Everyone credited by P86, in the shape `credits.director[]` has."},{"name":"credits.director[].qid","type":"string","nullable":false,"desc":"The person's Wikidata item. The stable key."},{"name":"credits.director[].name_az","type":"string","nullable":true,"desc":"Their Azerbaijani label, or null where they have none."},{"name":"credits.director[].name_en","type":"string","nullable":true,"desc":"Their English label, or null where they have none."}]}]},{"slug":"migration","name":"Migration Data API","status":"retired","operations":[{"id":"list-un-stock-countries","verb":"GET","path":"/v1/migration/un-stock/countries","summary":"The countries of a UN DESA revision","returns":"array","params":[{"name":"revision","in":"query","type":"integer","required":false,"desc":"UN DESA's revision, by year — `2024`. Omit it for the newest held. **A walk stays on the revision its first page was served from** even if a newer one lands while you page; the next walk gets the newer one. A revision not held is a `400` naming the ones that are.","example":"2024"},{"name":"has_data","in":"query","type":"boolean","required":false,"desc":"`true` for the countries with a migrant stock (232 in Rev.2024), `false` for those UN DESA lists without one (Saint Barthélemy, Saint Martin).","example":"true"},{"name":"location_code","in":"query","type":"integer","required":false,"desc":"Location codes, comma-separated or repeated — `31,51`. Each must be a location of the revision, or the request is a `400` naming it; a code is never silently matched against nothing.","example":"31"},{"name":"iso3","in":"query","type":"string","required":false,"desc":"ISO 3166-1 alpha-3 codes, comma-separated or repeated — `AZE,ARM`. A code ISO does not assign, or one UN DESA reports no country under (`ATA`), is a `400` saying which. Not combinable with `location_code`.","example":"AZE"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, `\"\u003csource\u003e:\u003crevision\u003e:un_location:\u003clocation code\u003e\"` — `undesa:2024:un_location:31`. Opaque: treat the whole string as the id."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `undesa`."},{"name":"revision","type":"integer","nullable":false,"desc":"UN DESA's revision this location is from, `2024`."},{"name":"location_code","type":"integer","nullable":false,"desc":"UN DESA's location code. For a country it is its ISO 3166-1 numeric code — `31` is Azerbaijan — and for every other location one of UN DESA's own: `900` World, `903` AFRICA, `1829` Europe and Northern America. Pass it back as `?location_code=`."},{"name":"name","type":"string","nullable":false,"desc":"The workbook's own name, after removing surrounding spaces and UN DESA's footnote asterisks (`France*` is `France`) — and otherwise **byte for byte**: `Türkiye`, `Côte d'Ivoire`, `China, Hong Kong SAR`. Never translated or tidied, because it is a join key downstream. Join on `location_code` or `iso3` where you can."},{"name":"kind","type":"string","nullable":false,"desc":"What the location is, decided by THIS platform because the workbooks never say: `country` (its code is an ISO 3166-1 numeric code, from a table pinned in the migration), `area` (a country or area UN DESA reports that ISO does not list — the Channel Islands), `group` (World and the development, income and SDG groups above the first region), `region` (the six ALL-CAPS regions), `subregion`, or `other` (the origin workbook's `OTHER`). `/countries` serves `country`; `/aggregates` serves the rest."},{"name":"iso2","type":"string","nullable":true,"desc":"ISO 3166-1 alpha-2 for a `country`; null on every other kind."},{"name":"iso3","type":"string","nullable":true,"desc":"ISO 3166-1 alpha-3 for a `country`; null on every other kind. **The key to join the World Bank collections on** — never the name."},{"name":"in_m49","type":"boolean","nullable":true,"desc":"Whether UNSD's M49 standard lists the code: `true` for every country but Taiwan (`158`), which ISO 3166-1 assigns and M49 does not, and which UN DESA reports as `China, Taiwan Province of China`. Null off countries."},{"name":"region_code","type":"integer","nullable":true,"desc":"The code of `region`."},{"name":"region","type":"string","nullable":true,"desc":"The ALL-CAPS region the location sits under, verbatim — one of six. Null for the groups, a region itself and `OTHER`. Reconstructed by walking the destination workbook top to bottom, because it is not a column of it."},{"name":"subregion_code","type":"integer","nullable":true,"desc":"The code of `subregion`."},{"name":"subregion","type":"string","nullable":true,"desc":"The subregion it sits under, verbatim. Null where the region has none — all five NORTHERN AMERICA countries — and on a region or subregion itself. Never backfilled."},{"name":"parent_code","type":"integer","nullable":true,"desc":"The nearest location above it in the hierarchy: the subregion, or the region where there is none. Null at the top. This is how the classification can be audited."},{"name":"parent","type":"string","nullable":true,"desc":"The name of `parent_code`."},{"name":"data_type","type":"string","nullable":true,"desc":"How UN DESA estimated the destination's stock, verbatim: `B` foreign-born, `C` foreign citizens, `R` refugees added, `I` imputed, alone or combined (`B R`). **It is why cross-country comparison is caveated.** Null on most aggregates."},{"name":"workbook_index","type":"integer","nullable":true,"desc":"The workbook's own Index column — its row order."},{"name":"has_data","type":"boolean","nullable":false,"desc":"Whether the revision publishes a migrant stock for the location at all, as a destination or as an origin. `false` is a real answer, not missing data: Saint Barthélemy (`652`) and Saint Martin (`663`) are countries UN DESA lists with a population and no stock. `?has_data=true` is the 232 countries with one."},{"name":"has_destination_stock","type":"boolean","nullable":false,"desc":"Whether `/destination-stock` has any row for it."},{"name":"has_origin_stock","type":"boolean","nullable":false,"desc":"Whether `/origin-stock` has any row for it."},{"name":"has_population","type":"boolean","nullable":false,"desc":"Whether `/population` has any row for it."},{"name":"licence","type":"string","nullable":false,"desc":"`CC-BY-3.0-IGO`, the terms every sheet of every workbook states. **An attribution obligation that travels with the data**: if you publish these figures, reproduce `attribution`."},{"name":"attribution","type":"string","nullable":false,"desc":"The workbook's own suggested citation, verbatim — `United Nations Department of Economic and Social Affairs, Population Division (2024). International Migrant Stock 2024.`"},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored this row. A revision lands whole, so every row of it shares this; it is the key a walk is ordered on, because it does not move when the revision is stored again."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored this row. **Not the reference date** — `year` is, at 1 July of it."}]},{"id":"list-un-stock-aggregates","verb":"GET","path":"/v1/migration/un-stock/aggregates","summary":"UN DESA's own aggregates, the one area and OTHER","returns":"array","params":[{"name":"revision","in":"query","type":"integer","required":false,"desc":"UN DESA's revision, by year — `2024`. Omit it for the newest held. **A walk stays on the revision its first page was served from** even if a newer one lands while you page; the next walk gets the newer one. A revision not held is a `400` naming the ones that are.","example":"2024"},{"name":"kind","in":"query","type":"string","required":false,"desc":"Kinds, comma-separated: `group`, `region`, `subregion`, `area`, `other`. Default all five. Not combinable with `location_code`.","example":"subregion"},{"name":"location_code","in":"query","type":"integer","required":false,"desc":"Location codes, comma-separated or repeated — `31,51`. Each must be a location of the revision, or the request is a `400` naming it; a code is never silently matched against nothing."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, `\"\u003csource\u003e:\u003crevision\u003e:un_location:\u003clocation code\u003e\"` — `undesa:2024:un_location:31`. Opaque: treat the whole string as the id."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `undesa`."},{"name":"revision","type":"integer","nullable":false,"desc":"UN DESA's revision this location is from, `2024`."},{"name":"location_code","type":"integer","nullable":false,"desc":"UN DESA's location code. For a country it is its ISO 3166-1 numeric code — `31` is Azerbaijan — and for every other location one of UN DESA's own: `900` World, `903` AFRICA, `1829` Europe and Northern America. Pass it back as `?location_code=`."},{"name":"name","type":"string","nullable":false,"desc":"The workbook's own name, after removing surrounding spaces and UN DESA's footnote asterisks (`France*` is `France`) — and otherwise **byte for byte**: `Türkiye`, `Côte d'Ivoire`, `China, Hong Kong SAR`. Never translated or tidied, because it is a join key downstream. Join on `location_code` or `iso3` where you can."},{"name":"kind","type":"string","nullable":false,"desc":"What the location is, decided by THIS platform because the workbooks never say: `country` (its code is an ISO 3166-1 numeric code, from a table pinned in the migration), `area` (a country or area UN DESA reports that ISO does not list — the Channel Islands), `group` (World and the development, income and SDG groups above the first region), `region` (the six ALL-CAPS regions), `subregion`, or `other` (the origin workbook's `OTHER`). `/countries` serves `country`; `/aggregates` serves the rest."},{"name":"iso2","type":"string","nullable":true,"desc":"ISO 3166-1 alpha-2 for a `country`; null on every other kind."},{"name":"iso3","type":"string","nullable":true,"desc":"ISO 3166-1 alpha-3 for a `country`; null on every other kind. **The key to join the World Bank collections on** — never the name."},{"name":"in_m49","type":"boolean","nullable":true,"desc":"Whether UNSD's M49 standard lists the code: `true` for every country but Taiwan (`158`), which ISO 3166-1 assigns and M49 does not, and which UN DESA reports as `China, Taiwan Province of China`. Null off countries."},{"name":"region_code","type":"integer","nullable":true,"desc":"The code of `region`."},{"name":"region","type":"string","nullable":true,"desc":"The ALL-CAPS region the location sits under, verbatim — one of six. Null for the groups, a region itself and `OTHER`. Reconstructed by walking the destination workbook top to bottom, because it is not a column of it."},{"name":"subregion_code","type":"integer","nullable":true,"desc":"The code of `subregion`."},{"name":"subregion","type":"string","nullable":true,"desc":"The subregion it sits under, verbatim. Null where the region has none — all five NORTHERN AMERICA countries — and on a region or subregion itself. Never backfilled."},{"name":"parent_code","type":"integer","nullable":true,"desc":"The nearest location above it in the hierarchy: the subregion, or the region where there is none. Null at the top. This is how the classification can be audited."},{"name":"parent","type":"string","nullable":true,"desc":"The name of `parent_code`."},{"name":"data_type","type":"string","nullable":true,"desc":"How UN DESA estimated the destination's stock, verbatim: `B` foreign-born, `C` foreign citizens, `R` refugees added, `I` imputed, alone or combined (`B R`). **It is why cross-country comparison is caveated.** Null on most aggregates."},{"name":"workbook_index","type":"integer","nullable":true,"desc":"The workbook's own Index column — its row order."},{"name":"has_data","type":"boolean","nullable":false,"desc":"Whether the revision publishes a migrant stock for the location at all, as a destination or as an origin. `false` is a real answer, not missing data: Saint Barthélemy (`652`) and Saint Martin (`663`) are countries UN DESA lists with a population and no stock. `?has_data=true` is the 232 countries with one."},{"name":"has_destination_stock","type":"boolean","nullable":false,"desc":"Whether `/destination-stock` has any row for it."},{"name":"has_origin_stock","type":"boolean","nullable":false,"desc":"Whether `/origin-stock` has any row for it."},{"name":"has_population","type":"boolean","nullable":false,"desc":"Whether `/population` has any row for it."},{"name":"licence","type":"string","nullable":false,"desc":"`CC-BY-3.0-IGO`, the terms every sheet of every workbook states. **An attribution obligation that travels with the data**: if you publish these figures, reproduce `attribution`."},{"name":"attribution","type":"string","nullable":false,"desc":"The workbook's own suggested citation, verbatim — `United Nations Department of Economic and Social Affairs, Population Division (2024). International Migrant Stock 2024.`"},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored this row. A revision lands whole, so every row of it shares this; it is the key a walk is ordered on, because it does not move when the revision is stored again."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored this row. **Not the reference date** — `year` is, at 1 July of it."}]},{"id":"list-un-destination-stock","verb":"GET","path":"/v1/migration/un-stock/destination-stock","summary":"International migrant stock by destination","returns":"array","params":[{"name":"revision","in":"query","type":"integer","required":false,"desc":"UN DESA's revision, by year — `2024`. Omit it for the newest held. **A walk stays on the revision its first page was served from** even if a newer one lands while you page; the next walk gets the newer one. A revision not held is a `400` naming the ones that are.","example":"2024"},{"name":"kind","in":"query","type":"string","required":false,"desc":"Kinds of location, comma-separated — `country,area`. **Default `country`**, so a page never mixes World, the regions and the groups in with the countries a consumer sums. Naming locations with `location_code` or `iso3` serves them whatever their kind, and cannot be combined with `kind`."},{"name":"location_code","in":"query","type":"integer","required":false,"desc":"Location codes, comma-separated or repeated — `31,51`. Each must be a location of the revision, or the request is a `400` naming it; a code is never silently matched against nothing.","example":"31"},{"name":"iso3","in":"query","type":"string","required":false,"desc":"ISO 3166-1 alpha-3 codes, comma-separated or repeated — `AZE,ARM`. A code ISO does not assign, or one UN DESA reports no country under (`ATA`), is a `400` saying which. Not combinable with `location_code`."},{"name":"year","in":"query","type":"integer","required":false,"desc":"Reference years, comma-separated or repeated — `1990,2024`. Only the revision's own (`reference_years` on `/meta`): `2023` is a `400`, because UN DESA publishes no value there and an empty page would read as \"no migrants in 2023\".","example":"2024"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, `\"\u003csource\u003e:\u003crevision\u003e:un_destination_stock:\u003clocation code\u003e:\u003cyear\u003e\"` — `undesa:2024:un_destination_stock:31:2024`. Opaque: treat the whole string as the id."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `undesa`."},{"name":"revision","type":"integer","nullable":false,"desc":"UN DESA's revision this row is from, `2024`. Every page of a walk is from one revision."},{"name":"year","type":"integer","nullable":false,"desc":"The reference year: the figure is the stock **at 1 July of it**. UN DESA publishes eight in Rev.2024 — 1990, 1995, 2000, 2005, 2010, 2015, 2020 and 2024 — and nothing between them; never interpolate. A year with no published value has no row."},{"name":"location_code","type":"integer","nullable":false,"desc":"UN DESA's location code. For a country it is its ISO 3166-1 numeric code — `31` is Azerbaijan — and for every other location one of UN DESA's own: `900` World, `903` AFRICA, `1829` Europe and Northern America. Pass it back as `?location_code=`."},{"name":"name","type":"string","nullable":false,"desc":"The workbook's own name, after removing surrounding spaces and UN DESA's footnote asterisks (`France*` is `France`) — and otherwise **byte for byte**: `Türkiye`, `Côte d'Ivoire`, `China, Hong Kong SAR`. Never translated or tidied, because it is a join key downstream. Join on `location_code` or `iso3` where you can."},{"name":"kind","type":"string","nullable":false,"desc":"What the location is, decided by THIS platform because the workbooks never say: `country` (its code is an ISO 3166-1 numeric code, from a table pinned in the migration), `area` (a country or area UN DESA reports that ISO does not list — the Channel Islands), `group` (World and the development, income and SDG groups above the first region), `region` (the six ALL-CAPS regions), `subregion`, or `other` (the origin workbook's `OTHER`)."},{"name":"iso3","type":"string","nullable":true,"desc":"ISO 3166-1 alpha-3 for a `country`, from the same pinned table; null on every other kind. **This is the key to join the World Bank collections on** — never the name."},{"name":"region","type":"string","nullable":true,"desc":"The ALL-CAPS region above the location in the workbook, verbatim: `AFRICA`, `ASIA`, `EUROPE`, `LATIN AMERICA AND THE CARIBBEAN`, `NORTHERN AMERICA` or `OCEANIA`. Reconstructed by walking the destination workbook, because it is not a column. Null for the groups, a region itself and `OTHER`."},{"name":"subregion","type":"string","nullable":true,"desc":"The subregion above it, verbatim — `Western Asia`. Null where the region has none: all five NORTHERN AMERICA countries. Never backfilled."},{"name":"data_type","type":"string","nullable":true,"desc":"How UN DESA estimated this destination's stock, verbatim: `B` foreign-born, `C` foreign citizens, `R` refugees added, `I` imputed, alone or combined. **It is why cross-country comparison is caveated.**"},{"name":"immigrant_stock","type":"integer","nullable":false,"desc":"**People living here who were born elsewhere**, both sexes, at 1 July of `year`. Whole people as published — never rounded. Summing the `country` rows of a year gives the 232-country total; UN DESA's own `World` row (`?location_code=900`) is a different number."},{"name":"male","type":"integer","nullable":true,"desc":"Males, whole people. Null where the workbook leaves the cell empty."},{"name":"female","type":"integer","nullable":true,"desc":"Females, whole people. Null where the workbook leaves the cell empty."},{"name":"source_url","type":"string","nullable":true,"desc":"The workbook this row was read from, as UN DESA serves it. Its sha256 is in `/v1/migration/un-stock/meta`'s `files`."},{"name":"source_table","type":"string","nullable":false,"desc":"The sheet within it — `Table 1`."},{"name":"licence","type":"string","nullable":false,"desc":"`CC-BY-3.0-IGO`, the terms every sheet of every workbook states. **An attribution obligation that travels with the data**: if you publish these figures, reproduce `attribution`."},{"name":"attribution","type":"string","nullable":false,"desc":"The workbook's own suggested citation, verbatim — `United Nations Department of Economic and Social Affairs, Population Division (2024). International Migrant Stock 2024.`"},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored this row. A revision lands whole, so every row of it shares this; it is the key a walk is ordered on, because it does not move when the revision is stored again."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored this row. **Not the reference date** — `year` is, at 1 July of it."}]},{"id":"list-un-origin-stock","verb":"GET","path":"/v1/migration/un-stock/origin-stock","summary":"International migrant stock by origin","returns":"array","params":[{"name":"revision","in":"query","type":"integer","required":false,"desc":"UN DESA's revision, by year — `2024`. Omit it for the newest held. **A walk stays on the revision its first page was served from** even if a newer one lands while you page; the next walk gets the newer one. A revision not held is a `400` naming the ones that are.","example":"2024"},{"name":"kind","in":"query","type":"string","required":false,"desc":"Kinds of location, comma-separated — `country,area`. **Default `country`**, so a page never mixes World, the regions and the groups in with the countries a consumer sums. Naming locations with `location_code` or `iso3` serves them whatever their kind, and cannot be combined with `kind`."},{"name":"location_code","in":"query","type":"integer","required":false,"desc":"Location codes, comma-separated or repeated — `31,51`. Each must be a location of the revision, or the request is a `400` naming it; a code is never silently matched against nothing.","example":"31"},{"name":"iso3","in":"query","type":"string","required":false,"desc":"ISO 3166-1 alpha-3 codes, comma-separated or repeated — `AZE,ARM`. A code ISO does not assign, or one UN DESA reports no country under (`ATA`), is a `400` saying which. Not combinable with `location_code`."},{"name":"year","in":"query","type":"integer","required":false,"desc":"Reference years, comma-separated or repeated — `1990,2024`. Only the revision's own (`reference_years` on `/meta`): `2023` is a `400`, because UN DESA publishes no value there and an empty page would read as \"no migrants in 2023\".","example":"2024"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, `\"\u003csource\u003e:\u003crevision\u003e:un_origin_stock:\u003clocation code\u003e:\u003cyear\u003e\"` — `undesa:2024:un_origin_stock:31:2024`. Opaque: treat the whole string as the id."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `undesa`."},{"name":"revision","type":"integer","nullable":false,"desc":"UN DESA's revision this row is from, `2024`. Every page of a walk is from one revision."},{"name":"year","type":"integer","nullable":false,"desc":"The reference year: the figure is the stock **at 1 July of it**. UN DESA publishes eight in Rev.2024 — 1990, 1995, 2000, 2005, 2010, 2015, 2020 and 2024 — and nothing between them; never interpolate. A year with no published value has no row."},{"name":"location_code","type":"integer","nullable":false,"desc":"UN DESA's location code. For a country it is its ISO 3166-1 numeric code — `31` is Azerbaijan — and for every other location one of UN DESA's own: `900` World, `903` AFRICA, `1829` Europe and Northern America. Pass it back as `?location_code=`."},{"name":"name","type":"string","nullable":false,"desc":"The workbook's own name, after removing surrounding spaces and UN DESA's footnote asterisks (`France*` is `France`) — and otherwise **byte for byte**: `Türkiye`, `Côte d'Ivoire`, `China, Hong Kong SAR`. Never translated or tidied, because it is a join key downstream. Join on `location_code` or `iso3` where you can."},{"name":"kind","type":"string","nullable":false,"desc":"What the location is, decided by THIS platform because the workbooks never say: `country` (its code is an ISO 3166-1 numeric code, from a table pinned in the migration), `area` (a country or area UN DESA reports that ISO does not list — the Channel Islands), `group` (World and the development, income and SDG groups above the first region), `region` (the six ALL-CAPS regions), `subregion`, or `other` (the origin workbook's `OTHER`)."},{"name":"iso3","type":"string","nullable":true,"desc":"ISO 3166-1 alpha-3 for a `country`, from the same pinned table; null on every other kind. **This is the key to join the World Bank collections on** — never the name."},{"name":"region","type":"string","nullable":true,"desc":"The ALL-CAPS region above the location in the workbook, verbatim: `AFRICA`, `ASIA`, `EUROPE`, `LATIN AMERICA AND THE CARIBBEAN`, `NORTHERN AMERICA` or `OCEANIA`. Reconstructed by walking the destination workbook, because it is not a column. Null for the groups, a region itself and `OTHER`."},{"name":"subregion","type":"string","nullable":true,"desc":"The subregion above it, verbatim — `Western Asia`. Null where the region has none: all five NORTHERN AMERICA countries. Never backfilled."},{"name":"emigrant_stock","type":"integer","nullable":false,"desc":"**People born here and living in another country**, both sexes, at 1 July of `year`. Whole people as published — never rounded. There is no `data_type` on this table: the origin workbook prints codes in a column it leaves unlabelled, and they disagree with the destination workbook's `Data type` for 18 countries, so UN DESA does not say what they describe. How each country's own stock was estimated is `data_type` on `/destination-stock` and `/countries`."},{"name":"male","type":"integer","nullable":true,"desc":"Males, whole people. Null where the workbook leaves the cell empty."},{"name":"female","type":"integer","nullable":true,"desc":"Females, whole people. Null where the workbook leaves the cell empty."},{"name":"source_url","type":"string","nullable":true,"desc":"The workbook this row was read from, as UN DESA serves it. Its sha256 is in `/v1/migration/un-stock/meta`'s `files`."},{"name":"source_table","type":"string","nullable":false,"desc":"The sheet within it — `Table 1`."},{"name":"licence","type":"string","nullable":false,"desc":"`CC-BY-3.0-IGO`, the terms every sheet of every workbook states. **An attribution obligation that travels with the data**: if you publish these figures, reproduce `attribution`."},{"name":"attribution","type":"string","nullable":false,"desc":"The workbook's own suggested citation, verbatim — `United Nations Department of Economic and Social Affairs, Population Division (2024). International Migrant Stock 2024.`"},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored this row. A revision lands whole, so every row of it shares this; it is the key a walk is ordered on, because it does not move when the revision is stored again."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored this row. **Not the reference date** — `year` is, at 1 July of it."}]},{"id":"list-un-population","verb":"GET","path":"/v1/migration/un-stock/population","summary":"Total population, in people","returns":"array","params":[{"name":"revision","in":"query","type":"integer","required":false,"desc":"UN DESA's revision, by year — `2024`. Omit it for the newest held. **A walk stays on the revision its first page was served from** even if a newer one lands while you page; the next walk gets the newer one. A revision not held is a `400` naming the ones that are.","example":"2024"},{"name":"kind","in":"query","type":"string","required":false,"desc":"Kinds of location, comma-separated — `country,area`. **Default `country`**, so a page never mixes World, the regions and the groups in with the countries a consumer sums. Naming locations with `location_code` or `iso3` serves them whatever their kind, and cannot be combined with `kind`."},{"name":"location_code","in":"query","type":"integer","required":false,"desc":"Location codes, comma-separated or repeated — `31,51`. Each must be a location of the revision, or the request is a `400` naming it; a code is never silently matched against nothing.","example":"31"},{"name":"iso3","in":"query","type":"string","required":false,"desc":"ISO 3166-1 alpha-3 codes, comma-separated or repeated — `AZE,ARM`. A code ISO does not assign, or one UN DESA reports no country under (`ATA`), is a `400` saying which. Not combinable with `location_code`."},{"name":"year","in":"query","type":"integer","required":false,"desc":"Reference years, comma-separated or repeated — `1990,2024`. Only the revision's own (`reference_years` on `/meta`): `2023` is a `400`, because UN DESA publishes no value there and an empty page would read as \"no migrants in 2023\".","example":"2024"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, `\"\u003csource\u003e:\u003crevision\u003e:un_population:\u003clocation code\u003e:\u003cyear\u003e\"` — `undesa:2024:un_population:31:2024`. Opaque: treat the whole string as the id."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `undesa`."},{"name":"revision","type":"integer","nullable":false,"desc":"UN DESA's revision this row is from, `2024`. Every page of a walk is from one revision."},{"name":"year","type":"integer","nullable":false,"desc":"The reference year: the figure is the stock **at 1 July of it**. UN DESA publishes eight in Rev.2024 — 1990, 1995, 2000, 2005, 2010, 2015, 2020 and 2024 — and nothing between them; never interpolate. A year with no published value has no row."},{"name":"location_code","type":"integer","nullable":false,"desc":"UN DESA's location code. For a country it is its ISO 3166-1 numeric code — `31` is Azerbaijan — and for every other location one of UN DESA's own: `900` World, `903` AFRICA, `1829` Europe and Northern America. Pass it back as `?location_code=`."},{"name":"name","type":"string","nullable":false,"desc":"The workbook's own name, after removing surrounding spaces and UN DESA's footnote asterisks (`France*` is `France`) — and otherwise **byte for byte**: `Türkiye`, `Côte d'Ivoire`, `China, Hong Kong SAR`. Never translated or tidied, because it is a join key downstream. Join on `location_code` or `iso3` where you can."},{"name":"kind","type":"string","nullable":false,"desc":"What the location is, decided by THIS platform because the workbooks never say: `country` (its code is an ISO 3166-1 numeric code, from a table pinned in the migration), `area` (a country or area UN DESA reports that ISO does not list — the Channel Islands), `group` (World and the development, income and SDG groups above the first region), `region` (the six ALL-CAPS regions), `subregion`, or `other` (the origin workbook's `OTHER`)."},{"name":"iso3","type":"string","nullable":true,"desc":"ISO 3166-1 alpha-3 for a `country`, from the same pinned table; null on every other kind. **This is the key to join the World Bank collections on** — never the name."},{"name":"region","type":"string","nullable":true,"desc":"The ALL-CAPS region above the location in the workbook, verbatim: `AFRICA`, `ASIA`, `EUROPE`, `LATIN AMERICA AND THE CARIBBEAN`, `NORTHERN AMERICA` or `OCEANIA`. Reconstructed by walking the destination workbook, because it is not a column. Null for the groups, a region itself and `OTHER`."},{"name":"subregion","type":"string","nullable":true,"desc":"The subregion above it, verbatim — `Western Asia`. Null where the region has none: all five NORTHERN AMERICA countries. Never backfilled."},{"name":"population","type":"integer","nullable":false,"desc":"Total population at 1 July of `year`, **in people**. The workbook publishes thousands, and the figure is converted to the nearest whole person; a cell further than a thousandth of a person from one is refused rather than rounded."},{"name":"male","type":"integer","nullable":true,"desc":"Males, whole people. Null where the workbook leaves the cell empty."},{"name":"female","type":"integer","nullable":true,"desc":"Females, whole people. Null where the workbook leaves the cell empty."},{"name":"source_url","type":"string","nullable":true,"desc":"The workbook this row was read from, as UN DESA serves it. Its sha256 is in `/v1/migration/un-stock/meta`'s `files`."},{"name":"source_table","type":"string","nullable":false,"desc":"The sheet within it — `Table 2`."},{"name":"licence","type":"string","nullable":false,"desc":"`CC-BY-3.0-IGO`, the terms every sheet of every workbook states. **An attribution obligation that travels with the data**: if you publish these figures, reproduce `attribution`."},{"name":"attribution","type":"string","nullable":false,"desc":"The workbook's own suggested citation, verbatim — `United Nations Department of Economic and Social Affairs, Population Division (2024). International Migrant Stock 2024.`"},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored this row. A revision lands whole, so every row of it shares this; it is the key a walk is ordered on, because it does not move when the revision is stored again."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored this row. **Not the reference date** — `year` is, at 1 July of it."}]},{"id":"list-un-bilateral","verb":"GET","path":"/v1/migration/un-stock/bilateral","summary":"Migrant stock from one country living in another","returns":"array","params":[{"name":"revision","in":"query","type":"integer","required":false,"desc":"UN DESA's revision, by year — `2024`. Omit it for the newest held. **A walk stays on the revision its first page was served from** even if a newer one lands while you page; the next walk gets the newer one. A revision not held is a `400` naming the ones that are.","example":"2024"},{"name":"destination","in":"query","type":"string","required":false,"desc":"Countries of destination, ISO 3166-1 alpha-3, comma-separated — `AZE`. Each must be a country of the revision, or the request is a `400`.","example":"BLR"},{"name":"origin","in":"query","type":"string","required":false,"desc":"Countries of origin, ISO 3166-1 alpha-3, comma-separated — `ARM`.","example":"AZE"},{"name":"year","in":"query","type":"integer","required":false,"desc":"Reference years, comma-separated or repeated — `1990,2024`. Only the revision's own (`reference_years` on `/meta`): `2023` is a `400`, because UN DESA publishes no value there and an empty page would read as \"no migrants in 2023\".","example":"2024"},{"name":"min_stock","in":"query","type":"integer","required":false,"desc":"Only pairs with at least this many people. `?year=2024\u0026min_stock=1000000` is the 53 corridors of 2024 with a million people or more; `1` drops the reported zeros.","example":"1000000"},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. Defaults to 25.","example":"50"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."},{"name":"order","in":"query","type":"string","required":false,"desc":"Sort field: `first_seen_at`, each optionally prefixed with `-` to reverse. Default is `first_seen_at`. Rows with no value for the sort column always sort LAST in either direction, so paging never leads with undated rows.","example":"first_seen_at"},{"name":"source","in":"query","type":"string","required":false,"desc":"Restrict to ONE scraper source, by its stem — `abb`, not `ABB` or `abb-bank.az`. Not a list: `?source=a,b` and a repeated `?source=` do not select two sources. On `/v1/jobs` an unknown stem is a `400` naming it, and `source_type` is the way to select several sources at once. `GET /v1/sources` lists every stem."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable identifier, `\"\u003csource\u003e:\u003crevision\u003e:un_bilateral:\u003cdestination code\u003e:\u003corigin code\u003e:\u003cyear\u003e\"` — `undesa:2024:un_bilateral:840:484:2024`. Opaque."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `undesa`."},{"name":"revision","type":"integer","nullable":false,"desc":"UN DESA's revision this row is from, `2024`."},{"name":"year","type":"integer","nullable":false,"desc":"The reference year, at 1 July of it. Eight in Rev.2024; never interpolate."},{"name":"destination_code","type":"integer","nullable":false,"desc":"The country of destination's code — ISO 3166-1 numeric."},{"name":"destination_name","type":"string","nullable":false,"desc":"Its name, verbatim as the workbook spells it."},{"name":"destination_iso3","type":"string","nullable":false,"desc":"Its ISO 3166-1 alpha-3. `?destination=` takes it."},{"name":"origin_code","type":"integer","nullable":false,"desc":"The country of origin's code — ISO 3166-1 numeric."},{"name":"origin_name","type":"string","nullable":false,"desc":"Its name, verbatim as the workbook spells it."},{"name":"origin_iso3","type":"string","nullable":false,"desc":"Its ISO 3166-1 alpha-3. `?origin=` takes it."},{"name":"migrant_stock","type":"integer","nullable":false,"desc":"People born in the origin and living in the destination, both sexes, at 1 July of `year`. **`0` is a zero UN DESA reported; a pair with no row is one it does not publish** — the two are never collapsed, and `?min_stock=1` is how to drop the zeros. Country to country only: pairs with a region, a group, an area or OTHER at either end are not served."},{"name":"male","type":"integer","nullable":true,"desc":"Males, whole people. Null where the workbook leaves the cell empty."},{"name":"female","type":"integer","nullable":true,"desc":"Females, whole people. Null where the workbook leaves the cell empty."},{"name":"source_url","type":"string","nullable":true,"desc":"The workbook this row was read from, as UN DESA serves it. Its sha256 is in `/v1/migration/un-stock/meta`'s `files`."},{"name":"source_table","type":"string","nullable":false,"desc":"The sheet within it — `Table 1`."},{"name":"licence","type":"string","nullable":false,"desc":"`CC-BY-3.0-IGO`, the terms every sheet of every workbook states. **An attribution obligation that travels with the data**: if you publish these figures, reproduce `attribution`."},{"name":"attribution","type":"string","nullable":false,"desc":"The workbook's own suggested citation, verbatim — `United Nations Department of Economic and Social Affairs, Population Division (2024). International Migrant Stock 2024.`"},{"name":"first_seen_at","type":"timestamp","nullable":false,"desc":"When this platform first stored this row. A revision lands whole, so every row of it shares this; it is the key a walk is ordered on, because it does not move when the revision is stored again."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform last stored this row. **Not the reference date** — `year` is, at 1 July of it."}]},{"id":"get-un-stock-meta","verb":"GET","path":"/v1/migration/un-stock/meta","summary":"The revision served, its files and its terms","returns":"object","params":[{"name":"revision","in":"query","type":"integer","required":false,"desc":"UN DESA's revision, by year — `2024`. Omit it for the newest held. **A walk stays on the revision its first page was served from** even if a newer one lands while you page; the next walk gets the newer one. A revision not held is a `400` naming the ones that are.","example":"2024"}],"fields":[{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `undesa`."},{"name":"revision","type":"integer","nullable":false,"desc":"The revision this object describes, `2024`."},{"name":"revisions_held","type":"array of integer","nullable":false,"desc":"Every revision this platform holds, newest first. Older revisions stay held when a newer one lands; `?revision=` reaches them on every route."},{"name":"citation_code","type":"string","nullable":false,"desc":"UN DESA's own, `POP/DB/MIG/Stock/Rev.2024`."},{"name":"title","type":"string","nullable":false,"desc":"`International Migrant Stock 2024`."},{"name":"attribution","type":"string","nullable":false,"desc":"The workbook's suggested citation, verbatim. **Reproduce it if you publish these figures**; every row carries it too."},{"name":"licence","type":"string","nullable":false,"desc":"`CC-BY-3.0-IGO`."},{"name":"licence_url","type":"string","nullable":false,"desc":"The licence deed the workbooks point at."},{"name":"licence_statement","type":"string","nullable":false,"desc":"Row 6 of every sheet, verbatim: \"Copyright © 2024 by United Nations, made available under a Creative Commons license CC BY 3.0 IGO: …\". The scraper refuses a revision whose sheets state anything else."},{"name":"reference_years","type":"array of integer","nullable":false,"desc":"The reference years **read from the workbooks' header rows and checked against each other**, never assumed — the list changes between revisions. `?year=` takes only these."},{"name":"parser_version","type":"string","nullable":false,"desc":"Bumped when the parse would read the same bytes into different rows, so a re-parse can be told from a new revision. When it moves and `revision` does not, the same pinned files were parsed again and every row of the revision was replaced: walk again what you hold."},{"name":"files","type":"array of object","nullable":false,"desc":"The three workbooks the revision was parsed from: `table` (destination, origin, bilateral), `url`, `sha256`, `size_bytes`, `etag`, `last_modified` and `fetched_at`. Each sha256 is a pin: a revision is parsed only from bytes that hash to it, so you can check that what is served came out of the files UN DESA published."},{"name":"countries_total","type":"integer","nullable":false,"desc":"Countries in the revision, with or without a value — 234 in Rev.2024."},{"name":"countries_with_data","type":"integer","nullable":false,"desc":"Countries with a migrant stock either way — 232 in Rev.2024. The difference is `has_data: false` on `/countries`, not a gap in this platform's copy."},{"name":"areas","type":"integer","nullable":false,"desc":"Countries or areas UN DESA reports that ISO 3166-1 does not list — 1."},{"name":"aggregates","type":"integer","nullable":false,"desc":"UN DESA's own rows: World, the groups, regions and subregions, and OTHER."},{"name":"last_checked_at","type":"timestamp","nullable":true,"desc":"When the weekly run last asked UN DESA about this revision. Null until a run has."},{"name":"newest_listed","type":"integer","nullable":true,"desc":"The newest revision UN DESA's landing page named at that check. **Above `revision`, it is a new revision published and not yet served**: one is never ingested automatically, because it would silently change every figure built on this one."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the revision."}]}]},{"slug":"weather","name":"Weather Forecast API","status":"retired","operations":[{"id":"list-weather-cities","verb":"GET","path":"/v1/weather/cities","summary":"The 72 cities the forecasts are interpolated to","returns":"array","params":[],"fields":[{"name":"slug_az","type":"string","nullable":false,"desc":"The key: what every forecast row carries as `city`, and what `?city=` takes. Lower-case ASCII — `baki`, `gence`, `lenkeran`."},{"name":"slug_en","type":"string","nullable":false,"desc":"The English slug, `baku`."},{"name":"name_az","type":"string","nullable":false,"desc":"The Azerbaijani name, with its diacritics: `Bakı`, `Gəncə`."},{"name":"name_en","type":"string","nullable":false,"desc":"The English name, `Baku`."},{"name":"latitude","type":"number","nullable":false,"desc":"WGS 84 latitude of the point the forecast is interpolated to."},{"name":"longitude","type":"number","nullable":false,"desc":"WGS 84 longitude of that point."},{"name":"region","type":"string","nullable":false,"desc":"The economic region of the 7 July 2021 division — fourteen of them, e.g. `Gəncə-Daşkəsən`. Names from the earlier division (`Gəncə-Qazax`, `Yuxarı Qarabağ`) no longer exist and are not used."},{"name":"is_capital","type":"boolean","nullable":false,"desc":"`true` for Bakı only."},{"name":"elevation_m","type":"integer","nullable":true,"desc":"Metres. `-28` for Bakı and **null for the other 71**: no source we can cite gives an elevation at these exact points, and a null here is never filled with an estimate."}]},{"id":"list-weather-hourly","verb":"GET","path":"/v1/weather/forecast/hourly","summary":"One run's forecast points, 3-hourly then 6-hourly","returns":"array","params":[{"name":"city","in":"query","type":"string","required":false,"desc":"One city's `slug_az` — `baki`. Omit it for every city, one city's rows and then the next's. Not a name and not a list; a slug that names no city is a `400`, because an empty forecast would read as \"no forecast for that city\".","example":"baki"},{"name":"hours","in":"query","type":"integer","required":false,"desc":"The lead time to stop at, in hours after `model_run`: points whose `step` is at most this. 0–240; omit it for the whole run. A 06Z or 18Z run ends at 144.","example":"48"},{"name":"model_run","in":"query","type":"timestamp","required":false,"desc":"One run, as `model_run` carries it — `2026-10-05T00:00:00Z`. Omit it for the newest run held whole. A walk that names none stays on the run it started on even if a newer one lands mid-walk. A run not held — never landed whole, or older than its rows are kept — answers an empty page with `meta.model_run: null`. Until the first run has landed on a deployment, every forecast endpoint answers `503` instead of an empty page: retry with backoff."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. **Defaults to 100 here, not 25**: one city's whole run is 65 points and comes back in one page.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"Stable for as long as the point is held (31 days): `\u003csource\u003e:\u003crun\u003e:\u003ccity\u003e:\u003cstep\u003e`, `ecmwf_ifs:2026100500:baki:003`. Opaque."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `ecmwf_ifs`."},{"name":"city","type":"string","nullable":false,"desc":"The city's `slug_az` — the same value the pipeline this replaces published as `city_id`, under this name."},{"name":"model","type":"string","nullable":false,"desc":"The literal `ecmwf_ifs`: ECMWF's IFS deterministic forecast."},{"name":"model_run","type":"timestamp","nullable":false,"desc":"The cycle this point comes from — the run's base time, `00`, `06`, `12` or `18` UTC. **Never a fetch time.** The same value `meta.model_run` carries."},{"name":"target_time","type":"timestamp","nullable":false,"desc":"The instant forecast: `model_run` plus `step` hours, UTC."},{"name":"step","type":"integer","nullable":false,"desc":"Lead time in hours: 0 to 144 by 3, then 150 to 240 by 6. A 06Z or 18Z run stops at 144. So this collection is **3-hourly, then 6-hourly** — not hourly, whatever its name."},{"name":"temperature","type":"number","nullable":true,"desc":"2 m temperature, °C, 1 dp."},{"name":"apparent_temperature","type":"number","nullable":true,"desc":"°C, 1 dp: the Australian Bureau of Meteorology formula, `T + 0.33·e − 0.70·ws − 4.00`, with wind in m/s and `e` from the Magnus humidity BEFORE its 0–100 clip — the clip is `relative_humidity`'s, as published, and the two part only where the dew point is above the temperature."},{"name":"relative_humidity","type":"number","nullable":true,"desc":"%, 1 dp, clipped to 0–100: Magnus, from 2 m temperature and dew point. **Never null where `temperature` is not** — both come from parameters published at every step."},{"name":"precipitation_probability","type":"integer","nullable":true,"desc":"**Not a probability.** A deterministic run cannot give one; this is a step function of `precipitation` — 0 → 0, \u003c 0.1 → 15, \u003c 0.5 → 35, \u003c 2 → 55, \u003c 5 → 75, \u003c 10 → 88, else 95 — published under the name it has always had and labelled for what it is."},{"name":"precipitation","type":"number","nullable":true,"desc":"mm **over this step**, 2 dp: 3 hours to 144h, 6 hours after. ECMWF's accumulation since the run began, de-accumulated; a gap in it is null here and in the step after, never read as no rain."},{"name":"rain","type":"number","nullable":true,"desc":"mm, 2 dp: `precipitation` when `ptype` is 0, 1 or 3, else `0.0`. Counting 0 (no precipitation type) as rain is the behaviour #166 asked to keep as it was. It reads the nearest cell's `ptype`, so where a city's four cells disagree it can differ from the pipeline this replaces, which read a blend of them — see `ptype`."},{"name":"cloud_cover","type":"number","nullable":true,"desc":"Total cloud cover, %, 1 dp."},{"name":"wind_speed","type":"number","nullable":true,"desc":"10 m wind, **km/h**, 1 dp."},{"name":"wind_direction","type":"number","nullable":true,"desc":"Degrees, meteorological: where the wind blows FROM."},{"name":"wind_gust","type":"number","nullable":true,"desc":"10 m gust, **km/h**, 1 dp. **Null at 93–144h of every run**: ECMWF publishes no gust over the hour there, and the 3-hour gust it does publish is a different window, so it is not used to fill the gap."},{"name":"pressure","type":"number","nullable":true,"desc":"Mean sea-level pressure, **hPa**, 1 dp."},{"name":"visibility","type":"number","nullable":true,"desc":"**Always null**: this feed has no visibility."},{"name":"uv_index","type":"number","nullable":true,"desc":"**Always null**: this feed has no UV field."},{"name":"weather_code","type":"string","nullable":true,"desc":"One of fifteen: `clear`, `mostly_clear`, `partly_cloudy`, `cloudy`, `overcast`, `fog`, `drizzle`, `rain_light`, `rain`, `rain_heavy`, `thunderstorm`, `snow_light`, `snow`, `sleet`, `windy`. Never a sixteenth. `fog` is never emitted — it needs a visibility — and nor is `sleet`, whose branch the classifier reaches only after the snow branch has taken every code it would."},{"name":"ptype","type":"integer","nullable":true,"desc":"ECMWF's precipitation-type code at the NEAREST grid cell — a code is not averaged: 0 none, 1 rain, 3 freezing rain, 5 snow, 6 wet snow, 7 sleet, 8 ice pellets. One of the two inputs to `weather_code`, served raw so you can classify for yourself. **A difference from the pipeline this replaces**, which interpolated it bilinearly like every other field: where a city's four cells disagree, that gives a number that is no code (0.298 at Bakı, whose cells read 0, 1, 0 and 0), which its `rain` read as 0.0 and its classifier as no snow. At those cities and steps `rain` and `weather_code` here can differ from the rows it produced."},{"name":"cape","type":"number","nullable":true,"desc":"Most-unstable CAPE, J/kg, 1 dp: the other input. `weather_code` is `thunderstorm` from 800 J/kg when precipitation is at least 0.1 mm."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the point."}]},{"id":"list-weather-daily","verb":"GET","path":"/v1/weather/forecast/daily","summary":"One run's forecast per local day","returns":"array","params":[{"name":"city","in":"query","type":"string","required":false,"desc":"One city's `slug_az` — `baki`. Omit it for every city, one city's rows and then the next's. Not a name and not a list; a slug that names no city is a `400`, because an empty forecast would read as \"no forecast for that city\".","example":"baki"},{"name":"days","in":"query","type":"integer","required":false,"desc":"How many of the run's local dates, counted from its first: 1–11; omit it for all of them. The first and last are partial — `steps` says how many points each one covers.","example":"7"},{"name":"model_run","in":"query","type":"timestamp","required":false,"desc":"One run, as `model_run` carries it — `2026-10-05T00:00:00Z`. Omit it for the newest run held whole. A walk that names none stays on the run it started on even if a newer one lands mid-walk. A run not held — never landed whole, or older than its rows are kept — answers an empty page with `meta.model_run: null`. Until the first run has landed on a deployment, every forecast endpoint answers `503` instead of an empty page: retry with backoff."},{"name":"limit","in":"query","type":"integer","required":false,"desc":"Items per page, 1–100. **Defaults to 100 here, not 25**: one city's whole run is 65 points and comes back in one page.","example":"100"},{"name":"cursor","in":"query","type":"string","required":false,"desc":"Opaque cursor from a previous response's meta.next. Do not construct one."}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`\u003csource\u003e:\u003crun\u003e:\u003ccity\u003e:\u003cdate\u003e`, `ecmwf_ifs:2026100500:baki:2026-10-05`. Kept indefinitely, so stable forever. Opaque."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `ecmwf_ifs`."},{"name":"city","type":"string","nullable":false,"desc":"The city's `slug_az` — published as `city_id` by the pipeline this replaces."},{"name":"model","type":"string","nullable":false,"desc":"The literal `ecmwf_ifs`."},{"name":"model_run","type":"timestamp","nullable":false,"desc":"The cycle this day comes from. Never a fetch time."},{"name":"date","type":"date","nullable":false,"desc":"The local calendar date at a **fixed UTC+4** — Azerbaijan keeps no daylight saving. A 00Z run starts at 04:00 local, so it covers eleven dates."},{"name":"steps","type":"integer","nullable":false,"desc":"How many of the run's points fall in this date. **The first and last dates of a run are partial**: a 00Z run's first date has 7 points and its last has 1."},{"name":"temp_min","type":"number","nullable":true,"desc":"°C, 1 dp: the lowest `temperature` among the day's points."},{"name":"temp_max","type":"number","nullable":true,"desc":"°C, 1 dp: the highest."},{"name":"precipitation_sum","type":"number","nullable":true,"desc":"mm, 1 dp: the sum of the day's `precipitation`, not a re-read of ECMWF's accumulation. Null when any of the day's points is null — a day with a gap in it has no total. Like every value on this row it is taken from the points **as served**, already rounded, so you can recompute it from them; a total taken before rounding, as the pipeline this replaces took it, can differ in the last digit."},{"name":"precipitation_probability_max","type":"integer","nullable":true,"desc":"The highest `precipitation_probability` of the day — the same proxy, not a probability."},{"name":"wind_speed_max","type":"number","nullable":true,"desc":"km/h, 1 dp."},{"name":"wind_gust_max","type":"number","nullable":true,"desc":"km/h, 1 dp, over the day's points that have a gust; null when none does."},{"name":"uv_max","type":"number","nullable":true,"desc":"**Always null**: not in this feed."},{"name":"sunrise","type":"string","nullable":true,"desc":"**Always null**: deliberately not computed."},{"name":"sunset","type":"string","nullable":true,"desc":"**Always null**: deliberately not computed."},{"name":"weather_code","type":"string","nullable":true,"desc":"The day's **most severe** `weather_code`, ranked `clear`, `mostly_clear`, `partly_cloudy`, `cloudy`, `overcast`, `windy`, `fog`, `drizzle`, `rain_light`, `rain`, `snow_light`, `sleet`, `snow`, `rain_heavy`, `thunderstorm` — which is not the enum's order: `windy` outranks `overcast`, and `snow` sits below `rain_heavy`."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the row."}]},{"id":"list-weather-runs","verb":"GET","path":"/v1/weather/forecast/runs","summary":"What every run predicted for one city on one date","returns":"array","params":[{"name":"city","in":"query","type":"string","required":true,"desc":"**Required.** One city's `slug_az`, `baki`.","example":"baki"},{"name":"date","in":"query","type":"date","required":true,"desc":"**Required.** The local (UTC+4) date the runs predicted, `2026-10-07`. Every run held whole that covered it answers, oldest first — the series a forecast-stability measure reads, eight or more deep once two days of runs are held.","example":"2026-10-07"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`\u003csource\u003e:\u003crun\u003e:\u003ccity\u003e:\u003cdate\u003e`, `ecmwf_ifs:2026100500:baki:2026-10-05`. Kept indefinitely, so stable forever. Opaque."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `ecmwf_ifs`."},{"name":"city","type":"string","nullable":false,"desc":"The city's `slug_az` — published as `city_id` by the pipeline this replaces."},{"name":"model","type":"string","nullable":false,"desc":"The literal `ecmwf_ifs`."},{"name":"model_run","type":"timestamp","nullable":false,"desc":"The cycle this day comes from. Never a fetch time."},{"name":"date","type":"date","nullable":false,"desc":"The local calendar date at a **fixed UTC+4** — Azerbaijan keeps no daylight saving. A 00Z run starts at 04:00 local, so it covers eleven dates."},{"name":"steps","type":"integer","nullable":false,"desc":"How many of the run's points fall in this date. **The first and last dates of a run are partial**: a 00Z run's first date has 7 points and its last has 1."},{"name":"temp_min","type":"number","nullable":true,"desc":"°C, 1 dp: the lowest `temperature` among the day's points."},{"name":"temp_max","type":"number","nullable":true,"desc":"°C, 1 dp: the highest."},{"name":"precipitation_sum","type":"number","nullable":true,"desc":"mm, 1 dp: the sum of the day's `precipitation`, not a re-read of ECMWF's accumulation. Null when any of the day's points is null — a day with a gap in it has no total. Like every value on this row it is taken from the points **as served**, already rounded, so you can recompute it from them; a total taken before rounding, as the pipeline this replaces took it, can differ in the last digit."},{"name":"precipitation_probability_max","type":"integer","nullable":true,"desc":"The highest `precipitation_probability` of the day — the same proxy, not a probability."},{"name":"wind_speed_max","type":"number","nullable":true,"desc":"km/h, 1 dp."},{"name":"wind_gust_max","type":"number","nullable":true,"desc":"km/h, 1 dp, over the day's points that have a gust; null when none does."},{"name":"uv_max","type":"number","nullable":true,"desc":"**Always null**: not in this feed."},{"name":"sunrise","type":"string","nullable":true,"desc":"**Always null**: deliberately not computed."},{"name":"sunset","type":"string","nullable":true,"desc":"**Always null**: deliberately not computed."},{"name":"weather_code","type":"string","nullable":true,"desc":"The day's **most severe** `weather_code`, ranked `clear`, `mostly_clear`, `partly_cloudy`, `cloudy`, `overcast`, `windy`, `fog`, `drizzle`, `rain_light`, `rain`, `snow_light`, `sleet`, `snow`, `rain_heavy`, `thunderstorm` — which is not the enum's order: `windy` outranks `overcast`, and `snow` sits below `rain_heavy`."},{"name":"ingested_at","type":"timestamp","nullable":false,"desc":"When this platform stored the row."}]},{"id":"get-weather-snapshot","verb":"GET","path":"/v1/weather/snapshot","summary":"Every city now, today and tomorrow, in one response","returns":"array","params":[],"fields":[{"name":"city","type":"object","nullable":false,"desc":"The city, exactly as `GET /v1/weather/cities` serves it."},{"name":"current","type":"object","nullable":true,"desc":"The forecast point nearest to now by absolute distance, the earlier one on a tie — an object of `GET /v1/weather/forecast/hourly`. **Not interpolated in time**, so on a 3-hourly grid it can be up to 90 minutes either side of now; read its `target_time`."},{"name":"today","type":"object","nullable":true,"desc":"Today at UTC+4, an object of `GET /v1/weather/forecast/daily`. Partial when the run began during it — read `steps`."},{"name":"tomorrow","type":"object","nullable":true,"desc":"Tomorrow at UTC+4, the same shape."}]}]},{"slug":"entities","name":"Entity Lookup API","status":"retired","operations":[{"id":"lookup-entity-countries","verb":"GET","path":"/v1/entities/countries","summary":"Look up ISO codes for a list of Wikidata country items","returns":"array","params":[{"name":"ids","in":"query","type":"string","required":true,"desc":"**Required.** Up to 200 Wikidata item ids, comma-separated — `Q55,Q30` — or the parameter repeated, which means the same thing. `wikidata:Q55`, the form a row's `id` carries, is read as `Q55`. A 201st distinct id is a `400` naming the count rather than a silent truncation, and so is anything that is not an item id. Every id gets a row, in the order sent; an id this API holds nothing for is a row of nulls and is named in `meta.not_found`.\n\nAnswered from a daily read of Wikidata, never by calling it in your request: the ids a run reads are every Nobel laureate the Nobel Prize API lists and every country those answers name. Until the first run has landed on a deployment, every request here answers `503` rather than a page of nulls.","example":"Q55,Q30,Q227"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`wikidata:\u003cQID\u003e` — `wikidata:Q55`. The same on a row this API resolved and on one it did not: it names the Wikidata item either way."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `wikidata`, and the first half of `id`."},{"name":"qid","type":"string","nullable":false,"desc":"The Wikidata item id you asked about, normalised — a `wikidata:` prefix removed and a lower-case `q` raised. Rows come back in the order you sent the ids."},{"name":"label","type":"string","nullable":true,"desc":"The item's English label, else its `mul` one — the single label Wikidata now keeps for a name written the same in every language — else null. Never the bare QID."},{"name":"iso3","type":"string","nullable":true,"desc":"ISO 3166-1 alpha-3 (Wikidata P298), exactly as Wikidata records it. Null when neither the item nor any country it sits in records one: a state ISO never coded (the Russian Empire, Prussia) or a citizenship value that is not a state (statelessness)."},{"name":"iso3_source","type":"string","nullable":true,"desc":"Where `iso3` came from. `direct` — the item's own P298. `parent_country` — the item records none, and the country it sits in (P17) does: **Q55, the Netherlands, gets `NLD` from Q29999, the Kingdom of the Netherlands**, and Q131337, the Azerbaijan SSR, gets `SUN` from Q15180, the Soviet Union. Null exactly when `iso3` is. Join on ISO3 knowing which kind you hold: an inherited code is the parent's, not the item's."},{"name":"iso2","type":"string","nullable":true,"desc":"ISO 3166-1 alpha-2 (P297) of the SAME item `iso3` came from, verbatim — the two codes always come as a pair. Null where that item records none (the Soviet Union has `SUN` and no alpha-2), and always null when `iso3` is."},{"name":"parent_qid","type":"string","nullable":true,"desc":"The country both codes were inherited from, when `iso3_source` is `parent_country` — `Q29999` for Q55 — and null otherwise. Look it up here too: every parent named is."},{"name":"redirect","type":"string","nullable":true,"desc":"The item this id now redirects to on Wikidata, whose facts these are — set when two items were merged and you hold the old id. Null when the id is not a redirect."},{"name":"fetched_at","type":"timestamp","nullable":true,"desc":"The newest Wikidata Query Service read that returned exactly this row: the upstream time, never the time this API served it. A daily run moves it on every row it reads unchanged. **This is the one to quote.** Null only on an unresolved row."},{"name":"ingested_at","type":"timestamp","nullable":true,"desc":"When this platform last stored or confirmed the row. Null only on an unresolved row."}]},{"id":"lookup-entity-people","verb":"GET","path":"/v1/entities/people","summary":"Look up citizenships, educations and ORCID for a list of Wikidata items","returns":"array","params":[{"name":"ids","in":"query","type":"string","required":true,"desc":"**Required.** Up to 200 Wikidata item ids, comma-separated — `Q55,Q30` — or the parameter repeated, which means the same thing. `wikidata:Q55`, the form a row's `id` carries, is read as `Q55`. A 201st distinct id is a `400` naming the count rather than a silent truncation, and so is anything that is not an item id. Every id gets a row, in the order sent; an id this API holds nothing for is a row of nulls and is named in `meta.not_found`.\n\nAnswered from a daily read of Wikidata, never by calling it in your request: the ids a run reads are every Nobel laureate the Nobel Prize API lists and every country those answers name. Until the first run has landed on a deployment, every request here answers `503` rather than a page of nulls.","example":"Q937,Q7186"}],"fields":[{"name":"id","type":"string","nullable":false,"desc":"`wikidata:\u003cQID\u003e` — `wikidata:Q937`. The same on a resolved and an unresolved row."},{"name":"source","type":"string","nullable":false,"desc":"The scraper stem, `wikidata`, and the first half of `id`."},{"name":"qid","type":"string","nullable":false,"desc":"The Wikidata item id you asked about, normalised as on the countries lookup."},{"name":"label","type":"string","nullable":true,"desc":"The English label, else the `mul` one, else null. Q937, Albert Einstein, now has only a `mul` label, which is why the fallback exists."},{"name":"citizenships","type":"array of object","nullable":true,"desc":"Every P27 (country of citizenship) value, each `{qid, label}`, ordered by QID number. Historical states and statelessness are kept as Wikidata records them. `[]` when Wikidata records none — an organisation, usually — and null only on an unresolved row. Every `qid` here can be looked up on `/v1/entities/countries`."},{"name":"educations","type":"array of object","nullable":true,"desc":"P69 (educated at) with **each institution's own country** (its P17), one entry per (institution, country) pairing, ordered by institution and then country number. An institution Wikidata places in two countries is two entries — Baku State University, in Azerbaijan and in the Soviet Union — so count distinct `qid` for institutions; one placed in none is one entry with a null country. `[]` when none is recorded, null only on an unresolved row."},{"name":"orcid","type":"string","nullable":true,"desc":"P496, the ORCID iD. Where more than one is recorded, the first in text order is served and the others are not. Null where none is recorded."},{"name":"redirect","type":"string","nullable":true,"desc":"The item this id now redirects to on Wikidata, whose facts these are. The Nobel Prize API lists two laureates by ids Wikidata has merged away; both resolve, and so do the items they point at."},{"name":"fetched_at","type":"timestamp","nullable":true,"desc":"The newest Wikidata Query Service read that returned exactly this row. **This is the one to quote.** Null only on an unresolved row."},{"name":"ingested_at","type":"timestamp","nullable":true,"desc":"When this platform last stored or confirmed the row. Null only on an unresolved row."},{"name":"citizenships[].qid","type":"string","nullable":false,"desc":"The country's Wikidata item. The stable key."},{"name":"citizenships[].label","type":"string","nullable":true,"desc":"Its English label, else its `mul` one, else null."},{"name":"educations[].qid","type":"string","nullable":false,"desc":"The institution's Wikidata item."},{"name":"educations[].label","type":"string","nullable":true,"desc":"Its English label, else its `mul` one, else null."},{"name":"educations[].country_qid","type":"string","nullable":true,"desc":"The country THIS institution sits in (its P17). Null when Wikidata records none."},{"name":"educations[].country_label","type":"string","nullable":true,"desc":"That country's label, as `label` is read."}]}]}],"shared":[{"id":"list-sources","verb":"GET","path":"/v1/sources","summary":"Every source feeding every dataset, with how much it holds and when it last delivered","returns":"array","params":[],"fields":[{"name":"release_version","type":"string","nullable":true,"desc":"Present on ncbi_gene once a complete dimension is served: poll this free field before fetching the bulk snapshot."},{"name":"source","type":"string","nullable":false,"desc":"The stem, and the value `?source=` takes — `abb`, not `ABB` or `abb-bank.az`. It is also the first half of every posting `id`."},{"name":"name","type":"string","nullable":true,"desc":"What the upstream calls itself (`1is.az`, `ABB`). Null for a source with no registry entry — the events sources have none. On `oews` it is BLS's name for the dataset, `Occupational Employment and Wage Statistics (OEWS)`."},{"name":"url","type":"string","nullable":true,"desc":"The source's own website. The same value `source_url` carries on a job row, and set for the same sources. Null rather than guessed for sources that live entirely on a third party's ATS, a source with only its ATS vendor's domain recorded, `other`, a catch-all, and sources we have measured as gone. Also null on the events, quotes, rates, cyber, climate, geo, seismic, labour, worldbank, bibliometrics, films, migration, weather and entities rows, which have no registry entry at all — the same reason `name` is null there — except `oews`, whose `url` is BLS's OEWS home page, `https://www.bls.gov/oes/`."},{"name":"jobs_url","type":"string","nullable":true,"desc":"The page this platform reads this source's vacancies from — `https://jobs.glorri.az` for `glorri`, an employer's careers page, or the candidate-facing page on the ATS for an employer hosted on one. **This is the one to link a human to.**\n\nRead off the scraper rather than curated, so unlike `source_type` it is a measurement and not an opinion. Null where there is no page to point at: sources whose scraper was archived, whose rows are still in the corpus and whose site is often still up but whose listing page is recorded nowhere; sources that only ever arrived through a history import; and sources whose page we have measured as gone. This list says which, row by row."},{"name":"dataset","type":"string","nullable":false,"desc":"Which collection this source feeds: `jobs`, `events`, `quotes`, `rates`, `genome`, `reachability`, `cyber`, `climate`, `geo`, `seismic`, `labour`, `worldbank`, `nobel`, `bibliometrics`, `films`, `migration`, `weather` or `entities`. `wikidata` feeds two: `films` and `entities`."},{"name":"source_type","type":"string","nullable":true,"desc":"What KIND of publisher this is — `employer`, `platform`, `agency`, `board`, `aggregator` or `unknown` — and the value `/v1/jobs?source_type=` takes. **This is our classification, not a fact the source asserts**: it is the one editorial field in this API, a value can change when a source is re-read, and the SET of values is stable. Null on the events, quotes, rates, genome, cyber, climate, geo, seismic, labour, worldbank, nobel, bibliometrics, films, migration, weather, entities and reachability rows, because the distinction is about who publishes a VACANCY and means nothing applied to a ticketing site, a quotations corpus, a central bank, a sequence archive, a climate portal, a map, a fault database, a UN statistics database, a prize catalogue, a research catalogue, a knowledge base, a forecast model, or this platform's own probe."},{"name":"records","type":"integer","nullable":false,"desc":"How many rows of this source are in the current-state table — what `/v1/jobs?source=\u003cthis\u003e` returns with `?active=` left at its default of `any`. **It counts postings the source has already taken down**, and for jobs most of them are: 149,383 against 39,046 still open when this was written. Two sources report thousands here and return nothing at all, their scrapers having stopped. If you are sizing live vacancies, read `live_records` instead. It also does not apply the suppression anti-join, which is harmless while nothing is suppressed and wrong the day something is. On `labour`'s `onet` it is the occupations, task statements and importance ratings of the newest complete O*NET release — what a default walk of its three collections returns — not the release before it, kept for `?version=`. On `bibliometrics` it is the newest complete snapshot's row count — what a default walk of `/v1/bibliometrics/country-years` returns — not the rows of every snapshot kept, and on `films` it is the films of the newest complete rebuild — what a default walk of `/v1/films` returns — not the rows of every rebuild kept. On `migration` it is the stock and population figures of every revision held, aggregates' rows included — more than a default walk of any one `/v1/migration/un-stock` table returns. On `weather` it is the hourly forecast points of every run held, for the 31 days they are kept — so it levels off rather than growing, and is no single run's count. On `entities` it is the countries and the people the two lookups can answer for together. On `climate`, `districts` counts the districts of the boundary sets it holds — 78 — and `ghsl_builtup` the district totals of the GHSL releases it holds, one per district per epoch — 156 for R2023A — never the monthly receipts or Baku's wedges. On `oews` it is the rows of every year's served file at all five SOC levels — `/v1/labour/oews/occupations?o_group=all` over every year `/years` lists — not the 830 a default walk of one year returns.\n\nIt keeps this meaning: moving it with a `true` default was announced for 2027-01-08 and withdrawn on 2026-10-15. For only the postings still open, read `live_records`."},{"name":"live_records","type":"integer","nullable":false,"desc":"How many postings of this source are STILL OPEN — the number `/v1/jobs?source=\u003cthis\u003e\u0026active=true` returns, counted the same way the read API counts it. **This is the number to plan against.** For events, quotes, rates, cyber, genome, reachability, climate, geo, seismic, labour, worldbank, nobel, bibliometrics, films, migration, weather and entities it equals `records`: an event is not retired, it simply passes, the quotes view is already active-only, a published exchange rate for a past date is a fact rather than a listing that can close, a release marker, a watched target or a climatology series is a current state rather than a row that expires — one whose latest read failed is still counted, which is what `status`, `failure_class` and `reason` on those rows are for — a boundary document is current until a rebuild replaces it, a fault trace is in GEM's file or is withdrawn from this one, an employment estimate is a figure of the ILO load being served, an OEWS row is a figure of the BLS file served for its year, an O*NET row is an occupation or its task or rating in the release served, a country-year is a sum over the OpenAlex release being served, a film is a member of the newest rebuild of its corpus or is not counted at all, a migrant stock figure is one of a revision UN DESA published and is replaced only with it, a forecast point is pruned with its run after 31 days rather than retired, and an entity is the newest read of a Wikidata item, which nothing retires. On geo both count whole documents, not the features inside them. On nobel both count the laureates and prize slots served — the two catalogues the source reads — and not the awards, affiliations and countries derived from them."},{"name":"aggregator","type":"boolean","nullable":false,"desc":"True when this source republishes OTHER boards' postings under its own id, with the ORIGINAL board's url. Two records then exist for one posting — same `url`, different `source`, different `id` — so a consumer keying a table on `url` will collide its copy with the owning board's record."},{"name":"last_ingested_at","type":"string","nullable":true,"desc":"When a batch from this source was last accepted, RFC 3339. It says the scraper ran, NOT that anything changed — and a source quiet for months still answers with its rows. Null only if no batch was ever recorded.\n\n**Accepted is not stored.** A batch whose records carry no source id lands, moves this timestamp, and adds nothing — three job sources did exactly that three times a day for five weeks. `last_observed_at` is the one that says when something was actually delivered."},{"name":"last_observed_at","type":"string","nullable":true,"desc":"When a run of this source last delivered a posting that was stored, RFC 3339: the newest `ingested_at` among every row it holds, retired ones included. **This is the timestamp to judge a source's rows by.** Batches landing while this stands still is a scraper whose output is being discarded; both old is a scraper that stopped. Either way, every `active: true` row of this source has been unconfirmed since this moment, however recent `last_ingested_at` is.\n\nNull outside `jobs`, the one dataset whose batches can land without storing and whose rows carry a liveness verdict that can outlive the source — and null on a jobs source that has never stored a row."},{"name":"stale_live_records","type":"integer","nullable":true,"desc":"How many of `live_records` no run has carried for seven days — postings still published as open that nothing has re-confirmed in a week. `0` on a healthy source; all of `live_records` on one that has gone dark, which is the case `live_records` alone reports as hundreds of open vacancies.\n\nSeven days because it is the window this platform already calls \"seen recently\", and half its longest retirement sweep, so a board that presents itself over several days is not called stale for being slow. If you want a different line, each row's own `ingested_at` is the same measurement. Null outside `jobs`."},{"name":"quality","type":"object","nullable":true,"desc":"How USABLE this source's live rows are, as opposed to how many there are. Three whole percentages over the rows `?source=\u003cthis\u003e\u0026active=true` returns: `description_pct`, `location_pct` and `duplicate_url_pct`.\n\n`records` and `live_records` say how much is there and nothing about whether it can be used. Two sources publish rows with no body at all — one carries a title, a company and a date and nothing else — so a consumer filtering for postings with a description finds those rows contribute zero, after building the integration rather than before.\n\n`duplicate_url_pct` is the same-source arity `url` already documents — one vacancy under two of the board's own ids — and the advice there still holds: collapse on the newer `ingested_at`. What this adds is the RATE, because a quirk affecting a handful of rows and one affecting a quarter of a source are different facts if you are sizing a mirror. Cross-source duplication is a different problem and `aggregator` marks it.\n\nNull on events, quotes, rates, genome, reachability, cyber, climate, geo, seismic, labour, worldbank, nobel, bibliometrics, films, migration, weather and entities, and on a jobs source with no live rows left to measure — which is not the same as a source whose rows all score zero."},{"name":"provider","type":"string","nullable":true,"desc":"Who publishes the source's data — `U.S. Bureau of Labor Statistics` on `oews`. Set where this list records a dataset's provenance, which is `oews` today; null elsewhere, where the terms travel on the dataset's own rows and provenance routes instead."},{"name":"licence","type":"string","nullable":true,"desc":"The terms the data comes under, as the dataset's own rows state them — `public_domain_us_government` on `oews`. Null where `provider` is."},{"name":"citation","type":"string","nullable":true,"desc":"How the publisher asks to be cited — on `oews`, `U.S. Bureau of Labor Statistics, Occupational Employment and Wage Statistics.` Null where `provider` is."}]}],"envelope":[{"name":"data","type":"array | object","nullable":false,"desc":"The result. An array for collections, one object for a single resource."},{"name":"meta","type":"object","nullable":false,"desc":"Pagination and request metadata. Always present."},{"name":"meta.next","type":"string","nullable":true,"desc":"Opaque cursor for the following page. null on the last page."},{"name":"meta.count","type":"integer","nullable":true,"desc":"Items in data. Present for collections only."},{"name":"meta.total","type":"integer","nullable":true,"desc":"Rows the FILTER matches across every page, not just this one. Present only when you ask with `?count=true`, because it costs a second query."},{"name":"meta.requested","type":"integer","nullable":true,"desc":"Ids you sent. Batch endpoints only."},{"name":"meta.answered","type":"integer","nullable":true,"desc":"Ids this API had a row for. Batch endpoints only. `answered` plus the length of `meta.not_found` always equals `requested` — a batch never drops an id quietly. Where `not_found` is absent, nothing can go missing, so `answered` equals `requested`: those endpoints answer every id or fail. `data` holds the `answered` rows, except on `GET /v1/entities/countries` and `GET /v1/entities/people`, which also return a row of nulls for every id in `not_found` — so there `data` always has one row per distinct id sent, in the order sent."},{"name":"meta.not_found","type":"array of string","nullable":true,"desc":"The ids you sent that have no row here, named rather than left for you to diff two lists. Present only where a miss is an ordinary answer rather than a failure — today `GET /v1/cyber/epss`, where a CVE that FIRST has published no score for is expected, `GET /v1/cyber/nvd`, where a CVE outside CISA's catalog is not held, and the two `/v1/entities` lookups, where an id the daily run never read is — and on those endpoints it is always present, `[]` when every id was found. Absent elsewhere, including on `POST /v1/jobs/liveness`: absence means no id was missed, never that the endpoint declines to say."},{"name":"meta.model_run","type":"timestamp","nullable":true,"desc":"The forecast run the answer was read from — its base time, never a fetch time. On `/v1/weather` responses only, and on every one of them: null only when nothing was served. On `/v1/weather/forecast/runs` it is the newest run in the answer. On `/v1/weather/cities` it is the run the forecast endpoints serve by default, and null there until the first run has landed — when they answer `503`."},{"name":"meta.licence","type":"string","nullable":true,"desc":"SPDX identifier of the licence the data comes under, `CC-BY-4.0`. On `/v1/weather` responses, on the three World Bank routes that serve WDI's values — `/v1/worldbank/context`, `/v1/worldbank/indicators` and `/v1/worldbank/indicators/{code}` — and on `/v1/climate/ghsl` responses; `CC0-1.0` on the gene constraint collection and its by-symbol route."},{"name":"meta.licence_url","type":"string","nullable":true,"desc":"The World Bank's licence page, which carries the terms it adds to CC BY 4.0: a dispute goes to non-binding mediation, then to arbitration. On the three WDI routes only."},{"name":"meta.attribution","type":"string","nullable":true,"desc":"The statement to display with the data if you republish it — on `/v1/weather`, the one ECMWF's terms require with what was modified; on the three WDI routes, the World Bank's source line; on gene constraint, gnomAD's release-specific attribution; on `/v1/genome/expression`, GTEx's acknowledgement — the GTEx Portal, the date softon read the file and the dbGaP accession; on `/v1/climate/ghsl`, `European Commission, Joint Research Centre - Global Human Settlement Layer`, verbatim, which #130 makes mandatory. **If you republish this data, display it.** On those responses. On gene constraint it names the object read from gnomAD's bucket."},{"name":"meta.source_name","type":"string","nullable":true,"desc":"`World Bank World Development Indicators`. On the three WDI routes only."},{"name":"meta.source_url","type":"string","nullable":true,"desc":"Where the data and its terms are published — the link to display beside `meta.attribution`. On `/v1/weather` responses, the three WDI routes and `/v1/climate/ghsl` responses. On gene constraint it names the object read from gnomAD's bucket."},{"name":"meta.snapshot_date","type":"date","nullable":true,"desc":"WDI's own `lastupdated` for the release the answer's values come from, `2026-10-08` — the date to pin a copy to. **The oldest among the series the answer reads**: a release the short-read guard refused leaves its series served under the release before, and an answer is never dated newer than its stalest value. Null only when none of them holds a series. On the three WDI routes only."},{"name":"meta.retrieved_at","type":"timestamp","nullable":true,"desc":"When this platform fetched and stored what the answer comes from. On the three WDI routes, that release — likewise the oldest among the series read. On `/v1/genome/expression`, when softon read GTEx's file for the release: it moves only when the release is read again from a replaced file, so a conditional request stays a `304`. On those responses only."},{"name":"meta.release","type":"string","nullable":true,"desc":"The release the answer was read from. On `/v1/climate/ghsl`, the GHSL release, `R2023A`: a release bump changes every value, so this is what tells two answers apart. On `/v1/genome/expression`, GTEx's own name for the release `meta.dataset` names, `v10`. On the gene constraint collection and its by-symbol route, the gnomAD release every row comes from, such as `4.1.2`."},{"name":"meta.boundary_set","type":"string","nullable":true,"desc":"The district set the totals were summed over — `az-adm2-gb-9469f09-simplified-naftalan-osm-3812595v20`, whose outlines `GET /v1/climate/districts` serves. Null on `/v1/climate/ghsl/builtup/zones`, whose wedges owe nothing to a district outline. On `/v1/climate/ghsl` responses only."},{"name":"meta.tiles","type":"array","nullable":true,"desc":"The GHSL tiles the answer's epochs were read from, four per epoch, by `epoch` and `tile`: the provenance #130 says its consumer never recorded. See `meta.tiles[].*`. On `/v1/climate/ghsl` responses only."},{"name":"meta.tiles[].epoch","type":"integer","nullable":false,"desc":"The epoch the tile is of, `2010`."},{"name":"meta.tiles[].tile","type":"string","nullable":false,"desc":"GHSL's tile id, `R5_C23`: four 10° tiles cover the grid, R5_C23, R5_C24, R6_C23 and R6_C24."},{"name":"meta.tiles[].url","type":"string","nullable":false,"desc":"The resolved download URL the tile was fetched from — a zip holding one GeoTIFF — under `https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/GHSL/`."},{"name":"meta.tiles[].bytes","type":"integer","nullable":false,"desc":"The zip's size as fetched: 3.6 to 14.2 MB, 80.5 MB for the eight."},{"name":"meta.tiles[].sha256","type":"string","nullable":false,"desc":"The zip's sha256 as fetched, checked against a pin before a byte of it was read. A tile that is not the pinned one is refused, never summed."},{"name":"meta.tiles[].last_modified","type":"string","nullable":true,"desc":"The `Last-Modified` JRC answered with, verbatim — `Mon, 27 Jan 2025 17:44:43 GMT`: the day JRC re-packed the zips. The GeoTIFFs inside are dated 2023-10-06, the release's publication."},{"name":"meta.grid","type":"object","nullable":true,"desc":"The analysis grid every GHSL number is summed on: EPSG:4326, `bounds` [44, 37.8, 51, 42.2], 1680 × 1056 pixels of 1/240° — published so a grid half a pixel off is a visible diff rather than a silent drift in every district. See `meta.grid.*`. On `/v1/climate/ghsl` responses only."},{"name":"meta.grid.crs","type":"string","nullable":false,"desc":"`EPSG:4326`."},{"name":"meta.grid.width","type":"integer","nullable":false,"desc":"1680 pixels."},{"name":"meta.grid.height","type":"integer","nullable":false,"desc":"1056 pixels."},{"name":"meta.grid.bounds","type":"array of number","nullable":false,"desc":"West, south, east, north, exactly: `[44, 37.8, 51, 42.2]`."},{"name":"meta.grid.pixel_deg","type":"number","nullable":false,"desc":"1/240 of a degree, `0.004166666666666667`."},{"name":"meta.grid.pixel_arcsec","type":"number","nullable":false,"desc":"15: five GHSL pixels to a side."},{"name":"meta.grid.transform","type":"array of number","nullable":false,"desc":"The affine transform as rasterio writes it — pixel width, row rotation, west edge, column rotation, pixel height (negative), north edge — so a raster on this grid can be checked against it to the last digit."},{"name":"meta.method","type":"object","nullable":true,"desc":"How every number in the answer was made, verbatim as the computation recorded it: `version`, and one key per step in words — `mosaic`, `resampling`, `units`, `rasterize`. Compare it as an object; its key order means nothing. A release recomputed by another method REPLACES the one held under the same `meta.release`, so this, `meta.inputs_sha256` and the `ETag` are what say its numbers moved: compare them, not the release alone, to know you still hold the same answer. On `/v1/climate/ghsl` responses only."},{"name":"meta.method.version","type":"string","nullable":false,"desc":"The method's version, `1`: the tiles mosaicked as rasterio.merge does, each pixel moved half a pixel north-west and counted wholly in one grid pixel, then summed 5 × 5 in whole square metres. A new version is a change to the numbers."},{"name":"meta.inputs_sha256","type":"string","nullable":true,"desc":"sha256 over everything the computation read — the tiles' pins, the boundary set's geometry digest, the grid, the zone sets and `meta.method.version` — 64 hex digits. Equal digests are the same computation over the same inputs. On `/v1/climate/ghsl` responses only."},{"name":"meta.boundary_licence","type":"string","nullable":true,"desc":"The terms of the district outlines the totals were summed over, `CC-BY-SA-3.0 AND ODbL-1.0`, beside GHSL's own. Null on `/v1/climate/ghsl/builtup/zones`. On `/v1/climate/ghsl` responses only."},{"name":"meta.boundary_attribution","type":"string","nullable":true,"desc":"The credit lines those outlines require — geoBoundaries' and OpenStreetMap's. Null on `/v1/climate/ghsl/builtup/zones`. On `/v1/climate/ghsl` responses only."},{"name":"meta.last_poll_at","type":"timestamp","nullable":true,"desc":"When this platform last polled USGS ComCat successfully — RFC 3339, UTC, three fractional digits. On `/v1/seismic/quakes` and `/v1/seismic/quakes/{id}` only, and on every answer of theirs. A failed poll does not move it; the snapshot says what the newest run found."},{"name":"meta.dataset","type":"string","nullable":true,"desc":"The GTEx release the answer was read from, `gtex_v10` or `gtex_v8` — named even when `data` is null, because a gene absent from one release can be in another. On `/v1/genome/expression` responses only."},{"name":"meta.gencode_version","type":"string","nullable":true,"desc":"The GENCODE release it was quantified against, `v39`. On `/v1/genome/expression` responses only."},{"name":"meta.genome_build","type":"string","nullable":true,"desc":"`GRCh38/hg38`. On `/v1/genome/expression` responses only."},{"name":"meta.tissue_count","type":"integer","nullable":true,"desc":"How many tissues the release has, `54`. On `/v1/genome/expression` responses only."},{"name":"meta.file_url","type":"string","nullable":true,"desc":"The GTEx median TPM file the release was read from. On `/v1/genome/expression` responses only."},{"name":"meta.reason","type":"string","nullable":true,"desc":"Why `data` is null on a `200`: `not_in_reference`, the release's GENCODE has no gene by that name or id. Present only then, on `GET /v1/genome/expression/{symbol}`."},{"name":"meta.found_in","type":"array of string","nullable":true,"desc":"Beside `meta.reason`: the other releases served here that do name the gene, `[\"gtex_v10\"]` for `SEPTIN7` asked of `gtex_v8`. `[]` when none does. Present only with `meta.reason`."},{"name":"meta.newest_release","type":"string","nullable":true,"desc":"GTEx's newest published release when softon last checked, `v11` — null if that could not be read. On `/v1/genome/expression` responses only."},{"name":"meta.release_notice","type":"string","nullable":true,"desc":"Null when the release answered from is GTEx's newest; otherwise a sentence saying it is not the most current data the GTEx Portal publishes, which GTEx's licence asks a service redistributing an older release to make known. **If you republish this data, display it.** On `/v1/genome/expression` responses only."},{"name":"meta.terms_url","type":"string","nullable":true,"desc":"The terms the data comes under, `https://gtexportal.org/home/license`. On `/v1/genome/expression` responses only."},{"name":"meta.columns_absent","type":"array of string","nullable":true,"desc":"The optional columns that release's table lacks — `mis.oe`, `syn.oe` or `lof.oe_ci.upper_bin_decile` — whose fields are therefore null on every gene. `[]` when it has them all, as every release served today does. On `/v1/genome/constraint` and `/v1/genome/constraint/{symbol}` only."},{"name":"meta.source","type":"object","nullable":true,"desc":"Where the answer was read from. On `/v1/genome/gwas` responses, and on every one of them: the parse — release, when it was read, parser, the zip's sha256 and the row-count check, and the attribution — the same object the documents carry as `data.source`. On `/v1/genome/genes/snapshot`: the NCBI Gene release's file and what was found in it, `meta.source.*` below. Absent elsewhere."},{"name":"meta.source.source_id","type":"string","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: `ncbi_gene` — also the file's row on `/v1/genome/releases`, which says which day NCBI last wrote it."},{"name":"meta.source.source_name","type":"string","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: `NCBI Gene`."},{"name":"meta.source.authority","type":"string","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: `NCBI`."},{"name":"meta.source.url","type":"string","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: `https://ftp.ncbi.nlm.nih.gov/gene/DATA/GENE_INFO/Mammalia/Homo_sapiens.gene_info.gz`."},{"name":"meta.source.last_modified","type":"string","nullable":true,"desc":"On `/v1/genome/genes/snapshot`: The `Last-Modified` NCBI sent with those bytes, verbatim. The release is named for its UTC day; a second content written the same day is `.2`."},{"name":"meta.source.content_length","type":"integer","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: The gzipped file's size as served, 5,194,256 bytes on 2026-10-09."},{"name":"meta.source.gz_sha256","type":"string","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: sha256 of the gzipped bytes as served."},{"name":"meta.source.decompressed_bytes","type":"integer","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: The size of the TSV `sha256` is over."},{"name":"meta.source.header","type":"string","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: The file's first line, verbatim: the sixteen tab-separated column names every field was read by. A file whose header lacks one, repeats one or adds one is refused whole, and the release before it stays served."},{"name":"meta.source.rows","type":"integer","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: The file's data rows across every taxon, 193,998 on 2026-10-09."},{"name":"meta.source.rows_by_tax_id","type":"array of object","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: Per taxon: `tax_id`, `rows`, and `ambiguous_symbols` — every symbol more than one of its genes bears, each a lookup that answers several genes. Seventeen for 9606 today."},{"name":"meta.source.parser_version","type":"integer","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: The parse rules. A release is its content read by one parser, so a new parser reading the same file is a new release."},{"name":"meta.source.fetched_at","type":"timestamp","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: When the file this release was read from was first read."},{"name":"meta.source.served_at","type":"timestamp","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: When the release became served — when the last of its genes landed."},{"name":"meta.source.checked_at","type":"timestamp","nullable":true,"desc":"On `/v1/genome/genes/snapshot`: The newest morning that found NCBI's file still holding this release."},{"name":"meta.source.withdrawn","type":"array of integer","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: GeneIDs held when this release was read that its file no longer lists: the genes its promotion removed. Gone from every route from this release on."},{"name":"meta.source.attribution","type":"string","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: The line to carry with any gene you republish: `NCBI Gene (National Center for Biotechnology Information, U.S. National Library of Medicine); NCBI places no restrictions on the use or distribution of the data`."},{"name":"meta.source.terms_url","type":"string","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: NCBI's Website and Data Usage Policies: \"NCBI itself places no restrictions on the use or distribution of the data contained therein.\""},{"name":"meta.source.rows_by_tax_id[].tax_id","type":"integer","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: The taxon."},{"name":"meta.source.rows_by_tax_id[].rows","type":"integer","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: Its rows in the file."},{"name":"meta.source.rows_by_tax_id[].ambiguous_symbols","type":"array of string","nullable":false,"desc":"On `/v1/genome/genes/snapshot`: Every symbol more than one of its genes bears, compared without regard to case, as the file spells it."},{"name":"meta.upstream_release","type":"string","nullable":true,"desc":"The newest release EBI's stats endpoint has named on a daily check. When it differs from `meta.source.release_version`, a newer release has not been read whole yet — not published, or refused — and the one before stays served. On `/v1/genome/gwas` responses only."},{"name":"meta.version","type":"string","nullable":true,"desc":"The O*NET release the answer was read from -- `31.0`. On `/v1/labour/onet` responses only, and on every one of them. Every page of one walk carries the same value: a walk stays in the release it began in."},{"name":"meta.release","type":"string","nullable":true,"desc":"The NCBI Gene release the answer was read from, `2026-10-09`: NCBI's `gene_info` as it stood on the day it is named for. It moves only when a gene did — equal to the one you hold, nothing has changed. On `/v1/genome/genes` responses only, and on every one of them."},{"name":"meta.tax_id","type":"integer","nullable":true,"desc":"The taxon served; 9606 unless requested otherwise."},{"name":"meta.record_count","type":"integer","nullable":true,"desc":"The number of records in data. Reject a truncated body."},{"name":"meta.sha256","type":"string","nullable":true,"desc":"SHA-256 of NCBI's decompressed source TSV, identifying the release."},{"name":"meta.generated_at","type":"timestamp","nullable":true,"desc":"When this release became complete and served."},{"name":"meta.request_id","type":"string","nullable":false,"desc":"Quote this when reporting a problem."},{"name":"error","type":"object","nullable":true,"desc":"null on success. On failure, carries `code` and `message`, and `reason` where one code covers several causes: today only `unauthorized`, whose `reason` is `invalid_api_key` or `revoked_api_key`."}],"errors":[{"status":400,"code":"invalid_request","meaning":"A parameter is missing, malformed or out of range. The message names it.","retry":false},{"status":401,"code":"missing_authorization","meaning":"No usable credential arrived: either no Authorization header at all, or one this API could not read as a bearer token. Distinct from unauthorized, which means a key did arrive and was not recognised. A proxy or a redirect that drops the header looks exactly like a client that never sent one, and this code is what tells the two apart.","retry":false},{"status":401,"code":"unauthorized","meaning":"A key was presented and this API does not serve it. `error.reason` says why: `invalid_api_key` for a key it never issued, `revoked_api_key` for one that was revoked or whose account is suspended (the message says which). The code itself stays `unauthorized`: splitting it was announced on 2026-10-03 and withdrawn on 2026-10-15, so a branch on it keeps working. If the key worked yesterday, your dashboard lists revoked keys rather than deleting them.","retry":false},{"status":403,"code":"forbidden","meaning":"The key is valid but your plan does not include this dataset.","retry":false},{"status":404,"code":"not_found","meaning":"No resource with that id on this dataset.","retry":false},{"status":410,"code":"gone","meaning":"This record was held and has since been removed at the request of the employer or the individual concerned. It will not come back. If you hold a copy, stop serving it. A single-resource path answers it only for an id this platform actually had -- an id that never existed here is still a 404. Every route of a retired dataset answers it too, collections included, with a message naming the day the dataset stopped; this schema lists those datasets with status `retired`.","retry":false},{"status":429,"code":"rate_limited","meaning":"Plan rate exceeded. Retry-After gives the wait in seconds.","retry":true},{"status":429,"code":"quota_exceeded","meaning":"The account's monthly request allowance is spent. It resets on the 1st; X-Quota-Limit and X-Quota-Used say where you are. Retrying sooner cannot help, which is why this is a different code from rate_limited.","retry":false}],"limits":{"default_page_size":25,"max_page_size":100,"plans":[{"name":"free","per_second":5,"burst":10,"monthly_requests":100,"datasets":["jobs"]},{"name":"starter","per_second":10,"burst":20,"monthly_requests":5000,"datasets":["jobs"]},{"name":"builder","per_second":50,"burst":100,"monthly_requests":50000,"datasets":["jobs"]},{"name":"scale","per_second":100,"burst":200,"monthly_requests":250000,"datasets":["jobs"]}],"caching":{"etag":true,"if_none_match":true,"not_modified_status":304,"not_modified_is_billed":false,"cache_control":"private, max-age=0, must-revalidate","notes":"Every 200 on a collection or a single resource carries a strong ETag over the DATA — not the envelope, so the per-request `request_id` does not change it. Send it back as `If-None-Match` and an unchanged page answers 304 with no body. A 304 costs no credit, because a credit is one successful (2xx) request and 304 is not one; the per-second rate limit still applies. `private` because a response is scoped to your key — do not put these in a shared cache. For tailing new and changed rows, `updated_after` is still cheaper than any revalidation; conditional requests are for periodically verifying a FULL set."}}},"meta":{"next":null,"request_id":"req_13320ba9aa4fd85f"},"error":null}
