API Scoutee : documentation développeur
Documentation de l'API Scoutee : clé X-API-Key, recherche d'appels d'offres GET /tenders, paramètres, format des réponses, quotas, codes d'erreur et serveur MCP.
Mis à jour le 7 septembre 2026
L'API Scoutee donne accès en lecture à la base d'appels d'offres depuis vos propres outils : script de veille, CRM, tableau de bord interne. Elle expose deux points d'entrée, la recherche et le détail d'un avis, ainsi que les deux mêmes opérations sous forme de serveur MCP pour les assistants.
Base URL : https://scoutee.org/api
Toutes les réponses sont en JSON (application/json), en UTF-8. Les dates sont au format ISO 8601 (2026-09-05T14:30:00Z).
Clés API
Une clé appartient à un workspace, pas à une personne : elle survit au départ de celui qui l'a créée, et les administrateurs du workspace peuvent la révoquer à tout moment.
Une clé n'est créée et n'est utilisable que si le workspace est sur une offre payante — Standard, ou Beta. Si le workspace repasse sur l'offre gratuite, les clés existantes restent listées mais les appels répondent 403.
Pour créer une clé : page de votre workspace, section « API », bouton « Créer une clé ». La valeur complète n'est affichée qu'une fois, à la création. Scoutee n'en conserve que l'empreinte SHA-256 et ne peut donc pas vous la redonner ; en cas de perte, révoquez la clé et créez-en une nouvelle.
Format d'une clé : sct_ suivi de 40 caractères hexadécimaux. Les 12 premiers caractères (le préfixe, par exemple sct_1a2b3c4d) sont affichés dans la liste des clés pour vous permettre de les reconnaître.
Un workspace peut avoir 10 clés actives au maximum. Au-delà, la création répond 409 : révoquez une clé avant d'en créer une autre.
Authentification
Toutes les requêtes portent la clé dans l'en-tête X-API-Key :
curl -s "https://scoutee.org/api/tenders?page_size=5" \
-H "X-API-Key: sct_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
La clé est une identité de lecture seule : elle n'ouvre que GET /tenders et GET /tenders/{id}. Tout autre chemin répond 403, y compris les routes de gestion des clés elles-mêmes, qui exigent une session utilisateur.
La clé n'est jamais facultative. L'API n'a ni accès anonyme ni accès en offre gratuite : elle n'existe que pour les workspaces sur une offre payante. Une requête sans X-API-Key répond 401 {"detail": "Login required"}, et une clé inconnue ou révoquée répond 401 {"detail": "Clé API invalide"}.
Ne mettez jamais une clé dans du code exécuté par un navigateur ou dans un dépôt public : elle donne accès aux résultats complets que paie votre workspace.
Quotas
Chaque recherche consomme une unité de quota. Le compteur est tenu par clé : deux clés du même workspace ne se partagent pas le même seau. Le détail d'un avis (GET /tenders/{id}) ne consomme rien.
| Offre | Recherches par heure | Rafale par minute | Résultats par page | Page maximale |
|---|---|---|---|---|
| Standard | 10 000 | 240 | 200 | 500 |
| Beta | 10 000 | 240 | 200 | 500 |
Il ne peut y avoir que ces deux lignes : une clé n'existe que sur une offre payante, et les deux offres payantes portent exactement les mêmes limites. Que votre workspace soit en Standard ou en Beta, une clé dispose de 10 000 recherches par heure, 240 par minute, 200 résultats par page et 500 pages au maximum. (Les limites plus serrées appliquées aux visiteurs et à l'offre gratuite ne concernent que la recherche du site : rien n'atteint l'API sans clé.)
Les valeurs demandées au-delà de ces plafonds ne sont pas une erreur : elles sont ramenées au plafond. page_size=500 renvoie 200 résultats, et le champ page_size de la réponse indique la valeur réellement appliquée.
Chaque réponse de recherche porte trois en-têtes :
| En-tête | Contenu |
|---|---|
X-Quota-Limit |
Nombre de recherches autorisées par heure |
X-Quota-Remaining |
Recherches restantes dans l'heure en cours |
X-Quota-Plan |
Offre appliquée — celle du workspace, standard ou premium (le nom interne de l'offre Beta) |
GET /tenders
Recherche paginée. Par défaut : appels d'offres ouverts uniquement, du plus récent au plus ancien. Les avis publiés sur plusieurs portails sont dédupliqués : une seule ligne par avis, les autres portails apparaissant dans also_on.
Paramètres
Tous facultatifs, passés en query string.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
workspace_id |
entier | aucun | Ignoré avec une clé API : la clé est déjà liée à un workspace. |
page |
entier, minimum 1 | 1 |
Page demandée. Ramenée à 500 au-delà. |
page_size |
entier, 1 à 500 | 50 |
Résultats par page. Ramené à 200 au-delà. |
source_id |
entier | aucun | Ne garder que les avis d'un portail précis. |
q |
chaîne, 200 caractères au plus | aucun | Recherche libre dans le titre, l'acheteur et la description. Chaque mot doit correspondre au début d'un mot de l'avis (nettoy trouve nettoyage) ; une sous-chaîne au milieu d'un mot ne correspond pas. Insensible à la casse et aux accents. |
keyword |
chaîne, répétable | aucun | Mots-clés. Correspondance sur mots entiers (pluriel toléré), insensible à la casse et aux accents, dans le titre, l'acheteur ou la description, plus les traductions du mot-clé sur les sources de la langue concernée. Plusieurs keyword élargissent la recherche (au moins l'un d'eux). |
exclude_term |
entier, répétable | aucun | Identifiants de termes (traductions ou variantes d'un mot-clé) à retirer de la recherche. |
country |
chaîne, répétable | aucun | Pays du portail, par leur nom anglais (France, Belgium, Germany ; Europe pour TED). Plusieurs country sont cumulatifs. L'objet by_country d'une réponse liste les valeurs exactes en usage. |
sector |
chaîne de deux chiffres, répétable | aucun | Secteurs, c'est-à-dire divisions CPV 2008 : les deux premiers chiffres d'un code CPV (45 travaux de construction, 72 services informatiques, 85 santé et action sociale). Un avis correspond dès qu'il appartient à l'un des secteurs demandés. Jamais appliqué sans demande explicite : sans sector, tous les secteurs sont cherchés. Une valeur hors des 45 divisions renvoie 422 avec le code invalid_sector. L'objet by_sector d'une réponse liste les divisions en usage. |
min_value |
nombre | aucun | Montant estimé minimum, dans la devise de l'avis. |
max_value |
nombre | aucun | Montant estimé maximum. |
sort |
newest, oldest ou deadline |
newest |
Tri : publication décroissante, publication croissante, ou date limite croissante. |
include_closed |
booléen | false |
Inclure les avis clôturés. |
seen_after |
date-heure ISO 8601 | aucun | Ne garder que les avis vus pour la première fois après cet instant. Utile pour une veille incrémentale. |
Réponse
200 OK, objet TenderPage :
| Champ | Type | Description |
|---|---|---|
items |
tableau de Tender |
Les avis de la page. |
total |
entier | Nombre total d'avis correspondant aux critères. |
page |
entier | Page réellement renvoyée. |
page_size |
entier | Taille de page réellement appliquée. |
pages |
entier | Nombre de pages accessibles, plafonné par l'offre. |
by_country |
objet, code pays vers entier | Nombre d'avis par pays du portail, tous les autres filtres appliqués mais sans le filtre country. Sert à construire une facette. |
by_sector |
objet, division à deux chiffres vers entier | Nombre d'avis par secteur, tous les autres filtres appliqués — country compris — mais sans le filtre sector. Sert à construire une facette. Un avis portant plusieurs secteurs est compté une fois par secteur : la somme ne vaut donc pas total. |
Un objet Tender :
| Champ | Type | Description |
|---|---|---|
id |
entier | Identifiant Scoutee de l'avis. |
source_id |
entier | Identifiant du portail d'origine. |
source_name |
chaîne, nullable | Nom du portail. |
source_country |
chaîne, nullable | Pays du portail, deux lettres. |
external_id |
chaîne | Identifiant de l'avis chez le portail. |
title |
chaîne | Intitulé de la consultation. |
buyer |
chaîne, nullable | Acheteur public. |
description |
chaîne, nullable | Objet du marché, tel que publié. |
url |
chaîne | Page de l'avis sur le portail d'origine. |
location |
chaîne, nullable | Lieu d'exécution. |
procedure |
chaîne, nullable | Type de procédure, tel que publié. |
cpv_codes |
tableau de chaînes | Codes CPV rattachés à l'avis. |
sectors |
tableau de chaînes de deux chiffres | Secteurs (divisions CPV) de l'avis : les divisions de ses codes CPV quand il en a, sinon l'unique division attribuée par notre classement automatique. Vide si ni l'un ni l'autre. |
estimated_value |
nombre, nullable | Montant estimé. |
currency |
chaîne, nullable | Devise du montant. |
published_at |
date-heure, nullable | Date de publication. |
deadline_at |
date-heure, nullable | Date limite de remise des offres. |
first_seen_at |
date-heure | Première collecte par Scoutee. |
last_seen_at |
date-heure | Dernière collecte. |
closed_at |
date-heure, nullable | Clôture constatée. null si l'avis est ouvert. |
favorite |
booléen | Toujours false avec une clé API : les favoris sont propres à un utilisateur. |
also_on |
tableau de AlsoOn |
Autres portails ayant publié le même avis. |
Un objet AlsoOn : source_id (entier), source_name (chaîne), source_country (chaîne, nullable), url (chaîne).
Exemple
curl -s -G "https://scoutee.org/api/tenders" \
-H "X-API-Key: $SCOUTEE_API_KEY" \
--data-urlencode "keyword=voirie" \
--data-urlencode "keyword=signalisation" \
--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 }
}
En Python, avec 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": ["voirie", "signalisation"],
"country": ["France"],
"sort": "deadline",
"page_size": 50,
},
timeout=30,
)
response.raise_for_status()
page = response.json()
print(page["total"], "avis", "|", response.headers["X-Quota-Remaining"], "recherches restantes")
for tender in page["items"]:
print(tender["id"], tender["deadline_at"], tender["title"])
Parcourir toutes les pages :
def iter_tenders(**params):
"""Toutes les pages d'une recherche, dans l'ordre demandé."""
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
Pour une veille incrémentale, gardez la date du dernier passage et repassez-la en seen_after : seuls les avis découverts depuis reviennent.
GET /tenders/{id}
Un avis, enrichi comme un résultat de recherche. Ne consomme pas de quota.
curl -s "https://scoutee.org/api/tenders/918233" \
-H "X-API-Key: $SCOUTEE_API_KEY"
La réponse est un objet Tender, identique à ceux de items. Si l'identifiant demandé désigne la copie d'un avis publié sur plusieurs portails, c'est la copie de référence qui est renvoyée. Un identifiant inconnu répond 404.
Serveur MCP
Les deux mêmes opérations sont exposées sous forme de serveur MCP : un assistant — Claude Code, Cursor, VS Code, Windsurf, ou tout autre client du protocole — peut ainsi chercher des appels d'offres pour vous sans que vous écriviez une ligne de HTTP.
- URL :
https://scoutee.org/api/mcp - Transport : Streamable HTTP, sans session, réponses JSON (aucun flux à maintenir ouvert, donc compatible avec n'importe quel proxy)
- Authentification : la même clé de workspace, dans
X-API-Keyou dansAuthorization: Bearer, et tout aussi obligatoire — un appel sans clé revient en erreur d'outil réclamant une clé - Quotas : identiques au REST — 10 000 recherches par heure sur l'une comme sur l'autre offre payante, une unité par appel à
search_tenders, rien pourget_tender, comptées dans le même seau par clé
Outils
search_tenders reprend les paramètres de GET /tenders, avec la même sémantique : q, keyword (tableau), country (tableau), source_id, min_value, max_value, sort (newest, oldest, deadline), include_closed, seen_after, page et page_size. Tous facultatifs. page_size vaut 20 par défaut plutôt que 50, un avis étant un objet volumineux à donner à un modèle, et reste plafonné à 200. Le résultat est un TenderPage augmenté de trois champs qui portent ce que portent les en-têtes de quota en HTTP : quota_plan, quota_limit et quota_remaining.
get_tender prend un seul tender_id et renvoie le même objet Tender que GET /tenders/{id}. Il ne consomme pas de quota.
Claude Code
claude mcp add --transport http scoutee https://scoutee.org/api/mcp \
--header "X-API-Key: sct_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
Cursor, VS Code, Windsurf et les autres clients
La plupart lisent un fichier de configuration contenant un objet mcpServers (.cursor/mcp.json, .vscode/mcp.json, ~/.codeium/windsurf/mcp_config.json…) :
{
"mcpServers": {
"scoutee": {
"url": "https://scoutee.org/api/mcp",
"headers": {
"X-API-Key": "sct_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
}
}
}
}
Python
Avec le paquet mcp (pip install mcp) :
import asyncio
import os
import httpx2
from mcp.client import Client
from mcp.client.streamable_http import streamable_http_client
URL = "https://scoutee.org/api/mcp"
async def main():
headers = {"X-API-Key": os.environ["SCOUTEE_API_KEY"]}
async with httpx2.AsyncClient(headers=headers) as http:
async with Client(streamable_http_client(URL, http_client=http)) as client:
print([tool.name for tool in (await client.list_tools()).tools])
result = await client.call_tool(
"search_tenders",
{"keyword": ["voirie"], "country": ["France"], "page_size": 10},
)
page = result.structured_content
print(page["total"], "avis |", page["quota_remaining"], "recherches restantes")
for tender in page["items"]:
print(tender["id"], tender["deadline_at"], tender["title"])
asyncio.run(main())
Un appel en échec revient avec is_error positionné et le message anglais qu'aurait renvoyé l'API REST, suivi de son code stable entre crochets (Invalid API key [invalid_api_key]) : clé inconnue, quota épuisé, identifiant introuvable. Les codes sont ceux de la section Erreurs ci-dessous.
Erreurs
Le corps d'une erreur est toujours {"detail": "..."}, et le message est toujours en anglais, quelle que soit la langue de votre intégration : l'anglais est la langue de travail de l'API. Chaque erreur qu'un utilisateur peut rencontrer porte aussi un code stable dans l'en-tête de réponse X-Error-Code : votre intégration s'appuie sur ce code et rédige son propre message, plutôt que de comparer le texte.
| Code | X-Error-Code |
Cas | detail |
|---|---|---|---|
| 401 | — | Aucune clé (il n'y a pas d'accès anonyme) | Login required |
| 401 | invalid_api_key |
Clé inconnue ou révoquée | Invalid API key |
| 402 | plan_required |
Création ou révocation d'une clé sur un workspace sans offre payante | This feature requires a paid plan |
| 403 | api_key_disabled |
Le workspace de la clé n'est pas sur une offre payante (il l'a quittée, ou n'en a jamais eu) | API key disabled: this workspace is not on a paid plan |
| 403 | api_key_scope |
Chemin autre que la recherche d'appels d'offres | This key only grants access to the tender search |
| 404 | — | Identifiant d'avis inconnu | Tender not found |
| 429 | quota_exceeded |
Quota horaire épuisé | You have reached the limit of 10000 searches per hour of your plan. Try again in 12 min. |
| 429 | search_burst |
Trop de requêtes en une minute | Too many searches at once, try again in a minute |
Une réponse quota_exceeded répète ses nombres dans X-Error-Limit (recherches par heure) et X-Error-Minutes (l'attente) : le message peut ainsi être reconstruit dans n'importe quelle langue.
Les réponses 429 portent un en-tête Retry-After, en secondes. Respectez-le plutôt que de réessayer immédiatement :
import time
def search(**params):
"""Une recherche, avec attente si le quota par minute est atteint."""
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 toujours épuisé après trois tentatives")
Les codes 402 et 403 ne se corrigent pas en réessayant : vérifiez l'offre du workspace, ou créez une nouvelle clé si la vôtre a été révoquée.
En MCP, les mêmes échecs reviennent sous forme d'erreurs d'outil portant les mêmes messages, et non de codes HTTP.