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-Key ou dans Authorization: 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 pour get_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.

Ressources