Docs / Shared / Changelog
Changelog
Every change made to v1, newest first, and what each one means for an integration that is already running.
Everything that has changed on v1. Almost every entry is an addition. Five are not, and each says so where it appears: an unknown ?source= became a 400 on 2026-08-28, ?country= was narrowed to refuse a code that is not one on 2026-09-05, active: false stopped being only a finding on 2026-10-03, company_logo became null on 2026-10-07 wherever its value was not the URL it was documented as, and seventeen datasets were retired on 2026-10-16. The first two fall under the third notice exception — a value neither could ever have matched — and neither withdrew a row anyone was receiving. The third falls under no exception, and its entry says so. The fourth withdrew values that were being served, and its entry says why that is a correction of the data rather than a change to the contract. The fifth was made without the notice period, and its entry says that too.
Latest changes
2026-10-31
- Announced:
facts.birjob_source_nameis removed on 2027-01-30. Only postings imported from the archive carry the key, and it names the source thatsource_nameandsourcename on every posting: read those instead. Until then a response that carries the key says so in the headers below. Announced. - A response touched by an announced removal says so in three headers,
Deprecation,SunsetandLink, and a browser can read all three.GET /v1/schemalists each announced removal under the operations it touches, indeprecations. Both are additions. Notice. GET /v1/schemapublishes four defaults, indefaultson the operations that have them:limitandorderonGET /v1/jobsandGET /v1/companies/{id}/jobs, andactiveandcountonGET /v1/jobs. They are the defaults the reference already gave, and none changed. An addition.- What
v1never changes is written down: a type, a nullability, a default and a meaning. Such a change would ship as a/v2path, with/v1kept for at least 12 months after. Retiring a dataset no key has called for 30 days is a fourth notice exception. Notice.
2026-10-25
- What the retired datasets collected is deleted. The seventeen datasets retired on 16 October kept what they had collected, as that entry says. On this date we decided to delete it, and it is no longer held. Nothing you can call changes: every route under those datasets still answers
410withcode: "gone",GET /v1/schemastill documents them withstatus: "retired", and/v1/jobs,/v1/jobs/{id},POST /v1/jobs/liveness,/v1/companies/{id}/jobsandGET /v1/sourcesare unchanged. Privacy.
2026-10-22
- The Free plan is 100 requests a month, down from 1,000. It applies to every account on the plan from this date, existing ones included; the paid plans and every rate limit are unchanged. An account already past 100 this month is refused with
429andquota_exceededuntil the allowance resets on the 1st.X-Quota-Limitcarries the figure applied to your account, and an allowance agreed for one account is not affected. No route, field or status changes. Limits.
Earlier changes
2026-10-18
- A posting with no body says so on
GET /v1/jobs/{id}.description_htmlandrequirements_htmlcome back asnullwhen the source published no body, where the keys used to be left out. The reference already called both nullable. A collection still leaves them out unless you ask for them withinclude=bodies. Reference.
2026-10-16
- softon serves job postings only: seventeen datasets are retired. Events, quotes, rates, genome, reachability, cyber, climate, geo, seismic, labour, worldbank, nobel, bibliometrics, films, migration, weather and entities stopped on this date. Every route under them answers
410withcode: "gone"and a message naming the date, to every caller with or without a key, and none of those answers is metered.GET /v1/schemastill documents them, withstatus: "retired"; the plans' dataset lists andGET /v1/sourcesname only jobs. Their scrapers have stopped, and nothing they collected was deleted. This is a breaking change made without the 90 days’ notice this page promises, and none of the exceptions covers it: when it was made, the only customer reading any of these datasets was one key reading events./v1/jobs,/v1/jobs/{id},POST /v1/jobs/livenessand/v1/companies/{id}/jobsare unchanged. Job Data API.
2026-10-15
- Why a posting is inactive:
inactive_reason. Every posting on/v1/jobsand/v1/jobs/{id}now carries it, null whileactiveistrue.absent_from_sourceandsource_goneare findings about the posting.source_unreachable,source_origin_downandsource_darksay only that we could not reach its source, so the posting may well still be open.imported_inactiveandimported_flaggedcome from an earlier archive.activeitself does not change. Reference. - Why a key was refused:
error.reason. A401withcode: "unauthorized"now saysinvalid_api_keyfor a key we never issued andrevoked_api_keyfor one that was revoked or whose account is suspended, which the message names. The code itself does not change. - The three announced breaking changes are withdrawn. Nothing changes on 2027-01-01 or 2027-01-08:
activestays a boolean,unauthorizedstays the code, and?active=keeps its default ofany, withrecordskeeping its meaning. The two additions above serve what those changes were for. Announced.
2026-10-12
- NCBI Gene's complete identity dimension.
GET /v1/genome/geneslists genes by GeneID with untruncated synonyms, and the lookup reports every candidate when a symbol is ambiguous. Human is the default taxon; the other taxa in NCBI's file remain explicit. Reference. - One request for the whole dimension.
GET /v1/genome/genes/snapshotstreams the taxon's complete array with gzip, its record count, release and upstream TSV hash in metadata. Conditional requests can answer an unbilled304. The free/v1/sourceslist exposesrelease_versiononncbi_gene, so callers can poll before downloading. Reference. - O*NET's occupations, tasks and importance ratings. Seven routes under
/v1/labour/onetserve one complete release, carrymeta.versionand retain the publisher's attribution and quality flags. Skills combine Essential and Transferable Skills; the original 1–5 rating remains beside its 0–100 rescale. Reference. - Stored places: three collections under
/v1/geo/places.azerbaijanholds OpenStreetMap's settlements, administrative areas and stations from Geofabrik's dated Azerbaijan extract (ODbL 1.0), and beside them four hand-placed pins, served assource: "manual"andconfidence: "approximate".world-citiesandworld-countriescome from GeoNames (CC BY 4.0).GET /v1/geo/places/lookupanswers every key you send, in the order sent: accents and punctuation are folded, a qualifier after|narrows by country or area, and manual pins rank first. A name it cannot place comes backunresolvedwith null coordinates and is listed inmeta.not_found. No request calls a geocoder, because every answer is read from the stored build, and a rebuild never moves a manual pin.GET /v1/geo/placeswalks one collection by ID, andGET /v1/geo/places/snapshotnames each collection's licence, counts and newest build receipt. A collection answers503until a build of it has been verified whole. Carry the collection's licence and attribution with anything you republish. Reference. - The CVE Program's own records:
GET /v1/cyber/cvelist. Send up to 100 CVE ids a request. Each comes back with the CVE JSON 5 record byte for byte as the CVE Program published it, every CNA and ADP container included, plus the commit and URL it was read from and when. Every id says what happened to it:found;not_found, an upstream 404 that was stored;failed; orunfetched. An id inmeta.not_foundis one this platform holds no record for, never a statement that CVE.org has none. The records held are the ones CISA's KEV catalog lists, read every Sunday at one pinned commit, and serving never contacts GitHub. They are not CC0: MITRE's copyright notice and usage grant come with every result and must stay with any copy.GET /v1/sourceslists a fourthcybersource,cvelist. Reference.
2026-10-11
- A third feed under
/v1/cyber: NVD's record of every CVE in CISA's KEV catalog.GET /v1/cyber/nvdanswers what the National Vulnerability Database says about each CVE you send — the CVSS metric and whose it is, every weakness and reference, NVD's own timestamps exactly as it prints them, the record's status and the four KEV fields NVD carries — for up to 100 ids a request, in the order you sent them. Same key, and the envelope ofGET /v1/cyber/epss, the other lookup:meta.requested,meta.answeredandmeta.not_found, and no cursor. Metered like any data route, on every plan. Reference. - What is held is the catalog, not NVD's corpus. 1,739 CVEs on 2026-10-11, the ones CISA's catalog lists, read every night from NVD's API; a night on which NVD revised none of them stores a receipt and nothing else. An id in
not_foundis one this platform does not hold — never a statement that NVD has no record of it. Until the first nightly run has landed on a deployment, both routes answer503rather than an empty answer. - The selection is NVD's order, and every record says whose number it is. The metric served is the first entry of the first non-empty family of CVSS 4.0, 3.1, 3.0 and 2.0, whoever published it, and
cvss_sourceandcvss_typename it: on 2026-10-11, 787 of the 1,739 selected a Secondary metric — a CNA's or CISA-ADP's — rather than NVD's own.metric_keys_presentlists every family NVD holds,ssvcV203, CISA's SSVC decision, among them. Nothing is normalised: CVSS 4.0'sADJACENTstays beside 3.x'sADJACENT_NETWORK,NVD-CWE-noinfoandNVD-CWE-Otherarrive verbatim incwes_raw, and a Rejected CVE is served as Rejected. Two values are this platform's, and the reference says so:cvss_confidence, and the nulling of a v2-only record's four vector components and severity. - Provenance per CVE-id year:
GET /v1/cyber/nvd/partitions. One row per year held — how many, the newestlastModified, the oldest read still served, how many the newest run did not confirm, and a sha256 fingerprint over the year's records as held here, which is not a checksum of any NVD file. A year that failed to refresh says so inunconfirmedrather than being skipped. The document names the newest run and carries the public-domain licence, the attribution, and the notice NVD's API terms ask for: “This product uses the NVD API but is not endorsed or certified by the NVD.” Metered, one request for the whole document. Reference. GET /v1/sourceslists a thirdcybersource,nvd, whoserecordsare the CVEs it holds, one row per CVE.- gnomAD's gene constraint:
GET /v1/genome/constraint. LOEUF, observed/expected for loss-of-function, missense and synonymous variants, pLI, the observed and expected loss-of-function counts and the LOEUF decile, one row per gene, read from the gene's MANE Select transcript where it has one and its canonical one otherwise. LOEUF ascending by default, the most constrained genes first;ordertakesloeuf,pli,oe_loforgene, andsymbollooks up to 100 genes at once. On every plan. Reference. - One release per answer, every release kept. Every release whose constraint table the public bucket lists and the gene rule can read is served — 4.1, 4.1.1 and 4.1.2 today — each from one whole read, because the numbers move between releases: A1BG's LOEUF is 1.34 in 4.1 and 1.229 in 4.1.2.
meta.releasenames the release an answer comes from, the newest unless you ask for one in?release=, and a walk's cursor stays on the release and the read it began on.GET /v1/genome/constraint/{symbol}answers one gene. Reference. - The release is found, never assumed.
GET /v1/genome/constraint/releasesnames every release the bucket lists, read from the bucket's own listing every morning, with the object each served read was made from — its Cloud Storagegenerationandmd5Hash, which the read was checked against byte for byte — and why a listed release is not served: 4.0's table has nocanonicalcolumn. A release gnomAD publishes is read the morning it appears, with no change on our side. Reference. - LOEUF is not a measure of how important a gene is. It is the upper bound of a 90% interval on observed over expected loss-of-function variants: lower means the gene tolerates less such variation than expected in gnomAD's reference population. 29 genes of 4.1.2 have a LOEUF above 10, every one with under one expected loss-of-function variant, and are served as gnomAD states them. Ratios are four significant digits, exponents kept.
- gnomAD releases its data under CC0;
meta.attributionon every answer is the line it asks you to keep with it.GET /v1/sourceslists a sixth genome source,gene_constraint, whoserecordsare the genes of every release served.
2026-10-10
- ClinVar's aggregates: four endpoints under
/v1/genome/clinvar.GET /v1/genome/clinvar/overviewanswers the totals and every small facet as one document — every ClinicalSignificance string and its count, the same rows in ten groups, review status with its stars, variant type, origin and the year each variant was last evaluated — andGET /v1/genome/clinvar/chromosomesthe variants, pathogenic and uncertain variants on every chromosome.GET /v1/genome/clinvar/genespages through every gene, busiest first, andGET /v1/genome/clinvar/gene-conditionsthrough the busiest 10,000 gene-condition pairs. All four are counted from one weekly file, ClinVar's 455 MBvariant_summary.txt.gz, over its GRCh38 rows only: ClinVar writes every variant once per assembly, and counting both would double every figure. Metered like any data route, on every plan. Reference. - Every column is found by its name. The file's
#header is read into names before a row is counted, and a file missing any of the fifteen columns the counts use is refused by name. A column ClinVar adds is read around; a row that does not line up with the header is counted as malformed, intotals.rows_malformed, and nowhere else. - A file that does not add up is never served. A file with no GRCh38 row, one nearly all GRCh38 — the filter would have stopped meaning anything — one with more than one row in a thousand that cannot be read, or one whose GRCh38 count moved more than 20% from the file served, is refused, however it arrives, and the last good file stays served,
ETagincluded. - The exact strings and the ten groups, both.
classification_exactis uncapped;classification_groupedalways carries all ten groups, a group with no rows as0, and ClinVar's-is inNot provided, neverOther. Review statusno classification providedis one row, where the file this replaces published two with one key. - One walk, one file. The cursor of either collection names the file the walk started on; a page asked for after the next weekly file has landed is a
400saying so, never that file's rows appended to the pages you hold. - Read once a week, and a
304in between. The md5 NCBI publishes beside the file is checked every morning and the file read only when it changes. Each document'ssourcenames the file by that md5 and by itsLast-Modified, verbatim, with the weekly release it was written for — the release/v1/genome/releases/clinvarnames every morning. Send theETagback asIf-None-Matchand the answer is a304, not billed, until ClinVar writes the next file. GET /v1/sourceslists a third genome source,clinvar_summary, whoserecordsare the genes of the file served.- BioSamples by country:
GET /v1/genome/biosample-geography. How many of NCBI BioSample's records come from each of seven countries — Azerbaijan, Georgia, Armenia, Turkey, Kazakhstan, Uzbekistan and Iran — as one document: a country infocus, the others incomparison, and for each its organisms, packages and sample years, with how the count was made inmethodology. Metered like any data route, on every plan, under the Genome Data API's key. Reference. - A country is counted from its records, never from a search. A search for
geo_loc_name Georgiamatches the two words anywhere in a record, so it counts everyUSA: Georgia, and one for Azerbaijan countsIran: East Azerbaijan. The records the search returns are read instead — every one, or 6,000 spread over all of them where there are more — and only the text before the first colon of each one'sgeo_loc_nameis compared, exactly. The search's own count is served asreported_by_search— deliberately the wrong number, beside the right one, so you can see how far off it is — and never as a country's count. Each country is searched for and compared under one name and no other spelling: Turkey counts records spelled Turkey, and records using INSDC's current spelling, Turkiye — 2,864 on 2026-10-10, when Turkey was estimated at 38,633 — are not counted.methodology.name_rulesays so in the document. - An estimate says it is one. A country whose search counts more than 6,000 records is estimated from 6,000 of them, one from the middle of each equal stretch of every id the search returns, and its
estimated_totalis that sample's match rate times the search count, withis_estimate: true. Not the first 6,000: a country's records are deposited in batches that sit together in NCBI's order, and on the day this shipped the first 6,000 put Georgia at 86,073 where two spread samples agreed on about 22,000. Where every record was read,estimated_totalis the exact count. Show the flag: an estimate and an exact count must not look the same. - Only the seven are served, and none is looked up while you wait.
?focus=and?compare=take any of the seven, in any case; any other country is a404that names them. A country is up to forty requests to NCBI against a rate limit every request from this platform shares, so the seven are counted weekly, at night in the US, and served from what was counted. - Viral sequences and SRA runs:
GET /v1/genome/sequencing-activity. 63 counts read each morning from NCBI Nucleotide and the Sequence Read Archive — ten viruses by taxonomy id with their complete genomes, viral sequences by year since 2015, and SRA runs in total, by strategy, by five named platforms and by year since 2010 — counts only, never a record. The five are the ones the file this replaces named, not every platform: DNBSEQ, the name BGISEQ's line is sold under now, is its own platform in SRA and is not among them.complete_genomesundercounts segmented viruses by orders of magnitude, so Influenza A and Rotavirus A carrysegmented: true; and the years are publication years, when a record entered the archive, not when its sample was collected. Reference. - A run lands whole or not at all. A country NCBI answered short, a document that fails its own checks, or a country whose records without a place jump past a quarter where the run served had them under one in twenty — how a reading that stopped finding the attribute shows — keeps the whole run out, and the last good document stays served — with
source.stale: trueonce the read that should have replaced it is half a day late, a day and a half after the sequencing counts were read and seven and a half days after BioSample was. These are counts of NCBI's records and tallies of three of their fields; NCBI places no restrictions on their use or distribution. GET /v1/sourceslists a fourth genome source,ncbi_eutils, whoserecordsare the rows the two documents serve: one sequencing document and the seven countries.- NCBI Taxonomy's backbone, as one document:
GET /v1/genome/taxonomy/backbone. Root and every node of NCBI's tree ranked domain down to family — about 14,800 — each parented onto its nearest ancestor among them, with the taxa ranked species and all the taxa beneath it in the whole tree, sorted byspecies_countdescending and thentax_idascending.?depth=classcuts it at class, about 1,020 nodes. One metered request either way, on every plan, and the document moves about once a week: send itsETagback asIf-None-Matchand you get a304, not billed, until a newer dump has landed whole. Reference. - Every parent resolves, every node is named, no count is negative. A dump's backbone is served only when it landed whole — exactly the nodes its read counted, root among them, every parent present — and a dump with a backbone node NCBI gives no scientific name is refused rather than served with a hole in the tree.
totals.by_rankcounts 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, so the two differ there on purpose. - Any taxon's lineage, to root:
GET /v1/genome/taxonomy/lineage. Up to 100 tax ids in?tax_id=— a species, a strain, an unranked clade — each answered with every ancestor in NCBI's own chain, the parent first and root last: 9606 walks 31 steps, through Primates, Mammalia, Chordata, Metazoa and Eukaryota. An id NCBI does not hold is named inmeta.not_found. The walk runs through the whole tree, three million taxa, which is what an assembly roll-up needs and what the backbone's own links, which stop at family, cannot do. Each answer names thedumpit was walked in, andmixed_dumpssays when a walk crossed part of a newer dump still landing — ask again later for a lineage of one dump. Reference. - The whole tree is not published as a file. The lineage lookup answers it a taxon at a time, and anyone who needs all three million edges at once is better served by NCBI's own monthly archive of the dump, kept since 2014; the backbone names the dump it was cut from by the md5 NCBI published for it, in
source.checksum. - Read from NCBI's taxdump about weekly, its md5 checked every day — the
ncbi_taxonomyrow of/v1/genome/releasesstill names the one NCBI publishes today. NCBI places no restrictions on the use or distribution of its taxonomy;source.attributionis the line to keep with it.GET /v1/sourceslists a fifth genome source,ncbi_taxdump, whoserecordsare the backbone's nodes. - GTEx's median expression of a gene, tissue by tissue:
GET /v1/genome/expression/{symbol}. One gene's median TPM in each of a GTEx release's 54 tissues, sorted bymediandescending and thentissue_id, so the first is the tissue the gene is most expressed in, each labelled with GTEx's tissue id with its underscores turned to spaces —Whole_BloodisWhole Blood, its case as GTEx writes it.?dataset=picks the release:gtex_v10, the default and the release GTEx's own API serves, quantified against GENCODE v39, orgtex_v8, against GENCODE v26. A symbol is matched in any case, and a GENCODE gene id, with or without its version, works too. Every median is GTEx's own, read whole from the release's median file in GTEx's open bucket. Reference. - A gene the release does not name is a
200, not a404. A well-formed symbol the release's GENCODE does not hold answersdata: null, withmeta.reasonnot_in_referenceandmeta.found_innaming the other release that does: SEPTIN7 asked ofgtex_v8, whose GENCODE still calls it SEPT7, is found ingtex_v10. So a page can say the gene is not in the reference rather than that something went wrong. It is the one read by name on this API whose miss is not a404; something that is no gene symbol or id at all is a400, and a release a deployment has not loaded yet is a404. - A release's tissues on their own:
GET /v1/genome/expression/tissues. The 54 tissues a gene's answer is given in, intissue_idorder, each with its label, GTEx's display name, its tissue site, its UBERON or EFO term, the colour GTEx draws it in and how many RNA-seq samples the release has of it — the axis a chart labels before it asks for a gene. Reference. - Which release, and whether a newer one exists. GTEx's licence asks anyone passing its data on to keep the most current version or to say clearly that they do not, so every answer carries
meta.newest_release, the newest release GTEx publishes, andmeta.release_notice, a sentence saying the answer is not GTEx's most current data whenever it is not — on every answer today, because GTEx has published v11 for download and its own API does not serve it yet.meta.attributionis the acknowledgement the licence asks you to keep with the data: the GTEx Portal, the date the file was read and the dbGaP accession. A release changes only when GTEx replaces its file, so send theETagback asIf-None-Matchand you get a304, not billed. A lookup was answered in 5 ms at the 95th percentile inside the server on a copy of both releases. - The
gtexrow joins/v1/genome/releases, which now holds eleven rows: it namesgtex_v10, the newest release GTEx's API serves, and itsextralists every dataset that API serves with its GENCODE version, genome build and tissue count, beside GTEx's own list of releases for download.GET /v1/sourceslists a sixth genome source,gtex_expression, whoserecordsare the genes of each release's served load — 115,233 for the two. - NCBI's genome assemblies, counted onto the taxonomy's backbone:
GET /v1/genome/assemblies/coverage. Every backbone node with GenBank'slatestassemblies anywhere beneath it — how many are reference genomes, representative genomes, annotated and complete genomes, how many distinct species they cover and the newest year one was released — and RefSeq's own count beside them inrefseq_assemblies. Never add the two: a RefSeq assembly is NCBI's curated copy of a GenBank submission, not another one. The default is the top of the tree, root and every node ranked domain down to class, about 1,020;?rank=allis every node down to family, about 14,800. One metered request either way. Reference. - The totals and root differ on purpose.
totals.genbank_assembliesis the GenBank table'slatestrows — 3,820,648 on 2026-10-10 — and root'sassembliesis the roll-up, short by exactlysource.genbank.unresolved: the rows whose tax id the taxonomy does not hold, merged, deleted or newer than the dump, 239 that day. Both are served, and the same of RefSeq. A read whose root does not come to exactly that, or with a node carrying more species with an assembly than the taxonomy has species, or more reference assemblies than assemblies, is never served. - Genome size, gene count and GC, one assembly per species:
GET /v1/genome/assemblies/genome-size. Each species' richest assembly — a reference genome over a representative one over the rest — for every species with both a genome size and a gene count, absent rather than zero otherwise. A group over 2,500 species ships every reference genome and an evenly spaced sample of the rest across their sizes, the same sample from the same tables, andper_groupsays how many each group holds in all.?group=takes one of NCBI's groups. Reference. - The organisms with the most assemblies, and assemblies by year:
GET /v1/genome/assemblies/top-organismsand/timeline. The 300 organism names with the most GenBank assemblies, keyed by the name as NCBI writes it, and GenBank's assemblies per release year from 1990, per year rather than cumulative. Reference. - Every column is found by its name in each table's header, and a table missing one is refused by name rather than read off by position. The tables are read weekly, 246 MB gzipped, on a Sunday, and a read whose
latestrows moved more than 20% from the read served waits for a person to look. Each document carries both tables byLast-Modifiedand the taxdump by md5 insource; send itsETagback asIf-None-Matchand you get a304, not billed, until the next read lands. Before the first, each answers404. NCBI places no restrictions on the use or distribution of these counts;source.attributionis the line to keep with them.GET /v1/sourceslists a seventh genome source,ncbi_assemblies, whoserecordsare the backbone nodes of the read served. - The GWAS Catalog's associations, counted: five endpoints under
/v1/genome/gwas.GET /v1/genome/gwas/overviewholds the totals, associations and studies per year and associations per p-value band;/chromosomesassociations and distinct variants per chromosome;/traitsevery mapped trait with its associations, studies, variants and genes;/traits/{id}one trait with its forty most-associated variants and genes; and/genesevery mapped gene by associations, or up to 100 of them named in?symbols=. All five are read from one parse of one release of the NHGRI-EBI GWAS Catalog — release 2026-10-03 today: 1,198,394 associations, 76,336 studies, 438,157 distinct variants, 16,861 traits and 23,426 genes. Metered like any data route, on every plan. Reference. - An association is not a cause, and every answer says so. The
sourceblock on every document, andmeta.sourceon every GWAS answer, carries the note “A GWAS association is a statistical relationship in a study population; it does not establish that the variant causes the trait.” Keep it beside any figure you republish. The same block carries theattribution, theterms_urland thecitationEBI asks for: the catalog's contents come under the EMBL-EBI Terms of Use, which add no restriction on redistribution and expect attribution. - A trait is a label paired with its ontology term by position, so a label with a comma in it is one trait. The pipeline this replaces split
MAPPED_TRAITon commas, soosteoarthritis, kneebecame two traits,osteoarthritisandknee, and both got the row's first term. Here the row's term list says how many traits it names, and each label is paired with the term in its position; where the label text does not split into that many parts, each term's label is the one the release gives it elsewhere. No two traits share anefo_id, andtrait_pairingon the overview counts how the rows were paired. A trait'sslugis its label as a path segment,body-height, and/traits/{id}takes the slug or the ontology id; a slug holds within a release, not across them, so where a trait has anefo_id, store that. - A release is read whole and checked, or not served. The zip is read from the release's dated directory, every column is found by its header name, and the rows must number exactly what EBI's stats endpoint says the release holds — 1,198,394 of 1,198,394 on 2026-10-03. A file that fails a check posts nothing, and a release is served only once every trait it names has landed; until then the previous release stays served, whole.
sourcenames the zip's sha256, its member's CRC and both counts. - The p-value bands keep their labels, and the reference says where their edges are. The five labels are written as the file this replaces wrote them, byte for byte, always all five, strongest first, a band with no rows at 0. Their edges are at a
PVALUE_MLOGof 100, 50, 20 and 8, so the fourth band,1e-20 – 5e-8, ends at p = 1e-8, and an association at p = 3e-8 is countedweaker than 5e-8— 89,036 rows on 2026-10-03 lie between the two. - Traits page in any of three orders.
?order=takes-associations, the default,-studiesor-variants, or the same without the-for fewest first, and a tie goes to the slug; the top fifteen by any of them is one request. Genes page busiest first, each symbol exactly asMAPPED_GENEwrites it, because a join to NCBI's gene symbols is by exact string, and a row naming one symbol twice credits it once;limit=30000returns every gene in one request. A cursor carries the release its walk began on: a page asked for after the next release is served is a400saying so, never that release's rows appended to yours. - Checked every morning, downloaded once a release. The daily genome run at
04:10UTC asks EBI's stats endpoint which release is current and reads the 73 MB zip only when it names a release other than the one served — every two or three weeks lately. Between releases nothing changes,ETagincluded, so a conditionalGETanswers304, which is not billed.meta.upstream_releaseis the release EBI names today, so a release we have not read yet shows on any answer. GET /v1/sourceslists a sixth genome source,gwas_catalog, whoserecordsare the 16,861 traits of the served release.GET /v1/cyber/epss/snapshotspends no credit. It is the receipt of the file the scores you looked up were read from — its URL, checksum, size and row count — and reading it beside every lookup was charging you twice for one answer. It still needs a key whose plan includes cyber, a plan without it is still a403, and the per-second rate limit still applies; it no longer counts against your month's allowance, sends noX-Quota-*headers, and answers after the allowance is spent. An unmetered operation says"unmetered": truein/v1/schema, and Rate limits & quotas lists them.- Four reference collections page above 100.
GET /v1/cyber/kevtakeslimitup to 3,000 — the whole current catalog, 1,739 vulnerabilities on 2026-10-08, in one request rather than eighteen —GET /v1/bibliometrics/country-yearsup to 5,000, a whole OpenAlex release of 3,907 rows in one rather than forty,GET /v1/genome/clinvar/genesup to 30,000, every gene of ClinVar's file — 24,031 on the 2026-10-05 file — in one rather than 241, andGET /v1/genome/gwas/genesup to 30,000 as well, every gene the GWAS Catalog maps — 23,426 in release 2026-10-03 — in one rather than 235. Each publishes its cap asmax_page_sizeon its operation in/v1/schema, and its reference page says how long the cap holds the whole set. The default stays 25, alimitabove the cap is a400naming it, andmeta.nextisnullwhen one page held everything. Every other collection, jobs and events among them, stays at 100; the list is generated from the schema. - Lowering a collection's cap, or starting to bill a route documented as unmetered, is now in What counts as breaking.
- Earthquakes under
/v1/seismic: USGS ComCat for the window latitude 35 to 45, longitude 40 to 55. Every event the ANSS Comprehensive Earthquake Catalog holds there — 3,813 from 1905 on, and seven ComCat deleted — mirrored at minute 40 of every hour but one. Three endpoints,GET /v1/seismic/quakes,GET /v1/seismic/quakes/{id}andGET /v1/seismic/quakes/snapshot, on the same key and the sameseismicdataset as the fault traces, at most 100 events a page. On the free tier. Reference. - The sync cursor is when ComCat last changed an event. ComCat revises events of any age — a magnitude, a depth, an epicentre — so
updated_sincefilters on ComCat's ownupdated, strictly after the instant you send, and walks oldest change first by default. A 1990 earthquake revised today comes back for today. Send the newestsource_updated_atyou hold, less an hour or two, and upsert byid: an event revised during your walk is met again at its end, as its new version. - Every event carries ComCat's own feature.
rawis the feature's properties and geometry byte for byte, and the thirty normalized fields beside it — the names and units the issue that asked for this collection lists — are read out of it:origin_timeandsource_updated_atas RFC 3339 with exactly three fractional digits, the comma-wrappedids,sourcesandtypesas arrays,tsunamias a boolean. Nopayload_hashis served; hashrawif you keep one.include=detailadds the six origin-product fields of the event's detail document, read once per version of every event of magnitude 4 or more, with a PAGER alert or with felt reports. - The window is what is answered. A box that reaches outside it is a
400naming it, never an empty page that reads as “no earthquakes there”, and before a deployment's first full read every route answers503.meta.last_poll_aton every answer is when ComCat was last read successfully, and the snapshot carries the source block, the windows and the newest poll whatever it found. - Licences by contributing network. USGS's own events are U.S. public domain; the ISC-GEM catalogue's, 300 of them in the window, are CC BY-SA 3.0. Each event's
licencesays which, and the snapshot carries the attribution each asks for. GET /v1/sourceslists theusgs_comcatsource underseismic. Itsrecordscounts the earthquakes held, deleted ones included, and never the receipt each hourly poll records.- The World Bank's indicators as country context:
GET /v1/worldbank/context. One record per country with the newest value the World Bank publishes of each indicator, as{value, year}under the indicator's field —population,gdp_per_capita_usd,tertiary_enrollment_pctand seven more by default, any served one by?indicators=— keyed by ISO3, never by a name. Each indicator carries its own year, values are unrounded with integers kept integers, a gross enrolment ratio above 100 is not clamped, and an indicator the World Bank has no value of is an absent key, never a null. An indicator this API has not fetched yet is never answered that way: the request is a503naming it. These are general country context, not a migration dataset. - A country the World Bank does not report is an answer, not an error.
?iso3=takes up to 100 ISO 3166-1 alpha-3 codes and answers every one in one response, in the order named: Réunion, the Holy See, Tokelau and Taiwan come back as empty records withwdi_economyfalse. The World Bank answers those and a code that is no country at all with the same error, so this API tells them apart against ISO 3166-1:ZZZis a400, and so is an aggregate such asWLD, whose series stay on the series route. Withoutiso3the route walks every economy, a page at a time. - The indicator catalogue:
GET /v1/worldbank/indicators. Every served code with its field, WDI's own name and definition, the unit, the release held, the licence its series was fetched under, anddeprecated— the World Bank's own statement that it has moved a code to the WDI Database Archives. A deprecated code's series stays served, dated by its release, and is no longer refreshed, soindicators_behindonGET /v1/worldbank/snapshotdoes not list it: it will never catch up. - The terms and the release ride on every answer that serves WDI's values.
/v1/worldbank/context,/v1/worldbank/indicatorsand/v1/worldbank/indicators/{code}carrymeta.source_name,meta.source_url,meta.licence,meta.licence_urlandmeta.attribution, andmeta.snapshot_datewithmeta.retrieved_at: the WDI release the values come from and when it was fetched — the oldest of them when an answer reads several series, so an answer is never dated newer than its stalest value. On the series route these keys are new beside the ones it had, and nothing it served before has changed. - A second source under
/v1/labour:/v1/labour/oews— the U.S. Bureau of Labor Statistics' Occupational Employment and Wage Statistics. Employment and the annual and hourly wage distribution of every occupation BLS estimates for the United States as a whole, across all industries, for each May reference year from May 2021 — 830 detailed occupations in May 2025, with the broad, minor and major groups and the all-occupations total beside them, 7,012 rows over the five years. Three endpoints:GET /v1/labour/oews/occupationsfor a year's rows,GET /v1/labour/oews/occupations/{soc_code}for one code, andGET /v1/labour/oews/yearsfor the years served and when BLS released each. - A withheld wage is
null, and the row says why. Where BLS withholds a figure it prints a marker instead — one for an estimate it does not publish, another for a wage at or above its top-code, $239,200 a year since May 2022. Every measure here is a JSON number ornull, never the marker, andmarkersnames the marker behind each null, so “at least $239,200” and “not published” are never read as the same thing. Five performers' occupations publish hourly wages only, so their annual wages are null on every row; May 2024 carries the top-code marker on 72 rows' 90th percentile, and May 2025 on none. - A year is served whole or not at all. A year is one BLS file, and its rows are counted against what the file holds before any of them is served: a year still landing is invisible, and a year that is not served is a
400naming the ones that are. A walk stays in the file it began in, so a corrected file landing while you page is met by your next walk. Before the first year has landed on a deployment every OEWS route answers503. - Detailed by default, one row per code. The default answer is the detailed occupations — nine pages at
limit=100— and?o_group=adds the other levels, each row naming its own. The codes are BLS's, hyphen included,15-1252, and a code BLS does not publish is a404on the by-code route, never an empty200.area_codeis the file's own,99for the United States. - Checked every Monday, without downloading what we hold. Each year's file is revalidated against its Last-Modified and BLS answers
304, so a quiet week posts a receipt and nothing else.GET /v1/labour/oews/yearsshows when each year was last confirmed and whether that confirmation isstale; itsreleased_atis BLS's own release date, from BLS's release feed. It spends no credit, so polling it to learn that nothing moved costs nothing; it still needs a key whose plan includes labour, and the per-second rate limit applies. - Public domain, and BLS's citation is on every row. The figures are a work of the U.S. Government, and BLS asks to be cited as the source. Every row carries
licenceand the citation,U.S. Bureau of Labor Statistics, Occupational Employment and Wage Statistics. GET /v1/sourceslists theoewssource underlabour, and gains three fields:provider,licenceandcitation, set where this list records a dataset's provenance —oewstoday — andnullelsewhere. Theoewsrow also carries BLS'snameandurlfor the dataset. An addition: no field changed meaning.- Every
GETthese pages show now answers as shown, on this API, and no endpoint moved to make it so — the examples did. Replayed one by one against production, several did not: the KEV pages sentcatalog_versiontogether withdate, which is the400those two parameters document, and pinned catalogs this API never held — they now pin2026.10.04, the first it stored; the film pages pinned a rebuild only a test stack held, and now pin20261007T082744Z; the Nobel list pages sentyeartogether withyear_fromandyear_to, and two by-id pages sentshape=upstreambeside an example of the derived row; the UN migrant stock pages sentkindtogether withlocation_code, and the aggregates page named a country; the EPSS page asked for two CVEs and showed the answer to a different pair. Examples on the older datasets whose rows had moved on since they were written show those rows as they are served today. Themetaprinted beside them was a template's, and what it says about the request is now what the request gets:meta.nextisnullwherever the request is answered in one page — 27 of the 34 collection pages, the labour employment page among them — and on every single object, rather than a cursor to a page that does not exist or nonextat all; and the two/v1/entitieslookups show therequested,answeredandnot_foundtheir requests are answered with, where they had shown EPSS's — two ids, one missing, and nonot_found, a key a lookup always sends. A collection example'scountis still the rows it shows, one.POST /v1/jobs/liveness, which noGETreplays, now shows its documented answer once, where it had been printed inside a second envelope. GET /v1/entities/countriesanswers the Azerbaijan SSR. Q131337, the inherited code the field reference gives as its example beside Q55, answered a row of nulls named inmeta.not_found: no laureate's record names it, and the daily run asked only about countries the laureates name. It now asks on every run about Q131337, and about Q227, Azerbaijan, and Q15180, the Soviet Union — the countries the film corpus's rules are written in — so Q131337 answersSUNfrom the Soviet Union, withiso3_sourceparent_countryand no alpha-2, from the first run after this release, at 14:10 UTC.- Corrections to what the reference said, and nothing about the data moved. On
epss,recordsinGET /v1/sourcesis not exactly the number of CVEs FIRST scores, as the 2026-09-21 entry said: a CVE FIRST has stopped scoring keeps its last score and is still answered — 385,010 rows on 2026-10-10 against the 384,993 in FIRST's file of the 9th — and such a score'sscore_atstays what it was, null included, because no later file re-scores it, which the 2026-10-07 entry and the field's description said every nightly run would. On the World Bank, the country list held 79 aggregates and 296 entries on 2026-10-09, not the 78 and 295 the 2026-10-07 entry gave; an aggregate'slending_typeisAggregates, as the World Bank files it, withlending_type_idnull;indicators_behindalso names a code whose own probe still answers with the release held, which a run reads on the first night the World Bank's cache catches up — so a series is not re-read the night the release stamp moves, as the freshness page said, but on the first night its own probe names the new release:SP.POP.TOTLtwo days after the 2026-10-08 release, andBX.TRF.PWKR.DT.GD.ZSnot yet. - Azerbaijan's 78 districts, as one boundary set:
GET /v1/climate/districts. One GeoJSON FeatureCollection — each district's code, name, geoBoundaries shape id, geodesic area and the terms of its outline, in the set's order — that every climate number over a district is summed over, starting with the built-up surface below. It is a public rebuild, not any consumer's own district file: geoBoundaries gbOpen AZE ADM2 at commit9469f09less a stray second Lankaran District, Naftalan from OpenStreetMap relation 3812595 at version 20, and the enclave's hole moved in Goranboy to match;inputsandoperationssay so, and no byte parity with another file is claimed. The outlines are CC BY-SA 3.0, and ODbL 1.0 for Naftalan's ring — share-alike applies if you publish them. On the free tier. Reference. - The European Commission's GHSL built-up surface by district:
GET /v1/climate/ghsl/builtup/districts. GHS-BUILT-S, release R2023A, summed over those 78 districts for 2010 and 2020, in exactly the document the pipeline that asked for it reads —epochsas integers beside string-keyed totals, every district under every epoch — and exact to the square metre over the mosaic that pipeline built, each tile pixel counted wholly in one grid pixel as rasterio.merge places it: 722.19 km² in 2010 and 848.62 in 2020.metacarries the release, the eight tiles each epoch was read from with each one's URL, size and sha256, the analysis grid, the method that made every number and the digest of every input it read, and GHSL's CC BY 4.0 attribution on every answer. Reference. - And inside Baku's eight compass wedges:
GET /v1/climate/ghsl/builtup/zones?set=baku-sectors. 45° sectors within 45 km of the city's centre, which no sum of districts can rebuild — 194.30 km² in 2010 and 237.34 in 2020, and the wedge that grew most,NE. The raster grid itself, as a file, is not served yet. Reference. - A newer GHSL release is served beside R2023A, never over it. Every value changes between releases, so a request that names none keeps answering R2023A, and
releasenames another once it is adopted. The monthly climate run checks GHSL's listing and the eight tiles against their pins; a new release is noticed there and adopted by hand, or declined by hand and left unserved. R2023A computed again by a new method is different: it replaces R2023A's numbers in place, and says so —meta.method.version,meta.inputs_sha256and theETagall change with it. GET /v1/sourceslists two more climate sources:districts, whoserecordsare the 78 districts of the set it holds, andghsl_builtup, whoserecordsare its 156 district totals — never the monthly receipts or the wedges.
2026-10-07
- A new dataset:
/v1/entities— Wikidata countries and people, looked up by id. Two endpoints,GET /v1/entities/countriesandGET /v1/entities/people, each taking up to 200 Wikidata item ids in?ids=and answering one row per id, in the order sent. A country carries its ISO 3166-1 alpha-3 and alpha-2 codes; a person carries every country of citizenship, every institution they were educated at with that institution's own country, and their ORCID iD. Same key, and the envelope ofGET /v1/cyber/epss, the other lookup:meta.requested,meta.answeredandmeta.not_found, and no cursor. On the free tier. Reference. - Every id gets a row. An id this API holds nothing for comes back as a row of nulls and is named in
meta.not_found, so an id with no code is never confused with an id that went missing. These two are the only endpoints whosedatacarries rows for the ids innot_found, and the envelope reference now says so. A 201st distinct id is a400naming the count, never a truncation. - Inherited codes are marked.
iso3_sourceisdirectwhen the item records its own code,parent_countrywhen the codes are those of the country it sits in — named inparent_qid— and null when there are none. The Netherlands, Q55, records no code of its own and answersNLDfrom Q29999, the Kingdom of the Netherlands. - Read daily, seeded from the Nobel Prize API. Every laureate it lists, by the Wikidata id it gives them, and every country those answers name — 1,022 laureates and 159 countries on the first run. A row's
fetched_atis the newest read that returned it, and it moves every day the row is read again, changed or not. Until the first run has landed on a deployment, both lookups answer503rather than a page of nulls. GET /v1/sourceslists thewikidatasource a second time, under a newdatasetvalue,entities. Itsrecordscounts the countries and the people the two lookups can answer for together.- A new dataset:
/v1/films— the Azerbaijani film corpus, read from Wikidata. Every film Wikidata connects to Azerbaijan, with its release date, runtime, IMDb and TMDb ids, titles in six languages, types, genres, countries, languages, studios and six credit roles, every id already joined to its Azerbaijani and English label — 1,380 films in the rebuild served today. Three endpoints:GET /v1/filmsfor the corpus,GET /v1/films/{id}for one film, and/snapshotfor which rebuild is being served. Same envelope, same key, same cursor paging and the samelimitcap of 100 as every other collection — fourteen pages for the whole corpus. On the free tier. Reference. - Discovery is six published rules, and every film names the ones that found it. Most of the Soviet-era Baku studio's output is recorded with country of origin Soviet Union, so country of origin alone finds about a third of this corpus.
discovery_ruleslists every rule that found a film, from a closed set —country_az,country_azssr,studio_az,language_az,ussr_locationandussr_director— and the names are a contract: a film found only by the last two is weak evidence, and that decision is yours to make rather than ours to hide.?discovery_rule=filters on them. - IMDb and TMDb ids, under CC0.
imdb_idandtmdb_idare Wikidata's P345 and P4947, passed through exactly as Wikidata records them, and?imdb_id=and?tmdb_id=take lists of them. IMDb's own datasets are licensed for non-commercial use and TMDb's terms need a written agreement to redistribute, so neither is served here; these ids are Wikidata's, and Wikidata's structured data is public domain. - A rebuild is served whole or not at all. The corpus is read in full every week, and a rebuild is only served once every film it names has landed — until then the previous one stays served. A film absent from the newest rebuild was found by none of the six rules that week, which is an answer rather than an outage:
GET /v1/films/{id}says so with a404naming the rebuild, and?corpus_version=reads an older one. Until the first rebuild has landed on a deployment, the collection andGET /v1/films/{id}answer503rather than an empty page or a404, and/snapshotanswers404; the503names the snapshot as the place to look. - A walk stays in the rebuild it began in. The cursor carries the rebuild, so a sweep that spans a new one neither skips nor repeats a film.
fetched_aton every row is when that rebuild began reading Wikidata, never the time we served it. GET /v1/sourceslists thewikidatasource, anddatasettakes a new value,films. Itsrecordscounts the films of the newest complete rebuild — whatGET /v1/filmsserves by default — and not every rebuild we keep.- A new dataset:
/v1/migration/un-stock— UN DESA's International Migrant Stock 2024. How many people live in a country other than the one they were born in, at eight reference years from 1990 to 2024:GET /v1/migration/un-stock/destination-stockby country of destination,/origin-stockby country of origin,/bilateralorigin to destination,/populationfor the total each is a share of,/countriesand/aggregatesfor which location is which, and/metafor the revision being served. Same envelope, same key, same cursor paging and the samelimitcap of 100 as every other collection. On the free tier. Reference. - No row of UN DESA's workbooks says it is a country, so every row here says what it is. The workbooks interleave 234 countries and areas with UN DESA's own aggregates — World, the income and development groups, six regions and their subregions — and the column that looks as if it tells them apart is wrong three times.
kindis decided from a pinned ISO 3166-1 table:countrywhen the code is one, otherwisearea,group,region,subregionorother. The three tables serve countries unless asked otherwise, so a page never mixes World into a sum, and/aggregateslists everything the test leaves out with the hierarchy we rebuilt, so the classification can be checked rather than trusted. 232 of the 234 countries have a migrant stock; Saint Barthélemy and Saint Martin are listed withhas_data: falserather than dropped. - The region is UN DESA's own, verbatim.
regionis one of six strings in UN DESA's capitals —AFRICA,ASIA,EUROPE,LATIN AMERICA AND THE CARIBBEAN,NORTHERN AMERICA,OCEANIA— read from the order of the workbook's rows, because it is not a column of it, andsubregionis null for the five NORTHERN AMERICA countries because the workbook gives them none. Every country also carriesiso3, the key to join on rather than the name. - An empty page is always UN DESA's answer. UN DESA publishes the stock at eight reference years and at nothing between them, so
?year=2023is a400rather than an empty page that would read as “no migrants in 2023”; so is a location the revision does not list, an alpha-3 code ISO does not assign or UN DESA reports nothing under, a?source=other thanundesa, and a single-valued parameter sent twice. Before the first revision has landed on a deployment every route answers503. - The revision is a dimension. Every row carries
revision—2024— and?revision=picks one, defaulting to the newest held. Older revisions stay when a newer one lands, and a walk stays on the revision its first page came from. - A reported zero is not a missing pair. On
/bilateralamigrant_stockof0is a zero UN DESA published, and a pair with no row is one it does not publish;?min_stock=1drops the zeros. Figures are whole people as published and never rounded; population, which the workbook gives in thousands, is converted to people. - CC BY 3.0 IGO, and the attribution is on every row. Every sheet of every workbook states the licence, and every row and
/metacarry UN DESA's own suggested citation asattribution./metaalso lists the three workbooks with the sha256 each was parsed from. - Checked every Wednesday, and a new revision is never taken in on its own. UN DESA publishes about every four years. The weekly run asks whether the three workbooks have changed and whether UN DESA's page names a newer revision; either is recorded and reported, and nothing changes here until a person has read the new files.
newest_listedon/metasays what that page named at the last check. GET /v1/sourceslists theundesasource under a newdatasetvalue,migration. Itsrecordscounts the stock and population figures held across every revision, aggregates included, so it is larger than any default walk.first_seen_atreaches another dataset. Every UN stock collection carries it and is walked on it, because it does not move when a revision is stored again. The freshness page says which datasets carry it.- GenBank's growth since 1982:
GET /v1/genome/genbank/growth. One document holds every release in GenBank's release notes — 237 today, from release 3 in December 1982 to release 273 in August 2026 — with the bases and records of each of GenBank's four series wherever its table has the release: traditional records, Whole Genome Shotgun, Transcriptome Shotgun Assembly and Targeted Locus Study. The newest release with traditional figures, each series' coverage and definition, the months GenBank delivered no release, and the release the figures were read from travel in the same document, with the attribution to keep beside them. These are NCBI's own counts of its database, and NCBI places no restrictions on their use or distribution. Metered like any data route, on every plan. Reference. - A series GenBank did not report is absent, not zero. 92 releases predate Whole Genome Shotgun, and their rows carry no
wgs_*key at all.set_based_*sums only the set-based series that reported and isnullwhere none did, andcombined_*isnullwherever traditional figures are absent.*_unchangedappears only astrue, on the rows GenBank marks(unchanged). - All 237 releases, where the pipeline this replaces read 236. Its parser dropped every row carrying text after its figures — traditional 227, 235 and 267, each with a note explaining a decrease, and TLS 262 — and the interim release 57.1 of October 1988. Those rows are here with their figures and their notes, as
traditional_noteandtls_note, andreleaseis a JSON number:57.1on one row and an integer on every other. - The four tables are told apart by where they start. GenBank's notes give all four one header and nothing else to tell them apart, so a parse is accepted only when there are exactly four and they begin at releases 3, 129, 201 and 217 — or, for the TSA and TLS tables, earlier, because their notes say earlier releases will be back-filled; never before the table above them — and its rows agree with the totals the notes print for their own release. Anything else fails the run by name, and the last good document stays served.
series_definitionsnames where each series starts in the document you are reading, so a back-filled table is described as it is served. - The timeline only grows. Notes that no longer list a release already served, or that are older than the notes already served, are never stored, however they arrive: the previous document stays as it was,
ETagincluded, and the run that brought them fails. - One billed request a release, not one a day. The document changes only when GenBank cuts a release, about six times a year; send its
ETagback asIf-None-Matchand the answer is a304, which is not billed. Which release GenBank is on today is still thegenbankrow of/v1/genome/releases. - The dataset is now called the Genome Data API. Its slug, its plan gating and both existing endpoints are unchanged.
GET /v1/sourceslists a second genome source,genbank_growth, whoserecordsare its 237 releases. GET /v1/cyber/epss/snapshotsays how big FIRST's file was, how many scores it listed, and whether a day was missed.file_bytesis the size of the gzipped file as served — the byteschecksum_sha256is over — androw_countthe number of CVEs it scored. Both come from a receipt the daily run posts after its last score, so they arenullwhen a run stopped part-way, never numbers about a file we hold only part of.staleistruewhen a daily file was missed or landed only in part — our run failed or stopped part-way, or FIRST had not published — and some or all of the scores you are reading are a day behind, each still labelled with its day. It is measured from the newest day that landed whole rather than fromscore_date, which a run that stops part-way moves forward on some rows only, and it counts from the 02:00 UTC run, not from midnight, so it does not flip every night. Reference.- Every EPSS score carries
score_at, the instant FIRST stamps its file with —2026-10-06T12:00:23Z— in UTC, as FIRST writes it, on the scores and on the snapshot.score_datedoes not change: it is still the day, and still half the id. The instant comes from the same receipt as the file's size and count, so it isnullfor a score whose run stopped part-way, and for every score stored before this until the next nightly run, which re-scores every CVE. - A new dataset under
/v1/weather: ECMWF's IFS forecast for 72 Azerbaijani cities. ECMWF's open forecast is decoded out of GRIB and interpolated to each city's point here, four runs a day, so nothing on your side needs a GRIB decoder. Five endpoints —cities,forecast/hourly,forecast/daily,forecast/runsandsnapshot— on the same key and the samelimitcap of 100 as every other collection. On the free tier. Reference. - Every read is one run, and only a run that landed whole is served. The newest such run by default, or the one
?model_run=names. A paged walk that names none stays on the run it started on, even if a newer one lands mid-walk. A run is served only once every row its receipt lists has arrived, so a forecast missing half its cities is never the answer. - The units are the contract and none of them is normalised: wind in km/h, pressure in hPa, precipitation in mm per step, and an “hourly” collection that is 3-hourly to 144h and 6-hourly to 240h.
precipitation_probabilityis a step function of the precipitation, not a probability — a single deterministic run cannot give one — andvisibility,uv_index,uv_max,sunriseandsunsetare alwaysnull: this feed has none of them, and a null here is never filled with an estimate. - Run history is kept. Daily rows and each run's receipt are kept indefinitely and hourly points for 31 days, so
GET /v1/weather/forecast/runsanswers what every run held predicted for one city on one date — how a forecast moved as the day came closer. - Every weather response carries
meta.model_run, and the terms the data comes under.model_runis the run's base time, never a fetch time — the value to show as “last updated”.meta.licence,meta.attributionandmeta.source_urlare ECMWF's: the data is CC BY 4.0 plus ECMWF's terms of use, which ask that the attribution be displayed with anything built on it and that a modification be indicated. Reproducemeta.attributionand that is done. This is the second dataset here, after EPSS, that hands you an obligation along with the data. GET /v1/sourceslists theecmwf_ifssource, anddatasettakes the valueweather. Itsrecordscounts the hourly points held, every run's for the 31 days they are kept, so it is no single run's count: one run is 4,680 points, or 3,528 for a 06Z or 18Z run.- Where it differs from the pipeline it replaces.
ptypeis the code at the grid cell nearest the city, not a blend of the four around it, because a blend of codes is not a code: where a city's cells disagree, that pipeline's blend read as no precipitation type, sorainandweather_codeat those cities and steps can differ from the rows it produced. The city iscity, the sameslug_azit published ascity_id. And every daily value is taken from that day's hourly points as served, already rounded, so you can recompute it from them; a total taken before rounding can differ in its last digit. - A new dataset under
/v1/nobel: every laureate and every category-year prize slot the Nobel Prize API publishes. Five collections —laureates,prizes,awards,affiliationsandcountries, each with a route by id but affiliations — andGET /v1/nobel/snapshot, on the same envelope, the same key, the same cursor paging and the samelimitcap of 100 as every other collection. On the free tier. Reference. - The 49 slots no prize was awarded in are rows.
/v1/nobel/prizesis the upstream's whole category-year grid — five categories a year since 1901 and Economic Sciences since 1969, 682 slots through 2025 — read from its own prize catalogue rather than joined from the laureates, which is the join that silently deletes them.awardedistruewhen a slot names laureates and never read off a date, because seven of the 49 carry one, andtop_motivationsays what happened to the money. A slot whose prize has not been announced yet isawarded: nullrather than a gap: the upstream lists a year's six slots as its announcements begin, and on 2026-10-05 five of the six 2026 slots named nobody yet. - Two countries on every place, never collapsed.
countryis the label the data recorded at the time —USSR,Prussia,Austria-Hungary— andcountry_nowis the Foundation's own present-day mapping, with its Wikidata Q-id./v1/nobel/countriesis keyed on that Q-id, because the historical label never carries one upstream, and every historical label rides on the present-day row it maps to:?historical_label=USSRanswers Russia, Belarus and every other country a place recorded under it is in today. Nothing is dropped, and no successor state is chosen for you. - A refresh that cannot vouch for the catalogue posts nothing. A walk short of the upstream's own
meta.count, a prize grid with a hole in it, or an awarded slot whose shares do not sum to 1 within 0.02, and the run stops before it sends anything, so the previous catalogue keeps serving. A half-fetched catalogue that looks complete is what this exists to prevent: every count built on it is a share of a total. - Provenance is
GET /v1/nobel/snapshot, and the terms on it are the upstream's own. It names the version served by a hash over every record, when that version was first fetched and when a run last confirmed it, and theterms,licenseanddisclaimerURLs the API stated on that fetch — carried verbatim,nullif a fetch arrives without them, and never remembered here.fetched_atis on every row too, and it moves only when that row's content does. The data is CC0, and the terms encourage citing the Nobel Prize API as the source. ?shape=upstreamon laureates and prize slots answers each record as the API served it, in place of the derived row — the same JSON values, not the same bytes.GET /v1/sourceslists thenobel_apisource, anddatasettakes the valuenobel. Itsrecordsis laureates plus prize slots — the two catalogues it reads — so it is no single collection's count; the snapshot'sservedhas each.qualityisnull, as on every dataset but jobs.- A new dataset:
/v1/bibliometrics— OpenAlex's institutions summed by country and year. For each country and year, how many scholarly works OpenAlex attributes to that country's institutions and how many citations those works have: 3,907 rows over 231 countries and territories, 2010 to 2027, on the 2026-09-23 release. Two endpoints:GET /v1/bibliometrics/country-yearsfor the rows andGET /v1/bibliometrics/snapshotfor which release they were summed from. Same envelope, same key, same cursor paging and the samelimitcap of 100 as the other seven — so a whole release is about 40 requests, each one metered. On the free tier. Reference. - Exactly four fields on a row, and they are the contract.
country_code,year,works_countandcited_by_count, ordered by country and then year, so a walk reassembles into one array in that order. Whole counting: a work counts in full for the country of every institution it is attributed to, so the rows do not add up to a world total. Works and citations come from the same observation. The current, partial year is served, and so is OpenAlex's “forthcoming” year. No country list is applied: Taiwan, Kosovo and the dependencies OpenAlex reports are there. - A release is served whole or not at all. A sum missing some of its institutions is not missing data, it is a wrong number that reads as a fact about a country — so the scraper holds every file to the release manifest's bytes and record count before anything is posted, and the API serves a release only once every row it adds up to is here. A read that fails leaves the previous release serving. The newest four releases are kept,
?snapshot=names one, and a cursor walk stays on the release it started on even if a new one lands mid-walk. - The snapshot endpoint carries the header block, and what it adds.
institution_countcounts every institution read, the ones with no country included. Years outside 1990 to the year after the read are junk in the upstream — 10000, 9999 and 2025002 all occur — and are left out of every sum and counted indropped_out_of_range, 1,708 on the 2026-09-23 release.suspect_institutionslists the institution-years shaped like a mis-affiliated record: on that release the National Institute for Fusion Science carries 10.0M, 18.9M and 12.2M works for 2024 to 2026 against a few hundred a year before, which is 96% to 98% of Japan's rows for those years. They are listed beside the sums and never subtracted from them. - Read from OpenAlex's public snapshot, checked every Thursday. The snapshot is released quarterly, on the second Wednesday of January, April, July and October; the API it mirrors moves daily. So these numbers are one quarterly release's, the release is named on the snapshot endpoint, and a comparison against a walk of OpenAlex's own API on the same day will not match row for row — compare against the same release. The gap is not small: 13 days after the 2026-09-23 release, OpenAlex's API counted 7% to 9% more works than that release for every year from 2010 to 2023 on the 200 institutions of its first page, and 13% more works and 30% more citations for 2026. A week with no new release reads one 15 KB manifest and stores a receipt, so
last_checked_atmoves weekly andfetched_atdoes not. - OpenAlex dedicates its data to the public domain under CC0, so there is no attribution obligation to pass on.
GET /v1/sourceslists theopenalex_institutionssource, anddatasettakes a new value,bibliometrics. Itsrecordscounts the rows of the newest complete release — what a walk that names no?snapshot=returns — and not the older releases kept beside it.- An eighth dataset:
/v1/climate, opening with the World Bank's Climate Change Knowledge Portal.GET /v1/climate/cckp/climatologyserves eight 1991–2020 climatologies for Azerbaijan — mean temperature and precipitation, annual and monthly, from CRU's gridded station observations and from the ERA5 reanalysis — andGET /v1/climate/cckp/vocabularyis the portal's query grammar as a table. Same key, same envelope, same cursor paging and the samelimitcap of 100. On the free tier. Reference. - An empty series here is a finding about the query, never about the country. The portal answers
successwith no data for any query one code wrong —medianwhere an observational collection takesmean, a model code from another collection — and cannot say which, so a pipeline once read that answer as “no data for Azerbaijan” for months while the data was there. A series the portal answers with nothing is served anyway, withreasonsaying which kind of empty it is andresolved_querynaming the exact eleven-code query that produced it. And a 1991–2020 climatology does not change, so a later answer without values leaves the earlier ones standing, dated byretrieved_at. - Every filter is closed to the stored series.
?variable=tasmaxis a400namingprandtas, not an empty page:tasmaxis a real portal code, just not a series stored here, and an empty page would say there was no such data.?source=is closed the same way, tocckp, and each filter takes one value:?variable=tas&variable=pris a400too, rather than thetasrows withprdropped. The vocabulary lists every code and every combination we have run, including the ones that returned nothing. - CC BY 4.0, and the attribution is on every row.
licence,attribution(“World Bank Group”) andattribution_urltravel with each series, for the reason they travel with EPSS: the licence binds whoever republishes, and this API republishes to you. GET /v1/sourceslists thecckpsource, anddatasettakes an eighth value.recordsandlive_recordsare equal on it andqualityisnull, as on rates, genome and reachability: a series is not retired, and one the portal last answered with nothing is still a row we serve.- A new dataset:
/v1/geo— country and sea outlines from Natural Earth, served as whole GeoJSON documents.GET /v1/geo/countries?set=caucasus-caspiananswers the seven countries around the Caspian — Russia, Georgia, Azerbaijan, Turkey, Armenia, Iran and Turkmenistan, in that order — andGET /v1/geo/marine/caspian-seaanswers the sea. Same key, same envelope, on the free tier. Reference. - A document, not rows.
datais one object holding a whole GeoJSONFeatureCollectionand where it came from. There is no cursor and nolimit: a fraction of a map is not an answer, and the order of the features is part of the answer — take the first that contains a point and a point on a shared border belongs to whichever comes first. Full resolution, at Natural Earth's 1:10m, never simplified or clipped, which makes the country set about 1 MB. - Built from a pinned release, and the response says which.
sourcenames the rebuild that made the document,naturalearth;releaseis the Natural Earth tag —v5.1.2, the newest it has published — andupstream_filethe file it was cut from.sha256is over the document as built, with Python'sjson.dumpsdefaults, sojson.dumps(json.loads(feature_collection))reproduces it from what you receive. A rebuild of the same release changes neither the bytes norrebuilt_atnor theETag, so a client revalidating withIf-None-Matchkeeps getting304, which is not billed. - The names are fixed. Countries are chosen by ISO 3166 alpha-3 code and named from a table, so a rename upstream — Natural Earth still says
Turkey— does not reach you as a new string. Each country feature carries exactlynameandiso_a3; the sea is Natural Earth's own feature, verbatim, chosen by its Wikidata id. - Public domain.
licencesays so andattributioncarries Natural Earth's own short citation. Unlike EPSS's, it is offered rather than required: crediting Natural Earth is welcome and not a condition of using the data.GET /v1/sourceslists thenaturalearthsource under a newdatasetvalue,geo, and itsrecordscounts documents, not features. - Six more sources on
/v1/genome/releases, which now holds ten rows. GenBank, NCBI Taxonomy, ClinVar, the GWAS Catalog, gnomAD and NCBI Gene join RefSeq, dbSNP, 1000 Genomes/IGSR and NCBI Datasets, read by the same probe at04:40UTC. Same envelope, same fields, samestatusvocabulary: a new source is a row, not a route, so nothing about either endpoint moved. NCBI Gene is the one source the upstream registry never had, atsort_order11; GTEx, the registry's seventh, is not served yet. Reference. - Still release markers only. No ClinVar variant, gnomAD constraint score, GWAS association, taxon or gene record is served here, or downloaded to produce these rows: every read is a release-number file, a published digest, the first 8 KB of a file, a
HEADor a listing. checksumis filled for the first time, onncbi_taxonomy— and that row has norelease_version. NCBI rebuilds its taxonomy dump every day at one unversioned path and publishes no version for it, so the md5 it publishes beside the dump is the only release there is, and it moves daily.ncbi_genehas no published version either: itsrelease_versionis the day its file was last written,2026-10-03in the first probe, and it moves daily too — whether or not any gene in the file changed, because NCBI writes the file anew every day.- gnomAD's release is read from where its data is, not from its API.
release_versionis the newest release in gnomAD's public storage bucket that publishes constraint metrics —4.1.2— andextranames the four releases that do and the exact object, with its size, generation and md5. The API's dataset list,gnomad_r4and thirteen more, is inextra.api_datasetsand is not a release list: it has no way to name 4.1.2. - A read made beside the release never holds the release back. Two rows read more than their release: gnomAD's also reads its API's dataset list, and ClinVar's the md5 and the file date of its tab-delimited file. If one of those fails after the release was read, the row is still
okand its release still moves; the field that read fills isnull, andextra.api_datasets_error,extra.md5_errororextra.file_last_modified_errorsays why.extra.erroris only ever about the release itself. statusat its edges, on all ten rows. A redirect is never followed, and it now readsdegraded: the host answered, with something that is not the file. It used to readunreachable, which says the host never answered at all.extra.errornames where the redirect pointed, without its query string.erroralso covers a path the host'srobots.txtdisallows to our crawler by name, in which case nothing was requested. Andunreachablealso covers a source the day's probe ran out of time to reach: no request starts more than five minutes into a probe, so one slow host costs its own row and not the other nine.- The sources' own terms, for anyone going on to use their data. NCBI places no restrictions on the use or distribution of its data, though a submitter may hold rights in what it submitted, and NCBI grants no permission over those. gnomAD's primary data is CC0, but some of its annotations carry their own licences — SpliceAI's are for non-commercial use only — everyone using its data agrees not to try to re-identify participants, and its team asks for attribution and for its name to stay out of the names of tools built on it. The GWAS Catalog is provided under the EMBL-EBI Terms of Use, which expect attribution. A release marker carries none of that data, so no row carries a licence field.
GET /v1/sourcesstill lists one genome source,ncbi_ebi, and itsrecordsgo from 4 to 10.- A new dataset:
/v1/seismic/faults— active fault traces from the GEM Foundation's Global Active Faults Database. All 16,195 traces in the file at the pinned commit are served. Three endpoints,GET /v1/seismic/faults,GET /v1/seismic/faults/snapshotandGET /v1/seismic/faults/{id}, on the same envelope, the same key, the same cursor paging and the samelimitcap of 100 as every other collection. On the free tier. Reference. ?bbox=selects a trace when any one of its vertices is inside the box, and returns it whole. Edges included, never clipped and never rounded — the coordinates carry GEM's own digits, because distances get measured against them.bbox=40,35,55,45returns 348 traces, which is four pages atlimit=100, andcount=trueaddsmeta.totalso you can check you have them all. A box that crosses the antimeridian is a400rather than an empty page: send it as two.- An empty page always means an empty box. Everything else that could produce one is refused:
?source=takes onlygem_active_faults, any parameter given twice is a400rather than read for its first value, and before the first GEM file has landed on a deployment the collection andGET /v1/seismic/faults/{id}answer503— never an empty box, and never a404for an id you hold. Retry onceGET /v1/seismic/faults/snapshotanswers200. - The id is ours rather than GEM's, and this is the second collection whose ids carry no
<source>:prefix. GEM's owncatalog_idrepeats — 107 values occur more than once in the file — so a trace is keyed on the sha1 of<catalog_id>|<name>|<lon>,<lat>over its first vertex at four decimals, which is the key the dataset's first consumer already stores.id_keyon every row is the exact string that was hashed. Where two traces share that string — once in the whole file, in GEM's East African catalog — each is hashed with|1or|2appended, and neither takes the plain id. - GEM's
(preferred,low,high)strings are parsed into numbers, and the strings stay.slip_rate_mm_yr,dip_deg,rake_deg,upper_seis_depth_kmandlower_seis_depth_kmare the preferred values, each with a_lowand a_highbeside it, andupstream_propertiescarries GEM's own property bag verbatim. Every key is on every row,nullwhere GEM gives nothing. - CC BY-SA 4.0, and the terms are in the response.
GET /v1/seismic/faults/snapshotcarrieslicence, GEM'sattributionand thecitationGEM asks for, beside the commit, URL and sha256 of the file being served. Share-alike binds anyone who redistributes these traces or a dataset built from them, and the derived ids and parsed numbers are adaptations it covers too. - Not on a schedule. The file is pinned to one commit of GEM's repository and checked against its sha256 on every load, so it cannot change under you. A new commit is a deliberate change here, and it shows as the snapshot's
versionmoving — Data freshness says what to poll instead. GET /v1/sourceslists thegem_active_faultssource, anddatasettakes the valueseismic.recordsandlive_recordsare equal on it andqualityisnull, as on every dataset but jobs.GET /v1/cyber/kevserved an empty catalog for sixteen days. The daily collector was scheduled on 2026-09-21 and not one catalog landed in the sixteen days after it: the collection answered with no rows, and nothing on this API said why. The collector now reads the catalog from CISA's own copy on GitHub (cisagov/kev-data) whenever the feed does not answer us — the same document, byte for byte on the day this shipped, held to every check the feed is: no redirect followed, the envelope, a CVE id on every record, no CVE twice. Nothing about a row changed shape.GET /v1/cyber/kev/snapshot: which catalog is served, and where it came from. The version and the instant CISA released it, the URL the bytes were read from, thesha256of those bytes as served, CISA's owncountbeside the number of records the document actually listed and the rows we serve, and two timestamps:fetched_at, when we first fetched that catalog, andlast_checked_at, the newest run that read it whether or not it had changed. That last one is the field to watch — a catalog that has not moved and a collector that has stopped looking used to be the same silence.?catalog_version=and?date=select a past catalog as they do on the collection. The catalog is a US Government work that CISA distributes under CC0 1.0; the licence and attribution are in the response so you need not ask.GET /v1/cyber/kev/years: the CVE-id years one catalog lists.min_year,max_yearandyears, the sorted set of years present — not a range: no CVE assigned in 2003 is in the current catalog, so 2003 is absent. Read once here, with^CVE-(\d{4})-, so a consumer that loops over the years does not re-derive them. The same two selectors as the snapshot, and a catalog this API does not hold is a404rather than an empty list.- A correction to the
/v1/cyber/kevreference, and nothing about the endpoint moved. Its description saidordertakesingested_at. It never has:date_releasedis the one sort column, which the parameter table always said. Sendingorder=ingested_athas always been a400. - A new dataset:
/v1/labour/ilostat— the International Labour Organization's modelled estimates of employment by sex and occupation. The latest year of every series ILOSTAT publishes in that dataflow, for 189 countries and territories and 87 world, regional and income aggregates, in thousands of persons exactly as ILO publishes them — 7,452 figures in the snapshot served today. Four endpoints:GET /v1/labour/ilostat/employmentfor the figures,/areasand/isco-major-groupsfor ILO's two codelists, and/snapshotfor which load is being served. Same envelope, same key, same cursor paging and the samelimitcap of 100 as every other collection. On the free tier. Reference. - There is no occupation group 0, 6 or 9 in it, and that is the thing to know before writing code against it. ISCO-08 has ten major groups, and these estimates publish eight codes —
1to5,7,8and96— plusTOTAL.96is groups 6 and 9 combined, skilled agricultural with elementary occupations, and it is the largest group in 110 of the 189 countries: 40% of world employment, 43% of Azerbaijan's. A client that loops over 0 to 9 finds nothing under three of them and never asks for the one that matters, and nothing errors to say so.GET /v1/labour/ilostat/isco-major-groupssays what96holds and which codes are published, so a legend can be built from the data rather than from the standard. - The groups add up to the total, and a load where they do not is refused. For every area, sex and year the eight groups sum to
TOTALto within a tenth of a percent. The scraper checks that before anything is posted, and a load that fails it is not served — the last good one stays, and the snapshot endpoint says the check failed and why. Coverage is checked against ILO's own total rather than against the rows that arrived, because an answer that is short and internally consistent is exactly what a dropped code looks like. - Aggregates are out unless you ask for them.
X01is World, and a sum or a ranking over the default answer must never count it as a country, soinclude_aggregates=trueis how you get them, and naming one in?country=without it is a400that says so. Which areas are aggregates is read from two pieces of ILO's own metadata rather than from the code, becauseXA1does not fit the pattern the others do and one aggregate is not marked as a group by ILO's own annotation. - An empty page is always ILO's answer, and a walk stays in one load. Everything else that could produce an empty page is refused instead:
?source=takes onlyilostat,sourceorinclude_aggregatesgiven twice is a400rather than read for its first value, and before the first load has landed on a deployment the three collections answer503— retry onceGET /v1/labour/ilostat/snapshotanswers200. A walk is pinned to the load page 1 came from: if ILO's next load lands between two of your pages, the next page is a400that 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. - Every row says which year its country's figures reach.
reference_yearis 2025 for 184 countries and 2021 to 2023 for Lebanon, Palestine, Sudan, South Sudan and Ukraine, so a table of “latest figures” mixes years unless it shows that column beside the country. - Checked every Monday, and a week with nothing new posts nothing. The scraper asks ILO which load it is serving — ILO's own LAST_UPDATE — and when that is one we already hold, it records the check and stops.
GET /v1/labour/ilostat/snapshotshows both halves:source_last_updatefor the load you are reading,last_checked_atfor when we last confirmed it, andstalewhen the newest check failed or none has succeeded for nine days. - CC BY 4.0, and the attribution is on every row. ILO licenses its datasets under CC BY 4.0, which binds anyone who publishes them onward, and the attribution names the round of estimates —
Source: ILOSTAT, ILO modelled estimates, Nov. 2025— so it changes when the round does. Every row carrieslicenceandattributionfor that reason: a row copied out on its own still carries its terms. GET /v1/sourceslists theilostatsource under a newdatasetvalue,labour. Itsrecordscounts the employment rows of the load being served, aggregates included — see what the counts mean for why that is not what?source=ilostatreturns by default.- A new dataset:
/v1/worldbank— the World Bank's World Development Indicators.GET /v1/worldbank/countriesis the World Bank's country list,GET /v1/worldbank/indicators/{code}is one indicator's whole series — every economy, every year since 1960 — as rows of{iso3, country, year, value}, andGET /v1/worldbank/snapshotsays which WDI release is being served. Fifteen indicators to start, among them population, GDP and GDP per capita, R&D spending, unemployment and remittances. Same envelope, same key, same cursor paging and the samelimitcap of 100 as every other collection. On the free tier. Reference. - An aggregate is not a country, and its code will not tell you so. The World Bank lists 295 entries: 217 economies and 78 aggregates —
WLD,EUU,ARB,HIC— and every aggregate has a three-letter code exactly like a country's, so filtering on the code's shape keeps all of them. Every row says which it is inis_aggregate, read from the World Bank's own region field, and both collections leave the aggregates out unless you send?include_aggregates=true. With it, a series carries every aggregate the World Bank publishes values for — 47 onSP.POP.TOTL, among them the income groupsHIC,LIC,LMCandUMC, which it publishes with no ISO3 at all and which come back under the code its own country list gives them. Naming an aggregate in?countries=without the opt-in is a400, because an empty answer forWLDwould say the World has no data; so is naming one the World Bank publishes no values of that code for, or a code its list does not hold. - A short series is refused, never served as a whole one. On 2026-09-15 the World Bank's CDN served one indicator as twelve years out of sixty-six, with metadata that agreed with itself. Every series is now checked before it replaces the one held — its stated total against a separate probe, the full rectangle of every economy for every year, and its size against what is held — and a release that fails is refused while the previous release keeps being served. Every row carries
source_last_updated, the release it came from, andGET /v1/worldbank/snapshotlists any indicator that is behind inindicators_behind. - CC BY 4.0, and the attribution is in the response.
GET /v1/worldbank/snapshotcarrieslicence,attributionandlicence_terms. The last is there because the World Bank's CC BY is not the stock licence: it adds mandatory terms under which a dispute goes to non-binding mediation and then to arbitration under the UNCITRAL rules. And it is checked per series. The World Bank states a licence for every series, and not every series in WDI is open — military spending is under SIPRI's terms, CO2 per capita under CC BY-NC — so a series is stored only when its own metadata says CC BY 4.0, andindicator_licencesin the snapshot gives that statement for every served code. - Every World Bank filter takes one value except
countries, and?source=takes onlywdi: a repeated filter or another source is a400rather than half an answer or an empty page. GET /v1/sourceslists awdisource under a newdatasetvalue,worldbank. Itsrecordscounts the values held across every served indicator, the aggregates' included, so it is larger than what default walks return.first_seen_atreaches a third dataset. Both World Bank collections carry it and are walked on it, because it does not move when a release re-stores a series — a walk keyed oningested_atwould end early the moment a release landed mid-walk. The freshness page says which datasets carry it.company_logois an absolute URL ornull, which is what it was always documented as. It was the string each source's scraper read off a page or out of the source's API, unchecked, so some rows carried a path relative to the source's own site (/storage/companies/…), a file path out of somebody's repository, a site's ownnologo.png, or a query still HTML-escaped (?a=1&b=2) — and a client putting the field into JobPosting markup published those to search engines as the employer's logo. Every value is now cleaned on the way in, and every stored row has been cleaned once: a path is resolved only where its source is known to serve its uploads from a fixed address, and never against the posting's own page — on the rows that could be checked, that named a file the posting's site does not have; entities are decoded; a space in a file name is sent as%20; and anything that cannot be made an absolutehttporhttpsURL that way isnullrather than guessed at. Expect fewer logos on postings imported from the archive, and on one board's postings from before its site was rebuilt: those were paths that now answer 404.- An image that stands in for “no logo” is
nulltoo. A placeholder file —nologo.png,default-logo.png,placeholder.svg— and a board's own brand image on the postings of employers that have none: on one board that was 631 of 657 postings, naming 484 different companies. Expect fewer logos from those sources. Each one that remains is the employer's own. facts.company_logosays whatcompany_logosays. It echoed the string as stored, so a client falling back to it —company_logo ?? facts.company_logo— got the raw value straight back. Wherever it appears it is now the typed field's value, and it is absent where that isnull.- A cleaned row's
ingested_atdid not move, because nothing new was observed. A mirror pollingupdated_afterpicks the cleaned value up when the posting is next re-observed, which for an open posting is its source's next scheduled run. A retired posting's cleaned value reaches you only if you read it again.
On the last four entries and the 90 days. They are one correction, and of the data rather than of the documentation: the field was documented as an absolute URL, and what changed is the values that were not one, or were an image saying there is none. No field, type, parameter, default, status or nullability moved — company_logo was nullable and is null on more rows. If you were resolving relative values yourself, you no longer have to; if you were rendering one of the board images, it was never the employer's logo. If it broke something you depend on, say so.
The Cyber API adds stored CVE Program records, pinned source URLs, exact JSON bytes and per-ID acquisition errors. MITRE's notice and usage grant are included. CVE acquisition runs weekly alongside the existing daily KEV, EPSS and NVD sources.
The Geo API now includes stored Azerbaijan places, world cities and countries. Lookups answer caller keys, preserve approximate manual pins and return unresolved names with null coordinates. Rebuilds run on demand; every collection carries its licence and attribution.
2026-10-03
- A source that has stored nothing for 14 days has its postings retired, daily. Until now a posting turned
falseon its own only when its source delivered a complete scrape that did not carry it, so a source that stopped delivering altogether — a board that took its vacancy table down, an organisation that closed — never produced that run, and every posting it held stayedtrueindefinitely, as the field reference said. A daily pass now retires them: once a source has stored nothing for 14 days, every posting it still holds becomesfalse, with adeactivated_atof the day the pass ran — at most about fifteen days after the source's last delivery. The same retirement used to be run by hand, and the postings of a source that legitimately emptied went stale again between runs. A posting istrueagain on the next run that carries it. - That
falseis not a finding, and the response does not mark it. It says we stopped vouching for the posting, not that it closed — the board may be fine and our scraper broken. The source'slast_observed_atonGET /v1/sourcestells the two kinds apart, and the derivation already reads it first, so a client using it files these postings asunknown, notgone. A client readingactivealone will see them leave?active=true, andupdated_aftertells a mirror when, as it does for any retirement. - It refuses rather than guesses when the silence looks like ours. 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, where every source goes quiet at once — or when the postings it would retire are more than a tenth of everything live. Postings it declines to retire stay
trueuntil someone has looked.
On this entry and the 90 days. Nothing in it is in the breaking column: no field, type, parameter, default, status or nullability moved. But the first thing “stable” commits to is that a field will not change meaning inside v1, and what false covers did change — it stopped being only a finding. None of the notice exceptions covers that, so it is stated here rather than argued away. It went out without the 90 days on a judgement worth writing down so it can be argued with: the alternative was ninety more days of postings served as open vacancies from sources that had said nothing for weeks, and the same retirement was already being done by hand — most recently 313 postings on 2026-09-29. What changed is that it no longer waits for someone to remember. If it broke something you depend on, say so.
2026-09-27
GET /v1/sourcessays when a source last delivered, not only when it last ran:last_observed_at.last_ingested_atmoves whenever a batch is accepted, and accepted is not stored — a batch whose records carry no source id lands and adds nothing. Three job sources did that three times a day for five weeks, andlast_ingested_atcalled them current to the minute while their postings went unconfirmed.last_observed_atis the newestingested_atamong a source's rows, and it is the value to compare a posting'singested_atagainst; the worked example now uses it.stale_live_records: how many oflive_recordsnothing has re-confirmed in a week. A source that stops being scraped keeps its postingsactive: true— that has always been documented — andlive_recordswent on counting them: one board rebuilt on 2026-09-15 was still reported as 564 open vacancies twelve days later. This is the number that belongs beside it.0on a healthy source.- Both are
nulloff the jobs dataset, and both are additions: no field changed meaning, andrecords,live_recordsandlast_ingested_atsay exactly what they said yesterday.
2026-09-21
- A second feed under
/v1/cyber: FIRST's EPSS scores.GET /v1/cyber/epssanswers the probability that a CVE will be exploited in the next 30 days, for every published CVE FIRST scores — 377,238 of them on the day this shipped. It is the forward-looking half of what/v1/cyber/kevalready tells you: KEV is evidence that exploitation has happened, EPSS is an estimate that it will. Same key, same envelope, same free tier. Reference. - It is a lookup, not a walk, and that is the one place this API works differently. Send up to 100 CVE ids in
?cve=— comma-separated or as a repeated parameter — and get the score in force for each. There is nolimit, nocursorand nometa.next, because there is no page to turn: the corpus is rewritten nightly and walking it would be 3,773 requests for a 2.6 MB file that FIRST publishes free.GET /v1/cyber/epss/snapshothands you that file's URL, itsscore_dateand the instant FIRST stamped it with (score_at), itsmodel_version, thesha256and size (file_bytes) of its gzipped bytes as served and how many scores it listed (row_count), so you can verify what we served against what FIRST published — andstale, which says whether a daily file was missed or landed only in part. A 101st id is a400naming the count rather than a truncation — see the next item for why that matters. meta.not_foundis in the200, always,[]included. A CVE with no EPSS score is an ordinary answer: FIRST scores published CVEs and the corpus moves. What it must never be is a silently shorter array, because “there is genuinely no score for this” and “the lookup did not work” are different facts and they used to arrive identically. Read the list, turn a miss into your ownepss_missingflag, and do not diff your request against the response to guess. This is also why a 101st id is refused rather than dropped: the dropped ids would come back in that list, which would then be saying FIRST publishes no score for CVEs this API simply declined to look up.epssis a probability in[0, 1], not a percentage and not a severity.0.00042is four in ten thousand. It is served at the precision FIRST published — up to five decimals, unrounded, never padded, so0.5and0.00047both occur and neither is a rendering choice of ours. It says nothing about impact: a near-certain score on something unreachable from your network is still not your first job, and that is what the KEV catalog next door is for.percentileis the field to compare across days, because the raw score is re-scaled whenever the model is retrained — every row carries themodel_versionthat produced it.- Only the current score per CVE is kept, and that is a decision rather than a limitation. FIRST keeps every day's file at a stable dated URL forever, so a snapshot series here would be a slower, partial copy of an archive that is already complete, free and authoritative — the exact opposite of the KEV catalog, which CISA replaces in place and publishes no history of, which is why we keep every version of that one. Each score row carries its own
score_date, so you always know which day's model run you are reading. - EPSS is published under CC BY 4.0, and the attribution is in the response.
GET /v1/cyber/epss/snapshotcarrieslicenceandattributionas fields. That is not decoration: CC BY binds anyone who redistributes the work, this API redistributes it to you, and a credit line on a page you may never open does not pass that on. If you publish these scores, reproduce that string — it is FIRST's own and it is carried verbatim. No other dataset here carries those two fields, because no other one has an obligation to hand over. GET /v1/sourceslists a secondcybersource,epss. Unlikecisa_kev, itsrecordscount means what you would expect: one row per CVE, so it is the number of CVEs FIRST scores rather than a snapshot series added up.- A correction to
POST /v1/jobs/liveness's documented response, and nothing about that endpoint moved. Its example on this portal showed ameta.nextcursor. It has never emitted one — it answers every id you send, in one response, and there is no page after it — and the docs now show therequested/answeredenvelope it has always returned. If you wrote a paging loop against that example, it has been terminating on the first response by luck rather than by contract.
2026-09-20
- A seventh dataset:
/v1/cyber/kev— CISA's Known Exploited Vulnerabilities catalog. Every CVE the agency has evidence of active exploitation for, with the Binding Operational Directive action and deadline attached to each one. 1,716 entries in the catalog current today and all 1,716 are served — what we pull is what we publish, which is not true of every feed and is worth saying about this one. Two endpoints,GET /v1/cyber/kevandGET /v1/cyber/kev/{id}, on the same envelope, the same key, the same cursor paging and the samelimitcap of 100 as the other six. On the free tier. Reference. - What was actually missing here was the refresh, not the data. Nothing scheduled a re-fetch of this catalog, so a published copy stood at whatever version it had been read at. For most datasets that is staleness; here the rows carry
required_actionanddue_datestraight out of a BOD, so an unrefreshed copy serves a deadline that has already passed as though it were current. It is polled once a day now, at02:00UTC — after the American afternoon window CISA releases in, so an ordinary run picks up the same day's catalog. - Every published catalog version is kept, and the default is always the current one. A row is one CVE as one published catalog listed it, so its
idis<source>:<catalog version>:<CVE id>and it never changes meaning under you. Ask with nocatalog_versionand nodateand you get today's catalog and nothing else. Ask?date=2026-08-12and you get the catalog that was in force that day — which CISA's own feed cannot answer, because it is one document replaced in place, and which is the question an audit asks. Sending both is a400: they can name different snapshots and guessing is not an answer worth giving about a deadline. known_ransomware_campaign_useis CISA's ownKnownorUnknownand is deliberately not a boolean.Unknownis the agency saying it has no evidence of ransomware use — not that there is none — and publishing it asfalsewould put our reading out under their name.cweskeeps the same distinction:[]means classified with no weakness named,nullmeans the field was not carried, and 175 of the current 1,716 records are the first. Every upstream field is passed through unmodified, so a copy of CISA's JSON and a page of this collection diff cleanly.GET /v1/sourceslists thecisa_kevsource, anddatasettakes a seventh value.recordsandlive_recordsare equal on it andqualityisnull, for the same reasons they are on rates, genome and reachability — but note that the count is of rows we hold across every catalog version, not of vulnerabilities in the current one. The current catalog is what/v1/cyber/kevanswers with.401 missing_authorization— an eighth error code, andunauthorizedis not replaced. A401answered one code whether you sent no key at all or sent one we do not recognise — a configuration bug and a billing event, reported as the same string. The messages always said which; thecodedid not, and the code is the half that survives. A client that maps every non-2xxto its status keepserror.codein its logs and nothing else, so “I never sent a key” and “my key stopped working” were the same line to whoever read it afterwards. The status is unchanged for every case, and so is the code for a key that arrived and was rejected: if you branch onunauthorizedbecause a key of yours stopped working, nothing about your integration moves. What moved is the case where no readable credential reached us at all, which now says so.- The new code names the header rather than the key, deliberately. A proxy, an SDK or a redirect that rewrites headers produces a request that is indistinguishable here from one that never carried a key — a valid key whose
Authorizationheader did not survive a308cost a day of looking at the key, which was fine the whole time.missing_authorizationstates what we received rather than guessing why, so the first place it sends you is the transport between your client and us. You get it for noAuthorizationheader, for a header that is not a bearer scheme, and forBearerwith nothing after it; all three mean the same thing to you. - A revoked key still answers
unauthorized, and there is no code that says revoked. That is honesty rather than an omission. Revoking removes the key from the set requests are authenticated against, so by the time a request is refused a revoked key and one that never existed are the same fact — arevokedcode would be a guess published as a contract. The message says it may have been revoked for exactly that reason. Your dashboard settles it: a revoked key stays listed there, with the date it was revoked, so you can tell the two apart from your side even though the API cannot. - Nothing else about authentication changed, and no code was removed. This is the right-hand column — an error
codefor a case that used to be a generic failure — which it is only because the status stayed401for all three causes; moving one would have been the other column and the 90 days with it.
2026-09-15
- A fourth dataset:
/v1/rates, the Central Bank of Azerbaijan's official daily bulletin. 38 currencies and 4 bank metals against the manat — all 42 of them stored per document, with?codes=filtering on read. Three endpoints,GET /v1/rates,GET /v1/rates/latestandGET /v1/rates/{id}, on the same envelope, the same key, the same cursor paging and the samelimitcap of 100 as the other three. On the free tier. Reference. - Every row carries two dates, and that is the whole reason this dataset exists. The bank's XML is addressed by date and answers
200with a full, valid bulletin for every date string — Saturdays, public holidays, and dates that have not happened yet. Asking for Saturday 29.08.2026 returns Friday the 28th's numbers; asking for 01.01.2027 returns the newest bulletin there is. Nothing in the HTTP response says so. Sodateis the calendar date you asked about,effective_dateis the bulletin's ownValCurs Date=read out of the payload rather than inferred from the URL, andcarried_forwardis those two disagreeing. Passcarried_forward=falsefor a strict business-day series; take every row for a dense daily one and know which values are repeats. A mirror that keeps one date can express neither, and cannot tell a weekend from a fresh publication at all. rate_per_unitisvalue / nominal, unrounded — and it is almost certainly the number you want, notvalue. Seven currencies are published per 100: KRW, KZT, HUF, UZS, PKR, RUB and JPY. The rouble quotes as1.9579per hundred and is0.019579per rouble, and both figures are on the row, so nothing is rounded on the way to you and nothing has to be divided twice. The four bank metals are per troy ounce and say which they are:kindismetalandunitistroy_ounce.- Dates on this dataset are Asia/Baku. That is written down because it is a bug we inherited rather than a convention we chose: Azerbaijan is UTC+4 year-round with no daylight saving, so anything computing “today” in UTC is a day behind for the last four hours of every Baku day — and the pipeline this replaces did exactly that, silently, on every run started in the evening. Keyed on Baku here, and stated on every one of these three endpoints, so the next consumer does not re-derive it.
GET /v1/sourceslists thecbarsource, anddatasettakes a fourth value.recordsandlive_recordsare equal on it andqualityisnull, for the same reasons they are on events and quotes — the field table says which reason is which.- What this dataset does not carry, said here rather than discovered on the first sync. No
updated_afterand nofirst_seen_after; nofirst_seen_atand notimes_seen; no retirement pass, and noactive. A published bulletin is not withdrawn and a past date's numbers never move, so the watermark that means anything here isfrom— a filter on the bank's calendar, not on when we stored the row. Data freshness has the long version. - A fifth dataset:
/v1/genome/releases— which release each upstream genome data source is on, and when we last managed to read it. Four sources to start with: RefSeq, dbSNP, 1000 Genomes/IGSR and NCBI Datasets, probed once a day at04:40UTC and read from each source itself rather than hardcoded here. Two endpoints,GET /v1/genome/releasesandGET /v1/genome/releases/{id}, on the same envelope, the same key, the same cursor paging and the samelimitcap as the other four. On the free tier. Reference. - Release markers only — no sequence, no assembly, no annotation, no variant data, served here or held behind it. A row is one source's current release string, the date we last read it successfully, and how the last probe went. It is a provenance table rather than a genomics API, and it is published as one because four separate pipelines were each deriving that release string for themselves.
statussits besiderelease_version, and it carries the half a version number cannot. A release that has not moved in years and a probe that has been broken for two days are the same unchanged string otherwise — and both happen here: IGSR's newest data freeze really is20130502. Four values,ok,degraded(the host answered and the page no longer parses),unreachable(nothing answered at all) anderror(it answered, with a non-2xx), because a dead host and a changed template want different people. A failed probe keeps the last goodrelease_versionand movesstatusinstead, so compareretrieved_at, the last successful read, withingested_at, the last attempt, to see how old an answer is.- A sixth dataset:
/v1/reachability— whether a watched URL answered us, why not if it did not, and how long it has been that way. Six targets to start with, probed once a day at05:20UTC. No page content is read, stored or served — a status code is a fact about a request we made, which is why this is the one dataset here with nothing to license. Two endpoints,GET /v1/reachabilityandGET /v1/reachability/{id}, on the same envelope, the same key, the same cursor paging and the samelimitcap as the other five. On the free tier. Reference. reachable: falseis not a claim that a site is down, and the row says which it is.failure_classis a closed set of seven, andblocked_by_bot_protectionis separate fromhttp_erroron purpose: two of these six targets answer our client a bot-protection challenge page rather than the site, and we have measured that answer changing with nothing varying but the HTTP version negotiated — same client, same address, same minute. Every row also carriesvantage, the place the request left from, because the same URL has answered403to a datacenter address and200to a residential one on the same day.- Ids on this collection carry no
<source>:prefix, and it is the only place on this API where that is true.GET /v1/genome/releases/refseq, notncbi_ebi:refseq— which answers404. The row's identity is the upstream source, so the prefix would be a constant every caller had to strip to recoversource_id, which the same row already publishes. The database carries a unique constraint on that key, so the bare id resolves by rule rather than by coincidence. GET /v1/sourceslists thencbi_ebisource, anddatasettakes a fifth value.recordsandlive_recordsare equal on it andqualityisnull, for the same reasons they are on events, quotes and rates — nothing here is ever retired, and coverage percentages are questions about a vacancy.
2026-09-05
countryon every job row, and?country=on/v1/jobs. Both downstream consumers of this API are Azerbaijan-market job boards, and neither could express “only Azerbaijan” against it.citycould not substitute — it is deliberately an Azerbaijani-only closed vocabulary, so a posting in Warsaw is null there and no predicate asks for “any Azerbaijani city”.?location=could not either: it is an include match on free text, and most rows never name a country at all while the local spelling is Azərbaycan. So the geography constraint lived inside each scraper, which meant every source that could serve foreign roles had to be filtered forever and every consumer that wanted them was blocked. ISO 3166-1 alpha-2, and null means the source did not say — the same honesty rulecityfollows. It is set only where the source published a country, which is a minority of the corpus and fills as the fleet re-observes, so?country=AZis 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 on the country you get back rather than asking for one here.?country=refuses a code that is not a code, and names the right one when it can. This is the narrowing, and it is the one entry on this page that is not purely additive.?country=UKused to answer200with an empty page — andUKis not an ISO 3166-1 code at all; the United Kingdom isGB. An empty page for a mistyped code reads as “you hold nothing there”, which is exactly the sentence this filter exists to stop us saying by accident, and the caller has no way to tell that apart from a wrong string. SoUKis now a400that tells you the code isGB,ENis a400that says it is a language tag, and anything that is not two letters is a400naming what you sent — the same shape an unknown?source=has answered with since 2026-08-28. A code the standard does define but this corpus has no rows for still returns an empty page, because that answer is true — no posting carries it yet, and the column fills as sources re-observe. That is a different case fromZZ, which is refused at the door on the way in and so can never be stored at all; both answer200with nothing, and both are honest. One code per request, not a list.
On the second entry and the 90 days. Narrowing what a parameter accepts is in the breaking column, and this shipped with no notice at all — two days after ?country= itself went live on 2026-09-03, not in the same release. The basis is the third exception rather than the calendar: UK and EN are not in the 249-code vocabulary the column is constrained to, so no posting has ever carried either and no key can ever have received a row under one. What a caller loses is an empty page and what it gains is the reason. Had the parameter been live for weeks the argument would be the same but the risk would not, because by then someone would plausibly be branching on that empty page — which is why it was done now rather than queued.
2026-08-31
The same customer as the entry below, on the next two things they hit. One was a real defect on our side and had been for as long as the field has existed.
descriptionkeeps its paragraphs and its bullets. Every tag-stripper on the scraper side replaced an HTML tag with a space — right for an inline<strong>, wrong for everything else — so a body of twelve<li>bullets and three<p>paragraphs arrived as one flat line with no newline in it anywhere. It now converts block boundaries first:<br>to a newline,</p>and</div>to a blank line,<li>to a bulleted line, with at most one blank line between blocks as before. Three cosmetic defects reported alongside it are fixed indescriptionby the same pass: a bullet glued to its first word (•Proficiency), a list item that arrived with no bullet at all, and a space stranded before punctuation where an inline tag used to stand (Requirements :). Nothing about this field's meaning changed — if you were splitting this text yourself, you no longer have to; if you were rendering it as one paragraph, it will now break where the source broke it. (description_htmldid change in meaning, in the same release — see the next entry.) Rows fill as the fleet re-observes them, which is one cycle — eight hours — for anything still open. Postings whose source has already removed them keep the flat text they were stored with.source_urlon every job row, andurlandjobs_urlonGET /v1/sources. The response named a source asglorriand called itGlorri, and there was still nothing in it you could link to.source_urlis the source's own website —https://glorri.com,https://abb-bank.az— and it is what to pointsource_nameat./v1/sourcesaddsjobs_urlbeside it: the page we actually read that source's vacancies from, which is the better link if you are sending somebody to browse a source rather than to read about it. Set for 122 of 130 sources and for 106 respectively. Null rather than guessed on the rest: three sources live entirely on a third party's applicant-tracking system and their employer's own domain appears nowhere in our scrapers — theirjobs_urlstill points at the page the postings are on — one has only its ATS vendor's domain recorded against it, one is a catch-all bucket with no website, the 21 sources with no scraper keep a site with no recorded listings page, and three we have measured as gone lose both halves: a hosting placeholder that answers every path, an organisation that no longer exists and redirects to a different agency, and a decommissioned ATS tenant. Their rows stay; their link goes. A wrong link is worse than an absent one, because you would render it.description_htmlandrequirements_htmlare now populated on every row that has a body — they were null on 87% of them, because 79 of our 109 scrapers never see any HTML: the source publishes a plain-text body, or a listing card and no reachable detail page at all. Read the provenance note, because there are now two of them and the field does not tell you which you have. Where the source published markup — about 30 of 130 sources — it is still that source's own, passed through, and generated markup never replaces it. Where the source published only text, the markup is ours, generated fromdescription: paragraphs become<p>, a run of bullet lines becomes a real<ul>/<li>rather than a picture of one, and everything is escaped. It carries no informationdescriptiondoes not — what it gives you is one shape to render whichever scraper produced the row, and a<ul>that JSON-LD can actually recognise. It round-trips: converting it back to text returns thedescriptionit came from. StillGET /v1/jobs/{id}only, and still no backfill — live rows fill within one fleet cycle, and a posting whose source already removed it keeps its null, for the reason recorded when the columns were first published.- Two of the three markup defects reported alongside are fixed in
description_htmltoo, on the pass-through half: a block element holding nothing but a space or an is dropped, and a bullet glued to its first word gets the space back. The third is deliberately not. Inserting a bullet into a list item that has none would be writing content into a field whose entire value is that the source wrote it — and in markup it is usually not a defect at all, because a real<ul>draws its own markers. The plain-textdescriptiondoes supply one there, which is where that defect was actually visible. qualityonGET /v1/sourcesis now in the field table on this page. It has been in the response since 2026-08-23 and was never written down here.
2026-08-28
All of this came out of one customer's question: “I only use non-job-board vacancies, and most of them have no contact_email or description, so the real count is very small.” Two thirds of it turned out to be true, and the parts that were true were ours to fix.
source_type, and?source_type=on/v1/jobs. Every source is now classifiedemployer,platform,agency,board,aggregatororunknown, onGET /v1/sourcesand as a comma-separated filter. So “everything that did not come from a job board” is?source_type=employer,platform,agency— one request instead of the fifty?source=calls it used to take. This is our editorial judgement, not a fact the source asserts; the values can change when a source is re-read.live_recordsonGET /v1/sources, becauserecordswas answering a different question than people were asking it.recordscounts every posting in the current-state table, retired ones included — 149,383 rows, of which 39,046 were still open when this shipped. Two sources report thousands and return nothing at all, their scrapers having stopped months ago.live_recordsis what?active=truereturns.recordsis unchanged, and its description no longer claims a suppression filter it never applied.- An unknown
?source=is now a400.?source=abb,azercell,?source=ABBand any misspelled stem used to answer200with an empty array — indistinguishable from “that employer has nothing open”. The filter takes one stem and always has; now it says so. Usesource_typeto select several sources at once. contact_emailnow also picks up an application mailbox a posting publishes in its own body — 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, sohr.aliyeva@does not qualify; the domain is the posting's own; and the source's url identifies one organisation — a direct employer or a named agency, never a job board, a multi-tenant careers platform, or an ATS domain shared between employers. Together those mean a named individual's address cannot qualify and neither can a third party's. On employer career pages that took coverage from 2% to 28%; ten distinct mailboxes account for every row it filled. Nothing else is read out of advert prose, andcontact_phoneis unchanged — a number has no local part, so there is no equivalent way to tell a switchboard from someone's mobile.GET /v1/sourcesis now defined in the published contract, not just referenced by it. It spans three datasets, so it had no home in a schema organised by dataset — the endpoint was mentioned fifteen times in field descriptions and its own fields were documented nowhere,source_typeandlive_recordsincluded./v1/schemagains asharedsection for endpoints that belong to no single dataset and need no key. Field-by-field reference: whatGET /v1/sourcesgives you.- Two published claims were wrong and are corrected, with the old wording quoted rather than deleted. The
activedescription illustrated the “no verdict” problem withjobsite_azholding “over 12,000 postings, all stilltrue”. It holds 12,667 rows of which 1,199 are active; the rest were retired on 2026-08-12, before that sentence was written — a corpus total relabelled as an active count. Andurlwas documented as non-unique in two ways but never as unstable over time: onjobpilot_remoteoneidemitted 39 distinct urls over five weeks for one unchanged posting, withjobAgeincrementing 24 per scrape, so the url is guaranteed to differ every daily run. Key onid;urlis a link that moves. - A handful of
contact_emailvalues that predate the guards were removed. The two conditions above apply to everything this platform writes, but a small number of rows imported from the upstream board's database carried an address that board had extracted from advert text under different rules — including one belonging to a named individual. Anything whose local part is not a role name is now cleared, on the same rule the forward path applies. Twelve institutional mailboxes were unaffected. - Four published coverage figures were wrong and are now measured. Counted over a complete walk of every live posting rather than a sample:
cityis 25%, not “about 60% and climbing” — it fills only from the ~20 sources that publish a structured place, and it is not heading for 60%.descriptionis 92% (97% on non-board postings),requirements11% rather than 5%,contact_email~6% rather than 4%. The number that matters more than any of them: 19 in 20contact_emailrows are on four job boards, so filtering to direct employers and needing an address pull against each other.
2026-08-23
- Corrected what
activeclaims. Its description said the field told you whether the source was still publishing a posting; that is only true when the source has recently run, and for 15 of 96 sources it has not.trueis the default state and means “no verdict”. No behaviour changed — the documentation was overstating a field that always worked this way. Deciding liveness yourself is new, and is what to use instead. - Conditional requests. Every
200carries anETag;If-None-Matchgets you a304with no body, and no credit is charged. X-Quota-UsedandX-Quota-Remainingno longer count a request that was never billed. A404used to advertise a credit it did not take.
2026-08-22
410 gone— a seventh error code. A single-resource fetch for a record removed at someone's request now says so instead of answering404. Only for ids this platform actually held.GET /v1/sources— every source with its dataset, row count and last batch time. No key needed. (It gainedsource_typeandlive_recordson 2026-08-28.)salary_period—HOUR/DAY/WEEK/MONTH/YEAR, and null unless the source stated one.cityanddistrict, with?city=and?district=filters. Besidelocation, which is unchanged.description_htmlandrequirements_htmlonGET /v1/jobs/{id}only.work_type,company_logo,view_count,contact_email,contact_phonepromoted out offactsinto typed fields.- Acceptable-use policy published, at /docs/shared/use.
2026-08-21
activeanddeactivated_aton jobs. A posting whose source stopped publishing it is marked rather than deleted.?active=defaults toany— see Announced.updated_aftermatchesdeactivated_attoo, so a mirror is told when a posting dies.source_nameon every record — the source as a person would write it.- Pay and deadline filters:
has_salary,salary_from_at_least,salary_currency,deadline_after. updated_afteron/v1/events;slugon events.- Free plan raised from 50 to 1,000 requests a month.
- A credit is now one successful request —
4xxand5xxstopped being billed.
Before that
v1 has been serving jobs, events and quotes since March 2026. Changes before this log started were not tracked, which is the gap this page exists to close — everything from here is recorded.