UK Public Tender Lead Alert avatar

UK Public Tender Lead Alert

Pricing

from $8.50 / 1,000 new matching tender delivereds

Go to Apify Store
UK Public Tender Lead Alert

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

Tim Zinin

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

UK Public Tender Lead Alert: New Matching Notices from the Official Find a Tender Feed

What this Actor does: input, evidence, outcome

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=tender parameter silently drops 63 of 74 same-day tender-tagged releases while still returning HTTP 200 on 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 the tag filter 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 own maxNewPerRun cap 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 errorCode and a human-readable error string — never a bare empty result and never a vague failure.
  • A lead for human bid review, never an automated bid decision. recommendedAction on 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 region filter 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 stateKey per 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.

Where the Actor sits in your pipeline: trigger, run, action

How to run

  1. Set at least one filter criterion: keywords, cpvCodes, minValueGbp/maxValueGbp, or region. An empty filter is rejected as invalid input, free of charge — this Actor never treats "no filter" as "show me everything."
  2. 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 prefilled apify-daily-test value exists only for this Actor's own platform smoke test; replace it with your own watch name.
  3. Leave lookbackDays at its default of 1 unless you have a specific reason to widen it — see Operating guide for why the default is conservative on purpose.
  4. Start the run from the Console, the Apify API, a scheduled Task, or an MCP client.
  5. 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.
  6. Read OUTPUT (the default key-value store's OUTPUT record) for the run-level summary: candidatesMatched, newCandidates, delivered, billed, freeAmbiguous, whether the run stopped early, and why.
  7. Route every found:true row to a human for a bid/no-bid decision. recommendedAction is set to human_review_before_bid_decision specifically 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's title and description combined.
  • 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's updatedFrom/updatedTo window 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 honestly partial. 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, default true. When true, the first run for a new stateKey writes one free, clearly labelled informational row summarizing what was captured as the reference point. When false, 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

FieldMeaningImportant boundary
stateKeyThe sanitized watch name this row belongs toSame value used to partition the baseline in storage
foundThe billing boundarytrue only on a delivered, charged new-tender row; false on every notice, error, baseline-creation and value-ambiguous row, even when isNew is true
isNewWhether this tender's ocid was not present in the saved baselineIndependent of billing — a value-currency-ambiguous match is isNew:true, found:false
baselineCreatedWhether this row belongs to the free, first-ever run for this watchtrue only on that one run
sourceFixed to "findATender"v1 reads Find a Tender only — see Evidence and boundaries for why Contracts Finder is excluded
ocidThe Open Contracting ID — this tender's stable identity within Find a Tendernull on every notice/error row that carries no specific tender
publicationNumberFind a Tender's own release id (format NNNNNN-YYYY)Also the basis of evidenceUrl; null on every notice/error row
titleThe tender's titlenull when no tender applies to this row
buyerThe contracting authority's published namenull when no tender applies to this row
regionBest-effort buyer/delivery region or country textnull when the notice publishes none — never treated as a mismatch on its own
cpvThe tender's CPV classification codenull when no tender applies, or the notice does not publish a CPV code
valueAmountThe tender's published valuenull when the source omits it
valueCurrencyISO 4217 currency code as published by the sourceNot guaranteed to be GBP — see errorCode:"value_currency_ambiguous"
deadlineThe tender's submission deadlinenull when no tender applies, or the source omits it
datePublishedTaken uniformly from the OCDS release's own date fieldFind a Tender releases carry no separate tender.datePublished field at all — this Actor never falls back to inventing one
statusThe source's own tender.status at observation timeEvery delivered row has "active" here — this Actor never sells a non-active notice
evidenceUrlThe canonical, human-readable Find a Tender notice pagenull 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)
matchedKeywordThe first buyer-supplied keyword that matchednull when no keyword filter was set, or no per-row match applies
matchedCpvThe first buyer-supplied CPV prefix that matchednull when no CPV filter was set
confidenceReserved for a future scored/fuzzy matchAlways null in this build — every match is an exact, deterministic filter hit
recommendedActionSet 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
partialTrue when this run's own coverage or delivery was incomplete for a stated reasonNever a silent gap — see partialReason
partialReasonWhich specific limit caused partial:truenull unless partial:true; see Decision routing for the full list
errorCodeBounded machine error codenull on every honestly-completed row, including "nothing new" and baseline-creation
errorHuman-readable message, or the source's own verbatim error textAlways "" (never null/omitted) on an honestly-completed row — this is what separates "answered, nothing new" from "something happened"
retryableWhether re-running this watch soon is expected to helpFixed true for rate_limited; depends on the specific failure for source_error/robots_unreadable
observedAtWhen this row's outcome was producedShared 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."
— a numeric ceiling that is not published in the API's static documentation, only discoverable by triggering it. Recovery was observed in well under the stated 120 seconds on a follow-up light request. Paginating one full 24-hour window (with no 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 shapeBillable?errorCodeWhat it means
New matching tenderYesnullThe one billable outcome — active, filter-matching, not seen before under this watch
Baseline createdNonullFree, first-ever run for this watch; establishes the reference point
Nothing newNonullA later run genuinely found no new matching tenders
Value-currency ambiguousNovalue_currency_ambiguousOtherwise matches, but its value is missing or not in GBP — check evidenceUrl by hand
Held back by run cap / budgetNonull, partial:trueMore matched than maxNewPerRun allowed, or the buyer's charge cap was reached mid-run; never lost, appears on a later run
Partial source coverageNonull, partial:trueThis run's own request budget or a mid-pagination failure stopped it before the full window was read; already-found rows are unaffected
robots_disallowedNorobots_disallowedThe source's own robots.txt explicitly disallows this path (not observed live; the policy is checked fresh every run in case it changes)
robots_unreadableNorobots_unreadablerobots.txt could not be read or parsed — treated fail-closed
rate_limitedNorate_limitedThe source's own measured request ceiling was hit; retryable
source_errorNosource_errorAn upstream timeout, malformed response, or unexpected failure
buyer_budget_exhaustedNobuyer_budget_exhaustedThe buyer's own remaining charge cap could not cover another row; stopped before any source request
baseline_unavailableNobaseline_unavailableThe 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 honest partial:true coverage 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.
  • region matching 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.
  • confidence is reserved for a future fuzzy-matching mode and is always null in 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.
  • recommendedAction never 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.