Scoutee
Busca más de 200,000 licitaciones públicas (RFPs, contratos gubernamentales) de 87 fuentes oficiales en 32 países de Europa y Norteamérica.
Documentación
Scoutee documentación de la API: autenticación X-API-Key, la búsqueda GET /tenders, cada parámetro de consulta, formas de respuesta, cuotas por plan, códigos de error y el servidor MCP.
La API de Scoutee da acceso de lectura a la base de datos de licitaciones desde tus propias herramientas: un script de monitoreo, un CRM, un panel interno. Expone dos endpoints, la búsqueda y un aviso individual, y las mismas dos operaciones como servidor MCP para asistentes.
URL base: https://scoutee.org/api
Cada respuesta es JSON (application/json), UTF-8. Las fechas están en ISO 8601 (2026-09-05T14:30:00Z).
Claves de API
Una clave pertenece a un espacio de trabajo, no a una persona: sobrevive a quien la creó, y los administradores del espacio de trabajo pueden revocarla en cualquier momento.
Una clave solo se puede crear, y solo funciona, mientras el espacio de trabajo esté en un plan de pago — Standard o Beta. Si el espacio de trabajo vuelve al plan gratuito, las claves existentes permanecen listadas pero las llamadas responden 403.
Para crear una clave: página de tu espacio de trabajo, sección "API", botón "Crear una clave". El valor completo se muestra una sola vez, en la creación. Scoutee solo almacena su digesto SHA-256 y no puede mostrártelo de nuevo; si la pierdes, revoca la clave y crea otra.
Formato de clave: sct_ seguido de 40 caracteres hexadecimales. Los primeros 12 caracteres (el prefijo, por ejemplo sct_1a2b3c4d) se muestran en la lista de claves para que puedas reconocer una clave.
Un espacio de trabajo puede tener como máximo 10 claves activas. Más allá de eso, la creación responde 409: revoca una clave antes de crear otra.
Autenticación
Cada solicitud lleva la clave en el encabezado X-API-Key:
curl -s "https://scoutee.org/api/tenders?page_size=5" \
-H "X-API-Key: sct_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
Una clave es una credencial de solo lectura: abre GET /tenders y GET /tenders/{id}, nada más. Cualquier otra ruta responde 403, incluidas las propias rutas de gestión de claves, que requieren una sesión iniciada.
La clave nunca es opcional. La API no tiene acceso anónimo ni acceso de plan gratuito: existe solo para espacios de trabajo en un plan de pago. Una solicitud sin X-API-Key responde 401 {"detail": "Login required"}, y una clave desconocida o revocada responde 401 {"detail": "Clé API invalide"}.
Nunca pongas una clave en código que se ejecute en un navegador, ni en un repositorio público: abre los resultados de búsqueda completos que tu espacio de trabajo paga.
Cuotas
Cada búsqueda consume una unidad de cuota. El contador se mantiene por clave: dos claves del mismo espacio de trabajo no comparten un mismo depósito. Obtener un aviso individual (GET /tenders/{id}) no consume nada.
| Plan | Búsquedas por hora | Ráfaga por minuto | Resultados por página | Página más profunda |
|---|---|---|---|---|
| Standard | 10,000 | 240 | 200 | 500 |
| Beta | 10,000 | 240 | 200 | 500 |
Estas son las únicas dos filas que pueden existir: una clave solo existe en un plan de pago, y ambos planes de pago llevan exactamente los mismos límites. Sea cual sea el plan, Standard o Beta, en el que esté tu espacio de trabajo, una clave obtiene 10,000 búsquedas por hora, 240 por minuto, 200 resultados por página y 500 páginas como máximo. (Los límites más estrictos que el sitio web aplica a los visitantes y al plan gratuito son asunto de la propia búsqueda del sitio; nada llega a la API sin una clave.)
Pedir más de estos límites no es un error: el valor se ajusta. page_size=500 devuelve 200 resultados, y el campo page_size de la respuesta informa el valor realmente aplicado.
Cada respuesta de búsqueda lleva tres encabezados:
| Encabezado | Contenido |
|---|---|
X-Quota-Limit | Búsquedas permitidas por hora |
X-Quota-Remaining | Búsquedas restantes en la hora actual |
X-Quota-Plan | Plan aplicado — el plan propio del espacio de trabajo, standard o premium (el nombre interno de Beta) |
GET /tenders
Búsqueda paginada. Por defecto: solo licitaciones abiertas, más recientes primero. Los avisos publicados en varios portales se deduplican: una fila por aviso, los otros portales aparecen en also_on.
Parámetros
Todos opcionales, pasados en la cadena de consulta.
| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
workspace_id | entero | ninguno | Ignorado con una clave de API: la clave ya está vinculada a un espacio de trabajo. |
page | entero, mínimo 1 | 1 | Página solicitada. Ajustada a 500. |
page_size | entero, 1 a 500 | 50 | Resultados por página. Ajustados a 200. |
source_id | entero | ninguno | Conservar solo los avisos de un portal. |
q | cadena, 200 caracteres como máximo | ninguno | Texto libre sobre el título, el comprador y la descripción. Cada palabra debe coincidir con el inicio de una palabra en el aviso (nettoy encuentra nettoyage); una subcadena dentro de una palabra no coincide. Insensible a mayúsculas y acentos. |
keyword | cadena, repetible | ninguno | Palabras clave. Coincidencia de palabra completa (plural tolerado), insensible a mayúsculas y acentos, en el título, el comprador o la descripción, más las traducciones en caché de la palabra clave en las fuentes de ese idioma. Varios keyword amplían la búsqueda (cualquiera de ellos). |
exclude_term | entero, repetible | ninguno | Identificadores de términos (traducciones o variantes de una palabra clave) para excluir de la búsqueda. |
country | cadena, repetible | ninguno | Países del portal, por su nombre en inglés (France, Belgium, Germany; Europe para TED). Varios country se suman. El objeto by_country de cualquier respuesta enumera los valores exactos en uso. |
sector | cadena de dos dígitos, repetible | ninguno | Sectores, es decir, divisiones CPV 2008: los primeros dos dígitos de un código CPV (45 trabajos de construcción, 72 servicios de TI, 85 salud y trabajo social). Un aviso coincide cuando pertenece a cualquiera de los sectores dados. Nunca se aplica a menos que se pida: sin sector, se busca en todos los sectores. Un valor fuera de las 45 divisiones responde 422 con el código invalid_sector. El objeto by_sector de una respuesta enumera las divisiones en uso. |
min_value | número | ninguno | Valor estimado mínimo, en la moneda del aviso. |
max_value | número | ninguno | Valor estimado máximo. |
sort | newest, oldest o deadline | newest | Publicación descendente, publicación ascendente o fecha límite ascendente. |
include_closed | booleano | false | Incluir avisos cerrados. |
seen_after | fecha-hora ISO 8601 | ninguno | Conservar solo los avisos recopilados por primera vez después de ese instante. Útil para monitoreo incremental. |
Respuesta
200 OK, un objeto TenderPage:
| Campo | Tipo | Descripción |
|---|---|---|
items | matriz de Tender | Los avisos de esta página. |
total | entero | Número total de avisos que coinciden con los criterios. |
page | entero | Página realmente devuelta. |
page_size | entero | Tamaño de página realmente aplicado. |
pages | entero | Número de páginas alcanzables, limitado por el plan. |
by_country | objeto, código de país a entero | Avisos por país del portal, con todos los demás filtros aplicados pero sin el filtro country. Pensado para construir una faceta. |
by_sector | objeto, división de dos dígitos a entero | Avisos por sector, con todos los demás filtros aplicados — country incluido — pero sin el filtro sector. Pensado para construir una faceta. Un aviso que lleva varios sectores se cuenta una vez por sector, por lo que los valores no suman total. |
Un objeto Tender:
| Campo | Tipo | Descripción |
|---|---|---|
id | entero | Identificador Scoutee del aviso. |
source_id | entero | Identificador del portal del que proviene. |
source_name | cadena, anulable | Nombre del portal. |
source_country | cadena, anulable | País del portal, dos letras. |
external_id | cadena | Identificador del aviso en el portal. |
title | cadena | Título de la consulta. |
buyer | cadena, anulable | Comprador público. |
description | cadena, anulable | Objeto del contrato, tal como se publicó. |
url | cadena | Página del aviso en el portal de origen. |
location | cadena, anulable | Lugar de ejecución. |
procedure | cadena, anulable | Tipo de procedimiento, tal como se publicó. |
cpv_codes | matriz de cadenas | Códigos CPV adjuntos al aviso. |
sectors | matriz de cadenas de dos dígitos | Sectores (divisiones CPV) del aviso: las divisiones de sus códigos CPV cuando los tiene, de lo contrario la división única que nuestro clasificador le asignó. Vacío cuando ninguno se aplica. |
estimated_value | número, anulable | Valor estimado. |
currency | cadena, anulable | Moneda de ese valor. |
published_at | fecha-hora, anulable | Fecha de publicación. |
deadline_at | fecha-hora, anulable | Fecha límite para presentaciones. |
first_seen_at | fecha-hora | Primera recopilación por Scoutee. |
last_seen_at | fecha-hora | Última recopilación. |
closed_at | fecha-hora, anulable | Cierre observado. null mientras el aviso está abierto. |
favorite | booleano | Siempre false con una clave de API: los favoritos pertenecen a un usuario. |
also_on | matriz de AlsoOn | Otros portales que publicaron el mismo aviso. |
Un objeto AlsoOn: source_id (entero), source_name (cadena), source_country (cadena, anulable), url (cadena).
Ejemplo
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 }
}
Los avisos se devuelven en el idioma en que se publicaron: la API no los traduce.
En Python, con 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"])
Recorriendo cada página:
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
Para monitoreo incremental, conserva la marca de tiempo de tu última ejecución y pásala de vuelta como seen_after: solo vuelven los avisos descubiertos desde entonces.
GET /tenders/{id}
Un aviso, enriquecido como un resultado de búsqueda. No consume cuota.
curl -s "https://scoutee.org/api/tenders/918233" \
-H "X-API-Key: $SCOUTEE_API_KEY"
La respuesta es un objeto Tender, idéntico a los de items. Si el identificador solicitado apunta a la copia de un aviso publicado en varios portales, se devuelve la copia canónica. Un identificador desconocido responde 404.
Servidor MCP
Las mismas dos operaciones se exponen como un servidor MCP, para que un asistente — Claude Code, Cursor, VS Code, Windsurf, o cualquier otra cosa que hable el protocolo — pueda buscar licitaciones en tu nombre sin que escribas una línea de HTTP.
- URL:
https://scoutee.org/api/mcp - Transporte: Streamable HTTP, sin estado, respuestas JSON (sin flujo que mantener abierto, por lo que funciona a través de cualquier proxy)
- Autenticación: la misma clave del espacio de trabajo, en
X-API-Keyo enAuthorization: Bearer, e igual de obligatoria — una llamada sin ella vuelve como un error de herramienta pidiendo una clave - Cuotas: idénticas a REST — 10,000 búsquedas por hora en cualquiera de los planes de pago, una unidad por llamada
search_tenders, nada paraget_tender, contadas en el mismo depósito por clave
Herramientas
search_tenders toma los parámetros de consulta de GET /tenders, con la misma semántica: q, keyword (matriz), country (matriz), source_id, min_value, max_value, sort (newest, oldest, deadline), include_closed, seen_after, page y page_size. Todos opcionales. page_size tiene como predeterminado 20 en lugar de 50, ya que un aviso es un objeto grande para entregar a un modelo, y sigue limitado a 200. El resultado es un TenderPage más tres campos que llevan lo que los encabezados de cuota llevan por HTTP: quota_plan, quota_limit y quota_remaining.
get_tender toma un único tender_id y devuelve el mismo objeto Tender que GET /tenders/{id}. No consume cuota.
Claude Code
claude mcp add --transport http scoutee https://scoutee.org/api/mcp \
--header "X-API-Key: sct_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
Cursor, VS Code, Windsurf y otros clientes
La mayoría de ellos leen un archivo de configuración que contiene un objeto 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
Con el paquete 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": ["roadworks"], "country": ["France"], "page_size": 10},
)
page = result.structured_content
print(page["total"], "notices |", page["quota_remaining"], "searches left")
for tender in page["items"]:
print(tender["id"], tender["deadline_at"], tender["title"])
asyncio.run(main())
Una llamada que falla regresa con is_error establecido y el mensaje en inglés que la API REST habría respondido, seguido de su código estable entre corchetes (Invalid API key [invalid_api_key]): una clave desconocida, una cuota agotada, un identificador desconocido. Los códigos son los de la sección Errores a continuación.
Errores
Un cuerpo de error es siempre {"detail": "..."}, y el mensaje siempre está en inglés, sea cual sea el idioma de tu integración: el inglés es el idioma de trabajo de la API. Cada error que un usuario puede encontrar también lleva un código estable en el encabezado de respuesta X-Error-Code, para que tu integración pueda ramificar según el código y escribir su propio mensaje en lugar de comparar con el texto.
| Código | X-Error-Code | Caso | detail |
|---|---|---|---|
| 401 | — | Sin clave alguna (no hay acceso anónimo) | Login required |
| 401 | invalid_api_key | Clave desconocida o revocada | Invalid API key |
| 402 | plan_required | Crear o revocar una clave en un espacio de trabajo sin plan de pago | This feature requires a paid plan |
| 403 | api_key_disabled | El espacio de trabajo de la clave no tiene un plan de pago (lo dejó o nunca lo tuvo) | API key disabled: this workspace is not on a paid plan |
| 403 | api_key_scope | Cualquier ruta distinta de la búsqueda de licitaciones | This key only grants access to the tender search |
| 404 | — | Identificador de aviso desconocido | Tender not found |
| 429 | quota_exceeded | Cuota horaria agotada | You have reached the limit of 10000 searches per hour of your plan. Try again in 12 min. |
| 429 | search_burst | Demasiadas solicitudes en un minuto | Too many searches at once, try again in a minute |
Una respuesta quota_exceeded repite sus números en X-Error-Limit (búsquedas por hora) y X-Error-Minutes (la espera), de modo que un mensaje puede reconstruirse en cualquier idioma.
Un 429 lleva un encabezado Retry-After, en segundos. Respétalo en lugar de reintentar de inmediato:
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 y 403 no se solucionan reintentando: revisa el plan del espacio de trabajo, o crea una nueva clave si la tuya fue revocada.
A través de MCP, los mismos fallos regresan como errores de herramienta con los mismos mensajes, en lugar de como códigos de estado HTTP.