Scoutee
Pesquise mais de 200 mil licitações públicas (RFPs, contratos governamentais) de 87 fontes oficiais em 32 países da Europa e América do Norte.
Documentação
Scoutee API documentation: autenticação por X-API-Key, a busca GET /tenders, todos os parâmetros de consulta, formatos de resposta, cotas por plano, códigos de erro e o servidor MCP.
A API Scoutee dá acesso de leitura ao banco de licitações a partir das suas próprias ferramentas: um script de monitoramento, um CRM, um painel interno. Ela expõe dois endpoints, a busca e um aviso individual, e as mesmas duas operações como servidor MCP para assistentes.
URL base: https://scoutee.org/api
Toda resposta é JSON (application/json), UTF-8. Datas estão em ISO 8601 (2026-09-05T14:30:00Z).
Chaves de API
Uma chave pertence a um workspace, não a uma pessoa: ela sobrevive a quem a criou, e os administradores do workspace podem revogá-la a qualquer momento.
Uma chave só pode ser criada, e só funciona, enquanto o workspace estiver em um plano pago — Standard ou Beta. Se o workspace voltar para o plano gratuito, as chaves existentes continuam listadas, mas as chamadas respondem 403.
Para criar uma chave: página do seu workspace, seção "API", botão "Criar chave". O valor completo é exibido uma única vez, na criação. A Scoutee armazena apenas o digest SHA-256 e não pode mostrá-lo novamente; se você perdê-la, revogue a chave e crie outra.
Formato da chave: sct_ seguido de 40 caracteres hexadecimais. Os primeiros 12 caracteres (o prefixo, por exemplo sct_1a2b3c4d) são exibidos na lista de chaves para que você possa reconhecê-la.
Um workspace pode ter no máximo 10 chaves ativas. Além disso, a criação responde 409: revogue uma chave antes de criar outra.
Autenticação
Toda requisição carrega a chave no cabeçalho X-API-Key:
curl -s "https://scoutee.org/api/tenders?page_size=5" \
-H "X-API-Key: sct_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
Uma chave é uma credencial somente de leitura: ela abre GET /tenders e GET /tenders/{id}, nada mais. Qualquer outro caminho responde 403, incluindo as próprias rotas de gerenciamento de chaves, que exigem uma sessão autenticada.
A chave nunca é opcional. A API não tem acesso anônimo nem acesso pelo plano gratuito: ela existe apenas para workspaces em plano pago. Uma requisição sem X-API-Key responde 401 {"detail": "Login required"}, e uma chave desconhecida ou revogada responde 401 {"detail": "Clé API invalide"}.
Nunca coloque uma chave em código que roda em navegador, ou em repositório público: ela abre os resultados completos de busca que o seu workspace paga.
Cotas
Cada busca consome uma unidade de cota. O contador é mantido por chave: duas chaves do mesmo workspace não compartilham o mesmo balde. Buscar um aviso individual (GET /tenders/{id}) não consome nada.
| Plano | Buscas por hora | Rajada por minuto | Resultados por página | Página mais profunda |
|---|---|---|---|---|
| Standard | 10.000 | 240 | 200 | 500 |
| Beta | 10.000 | 240 | 200 | 500 |
Estas são as únicas duas linhas possíveis: uma chave só existe em plano pago, e ambos os planos pagos têm exatamente os mesmos limites. Seja qual for o plano do seu workspace, Standard ou Beta, uma chave recebe 10.000 buscas por hora, 240 por minuto, 200 resultados por página e no máximo 500 páginas. (Os limites mais restritos que o site aplica a visitantes e ao plano gratuito são assunto da busca do próprio site; nada chega à API sem uma chave.)
Pedir mais do que esses limites não é um erro: o valor é limitado. page_size=500 retorna 200 resultados, e o campo page_size da resposta informa o valor efetivamente aplicado.
Toda resposta de busca traz três cabeçalhos:
| Cabeçalho | Conteúdo |
|---|---|
X-Quota-Limit | Buscas permitidas por hora |
X-Quota-Remaining | Buscas restantes na hora atual |
X-Quota-Plan | Plano aplicado — o plano do próprio workspace, standard ou premium (o nome interno do Beta) |
GET /tenders
Busca paginada. Por padrão: apenas licitações abertas, mais recentes primeiro. Avisos publicados em vários portais são deduplicados: uma linha por aviso, com os outros portais aparecendo em also_on.
Parâmetros
Todos opcionais, passados na query string.
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
workspace_id | inteiro | nenhum | Ignorado com uma chave de API: a chave já está vinculada a um workspace. |
page | inteiro, mínimo 1 | 1 | Página solicitada. Limitada a 500. |
page_size | inteiro, de 1 a 500 | 50 | Resultados por página. Limitados a 200. |
source_id | inteiro | nenhum | Manter apenas os avisos de um portal. |
q | string, no máximo 200 caracteres | nenhum | Texto livre sobre o título, o comprador e a descrição. Cada palavra deve corresponder ao início de uma palavra no aviso (nettoy encontra nettoyage); uma substring dentro de uma palavra não corresponde. Insensível a maiúsculas/minúsculas e acentos. |
keyword | string, repetível | nenhum | Palavras-chave. Correspondência de palavra inteira (plural tolerado), insensível a maiúsculas/minúsculas e acentos, no título, no comprador ou na descrição, além das traduções em cache da palavra-chave nas fontes daquele idioma. Vários keyword ampliam a busca (qualquer um deles). |
exclude_term | inteiro, repetível | nenhum | Identificadores de termos (traduções ou variantes de uma palavra-chave) a descartar da busca. |
country | string, repetível | nenhum | Países do portal, pelo nome em inglês (France, Belgium, Germany; Europe para TED). Vários country se somam. O objeto by_country de qualquer resposta lista os valores exatos em uso. |
sector | string de dois dígitos, repetível | nenhum | Setores, ou seja, divisões CPV 2008: os dois primeiros dígitos de um código CPV (45 obras de construção, 72 serviços de TI, 85 saúde e assistência social). Um aviso corresponde quando pertence a qualquer um dos setores informados. Nunca aplicado a menos que solicitado: sem sector, todos os setores são buscados. Um valor fora das 45 divisões responde 422 com o código invalid_sector. O objeto by_sector de uma resposta lista as divisões em uso. |
min_value | número | nenhum | Valor estimado mínimo, na moeda do aviso. |
max_value | número | nenhum | Valor estimado máximo. |
sort | newest, oldest ou deadline | newest | Publicação decrescente, publicação crescente ou prazo crescente. |
include_closed | booleano | false | Incluir avisos encerrados. |
seen_after | data-hora ISO 8601 | nenhum | Manter apenas os avisos coletados pela primeira vez após esse instante. Útil para monitoramento incremental. |
Resposta
200 OK, um objeto TenderPage:
| Campo | Tipo | Descrição |
|---|---|---|
items | array de Tender | Os avisos desta página. |
total | inteiro | Número total de avisos que correspondem aos critérios. |
page | inteiro | Página efetivamente retornada. |
page_size | inteiro | Tamanho de página efetivamente aplicado. |
pages | inteiro | Número de páginas alcançáveis, limitado pelo plano. |
by_country | objeto, código de país para inteiro | Avisos por país do portal, com todos os outros filtros aplicados, mas sem o filtro country. Feito para construir uma faceta. |
by_sector | objeto, divisão de dois dígitos para inteiro | Avisos por setor, com todos os outros filtros aplicados — country incluído — mas sem o filtro sector. Feito para construir uma faceta. Um aviso com vários setores é contado uma vez por setor, então os valores não somam total. |
Um objeto Tender:
| Campo | Tipo | Descrição |
|---|---|---|
id | inteiro | Identificador Scoutee do aviso. |
source_id | inteiro | Identificador do portal de origem. |
source_name | string, anulável | Nome do portal. |
source_country | string, anulável | País do portal, duas letras. |
external_id | string | Identificador do aviso no portal. |
title | string | Título da consulta. |
buyer | string, anulável | Comprador público. |
description | string, anulável | Objeto do contrato, como publicado. |
url | string | Página do aviso no portal de origem. |
location | string, anulável | Local de execução. |
procedure | string, anulável | Tipo de procedimento, como publicado. |
cpv_codes | array de strings | Códigos CPV anexados ao aviso. |
sectors | array de strings de dois dígitos | Setores (divisões CPV) do aviso: as divisões dos seus códigos CPV quando houver, caso contrário a divisão única que nosso classificador atribuiu. Vazio quando nenhum dos dois se aplica. |
estimated_value | número, anulável | Valor estimado. |
currency | string, anulável | Moeda desse valor. |
published_at | data-hora, anulável | Data de publicação. |
deadline_at | data-hora, anulável | Prazo para submissões. |
first_seen_at | data-hora | Primeira coleta pela Scoutee. |
last_seen_at | data-hora | Última coleta. |
closed_at | data-hora, anulável | Encerramento observado. null enquanto o aviso está aberto. |
favorite | booleano | Sempre false com uma chave de API: favoritos pertencem a um usuário. |
also_on | array de AlsoOn | Outros portais que publicaram o mesmo aviso. |
Um objeto AlsoOn: source_id (inteiro), source_name (string), source_country (string, anulável), url (string).
Exemplo
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 }
}
Os avisos são retornados no idioma em que foram publicados: a API não os traduz.
Em Python, com 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"])
Percorrendo todas as páginas:
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 monitoramento incremental, guarde o timestamp da sua última execução e passe-o de volta como seen_after: apenas os avisos descobertos desde então voltam.
GET /tenders/{id}
Um aviso, enriquecido como um resultado de busca. Não consome cota.
curl -s "https://scoutee.org/api/tenders/918233" \
-H "X-API-Key: $SCOUTEE_API_KEY"
A resposta é um objeto Tender, idêntico aos de items. Se o identificador solicitado apontar para a cópia de um aviso publicado em vários portais, a cópia canônica é retornada. Um identificador desconhecido responde 404.
Servidor MCP
As mesmas duas operações são expostas como um servidor MCP, para que um assistente — Claude Code, Cursor, VS Code, Windsurf, ou qualquer outra coisa que fale o protocolo — possa buscar licitações em seu nome sem que você escreva uma linha de HTTP.
- URL:
https://scoutee.org/api/mcp - Transporte: Streamable HTTP, sem estado, respostas JSON (sem stream para manter aberto, então funciona através de qualquer proxy)
- Autenticação: a mesma chave do workspace, em
X-API-Keyou emAuthorization: Bearer, e igualmente obrigatória — uma chamada sem ela retorna um erro de ferramenta pedindo uma chave - Cotas: idênticas ao REST — 10.000 buscas por hora em qualquer plano pago, uma unidade por chamada
search_tenders, nada paraget_tender, contadas no mesmo balde por chave
Ferramentas
search_tenders recebe os parâmetros de consulta de GET /tenders, com a mesma semântica: q, keyword (array), country (array), source_id, min_value, max_value, sort (newest, oldest, deadline), include_closed, seen_after, page e page_size. Todos opcionais. page_size tem padrão de 20 em vez de 50, já que um aviso é um objeto grande para entregar a um modelo, e ainda é limitado a 200. O resultado é um TenderPage mais três campos que carregam o que os cabeçalhos de cota carregam via HTTP: quota_plan, quota_limit e quota_remaining.
get_tender recebe um único tender_id e retorna o mesmo objeto Tender que GET /tenders/{id}. Não consome cota.
Claude Code
claude mcp add --transport http scoutee https://scoutee.org/api/mcp \
--header "X-API-Key: sct_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
Cursor, VS Code, Windsurf e outros clientes
A maioria deles lê um arquivo de configuração contendo um 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
Com o pacote 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())
Uma chamada que falha retorna com is_error definido e a mensagem em inglês que a API REST teria respondido, seguida do seu código estável entre colchetes (Invalid API key [invalid_api_key]): uma chave desconhecida, uma cota esgotada, um identificador desconhecido. Os códigos são os da seção Erros abaixo.
Erros
Um corpo de erro é sempre {"detail": "..."}, e a mensagem é sempre em inglês, independentemente do idioma da sua integração: o inglês é o idioma de trabalho da API. Todo erro que um usuário pode encontrar também carrega um código estável no cabeçalho de resposta X-Error-Code, para que sua integração possa ramificar com base no código e escrever sua própria mensagem em vez de comparar o texto.
| Código | X-Error-Code | Caso | detail |
|---|---|---|---|
| 401 | — | Nenhuma chave (não há acesso anônimo) | Login required |
| 401 | invalid_api_key | Chave desconhecida ou revogada | Invalid API key |
| 402 | plan_required | Criar ou revogar uma chave em um workspace sem plano pago | This feature requires a paid plan |
| 403 | api_key_disabled | O workspace da chave não está em um plano pago (saiu dele, ou nunca teve um) | API key disabled: this workspace is not on a paid plan |
| 403 | api_key_scope | Qualquer caminho diferente da busca de licitações | This key only grants access to the tender search |
| 404 | — | Identificador de aviso desconhecido | Tender not found |
| 429 | quota_exceeded | Cota horária esgotada | You have reached the limit of 10000 searches per hour of your plan. Try again in 12 min. |
| 429 | search_burst | Muitas solicitações em um minuto | Too many searches at once, try again in a minute |
Uma resposta quota_exceeded repete seus números em X-Error-Limit (buscas por hora) e X-Error-Minutes (a espera), para que uma mensagem possa ser reconstruída em qualquer idioma.
Um 429 carrega um cabeçalho Retry-After, em segundos. Respeite-o em vez de tentar novamente imediatamente:
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 e 403 não são corrigidos com novas tentativas: verifique o plano do workspace, ou crie uma nova chave se a sua foi revogada.
Pelo MCP, as mesmas falhas retornam como erros de ferramenta com as mesmas mensagens, em vez de códigos de status HTTP.