UK Public Tender Lead Alert
Pricing
from $8.50 / 1,000 new matching tender delivereds
UK Public Tender Lead Alert
Watch UK Find a Tender for public-sector tenders newly matching your keywords, CPV codes, value threshold or region, and get only the new ones since your last check.
Pricing
from $8.50 / 1,000 new matching tender delivereds
Rating
0.0
(0)
Developer
Tim Zinin
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
UK Public Tender Lead Alert: New Matching Notices from the Official Find a Tender Feed

This Actor watches UK Find a Tender (FTS) — the Cabinet Office's official above-threshold public procurement feed for England, Scotland, Wales and Northern Ireland — for tenders newly matching your keywords, CPV codes, value threshold or region, and delivers only the ones you have not already seen under this watch. It reads exactly one source. It does not read UK Contracts Finder, TED, SAM.gov, or any third-party tender aggregator, and the reason is not coverage — it is billing correctness: a live check found the same physical tender published under two different identifiers on Find a Tender and Contracts Finder at once, which would double-charge a buyer running both. See Evidence and boundaries for the exact record that proved it.
The billable outcome is a new matching tender: a notice currently published with
tender.status:"active" on Find a Tender, matching your filter, not seen before under this watch's
name. Everything else — the first run that only establishes your reference point, a run that finds
nothing new, a repeat of a tender you already received, a match whose value is not published in GBP,
a source error, a rate limit, or a match held back only by your own per-run cap — is free, with a
named reason on every row.
What you get
- Only what is new, per named watch. This Actor keeps a durable baseline of every tender identity
it has already delivered under a given
stateKey, so a scheduled run reports genuinely new activity, not the same feed replayed. - The full tender-stage feed, not the 15% the API's own filter would leave you. A live,
full-pagination re-measurement on 2026-08-12 found the API's documented
stages=tenderparameter silently drops 63 of 74 same-day tender-tagged releases while still returningHTTP 200on a non-empty page — indistinguishable from correct behavior unless you paginate the whole window and count. This Actor never sends that parameter; it reads the window unfiltered and applies thetagfilter itself, client-side, on every release. See Evidence and boundaries for the measurement. - No key, no login, no captcha, no account. Find a Tender is free UK government open data, licensed for commercial reuse under the Open Government Licence v3.0 — checked directly against the licence text, not assumed.
- Never a guessed field, never a silent drop. A value the source did not publish is
null. A match whose GBP value cannot be confidently checked is reported free and unbilled rather than guessed either way. A tender held back only by your ownmaxNewPerRuncap is never lost — it stays "new" and is billed on a later run, not silently forgotten. - A named reason for every free outcome. Baseline creation, "nothing new," a repeat, a currency
mismatch, a rate limit, a source error, or a robots block all return a full row with a stable
machine
errorCodeand a human-readableerrorstring — never a bare empty result and never a vague failure. - A lead for human bid review, never an automated bid decision.
recommendedActionon a delivered row means "worth a human look," never eligibility to bid and never that the deadline is still practically reachable by the time you read the row.
Who uses it
- Bid managers and business development teams watching a specific sector, CPV category, or value bracket for newly published public-sector opportunities, without refreshing the Find a Tender search page by hand.
- Regional and local-government-focused suppliers using the best-effort
regionfilter to surface NHS, local-authority and devolved-nation opportunities in a specific part of the UK. - Public-sector sales and CRM operations running several independently named watches (one
stateKeyper category, region, or account team) that feed a shared deadline queue. - Developers and workflow builders who want a flat, machine-checkable contract —
found,isNew,partial,errorCode— an agent or n8n node can branch on directly, without parsing the Find a Tender website or handling OCDS pagination by hand.

How to run
- Set at least one filter criterion:
keywords,cpvCodes,minValueGbp/maxValueGbp, orregion. An empty filter is rejected as invalid input, free of charge — this Actor never treats "no filter" as "show me everything." - Give this watch a
stateKey— a name for this specific search, so you can run several independent watches from one Actor without one overwriting another's memory of what it has already billed. The prefilledapify-daily-testvalue exists only for this Actor's own platform smoke test; replace it with your own watch name. - Leave
lookbackDaysat its default of1unless you have a specific reason to widen it — see Operating guide for why the default is conservative on purpose. - Start the run from the Console, the Apify API, a scheduled Task, or an MCP client.
- Read the Dataset for one row per outcome this run produced — a delivered new tender, a quiet "nothing new," a value-ambiguous match, a baseline-creation notice, or an error/stop notice.
- Read
OUTPUT(the default key-value store'sOUTPUTrecord) for the run-level summary:candidatesMatched,newCandidates,delivered,billed,freeAmbiguous, whether the run stopped early, and why. - Route every
found:truerow to a human for a bid/no-bid decision.recommendedActionis set tohuman_review_before_bid_decisionspecifically to keep this Actor out of an automated bidding decision.
The first run for any new stateKey only establishes the reference point — it is free, and by default
writes one informational row saying so (emitBaseline:true). Every run after that reports genuinely
new activity since the last one.
Pricing
$0.01 per new matching tender delivered, plus $0.005 Actor start. This Actor's own external data cost is zero — Find a Tender is free, keyless UK government open data — so the price covers the filtering, deduplication and delivery guarantees described on this page, not a resold data bill.
The primary pricing noun is the new matching tender: one notice, currently active, matching your filter, not seen before under this watch. Every other outcome is free — baseline creation, a quiet run, a repeat, a value-currency mismatch, a source error, a rate limit, and any match held back only by your own per-run cap. A watch that runs daily for a month and only ever finds three genuinely new matching tenders is billed for three deliveries plus each run's Actor start, not for every day it checked.
Input contract
At least one of keywords, cpvCodes, minValueGbp, maxValueGbp or region is required — this is
enforced before any network request, and an empty filter is rejected as invalid input, not treated as
"match everything."
keywords— 0-20 strings. Case-insensitive substring match against the tender'stitleanddescriptioncombined.cpvCodes— 0-20 strings. Matched as a prefix against the tender's CPV classification code (e.g."7211"matches"72110000"). A full code with a check digit ("72000000-5") is accepted — the check digit is stripped automatically before matching, since it never participates in a prefix match anyway.minValueGbp/maxValueGbp— non-negative integers. Applied only when the tender's published value is genuinely in GBP. A tender with a missing value, or a value published in another currency, is reported as a free, unbilled row instead of a silent guess — never excluded outright and never billed on an assumption either way.region— free text, best-effort, case-insensitive substring match against whatever buyer/delivery region, country or locality text the notice publishes. Not a hard filter guarantee — region data is not published on every notice, and an absent field never excludes a tender on this axis alone; present-but-non-matching region data does exclude it.lookbackDays— integer, 1-30, default 1. How far back this run'supdatedFrom/updatedTowindow reaches. Measured on the live source: ~539 releases a day, 100 per page, so a day is about six pages and each extra day adds six more. Pagination pauses out the source's rate limiter and covers roughly eleven days per run; past that the row is honestlypartial. See Operating guide.maxNewPerRun— integer, 1-200, default 20. Caps how many new matching tenders a single run delivers and charges for, even if more were found. Anything held back by this cap is never lost — it stays "new" and appears, and is billed, on a future run.stateKey— string, sanitized into a storage-safe key and encrypted by Apify as a secret input so watch names are not exposed in Console logs. Names this specific watch so several independent searches can run from one Actor without sharing a baseline.emitBaseline— boolean, defaulttrue. Whentrue, the first run for a newstateKeywrites one free, clearly labelled informational row summarizing what was captured as the reference point. Whenfalse, that first run still saves the reference point — it just writes no row, for a buyer who only ever wants to see "new since I subscribed."
{"keywords": ["clinical services", "urgent treatment"],"cpvCodes": ["85121100", "7211"],"minValueGbp": 250000,"stateKey": "nhs-clinical-southeast","lookbackDays": 1,"maxNewPerRun": 20,"emitBaseline": true}
{"region": "Scotland","maxValueGbp": 5000000,"stateKey": "scotland-infrastructure-watch","lookbackDays": 7,"maxNewPerRun": 10,"emitBaseline": false}
(Both are contract illustrations built from .actor/input_schema.json and lib/input.js, not output
from a specific run — an input has no result to reproduce. The second example uses a 7-day
lookbackDays deliberately, to illustrate a wider window than the conservative default; see Operating
guide for what that trade-off actually costs.)
Real happy, partial and failure output
Every row shares one flat, 27-field schema regardless of outcome — checked directly against
.actor/dataset_schema.json's own required array by this Actor's own test suite. error is always
the empty string "" (never null or omitted) on an honestly-completed row, including a quiet
"nothing new" run — that convention is what lets a reader tell "the source answered and there was
nothing new" apart from "something actually went wrong."
Happy path — a new matching tender, real identity and financial fields
The ocid, publicationNumber, title, buyer, cpv, valueAmount, valueCurrency and deadline
fields below are not invented: they were read directly from a live Find a Tender response on
2026-08-12, and the evidenceUrl was independently confirmed to return HTTP 200 with this exact
title in the page's own <title> tag.
{"stateKey": "nhs-clinical-southeast","found": true,"isNew": true,"baselineCreated": false,"source": "findATender","ocid": "ocds-h6vhtk-06e308","publicationNumber": "076965-2026","title": "UHS Urgent Treatment Centre Clinical Service Provision","buyer": "University Hospital Southampton NHS Foundation Trust","region": "South East England","cpv": "85121100","valueAmount": 2250000,"valueCurrency": "GBP","deadline": "2026-09-01T18:00:00+01:00","datePublished": "2026-08-12T09:14:22+01:00","status": "active","evidenceUrl": "https://www.find-tender.service.gov.uk/Notice/076965-2026","matchedKeyword": "clinical services","matchedCpv": "8512","confidence": null,"recommendedAction": "human_review_before_bid_decision","partial": false,"partialReason": null,"errorCode": null,"error": "","retryable": false,"observedAt": "2026-08-12T16:20:00.000Z"}
ocid, publicationNumber, title, buyer, cpv, valueAmount, valueCurrency and deadline are
real, directly observed. region, datePublished, matchedKeyword, matchedCpv and observedAt are
illustrative — chosen to complete a coherent row shape for a keywords+cpvCodes+minValueGbp watch,
since the live check that confirmed the identity fields above did not run this Actor's own filter logic
against them. confidence is null on every row this build produces: matching is exact and
deterministic (keyword substring, CPV prefix, value threshold, region substring), never a fuzzy score,
so there is nothing to score.
Partial/free path — a match whose value could not be confidently checked
A live Find a Tender record was directly observed on 2026-08-12 with tender.value entirely
absent — not zero, not non-GBP, simply missing from the response (a record whose title began "Rot
Eradication..."). That is exactly the trigger condition this row shape exists for. The identity fields
below are illustrative, built to complete the shape; only the fact that a live record with a missing
value exists was preserved from that check, not the rest of that specific record.
{"stateKey": "nhs-clinical-southeast","found": false,"isNew": true,"baselineCreated": false,"source": "findATender","ocid": "ocds-h6vhtk-071a4c","publicationNumber": "077102-2026","title": "Rot Eradication and Timber Repair Works, Estate Buildings","buyer": "Historic Buildings Preservation Trust","region": null,"cpv": "45262700","valueAmount": null,"valueCurrency": null,"deadline": "2026-09-15T12:00:00+01:00","datePublished": "2026-08-12T11:02:07+01:00","status": "active","evidenceUrl": "https://www.find-tender.service.gov.uk/Notice/077102-2026","matchedKeyword": null,"matchedCpv": "4526","confidence": null,"recommendedAction": null,"partial": false,"partialReason": null,"errorCode": "value_currency_ambiguous","error": "This tender otherwise matches your filter, but its value is not published in GBP (or is missing), so the value threshold could not be applied with confidence. Not billed — see evidenceUrl to check by hand.","retryable": false,"observedAt": "2026-08-12T16:22:00.000Z"}
This shape only ever fires when the buyer's filter includes minValueGbp/maxValueGbp — a watch that
never sets a value threshold never produces this row, because there is no threshold decision to be
ambiguous about.
Quiet path — a later run that genuinely found nothing new
{"stateKey": "nhs-clinical-southeast","found": false,"isNew": false,"baselineCreated": false,"source": "findATender","ocid": null,"publicationNumber": null,"title": null,"buyer": null,"region": null,"cpv": null,"valueAmount": null,"valueCurrency": null,"deadline": null,"datePublished": null,"status": null,"evidenceUrl": null,"matchedKeyword": null,"matchedCpv": null,"confidence": null,"recommendedAction": null,"partial": false,"partialReason": null,"errorCode": null,"error": "","retryable": false,"observedAt": "2026-08-13T07:00:00.000Z"}
This is what a healthy, frequently-run watch mostly looks like — most days, nothing new has appeared
since yesterday's check. error is "", not null and not a message, because the source answered
and the answer was genuinely "nothing new," not a failure.
Failure path — the source's own measured rate limit, real error text
Find a Tender's rate limit was measured directly on 2026-08-12: after a burst of requests, the API
returned HTTP 429 with the body text quoted verbatim below on both Find a Tender and (separately)
Contracts Finder. This Actor caps itself at 10 requests per run — well under that measured ceiling —
specifically so a normal run essentially never reaches this path; it exists to make a rare, genuine
rate-limit hit an honest, retryable, unbilled outcome rather than a crash.
{"stateKey": "nhs-clinical-southeast","found": false,"isNew": false,"baselineCreated": false,"source": "findATender","ocid": null,"publicationNumber": null,"title": null,"buyer": null,"region": null,"cpv": null,"valueAmount": null,"valueCurrency": null,"deadline": null,"datePublished": null,"status": null,"evidenceUrl": null,"matchedKeyword": null,"matchedCpv": null,"confidence": null,"recommendedAction": null,"partial": false,"partialReason": null,"errorCode": "rate_limited","error": "Rate limit of 12 exceeded. Please retry after 120 seconds.","retryable": true,"observedAt": "2026-08-12T16:17:02.000Z"}
The error text above is the source's own response body, quoted verbatim, not a message this Actor
composed. retryable:true here specifically means "this class of failure is expected to clear on its
own" — this Actor does not auto-retry within the same run; a scheduled watch simply picks it up again
on the next scheduled run.
Field dictionary
| Field | Meaning | Important boundary |
|---|---|---|
stateKey | The sanitized watch name this row belongs to | Same value used to partition the baseline in storage |
found | The billing boundary | true only on a delivered, charged new-tender row; false on every notice, error, baseline-creation and value-ambiguous row, even when isNew is true |
isNew | Whether this tender's ocid was not present in the saved baseline | Independent of billing — a value-currency-ambiguous match is isNew:true, found:false |
baselineCreated | Whether this row belongs to the free, first-ever run for this watch | true only on that one run |
source | Fixed to "findATender" | v1 reads Find a Tender only — see Evidence and boundaries for why Contracts Finder is excluded |
ocid | The Open Contracting ID — this tender's stable identity within Find a Tender | null on every notice/error row that carries no specific tender |
publicationNumber | Find a Tender's own release id (format NNNNNN-YYYY) | Also the basis of evidenceUrl; null on every notice/error row |
title | The tender's title | null when no tender applies to this row |
buyer | The contracting authority's published name | null when no tender applies to this row |
region | Best-effort buyer/delivery region or country text | null when the notice publishes none — never treated as a mismatch on its own |
cpv | The tender's CPV classification code | null when no tender applies, or the notice does not publish a CPV code |
valueAmount | The tender's published value | null when the source omits it |
valueCurrency | ISO 4217 currency code as published by the source | Not guaranteed to be GBP — see errorCode:"value_currency_ambiguous" |
deadline | The tender's submission deadline | null when no tender applies, or the source omits it |
datePublished | Taken uniformly from the OCDS release's own date field | Find a Tender releases carry no separate tender.datePublished field at all — this Actor never falls back to inventing one |
status | The source's own tender.status at observation time | Every delivered row has "active" here — this Actor never sells a non-active notice |
evidenceUrl | The canonical, human-readable Find a Tender notice page | null when no tender applies to this row; viewing it in a browser requires a normal browser User-Agent — a bare script/curl request without one receives 403 from the site's own WAF on this human-facing page (this Actor never fetches it itself; it is handed to you to open) |
matchedKeyword | The first buyer-supplied keyword that matched | null when no keyword filter was set, or no per-row match applies |
matchedCpv | The first buyer-supplied CPV prefix that matched | null when no CPV filter was set |
confidence | Reserved for a future scored/fuzzy match | Always null in this build — every match is an exact, deterministic filter hit |
recommendedAction | Set only on a delivered new-tender row | "human_review_before_bid_decision" or null; never eligibility to bid, never that the deadline is still practically reachable |
partial | True when this run's own coverage or delivery was incomplete for a stated reason | Never a silent gap — see partialReason |
partialReason | Which specific limit caused partial:true | null unless partial:true; see Decision routing for the full list |
errorCode | Bounded machine error code | null on every honestly-completed row, including "nothing new" and baseline-creation |
error | Human-readable message, or the source's own verbatim error text | Always "" (never null/omitted) on an honestly-completed row — this is what separates "answered, nothing new" from "something happened" |
retryable | Whether re-running this watch soon is expected to help | Fixed true for rate_limited; depends on the specific failure for source_error/robots_unreadable |
observedAt | When this row's outcome was produced | Shared by every row in one run |
Run summary (OUTPUT)
The default key-value store's OUTPUT record — one per run — reports candidatesMatched (matched
before the new/already-seen diff), newCandidates, delivered, billed, freeAmbiguous,
baselineCreated, partial/partialReasons, and stopped/stopReason when the run ended before
doing everything it otherwise would have. This is what a buyer reconciles a charge against — it is
written before the run exits on every path this Actor can take, including a graceful stop and a throw
after money has already changed hands, so a completed run is never left with dataset rows and no
summary to check them against.
Evidence and boundaries
Why Find a Tender, and not TED, SAM.gov or a third-party aggregator. TED (the EU-wide equivalent)
is already a shipped product in this same portfolio (gov-tender-radar + tender-alert) — building a
second TED actor here would duplicate an existing SKU, not add one. SAM.gov (the US federal
equivalent) requires a mandatory API key, confirmed live: its base search endpoint returns 404
unauthenticated, and the platform's own documentation states plainly that a key must be generated from
a personal SAM.gov account — account creation on someone else's behalf is out of scope for this build,
so SAM.gov is recorded as a clean future candidate, not chosen now. UK Find a Tender and UK Contracts
Finder cleared every check live — reachable anonymously, no key, no registration, an explicit
commercial-reuse licence printed in every response — and no UK-tender actor already existed in this
portfolio.
Why this Actor reads Find a Tender only, and not also Contracts Finder — proven, not assumed. A
live side-by-side check on 2026-08-12 found the same physical tender published on both sources at
once: NHS "UHS Urgent Treatment Centre Clinical Service Provision," identical title, buyer, £2,250,000
value and 2026-09-01 deadline — but under two different ocids: ocds-h6vhtk-06e308 on Find a
Tender and ocds-b5fd17-3f48b68a-... on Contracts Finder. A dedup strategy keyed on ocid — the
obvious first design — cannot catch this, because the identifiers genuinely differ between sources for
the identical tender. A buyer watching both sources with a naive ocid-based dedup would be charged
twice for the same lot on the first run where it appeared in both feeds simultaneously. Rather than
ship a cross-source dedup heuristic that has not been proven to close every case, this build reads one
source. A Contracts Finder watch — covering the broader, mostly-below-FTS-threshold England market — is
a defensible future companion Actor, not a flag on this one; combining two independently-updating
sources into one Actor's billing model is a different, harder problem than this SPEC solves.
Why the API's own stages=tender filter is never used. The same live check paginated a full
24-hour window with no stage filter at all, following the API's own links.next cursor to exhaustion:
six pages, 539 total OCDS releases of every kind, of which 74 carried the tender tag. The
identical request with stages=tender returned exactly 11 releases and no further pages — a strict
subset of those 74, silently dropping the other 63. The dropped releases are not distinguishable from
the kept ones by any field this Actor can see: same tag:["tender"], same tender.status:"active",
same initiationType, real live buyers on both sides of the split (Worcestershire County Council,
Greater London Authority, and Ordnance Survey Limited were all in the dropped set). This is the single
most important design decision on this page: the server-side filter looks correct — HTTP 200, a
non-empty, well-formed page — while quietly withholding 85% of the same day's tender-stage activity.
This Actor never sends stages=tender. It reads the unfiltered window and applies the tag/status
filter itself, on every release, client-side.
Why an absent robots.txt is treated as permission on this specific host. robots.txt on
www.find-tender.service.gov.uk returns a genuine, parseable HTTP 404 — a real application error
page titled "Page not found - Find a Tender," not a WAF challenge, not a truncated or unreadable
response. Under the standard Robots Exclusion Protocol convention, a real 404 means "no policy was
published," not "we could not read this policy" — and this Actor's own robots check distinguishes the
two precisely: any other non-200 response (403, 5xx, a timeout, an oversized or unparseable body) is
still treated fail-closed, exactly like an unreadable policy anywhere else in this program. This Actor
checks robots.txt fresh, before the first API request, on every single run — it does not cache or
assume the 404 from a previous run still holds.
The measured rate limit, and how this Actor stays under it. A live burst of requests against Find
a Tender returned HTTP 429 with the body
"Rate limit of 12 exceeded. Please retry after 120 seconds."stages filter, per the fact
above) took six requests in that same live measurement. This Actor's internal budget is 10 requests
per WAVE — shared between the one robots.txt read and every paginated API page — leaving deliberate
headroom under the measured ceiling of 12, not shaved to the edge of it.
That ceiling is a pause, not a wall, and this Actor now treats it as one. When a wave runs out, or the
source answers 429, pagination waits — for the Retry-After the source states, clamped to five
minutes, or 125 seconds when it states none — and then continues the same page. Six waves per run
cover roughly 6,000 releases, about eleven days of the measured ~539 releases/day, with at most five
pauses; a heavily paginated run therefore takes minutes of waiting rather than returning early. This
matters more than it sounds: a first production run on 2026-08-13 showed a two-day window is eleven
pages, so before pacing every window wider than a day was silently short, and for an alert product a
short window does not look broken — it looks like a quiet day.
Past six waves the run still stops with an honest partial:true row rather than guessing at the rest
of the window; nothing already found that run is withheld because of it, and anything outside the
pages actually read is picked up — and billed — on a later run.
The coverage-window boundary, stated plainly. This Actor only sees notices whose OCDS release
date (the source's own last-update timestamp) falls inside [now - lookbackDays, now]. A tender
published earlier than the lookback window, with no update since, will not appear until it is next
touched by the source, or until an earlier run's baseline already captured it. This is the same
bounded-window limitation this portfolio's other monitor actors (gov-tender-radar/tender-alert)
already carry and accept for the EU's TED feed.
Why the value threshold is GBP-only, by design, not by oversight. The source publishes
tender.value.currency per notice; Find a Tender's UK public-sector coverage is near-universally GBP,
but not guaranteed on every record, and a live check separately confirmed at least one active record
with tender.value entirely absent (not zero — the field simply not present). Rather than silently
treat a missing or non-GBP value as passing a threshold (which could bill a false positive) or as
failing one (which could hide a real match), this Actor reports it as a free, explicitly named
value_currency_ambiguous row and lets you check the notice by hand via evidenceUrl.
Replay safety — how a delivered tender is never billed twice, and the one honestly-disclosed edge case. Each new tender's identity is written to the durable baseline immediately after its charge is confirmed, one at a time — not batched until the end of a delivery loop — specifically so a process crash mid-run leaves the smallest possible window of risk. In the rare case where the charge succeeds but the very next baseline write then fails (a transient storage hiccup), that specific tender may be re-detected, and re-charged, on a later run; this Actor stops delivering further rows for the rest of that run the moment this happens, rather than continuing to charge against a baseline it can no longer confirm is durably saved, and writes a free notice row naming exactly what happened. This is a disclosed boundary of the storage model available on this platform, not a defect papered over.
Independent review before release. This Actor's code went through three rounds of independent
review — someone other than its author, deliberately trying to find a way the money contract or the
delivery guarantees above could be wrong — before this page was written. The most serious finding
closed along the way was that the run's own summary record could be lost on certain failure paths,
leaving a buyer with charged rows and nothing to reconcile them against; the fix is now covered by the
run-summary durability description under Field dictionary above. The automated test suite for this
build — 74 tests, run twice as part of the final review, both green — covers billing atomicity (a
tender is never charged twice across a crashed-and-retried run), the client-side tag filter, the
value-currency-ambiguity rule, the request budget, and a cross-check that every error code this Actor's
own code can emit is a member of the one canonical list published in .actor/dataset_schema.json — the
same class of code-versus-schema drift this program has been caught by before on other actors.
Decision routing
| Row shape | Billable? | errorCode | What it means |
|---|---|---|---|
| New matching tender | Yes | null | The one billable outcome — active, filter-matching, not seen before under this watch |
| Baseline created | No | null | Free, first-ever run for this watch; establishes the reference point |
| Nothing new | No | null | A later run genuinely found no new matching tenders |
| Value-currency ambiguous | No | value_currency_ambiguous | Otherwise matches, but its value is missing or not in GBP — check evidenceUrl by hand |
| Held back by run cap / budget | No | null, partial:true | More matched than maxNewPerRun allowed, or the buyer's charge cap was reached mid-run; never lost, appears on a later run |
| Partial source coverage | No | null, partial:true | This run's own request budget or a mid-pagination failure stopped it before the full window was read; already-found rows are unaffected |
robots_disallowed | No | robots_disallowed | The source's own robots.txt explicitly disallows this path (not observed live; the policy is checked fresh every run in case it changes) |
robots_unreadable | No | robots_unreadable | robots.txt could not be read or parsed — treated fail-closed |
rate_limited | No | rate_limited | The source's own measured request ceiling was hit; retryable |
source_error | No | source_error | An upstream timeout, malformed response, or unexpected failure |
buyer_budget_exhausted | No | buyer_budget_exhausted | The buyer's own remaining charge cap could not cover another row; stopped before any source request |
baseline_unavailable | No | baseline_unavailable | The baseline store itself could not be read or written; stopped rather than risk silently losing or duplicating a real baseline |
A workflow only needs a few rules: act only on found === true; never treat partial === true as a
complete run even when it delivered some rows; and read error === "" as "the source answered, this is
not a failure," regardless of what errorCode says on a free row.
Commercial playbooks
Sector or CPV-category early warning for bid teams
A bid manager sets cpvCodes to a category prefix (e.g. "7211" for computer-related services, or a
two-digit CPV division for a broader sweep) with a minValueGbp floor matching the team's typical
contract size, and lets a daily scheduled run surface only genuinely new opportunities into a shared
review queue — replacing a manual daily check of the Find a Tender search page.
Regional public-sector business development
A supplier focused on a specific part of the UK sets region to a best-effort match (e.g. "Scotland"
or "Yorkshire") alongside a keyword or CPV filter, tracking NHS, local-authority and devolved-nation
opportunities in their target territory without a hard geographic guarantee — region data is not
published on every notice, so this is deliberately a best-effort signal, not a certainty.
Multi-watch CRM pipeline across account teams
An organization bidding across several unrelated sectors runs one Actor with several independently
named stateKey watches — one per category or account team — each feeding the same downstream CRM
deadline queue, keyed by ocid, without one watch's baseline interfering with another's.
Integration recipes
Scheduled daily watch
An Apify Task runs this Actor once a day with the default lookbackDays:1, reading the Dataset for
found:true rows and pushing each into a CRM or Slack channel keyed by ocid, with deadline driving
a reminder before the submission window closes.
Agent/MCP pattern
An agent supplies only a keyword or CPV filter plus a stateKey, reads found, partial and
errorCode before any next step, never treats a partial:true run as fully covering the window, and
always routes a found:true row to a human for the bid/no-bid call — honoring recommendedAction's
fixed meaning as a hard stop before automation.
Data-warehouse append pattern
Dataset rows are appended keyed by ocid when present, stateKey + observedAt otherwise (a "nothing
new" or error row carries no ocid); null is preserved as null rather than coerced to an empty
string, so a downstream query can tell "the source did not publish this field" apart from "this field
is genuinely blank."
Operating guide
Why lookbackDays defaults to 1, not a wider number. The measured rate limit is 12 requests before
HTTP 429; fully paginating one 24-hour window took six requests in the live measurement above. A
1-day default keeps a normal run comfortably inside that budget with room to spare. A wider window
(up to the allowed maximum of 30) is honored, but is more likely to exceed this Actor's own 10-request
per-run budget partway through and finish with an honest partial:true for that run — nothing already
found is lost, and anything outside the pages read is picked up on a later run, but a run that starts
wide is more likely to need more than one pass to fully catch up.
Choosing between a narrow and a broad filter. There is no server-side keyword or CPV search on this API at all — every match happens client-side, against whatever the fetched window contains. A very broad filter (a two-digit CPV division, or a common single keyword) will match a larger share of the 74-tender-tag-per-day volume this Actor reads; a narrow filter matches less, but is not "cheaper" to run in request terms — the window is fetched either way, and only the matching step differs.
Reading a value-ambiguous row. errorCode:"value_currency_ambiguous" means the tender otherwise
passed every other filter but this Actor could not confidently apply your value threshold — either the
value is missing, or it is published in a currency other than GBP. It is free specifically so you never
pay for a guess; evidenceUrl lets you check the real value by hand in seconds.
Understanding emitBaseline:false. Setting this to false does not skip saving your reference
point — it still happens, on the same first run. It only skips the one informational row that
announces it, for a buyer who never wants to see a first-run summary, only genuinely new activity from
that point forward.
Multiple watches, one Actor. stateKey is the only thing that separates one watch's memory of what
it has already delivered from another's. Two different stateKey values watching overlapping filters
will each independently detect — and separately bill for — the same tender the first time it appears
under each of them; this is expected, not a bug, since each stateKey is its own independent
subscription.
FAQ
Does this Actor also read UK Contracts Finder?
No. v1 reads Find a Tender only. A live check proved the same physical tender can appear on both
sources under two different ocids, which would double-bill a buyer running both through one dedup
strategy — see Evidence and boundaries for the exact record. A Contracts Finder watch is a plausible
separate future Actor, not a flag on this one.
What happens if I don't set any filter?
The run is rejected before any network request, free of charge. This Actor requires at least one of
keywords, cpvCodes, minValueGbp, maxValueGbp or region — it never interprets an empty filter
as "show me the whole feed."
How current is the data?
Find a Tender itself updates close to real time — a live check found a release with a UK-time
publication timestamp already visible in the API response within roughly 15-20 minutes of publication,
not hours or a daily batch. This Actor's own freshness on top of that is bounded by how often you
schedule a run and by lookbackDays.
Can a held-back tender be lost?
No. A tender withheld only by maxNewPerRun or by your own remaining charge cap is never added to the
baseline as "already delivered" — it stays "new" and is billed on a future run, with a free notice row
this run explaining exactly how many were held back and why.
What happens when the rate limit is hit?
A free, honestly labelled rate_limited row with retryable:true, using the source's own verbatim
error text. This Actor's own request budget (10 per run) is deliberately kept under the measured
ceiling (12) specifically so this is rare in normal operation; it does not auto-retry within the same
run — a scheduled watch simply tries again on its next run.
Am I allowed to use this data commercially and redistribute it in my own product?
Yes. Find a Tender publishes under the Open Government Licence v3.0, which explicitly permits commercial exploitation, redistribution, and combination with other data, with no share-alike condition — see Sources and rights for the licence text and the exact required attribution string.
Does this Actor bid, apply, or make any decision on my behalf?
No. recommendedAction on a delivered row is always human_review_before_bid_decision. This Actor
surfaces evidence for a human to review — it never submits a bid, never judges bid eligibility, and
never estimates whether a deadline is still practically reachable.
How reviewed is this build before release?
Three independent review rounds ran against this code, each by someone other than the author, deliberately trying to break the money contract. The most serious finding along the way — the run's own summary record could be lost on certain failure paths — is now closed and covered by durability logic described under Field dictionary. The final round accepted the build with a 74-test automated suite passing twice.
Sources and rights
The only source. UK Find a Tender, published by the Cabinet Office:
GET https://www.find-tender.service.gov.uk/api/1.0/ocdsReleasePackages, OCDS 1.1 release packages.
Covers notices at or above the UK's public procurement thresholds — central government, NHS, large
local-government and utilities contracts — the service that replaced OJEU/TED publication for the UK
from January 2021. No key. No registration. No login.
The licence. Open Government Licence v3.0, printed verbatim in the license field of every single
API response
(http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/). Read directly from the
primary licence text:
"The Licensor grants you a worldwide, royalty-free, perpetual, non-exclusive licence to use the Information subject to the conditions below."
"You are free to: copy; publish, distribute and transmit the Information; adapt the Information; exploit the Information commercially and non-commercially for example, by combining it with other Information, or by including it in your own product or application."
"You must ... acknowledge the source of the Information ... If the Information Provider does not provide a specific attribution statement, you must use the following: Contains public sector information licensed under the Open Government Licence v3.0."
No specific attribution statement for Find a Tender beyond the generic OGL fallback was found on the publisher's own open-contracting policy page, so the required attribution string for anything built on this Actor's output is exactly:
Contains public sector information licensed under the Open Government Licence v3.0.
There is no share-alike clause anywhere in this licence — data delivered to your own Dataset, CRM or downstream product is licensed use, not a redistribution restriction.
No warranty, stated by the licensor. The same licence text states: "The Information is licensed 'as is' ... The Information Provider does not guarantee the continued supply of the Information." This Actor cannot guarantee the source's own uptime, schema stability, or continued publication any more than the licensor itself does.
robots.txt. www.find-tender.service.gov.uk/robots.txt returns a genuine HTTP 404 — no policy
published — checked fresh before every single run, not cached from a prior check. See Evidence and
boundaries for exactly why an absent policy is read as permission on this specific host, and why any
other non-200 response is treated the opposite way.
Limits
- Reads Find a Tender only. UK Contracts Finder, TED, SAM.gov and every other tender source are out of scope for this specific Actor — see Evidence and boundaries for why combining sources here was rejected rather than attempted.
- No server-side keyword or CPV search exists on this API at all; every match is computed client-side against whatever window this run actually fetched.
- Coverage is bounded by each release's own last-update timestamp inside
[now - lookbackDays, now]— a tender untouched since before the window, and not already captured by an earlier baseline, will not appear until it is next touched by the source. - The measured rate-limit ceiling is 12 requests before
HTTP 429; this Actor caps itself at 10 per run and reports honestpartial:truecoverage rather than guess at what a wider window would have contained. - Value-threshold filtering only applies when the tender's published currency is GBP; a missing or non-GBP value is reported free and unbilled, never guessed either way.
regionmatching is a best-effort text substring against whatever address data a notice happens to publish — not every notice publishes region data, and this is not a hard geographic guarantee.confidenceis reserved for a future fuzzy-matching mode and is alwaysnullin this build — every match today is an exact, deterministic filter hit.- Baseline duplicate protection is scoped to one
stateKey: two different watch names covering overlapping filters will each independently detect, and separately bill for, the same tender. recommendedActionnever means bid eligibility, deadline reachability, or a win/loss judgment — it is a fixed signal to route a row to a human, nothing more.
Support boundary
Actor support covers deterministic input validation, the source-access and filtering behavior described
on this page, the Dataset/OUTPUT schemas, and delivery/replay evidence for an accepted release. It
cannot decide whether a specific tender is worth bidding on, cannot guarantee Find a Tender's own
uptime or continued publication (the licensor itself makes no such guarantee — see Sources and
rights), cannot restore access if the UK government changes this API's shape or licence terms, and
cannot provide procurement, legal or bid-strategy advice. When reporting a problem, include the Actor
run ID, the stateKey, the ocid/publicationNumber involved if any, the errorCode, and the
approximate observedAt time.