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.

PlanoBuscas por horaRajada por minutoResultados por páginaPágina mais profunda
Standard10.000240200500
Beta10.000240200500

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çalhoConteúdo
X-Quota-LimitBuscas permitidas por hora
X-Quota-RemainingBuscas restantes na hora atual
X-Quota-PlanPlano 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âmetroTipoPadrãoDescrição
workspace_idinteironenhumIgnorado com uma chave de API: a chave já está vinculada a um workspace.
pageinteiro, mínimo 11Página solicitada. Limitada a 500.
page_sizeinteiro, de 1 a 50050Resultados por página. Limitados a 200.
source_idinteironenhumManter apenas os avisos de um portal.
qstring, no máximo 200 caracteresnenhumTexto 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.
keywordstring, repetívelnenhumPalavras-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_terminteiro, repetívelnenhumIdentificadores de termos (traduções ou variantes de uma palavra-chave) a descartar da busca.
countrystring, repetívelnenhumPaí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.
sectorstring de dois dígitos, repetívelnenhumSetores, 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_valuenúmeronenhumValor estimado mínimo, na moeda do aviso.
max_valuenúmeronenhumValor estimado máximo.
sortnewest, oldest ou deadlinenewestPublicação decrescente, publicação crescente ou prazo crescente.
include_closedbooleanofalseIncluir avisos encerrados.
seen_afterdata-hora ISO 8601nenhumManter apenas os avisos coletados pela primeira vez após esse instante. Útil para monitoramento incremental.

Resposta

200 OK, um objeto TenderPage:

CampoTipoDescrição
itemsarray de TenderOs avisos desta página.
totalinteiroNúmero total de avisos que correspondem aos critérios.
pageinteiroPágina efetivamente retornada.
page_sizeinteiroTamanho de página efetivamente aplicado.
pagesinteiroNúmero de páginas alcançáveis, limitado pelo plano.
by_countryobjeto, código de país para inteiroAvisos por país do portal, com todos os outros filtros aplicados, mas sem o filtro country. Feito para construir uma faceta.
by_sectorobjeto, divisão de dois dígitos para inteiroAvisos 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:

CampoTipoDescrição
idinteiroIdentificador Scoutee do aviso.
source_idinteiroIdentificador do portal de origem.
source_namestring, anulávelNome do portal.
source_countrystring, anulávelPaís do portal, duas letras.
external_idstringIdentificador do aviso no portal.
titlestringTítulo da consulta.
buyerstring, anulávelComprador público.
descriptionstring, anulávelObjeto do contrato, como publicado.
urlstringPágina do aviso no portal de origem.
locationstring, anulávelLocal de execução.
procedurestring, anulávelTipo de procedimento, como publicado.
cpv_codesarray de stringsCódigos CPV anexados ao aviso.
sectorsarray de strings de dois dígitosSetores (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_valuenúmero, anulávelValor estimado.
currencystring, anulávelMoeda desse valor.
published_atdata-hora, anulávelData de publicação.
deadline_atdata-hora, anulávelPrazo para submissões.
first_seen_atdata-horaPrimeira coleta pela Scoutee.
last_seen_atdata-horaÚltima coleta.
closed_atdata-hora, anulávelEncerramento observado. null enquanto o aviso está aberto.
favoritebooleanoSempre false com uma chave de API: favoritos pertencem a um usuário.
also_onarray de AlsoOnOutros 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-Key ou em Authorization: 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 para get_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ódigoX-Error-CodeCasodetail
401Nenhuma chave (não há acesso anônimo)Login required
401invalid_api_keyChave desconhecida ou revogadaInvalid API key
402plan_requiredCriar ou revogar uma chave em um workspace sem plano pagoThis feature requires a paid plan
403api_key_disabledO 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
403api_key_scopeQualquer caminho diferente da busca de licitaçõesThis key only grants access to the tender search
404Identificador de aviso desconhecidoTender not found
429quota_exceededCota horária esgotadaYou have reached the limit of 10000 searches per hour of your plan. Try again in 12 min.
429search_burstMuitas solicitações em um minutoToo 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.

Recursos