{"name":"Tender API","authentication":"Authorization: Bearer <key>","keys":"Invited people request a key from the tender search page (API button); it is emailed to the invited address. Each person can hold 3 keys; ask the person who invited you to reset them.","your key can call":{"GET /v1/tenders/search?q=&country=&source=&awarded=&from=&to=&sort=&limit=&offset=":"Full-text search over EU, Canadian and US tender and award notices. q: words matched in title, buyer, winner and description (\"quoted\" for a phrase, at most 12). country: buyer country as ISO 3166 alpha-2, e.g. FR. source: ted, canadabuys or sam. awarded: yes or no. from, to: YYYY-MM-DD publication dates. sort: relevance (default with q) or newest. limit: 1–100 (default 20). offset: 0–1000.","GET /v1/tenders/{history_key}":"The tender dossier: the notice, every notice explicitly linked to it (contract notice, amendments, award notices; at most 100, two hops), all awards with winner, amount and currency, buyer contacts where published, category codes, deadline, procedure and estimated value. Percent-encode the key.","GET /v1/tenders/countries":"Buyer countries and their notice counts.","GET /v1/tenders/health":"Index coverage: status (complete, partial or not_built), notices, objects_indexed, objects_remaining, built_at.","GET /v1/tenders/schema":"Every view and column you can query in SQL, with types and descriptions, and how to search text. Read this before writing SQL.","GET /v1/tenders/sql?q=SELECT ...":"One read-only SELECT (or WITH ... SELECT) over the tenders views in Postgres; at most 1000 rows or 4 MiB, 5 seconds, 4000 characters. Full-text search uses ParadeDB: WHERE title ||| 'any words' (or &&& all words, ### exact phrase), or id @@@ pdb.parse('title:\"bridge\" OR description:\"bridge\"') ORDER BY pdb.score(id) DESC. Filter countries with 'FR' = ANY(countries). Always include a text match or id @@@ pdb.all() on notices so filters run inside the search index; a query that reads all 19 million rows times out (use the Parquet exports for whole-table analysis).","GET /v1/tender-history?key=<history_key>":"Every retained version of a notice and its linked notices with source evidence (object key and row)."},"responses":{"search":"{results: [{history_key, family, source, countries[], awarded, awarded_value: {amount, currency, awards} | null, title, buyer, winner, published_at, notice_type, url, award_date, deadline, value, currency, value_kind}], has_more, offset, limit}. Page with offset += limit while has_more. Several winners are joined with \" · \" in winner.","tender":"{key, tender, timeline: [notice + relation {basis, confidence}], awards: [{winner, winner_country, winner_id, winner_address, amount, currency, date, lot, contract_number, offers, notice, notice_url}], contacts: [{name, email, phone, notice}], truncated, limitations}. A notice has key, family, source, countries, title, buyer, buyer_address, published_at, notice_type, url, description, deadline, procedure, value, currency, value_kind (estimated or total), place, awards, contacts, categories.","sql":"{columns, rows, truncated}: truncated is true when more rows existed than were returned.","errors":"{error}: 400 invalid parameters or SQL (the message says which), 401 missing, unknown or revoked key, 403 the key may not call this (the search page key cannot run SQL), 404 unknown endpoint or tender, 422 the query runs too long (narrow it), 429 more than 60 requests a minute on this key, 503 search temporarily unavailable."},"sql_examples":["Most frequent winners in Germany since 2025: SELECT a.winner, count(*) AS awards FROM tenders.awards a JOIN tenders.notices n ON n.id = a.notice WHERE n.id @@@ pdb.all() AND 'DE' = ANY(n.countries) AND n.published_at >= '2025-01-01' AND a.winner <> '' GROUP BY 1 ORDER BY 2 DESC LIMIT 20","Largest US cybersecurity awards: SELECT a.winner, a.amount, n.title, n.key FROM tenders.awards a JOIN tenders.notices n ON n.id = a.notice WHERE n.id @@@ pdb.parse('title:\"cybersecurity\"') AND n.family = 'sam' AND a.currency = 'USD' ORDER BY a.amount DESC NULLS LAST LIMIT 20"],"bulk":"The whole database is exported daily as Parquet (tables/tenders/<generation>/…, latest in state/tables-tenders.json) for offline analysis with DuckDB or pandas; ask the operator for access.","limitations":["Search covers retained source records: a missing notice was not collected, which does not prove it does not exist.","Contacts are published only by CanadaBuys and SAM; TED notices give the buyer address. TED lists winners and one total value without pairing them, so TED awards have no amount; TED CSV awards are in euro.","Buyer country is the contracting authority’s country. CanadaBuys and SAM are federal portals, so their notices are Canada and the United States.","Notices published the same day under one notice id (for example TED lots) are one result listing every winner."],"operator keys only":{"GET /v1/objects?prefix=&cursor=&limit=100, GET|HEAD /v1/objects/{percent-encoded-key}":"List and download original source objects.","PUT /v1/objects/{percent-encoded-key}":"Admin. Create an object up to 16 MiB; existing keys return 409 except derived history/* shards. Update state/* with If-Match; stale versions return 412.","DELETE /v1/objects/{percent-encoded-key}":"Admin. Delete a derived Parquet export under tables/; everything else is refused.","GET /v1/access/people, PUT|DELETE /v1/access/people/{email}":"Admin. List, invite or revoke people; revoking ends all their keys.","POST /v1/access/keys {\"email\"}, DELETE /v1/access/keys {\"email\", \"key\"}":"Admin or issuer. Issue a key to an invited email (returned once; at most 3 each), or withdraw one that was never delivered."}}