UK public tenders as JSON or CSV: Find a Tender and Contracts Finder OCDS APIs, a Python example, and a ready-made dataset
How to read UK procurement notices from the two official OCDS APIs yourself, what trips people up (a broken stages filter, many releases per notice, rate limits), and when a normalized feed is worth paying for.
The short version
- The UK has two official notice services, each with a free, key-less API that returns OCDS (Open Contracting Data Standard) release packages as JSON: Find a Tender and Contracts Finder. Both are run by the Cabinet Office and licensed under the Open Government Licence v3.0.
- They differ in URL, filters, rate-limit behaviour and in how a notice is represented. Neither can filter by CPV, keyword or value, so you filter after download.
- If you want one flat schema across both, de-duplication and CPV labels without maintaining a script, we run an Apify Actor for that. Real samples are at the bottom.
1. The two APIs
| Find a Tender | Contracts Finder | |
|---|---|---|
| Endpoint | GET https://www.find-tender.service.gov.uk/api/1.0/ocdsReleasePackages | GET https://www.contractsfinder.service.gov.uk/Published/Notices/OCDS/Search |
| Docs | GET-ocdsReleasePackages | GET-Published-Notice-OCDS-Search |
| Date filter | updatedFrom, updatedTo as YYYY-MM-DDTHH:MM:SS | publishedFrom, publishedTo (ISO 8601) |
| Other filters | stages (planning, tender, award), but see below | stages (planning, tender, award, implementation), comma-separated |
| Page size | limit 1 to 100, default 100 | limit 1 to 100, default 100 |
| Next page | links.next (URL containing a cursor) | links.next (URL containing a cursor) |
| Single notice | append /104923-2026 (notice id) or an ocds-h6vhtk-... process id | not covered here |
| Rate limiting | documented: 429 or 503 with a Retry-After header | documented: 403, wait 5 minutes |
| Covers | Above-threshold procurement for the UK, plus other notice types since the Procurement Act 2023 regime | Below-threshold opportunities and awards of English buyers and central government |
Minimal requests (both anonymous):
curl -s "https://www.find-tender.service.gov.uk/api/1.0/ocdsReleasePackages?limit=2&updatedFrom=2026-11-04T00:00:00"
curl -s "https://www.contractsfinder.service.gov.uk/Published/Notices/OCDS/Search?stages=tender&limit=2&publishedFrom=2026-11-04T00:00:00"
Both return a release package with the same envelope: uri, version ("1.1"), extensions, publishedDate, publisher (Cabinet Office), license (the OGL v3.0 URL), publicationPolicy, releases and links. Releases come newest first.
Find a Tender: what we found
- The
stagesfilter is not usable for Procurement Act notices. In our test (releases updated since 3 November 2026, 100 returned)stages=tenderreturned 15 releases, none of them with a UK notice type, so it only matches notices of the older regime.stages=tender,awardreturned200with zero releases. Do not rely on it; filter client-side. - Every change is a release, so one notice appears many times. In that 100-release page the tags were: 49
award,contract, 21tender, 9awardUpdate,contractUpdate, 8planning, 8tenderUpdate, 3planningUpdate, 2tenderCancellation. Only 26 of the 100 were UK4 tender notices. - The notice type is in
tender.documents[].noticeType(UK4= tender notice). In our 100-release page, 71 releases had no notice type intender.documents(including old-regime ones), so do not assume it is always there. - Values: a tender release we inspected had only
amountGross(noamount) intender.valueand in the lot value. Handle both. - CPV codes are in
tender.items[].additionalClassificationswithscheme: "CPV"; the deadline istender.tenderPeriod.endDate; regions are NUTS codes (UKK12) initems[].deliveryAddresses[].region. - Party records contain contact points (names, e-mail, phone) in
parties[].contactPoint. If you store the raw JSON, decide whether you want that personal data. - Errors are readable:
limit=500gives400with "'limit' must not exceed the maximum value"; a malformed date gives "'updatedFrom' must be in the correct format". Unknown parameters are rejected with the list of allowed ones, according to the docs.
Contracts Finder: what we found
stagesworks server-side. Without it the feed is dominated by award notices (86 of the 100 releases we pulled from 4 November were award or award update), so always pass the stage you want.- Release ids look like
<notice uuid>-<version number>and theocidstartsocds-b5fd17-. The same notice appears once per edit, so keep the highest version per uuid. - The main CPV code is in
tender.classification, further codes intender.additionalClassifications. Value is intender.value(amount,currency), the deadline intender.tenderPeriod.endDate. - Errors use a different shape from Find a Tender:
{"code": 3200, "message": "Incorrect request [limit must be a number greater than 0 and maximum is 100]", ...}. - Rate limit: the docs say
403and to wait five minutes. During exploratory scanning we hit a429withRetry-After: 120after a handful of requests spaced about a second apart; spacing requests by 2.5 seconds ran ten consecutive pages without a limit. Treat403,429and503as "stop and wait". - Published data can be odd: in one award notice we looked at, the buyer's name was a 32-character hash rather than an organisation name.
2. DIY: both sources to CSV in Python
Tested against both live APIs on 4 November 2026: it returned 91 tender releases (81 from Find a Tender, 10 from Contracts Finder) from the last 24 hours, capped at 5 pages per source. Needs pip install requests. Put a real contact address in the User-Agent.
import csv
import time
from datetime import datetime, timedelta, timezone
import requests
HEADERS = {"User-Agent": "my-tender-script/1.0 (you@example.com)"} # identify yourself
FTS = "https://www.find-tender.service.gov.uk/api/1.0/ocdsReleasePackages"
CF = "https://www.contractsfinder.service.gov.uk/Published/Notices/OCDS/Search"
def pages(url, params, pause, max_pages=5):
"""Follow links.next (a ready-made URL with the cursor) and obey Retry-After on 429/503."""
next_url, next_params = url, params
for _ in range(max_pages):
for attempt in range(4):
r = requests.get(next_url, params=next_params, headers=HEADERS, timeout=60)
if r.status_code in (429, 503):
time.sleep(int(r.headers.get("Retry-After", 10)) + 1)
continue
if r.status_code == 403: # Contracts Finder says: wait 5 minutes
raise RuntimeError("rate limited (403), stop and retry in 5+ minutes")
r.raise_for_status()
break
else:
raise RuntimeError("service unavailable")
data = r.json()
yield from data["releases"]
next_url, next_params = data.get("links", {}).get("next"), None
if not next_url:
return
time.sleep(pause)
def row(source, rel):
t = rel.get("tender", {})
value = t.get("value") or {}
cpvs = [t.get("classification")] + (t.get("additionalClassifications") or []) # Contracts Finder
for item in t.get("items", []): # Find a Tender
cpvs += item.get("additionalClassifications") or []
cpv = [c["id"] for c in cpvs if c and c.get("scheme") == "CPV"]
return {
"source": source,
"id": rel["id"],
"published": rel["date"][:10],
"title": t.get("title"),
"buyer": (rel.get("buyer") or {}).get("name"),
"cpv": ";".join(dict.fromkeys(cpv)),
"value": value.get("amount", value.get("amountGross")), # Find a Tender may give only amountGross
"currency": value.get("currency"),
"deadline": (t.get("tenderPeriod") or {}).get("endDate"),
}
since = (datetime.now(timezone.utc) - timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%S")
rows = []
# Find a Tender: no usable `stages` filter, so keep releases whose tender document is a UK4 tender notice.
for rel in pages(FTS, {"limit": 100, "updatedFrom": since}, pause=1.2):
docs = rel.get("tender", {}).get("documents", [])
if any(d.get("noticeType") == "UK4" for d in docs):
rows.append(row("find-a-tender", rel))
# Contracts Finder: `stages=tender` works server-side. Use a longer pause between pages.
for rel in pages(CF, {"stages": "tender", "limit": 100, "publishedFrom": since}, pause=2.5):
rows.append(row("contracts-finder", rel))
with open("uk_tenders.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys())
w.writeheader()
w.writerows(rows)
print(len(rows), "tender releases")
It does not de-duplicate notices published on both services, does not keep only the latest Contracts Finder version per notice, and does not handle award notices or values in other shapes. That is the part that grows.
3. When the Actor saves time
The Apify Actor UK Public Tenders (by Nexus Scrapers) reads both official APIs and returns one schema. It pays off when you would otherwise maintain the glue code above:
- Both sources, one record per procurement notice. Notices found on both services are merged (the Find a Tender record wins, the other link is kept in
ukAlsoPublishedOn); only the newest version of each Contracts Finder notice is kept. - Normalized schema v1, the same one our TED Actor uses: buyer object, dates, money as
{amount, currency}, unified notice types (UK codes likeUK4,UK6kept innoticeTypeCode),nullinstead of missing keys. UK-only extras are prefixeduk(OCDS process id, regions, SME/VCSE suitability, framework flag, OCDS link). - Filters the APIs lack: keyword, CPV prefix, notice types, value band, region, applied by the Actor on your behalf, up to 10,000 results per run. For rare filters over long periods it reads at most 60 pages per source and says so when it stops early.
- CPV codes with official labels in any of the 24 EU languages; main CPV and division name.
- Award view with winning suppliers, awarded value and award date.
- No contact details: names, e-mails and phone numbers from party records are not copied.
- Scheduling and integrations through Apify (daily runs, Slack, e-mail, Google Sheets, webhooks, API, MCP); export as JSON, CSV or Excel. Rate-limit handling and back-off are built in.
Pricing is pay per result: $0.003 per result at the time of writing; the Actor page shows the current price.
What it does not do: it does not return tender documents (only notice data and links), does not cover Public Contracts Scotland, Sell2Wales or eSourcing NI below-threshold notices, and does not give lot-level detail. For a one-off export of a single stage, the script above is enough.
4. Real sample: 20 normalized records
These 20 records were produced on 4 November 2026 by running the Actor's own source and normalizer code locally against the live APIs, without the Apify platform: 12 tender notices (latest three days, 6 from each service) and 8 award notices (latest ten days, 4 from each service), merged and de-duplicated like the Actor does. They are the newest matches, not hand-picked. One notice contained an organisation e-mail address inside its free-text description; we replaced it with [email removed]. Everything else is unchanged.
Download: sample.json (full records, UTF-8) and sample.csv (flattened: lists joined with | , opens in Excel). Free to use under the licence in section 5.
| Notice | Src | Type | Buyer | Title | Value | Winners |
|---|---|---|---|---|---|---|
| a17cebab… | CF | notice | THE NATIONAL TRUST FOR PLACES OF… | National Trust - NI - Lead Designer & Multidisciplinary t… | 40,000 GBP | - |
| 104923-2026 | FTS | notice | Stonewater Limited | Stoke Road, Henlade | 10,300,000 GBP | - |
| 8665d1a9… | CF | notice | Theatre Royal Plymouth | Theatre Royal Plymouth - Architect-led Multi-disciplinary… | 750,000 GBP | - |
| 104919-2026 | FTS | notice | North Northamptonshire Council | North Northamptonshire Council - Operation & maintenance… | 273,000 GBP | - |
| e3696531… | CF | notice | DERBY CITY COUNCIL | ENQ1406 - LED Screens, Staging, PA Equipment and Lighting… | 95,000 GBP | - |
| 104918-2026 | FTS | notice | Fermanagh and Omagh District Coun… | TENDER FOR CAPITAL YOUTH PROGRAMME (ENNISKILLEN ROUND O)… | 162,000 GBP | - |
| 81aa3ffe… | CF | notice | Newry, Mourne and Down District C… | CA18639 - Tender 23/2026 - SUPPLY AND DELIVERY OF WORKSHO… | - | - |
| 104917-2026 | FTS | notice | NHS England | National Education and Training for NHS Talking Therapies… | 125,264,006 GBP | - |
| b6442fed… | CF | notice | North East Lincolnshire Borough C… | Railway Street Footbridge (20/79/49) Geotechnical Investi… | 90,000 GBP | - |
| 104914-2026 | FTS | notice | London Borough of Hounslow | Tender for the Provision of Learning Disability And Autis… | 4,700,000 GBP | - |
| 6ea78e35… | CF | notice | Balfour Beatty VINCI JV HS2 | BBV Sublot 4 - HSST1020 - SALTLEY VIADUCT GABION WALL | 1,000,000 GBP | - |
| 104907-2026 | FTS | notice | VICO HOMES LIMITED | Kitchen Replacement Programme | 110,180,000 GBP | - |
| 429e8abe… | CF | award | South Tyneside Council | Provision of an attendance collection service for CLA/CIN… | 185,828 GBP | Welfare Call (LAC) Ltd |
| 104925-2026 | FTS | award | Royal Borough of Greenwich | Notice of Intention to Award the NextGen School Health an… | 10,014,104 GBP | BROMLEY HEALTHCARE COMMUNITY INTE… |
| a003256f… | CF | award | a3b773bedbb680101b84a5305b961983 | Transformation Delivery Services - Transforming and Conne… | 41,400,000 GBP | KPMG LLP |
| 104924-2026 | FTS | award | London Borough of Brent | Advocacy Services for Brent and Harrow | 2,224,358 GBP | POWHER |
| 9b32358e… | CF | award | South Tyneside Council | Contact Centre and CRM Enablement | 55,000 GBP | PRICE WATERHOUSE COOPERS LLP |
| 104922-2026 | FTS | award | University Hospitals Birmingham N… | UHB Meteor Park Warehouse Trucks | 166,413 GBP | Jungheinrich UK Ltd |
| 488e5053… | CF | award | NHS England | Transformation Delivery Services - Establishing Operation… | 41,400,000 GBP | PA CONSULTING SERVICES LIMITED |
| 104921-2026 | FTS | award | Construction Industry Training Bo… | Skills and Training 2026 Research (Sept) | 78,150 GBP | Navigator Insight Limited |
One Find a Tender record in full (description shortened):
{
"schemaVersion": "1.0",
"source": "find-a-tender",
"sourceId": "104923-2026",
"url": "https://www.find-tender.service.gov.uk/Notice/104923-2026",
"documentUrls": {
"html": "https://www.find-tender.service.gov.uk/Notice/104923-2026",
"pdf": null,
"xml": null
},
"noticeType": "contract-notice",
"noticeTypeCode": "UK4",
"procedureType": "open",
"contractNature": "works",
"title": "Stoke Road, Henlade",
"description": "This procurement exercise is to support the delivery of 39 mixed-tenure homes on a greenfield site off Stoke Road, Henlade TA3 5FF, south of Henlade Close (Pha…",
"language": "en",
"buyer": {
"name": "Stonewater Limited",
"country": "GB",
"city": "Leicester",
"nutsCode": "UKF22",
"buyerType": "regional-or-local-authority"
},
"cpvCodes": [
{
"code": "45000000",
"label": "Construction work"
}
],
"mainCpv": "45000000",
"mainCpvDivision": "Construction work",
"estimatedValue": {
"amount": 10300000,
"currency": "GBP"
},
"lots": 1,
"publicationDate": "2026-11-04",
"submissionDeadline": "2026-12-16T17:00:00+00:00",
"awardInfo": null,
"placeOfPerformance": {
"country": "GB",
"city": null,
"nutsCode": "UKK12"
},
"scrapedAt": "2026-11-04T21:56:10+00:00",
"ukOcid": "ocds-h6vhtk-07b368",
"ukBuyerReference": "00378-DVP",
"ukProcurementMethodDetails": "Open procedure",
"ukRegions": [
"South West"
],
"ukAboveThreshold": true,
"ukFramework": null,
"ukSuitability": {
"sme": true,
"vcse": true
},
"ukOcdsUrl": "https://www.find-tender.service.gov.uk/api/1.0/ocdsReleasePackages/104923-2026",
"ukAlsoPublishedOn": []
}
5. Data licence and attribution
Every release package carries a license field pointing to the Open Government Licence v3.0. Under the OGL you may copy, publish, distribute, adapt and use the information commercially, as long as you acknowledge the source and include the attribution statement. Use the standard wording: "Contains public sector information licensed under the Open Government Licence v3.0." Name the publisher too (Cabinet Office, Find a Tender / Contracts Finder). The OGL does not cover personal data protected by data protection law, and notices can include names and contact details, so handle those in line with UK GDPR. CPV labels in the sample come from the EU's CPV 2008 vocabulary (Regulation (EC) No 213/2008), published by the Publications Office of the European Union. This is a summary, not legal advice; read the licence for the exact terms.