Scoutee

搜索来自欧洲和北美32个国家87个官方来源的20多万份公开招标(RFP、政府合同)。

文档

Search tenders and find EU CPVs with AI. Register free for one daily email alert, limited to 20 results per search.

Search public tenders from your monitoring script, CRM or internal dashboard. Combine keywords with optional CPV classification filters for EU procurement, read full notices, and browse or download Scoutee's copy of the official CPV catalogue.

Get your CPV codes, found for you. Sign in to Scoutee for free: our AI matches your company's activity against our knowledge base to identify relevant codes automatically. Start with a company name or identifier, website, or activity description in the free AI CPV finder, review your suggestions, then use them in the API or workspace MCP.

Base URL: https://scoutee.org/api

Responses are UTF-8 JSON (application/json), except the catalogue's optional CSV download. Dates are ISO 8601 (2026-09-05T14:30:00Z).

One free daily email alert

Register for free, save your search on Scoutee and enable its daily email alert. Your free account includes one active search alert across your workspaces, only when new tenders match, with up to 20 results per search. The email previews 3 tenders; opening the search keeps the free allowance of 2 searches per hour and one page of 20 results. Paid plans support alerts for multiple searches.

If you ask your assistant to notify you by email, it can guide you to registration and your saved search on Scoutee. Subscription management requires your Scoutee session; the current MCP tools and OAuth REST tender endpoints cannot create subscriptions or schedule email. GET /api/public/plans publishes the per-plan email_alerts allowance, alongside search quotas.

CPV for EU procurement

CPV means Common Procurement Vocabulary, the common public procurement standard across the EU. It is an optional way to match notices by activity. Market eligibility follows the source's procurement jurisdiction, not the interface language; CPV is not a replacement for US or Canadian classifications.

The authenticated tender search accepts up to 50 cpv values, with or without their verified check digit. A selected code also matches its official descendants. CPV codes and keyword values combine with OR; country, budget and other filters still apply. Buyer-published cpv_codes remain separate from Scoutee's cpv_inferred_codes, which are used for matching only when the notice has no recognized published CPV.

curl -s -G "https://scoutee.org/api/tenders" \
  -H "Authorization: Bearer $SCOUTEE_ACCESS_TOKEN" \
  --data-urlencode "cpv=90911200-8" \
  --data-urlencode "country=France"

Public CPV catalogue API

These read-only endpoints need no account or credential. They provide catalogue lookup, not AI company discovery; use the signed-in AI finder for personalized suggestions.

EndpointPurpose
GET /public/classifications/cpvSearch labels/codes with q, resolve repeated exact code values, or browse a parent code.
GET /public/classifications/cpv/catalogueDownload the full catalogue as format=json (default) or format=csv.

Pass locale=fr, en, or another supported label language. Exact code lookup takes precedence over parent, then q. q accepts up to 100 characters and returns up to 50 matches; exact lookup accepts up to 50 codes. Unknown or invalid exact code values are omitted from items. An unknown parent returns 422; unsupported classification systems return 404.

Lookup returns system, items, total_codes and catalogue. Each item includes code, display_code, label, parent, retired and replaced_by. total_codes is the full catalogue size. Catalogue metadata includes the snapshot version, official nomenclature_version, sha256 and source provenance. Keep the version when storing labels so historical codes can be identified. The JSON download includes that metadata; CSV contains the code rows.

curl -s "https://scoutee.org/api/public/classifications/cpv?q=90911200-8&locale=en"
curl -s "https://scoutee.org/api/public/classifications/cpv/catalogue?locale=en&format=csv"

You can also browse the catalogue on Scoutee.

API keys

A key belongs to a workspace, not to a person: it outlives whoever created it, and the workspace administrators can revoke it at any time.

A key can only be created, and only works, while the workspace is on a paid plan — Pro or Team (a legacy paid workspace keeps Pro and Team limits respectively). If the workspace goes back to the free plan, the existing keys stay listed but calls answer 403.

To create a key: your workspace page, section "API", button "Create a key". The full value is shown once, at creation. Scoutee only stores its SHA-256 digest and cannot show it to you again; if you lose it, revoke the key and create another one.

Key format: sct_ followed by 40 hexadecimal characters. The first 12 characters (the prefix, for example sct_1a2b3c4d) are shown in the key list so that you can recognise a key.

A workspace can hold at most 10 active keys. Beyond that, creation answers 409: revoke a key before creating another one.

Authentication

Connect through OAuth on any plan, including Free, with the tenders.read scope. Send the access token as Authorization: Bearer $SCOUTEE_ACCESS_TOKEN. OAuth discovery is available at https://scoutee.org/.well-known/oauth-authorization-server. The token grants access to the selected workspace; its current plan and membership are checked on each call. Revoking the connection stops access immediately.

For scripts, a workspace API key on a paid plan works in X-API-Key: $SCOUTEE_API_KEY. Both credentials are restricted to searching and reading tenders. They do not grant access to billing, company memory or analysis execution. Keep credentials in your client's secret store.

Quotas

Each authenticated search consumes one unit. Reading a single notice is unmetered. OAuth shares a bucket per workspace; API keys each have a bucket. REST and MCP share that credential's bucket.

PlanSearches/hourBurst/minuteResults/pageMax pageOAuthAPI key
free22201YesNo
pro100105020YesYes
team3003010050YesYes

Standard inherits Pro limits; Premium inherits Team limits. These are traffic limits per API process, not purchased analysis credits. Responses include X-Quota-Plan, X-Quota-Limit and X-Quota-Remaining. MCP returns quota_plan, quota_limit and quota_remaining. Respect Retry-After on HTTP 429. The returned page size/depth may be capped: do not assume every matching notice is reachable.

GET /tenders

Paginated search. By default: open tenders only, newest first. Notices published on several portals are deduplicated: one row per notice, the other portals appearing in also_on.

Parameters

All optional, passed in the query string.

ParameterTypeDefaultDescription
workspace_idintegernoneIgnored with an API key: the key is already bound to a workspace.
pageinteger, minimum 11Requested page. Clamped to 500.
page_sizeinteger, 1 to 50050Results per page. Clamped to 200.
qstring, 200 characters at mostnoneFree text over the title, the buyer and the description. Every word must match the start of a word in the notice (nettoy finds nettoyage); a substring inside a word does not match. Case and accent insensitive.
keywordstring, repeatablenoneKeywords. Whole-word match (plural tolerated), case and accent insensitive, in the title, the buyer or the description, plus the cached translations of the keyword on the sources of that language; with a country selected, only the translations in that country's languages and in English are searched. Several keyword widen the search (any of them).
cpvstring, repeatablenoneOptional CPV codes: eight digits, with an optional verified check digit. Includes descendants in the official catalogue. Codes and keywords combine with OR; other filters still apply. Only sources in CPV jurisdictions can match. Maximum 50 codes; unknown or invalid codes return 422.
exclude_terminteger, repeatablenoneIdentifiers of terms (translations or variants of a keyword) to drop from the search.
countrystring, repeatablenoneCountries of the portal, by their English name (France, Germany, United States, Canada; Europe for TED). Several country add up. The by_country object of any response lists the exact values in use.
sectorstring of two digits, repeatablenoneSectors, that is CPV 2008 divisions: the first two digits of a CPV code (45 construction work, 72 IT services, 85 health and social work). A notice matches when it belongs to any of the given sectors. Never applied unless asked: with no sector, every sector is searched. A value outside the 45 divisions answers 422 with the code invalid_sector. The by_sector object of a response lists the divisions in use.
min_valuenumbernoneMinimum estimated value, in the currency of the notice.
max_valuenumbernoneMaximum estimated value.
sortnewest, oldest or deadlinenewestPublication descending, publication ascending, or deadline ascending.
include_closedbooleanfalseInclude closed notices.
seen_afterISO 8601 date-timenoneKeep only the notices first collected after that instant. Useful for incremental monitoring.

| seen_before | ISO 8601 | none | Keep notices first collected at or before this instant. Fix this value while paging an incremental window. |

Response

200 OK, a TenderPage object:

FieldTypeDescription
itemsarray of TenderThe notices of this page.
totalintegerTotal number of notices matching the criteria.
pageintegerPage actually returned.
page_sizeintegerPage size actually applied.
pagesintegerNumber of reachable pages, capped by the plan.
by_countryobject, country code to integerNotices per portal country, with every other filter applied but without the country filter. Meant to build a facet.
by_sectorobject, two-digit division to integerNotices per sector, with every other filter applied — country included — but without the sector filter. Meant to build a facet. A notice carrying several sectors is counted once per sector, so the values do not add up to total.

A Tender object:

FieldTypeDescription
idintegerScoutee identifier of the notice.
source_idintegerIdentifier of the portal it came from.
source_namestring, nullableName of the portal.
source_countrystring, nullableCountry of the portal, two letters.
external_idstringIdentifier of the notice on the portal.
titlestringTitle of the consultation.
buyerstring, nullablePublic buyer.
descriptionstring, nullableSubject of the contract, as published.
urlstringPage of the notice on the source portal.
locationstring, nullablePlace of performance.
procedurestring, nullableProcedure type, as published.
cpv_codesarray of stringsCPV codes attached to the notice.
cpv_inferred_codesarray of stringsScoutee CPV suggestions, separate from buyer-published codes. Used for search only when no recognized published CPV exists.
sectorsarray of two-digit stringsSectors (CPV divisions) of the notice: the divisions of its CPV codes when it has any, otherwise the single division our classifier gave it. Empty when neither applies.
estimated_valuenumber, nullableEstimated value.
currencystring, nullableCurrency of that value.
published_atdate-time, nullablePublication date.
deadline_atdate-time, nullableDeadline for submissions.
first_seen_atdate-timeFirst collection by Scoutee.
last_seen_atdate-timeLast collection.
closed_atdate-time, nullableClosure observed. null while the notice is open.
favoritebooleantrue when the tender is a favorite of the workspace. Favorites belong to the workspace and are shared by its members; with an API key, the key's workspace.
also_onarray of AlsoOnOther portals that published the same notice.

An AlsoOn object: source_id (integer), source_name (string), source_country (string, nullable), url (string).

Example

curl -s -G "https://scoutee.org/api/tenders" \
  -H "X-API-Key: $SCOUTEE_API_KEY" \
  --data-urlencode "keyword=roadworks" \
  --data-urlencode "keyword=signage" \
  --data-urlencode "country=France" \
  --data-urlencode "sort=deadline" \
  --data-urlencode "page_size=50"
{
  "items": [
    {
      "id": 918233,
      "source_id": 12,
      "source_name": "PLACE",
      "source_country": "FR",
      "external_id": "25-114287",
      "title": "Travaux de voirie et de signalisation horizontale",
      "buyer": "Communauté de communes du Val de Loire",
      "description": "Marché à bons de commande pour la réfection de voirie...",
      "url": "https://www.marches-publics.gouv.fr/?page=Entreprise.EntrepriseAdvancedSearch&id=25-114287",
      "location": "Loiret",
      "procedure": "Procédure adaptée",
      "cpv_codes": ["45233220", "45233221"],
      "sectors": ["45"],
      "estimated_value": 420000.0,
      "currency": "EUR",
      "published_at": "2026-09-01T08:00:00Z",
      "deadline_at": "2026-10-03T12:00:00Z",
      "first_seen_at": "2026-09-01T09:12:44Z",
      "last_seen_at": "2026-09-05T06:03:11Z",
      "closed_at": null,
      "favorite": false,
      "also_on": []
    }
  ],
  "total": 137,
  "page": 1,
  "page_size": 50,
  "pages": 3,
  "by_country": { "France": 137, "Belgium": 12 },
  "by_sector": { "45": 96, "71": 28, "50": 13 }
}

Notices are returned in the language they were published in: the API does not translate them.

In Python, with requests:

import os
import requests

BASE = "https://scoutee.org/api"
HEADERS = {"X-API-Key": os.environ["SCOUTEE_API_KEY"]}

response = requests.get(
    f"{BASE}/tenders",
    headers=HEADERS,
    params={
        "keyword": ["roadworks", "signage"],
        "country": ["France"],
        "sort": "deadline",
        "page_size": 50,
    },
    timeout=30,
)
response.raise_for_status()
page = response.json()

print(page["total"], "notices", "|", response.headers["X-Quota-Remaining"], "searches left")
for tender in page["items"]:
    print(tender["id"], tender["deadline_at"], tender["title"])

Walking every page:

def iter_tenders(**params):
    """Every page of a search, in the requested order."""
    page = 1
    while True:
        response = requests.get(
            f"{BASE}/tenders",
            headers=HEADERS,
            params={**params, "page": page, "page_size": 200},
            timeout=30,
        )
        response.raise_for_status()
        body = response.json()
        yield from body["items"]
        if page >= body["pages"]:
            return
        page += 1

For incremental monitoring, keep the timestamp of your last run and pass it back as seen_after: only the notices discovered since then come back.

GET /tenders/{id}

One notice, enriched like a search result. Consumes no quota.

curl -s "https://scoutee.org/api/tenders/918233" \
  -H "X-API-Key: $SCOUTEE_API_KEY"

The response is a Tender object, identical to those of items. If the requested identifier points at the copy of a notice published on several portals, the canonical copy is returned. An unknown identifier answers 404.

MCP server

The same two operations are exposed as an MCP server at https://scoutee.org/api/mcp, with the same OAuth token or workspace key, the same parameters and the same quotas, so Claude Code, Cursor, VS Code, Windsurf or any client of the protocol can search tenders on your behalf. Setup, tools and examples are on the MCP server page.

Errors

An error body is always {"detail": "..."}, and the message is always in English, whatever the language of your integration: English is the working language of the API. Every error a user can meet also carries a stable code in the X-Error-Code response header, so your integration can branch on the code and write its own message instead of matching on the text.

CodeX-Error-CodeCasedetail
401No credentials on a tender endpoint (catalogue endpoints are public)Login required
401invalid_api_keyKey unknown or revokedInvalid API key
402plan_requiredCreating or revoking a key on a workspace without a paid planThis feature requires a paid plan
403api_key_disabledThe workspace of the key is not on a paid plan (left it, or never had one)API key disabled: this workspace is not on a paid plan
403api_key_scopeA protected path outside tender search and detailThis key only grants access to the tender search
404Unknown notice identifierTender not found
429quota_exceededHourly quota exhaustedYou have reached the limit of 100 searches per hour of your plan. Try again in 12 min.
429search_burstToo many requests in one minuteToo many searches at once, try again in a minute

A quota_exceeded answer repeats its numbers in X-Error-Limit (searches an hour) and X-Error-Minutes (the wait), so a message can be rebuilt in any language.

A 429 carries a Retry-After header, in seconds. Honour it rather than retrying at once:

import time

def search(**params):
    """One search, waiting out the per-minute quota when it is hit."""
    for _ in range(3):
        response = requests.get(f"{BASE}/tenders", headers=HEADERS, params=params, timeout=30)
        if response.status_code != 429:
            response.raise_for_status()
            return response.json()
        time.sleep(int(response.headers.get("Retry-After", "60")))
    raise RuntimeError("quota still exhausted after three attempts")

402 and 403 are not fixed by retrying: check the plan of the workspace, or create a new key if yours was revoked.

Over MCP the same failures come back as tool errors carrying the same messages, rather than as HTTP status codes.

Resources