Brazil Government Tenders Search (PNCP) avatar

Brazil Government Tenders Search (PNCP)

Under maintenance

Pricing

$20.00 / 1,000 matched tender records

Go to Apify Store
Brazil Government Tenders Search (PNCP)

Brazil Government Tenders Search (PNCP)

Under maintenance

Search Brazil's official public-procurement platform (PNCP) for open tenders and published contracting notices: filter by date, UF/municipality, modality, contracting body, keyword, and minimum value. Pay only for matched records.

Pricing

$20.00 / 1,000 matched tender records

Rating

0.0

(0)

Developer

Alison Moura

Alison Moura

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Share

Search Brazil's official public-procurement platform — PNCP (Portal Nacional de Contratações Públicas), the national platform every Brazilian government body (federal, state, municipal — executive, legislative, and judiciary) is legally required to publish tenders and contracts to under Lei 14.133/2021 — for open tenders (licitações) and published contracting notices. Filter by date, state/municipality, contracting modality, contracting body, keyword, and minimum value, and get back clean, normalized JSON: contracting body, object/description, modality, estimated value, key dates, status, and a link back to the official PNCP listing.

Who this is for

Companies that sell to government and need to monitor open tenders to bid on, procurement/BD teams doing market research on public spending in a sector or region, compliance/vendor-vetting tools checking a supplier's public-contract history, and anyone building on top of Brazilian public procurement data without wanting to write their own PNCP client, pagination, and rate-limit handling from scratch.

What you send in

Example 1 — "what can I bid on right now" (mode: open_for_proposals):

{
"mode": "open_for_proposals",
"closingBefore": "2026-09-15",
"uf": "SP",
"modalities": ["Pregão - Eletrônico"],
"keyword": "informatica",
"minEstimatedValueBrl": 50000,
"maxResults": 100
}

Returns tenders currently open for proposals in the state of São Paulo, modality Pregão Eletrônico, closing on or before 2026-09-15, whose object mentions "informatica" and whose estimated value is at least R$50,000.

Example 2 — market research (mode: published):

{
"mode": "published",
"publishedFrom": "2026-08-01",
"publishedTo": "2026-08-17",
"modalities": ["Dispensa", "Inexigibilidade"],
"contractingBodyCnpj": "46177531000155"
}

Returns every tender published by a specific contracting body (by CNPJ) between two dates, restricted to two modalities.

FieldRequiredNotes
modeno (default open_for_proposals)open_for_proposals or published — see "Search modes" below, they map to two different PNCP endpoints with different required fields.
closingBeforeyes, if mode = open_for_proposalsYYYY-MM-DD. Proposal deadline cutoff.
publishedFrom / publishedToyes, if mode = publishedYYYY-MM-DD. Max 31-day span per run (see below).
modalitiesnoArray of modality names (see the 13 valid values in the input editor). Empty means "all modalities" — cheap in open_for_proposals mode (one extra API call skipped, not needed), expensive in published mode (loops all 13 — see below).
ufnoTwo-letter Brazilian state code.
municipalityIbgeCodenoIBGE municipality code.
contractingBodyCnpjnoCNPJ of the contracting body, punctuation optional.
keywordnoAll space-separated words must (case-insensitively) appear in the tender's object/description. Applied by this Actor, not PNCP (see "Search semantics").
minEstimatedValueBrlnoMinimum estimated value in BRL. Applied by this Actor, not PNCP.
maxResultsno (default 100, max 500)Caps matched+charged records per run.
maxConcurrencyno (default 3, max 5)Concurrent sub-queries (relevant with multiple modalities).

What you get back

One dataset record per matched tender. Example (real data, captured 2026-08-17 — see "Data sources & what was tested" for provenance):

{
"pncp_id": "46177531000155-1-000394/2025",
"process_number": "20782/2025",
"purchase_number": "90147",
"purchase_year": 2025,
"purchase_sequential": 394,
"object": "Registro de preços para aquisição de cortes de carnes bovinas (IQF)",
"additional_info": "Para as respostas de esclarecimentos e impugnações...",
"contracting_body": {
"cnpj": "46177531000155",
"name": "MUNICIPIO DE PRAIA GRANDE",
"power": { "code": "N", "description": "Não se aplica" },
"sphere": { "code": "M", "description": "Municipal" }
},
"delegated_body": null,
"administrative_unit": {
"name": "PREFEITURA MUNICIPAL DE PRAIA GRANDE - SP",
"code": "986921",
"city": "Praia Grande",
"state": "SP",
"state_name": "São Paulo",
"city_ibge_code": "3541000"
},
"modality": { "code": 6, "name": "Pregão - Eletrônico" },
"dispute_mode": { "code": 1, "name": "Aberto" },
"instrument_type": { "code": 1, "name": "Edital" },
"legal_basis": {
"code": 1,
"name": "Lei 14.133/2021, Art. 28, I",
"description": "pregão: modalidade de licitação obrigatória para aquisição de bens e serviços comuns"
},
"status": { "code": 1, "description": "Divulgada no PNCP" },
"is_price_registry": false,
"has_parliamentary_amendment": null,
"estimated_value_brl": 52100655.6,
"awarded_value_brl": null,
"dates": {
"published_at": "2025-09-09T07:28:44",
"included_at": "2025-09-09T07:28:44",
"proposal_opens_at": "2026-08-05T09:00:00",
"proposal_closes_at": "2026-08-21T09:30:00",
"updated_at": "2026-08-05T07:00:34"
},
"source_system_url": "https://cnetmobile.estaleiro.serpro.gov.br/comprasnet-web/public/landing?destino=acompanhamento-compra&compra=98692105901472025",
"electronic_process_url": null,
"pncp_url": "https://pncp.gov.br/app/editais/46177531000155/2025/394",
"submitted_by_system": "Compras.gov.br",
"_meta": { "source": "pncp", "search_mode": "open_for_proposals", "retrieved_at": "2026-08-17T14:20:00+00:00" }
}

A search that legitimately matches nothing still writes one visible, unbilled record so you can tell "ran fine, found nothing" apart from a crash:

{ "status": "no_matches", "message": "Search completed successfully but no tenders matched the given filters." }

Search modes

PNCP's own consulta API forces this Actor into two distinct modes because the two underlying endpoints have genuinely different required parameters — this isn't an arbitrary design choice, it's a direct consequence of how the API is built (verified against the live OpenAPI spec, https://pncp.gov.br/api/consulta/v3/api-docs):

  • open_for_proposalsGET /v1/contratacoes/proposta. Only dataFinal (→ closingBefore) is required; modality is optional. Best for "what can I bid on right now."
  • publishedGET /v1/contratacoes/publicacao. Requires dataInicial+dataFinal (→ publishedFrom/publishedTo) and codigoModalidadeContratacao — modality is not optional for this endpoint. If you don't specify modalities, this Actor queries all 13 modalities in turn to give you full coverage, which is real extra time and (bounded by maxResults) potentially extra cost. Prefer specifying the modalities you actually care about in this mode.

published mode is also capped at a 31-day date range per run — a direct consequence of PNCP's own latency (see "Data freshness & performance" below): a full-year scan across all 13 modalities, each paginated, would take a genuinely long time and isn't a good fit for a single Actor run. Run it repeatedly with narrower windows for a wider historical search.

Search semantics (what's server-side vs. client-side)

PNCP's consulta API has no full-text search and no minimum-value query parameter on any endpoint — verified against the complete OpenAPI spec, not assumed. The only server-side filters it exposes are date range, UF, municipality (IBGE code), contracting-body CNPJ, administrative unit code, and modality.

So keyword and minEstimatedValueBrl are applied by this Actor, after fetching each page from PNCP:

  • keyword: case-insensitive, "all words must appear" match against the tender's objetoCompra (object/description) field. Accent-naive but not accent-normalizing — "saude" will not match "saúde" in the source text (Portuguese text on PNCP is not consistently accented either, so there's no single normalization that's reliably correct; matching the words exactly as typed against the exact text PNCP stores is the honest, predictable behavior).
  • minEstimatedValueBrl: numeric >= comparison against valorTotalEstimado. Tenders with no published estimated value are excluded when this filter is set (there's no principled way to know if an unpublished value would have cleared the bar).

Because these are client-side, this Actor scans PNCP pages until it finds enough matches or hits an internal scan ceiling (10x your maxResults, capped at 1000 raw records per sub-query) — not an index of all PNCP data. A very narrow keyword against a very broad, common search (e.g. all Pregão Eletrônico tenders nationwide) may not find every real match if they're spread past the scan ceiling; narrowing by uf, modality, or date range first makes the keyword filter more reliable.

Pricing

Pay-per-event: you are charged only for tender records that actually match your search filters and are returned (event tender-record, $0.02/record — see "Pricing rationale" below). A search that legitimately matches zero tenders is not charged; it still writes one visible summary record so you can confirm the search ran correctly.

Pricing rationale

This product's shape is a search, not a resolve-by-ID lookup (unlike this developer's cnpj-lookup Actor, priced at $0.01/resolved record) — there's no natural "not found" outcome to distinguish, so the charging rule is simply: charge for every record actually delivered to you, don't charge when a search finds nothing to deliver.

Priced at 2x cnpj-lookup's rate for two concrete reasons, not just "feels richer":

  1. Higher operational cost per record. PNCP's own API is measurably slower than the community APIs cnpj-lookup wraps (12-20 seconds per page observed live, vs. sub-second for cnpj-lookup's sources) and has a real, empirically-hit rate limit with no published number to plan around, so this Actor must move more conservatively. published mode frequently needs multiple sequential API calls (one per modality) to answer a single search. All of that is real compute/wall-clock cost this Actor absorbs per delivered record that a single fast per-ID lookup doesn't have.
  2. Higher buyer value per record. A resolved CNPJ is a static company profile; a matched tender is an actionable, time-sensitive bid opportunity (or a real signal for market research) tied to a government contract that can be worth anywhere from thousands to millions of reais — the value delivered per record is plausibly higher even before accounting for cost.

Data freshness & performance

Data comes directly from PNCP's live consulta API on every run — not a cached or periodically-refreshed copy — so it's as current as PNCP itself (tenders are legally required to be published there, generally close to real time by the contracting body).

PNCP's own API is slow: 12-20 seconds per page was observed consistently during this Actor's development (2026-08-17), independent of load — this is the platform's own infrastructure speed, not something this Actor controls or can improve. A published-mode search spanning several modalities will take real wall-clock minutes, by design. Plan maxResults and date ranges accordingly, and prefer narrower, targeted searches (specific UF/modality/CNPJ) over broad nationwide scans.

Rate limits & fair use

PNCP enforces a real, empirically-observed rate limit (confirmed live, 2026-08-17 — sending unpaced sequential requests reliably triggered an HTTP 429 after roughly half a dozen requests). No numeric requests/second figure is published anywhere found (the OpenAPI spec, the public FAQ, or a search for the integration manual) — this Actor's pacing (a self-imposed minimum interval between requests, plus exponential backoff and retry on 429/5xx) is a deliberately conservative, empirically-informed default, not a number taken from documentation. It can be tuned later if real usage shows PNCP tolerates more.

Source: PNCP (pncp.gov.br), operated by Brazil's Ministério da Gestão e da Inovação em Serviços Públicos (MGI). PNCP's existence and its open-data obligation are not just policy — they're a direct legal requirement:

  • Lei nº 14.133/2021 (Brazil's Public Procurement Law), Art. 174, §4º, requires PNCP to adopt the "formato de dados abertos" (open data format) and observe Lei nº 12.527/2011 (the Lei de Acesso à Informação / Access to Information Law).
  • Decreto nº 8.777/2016, which defines and governs "dados abertos" (open data) for the federal executive branch, defines the term explicitly as: "dados acessíveis ao público, representados em meio digital, estruturados em formato aberto, processáveis por máquina, referenciados na internet e disponibilizados sob licença aberta que permita sua livre utilização, consumo ou cruzamento, limitando-se a creditar a autoria ou a fonte" — data under an open license permitting free use, consumption, or cross-referencing, limited only to crediting authorship/source. The same decree states as a governing principle "permissão irrestrita de reuso das bases de dados publicadas em formato aberto" — unrestricted permission to reuse databases published in open format.

This is a stronger, more direct legal basis than a typical API terms-of- service document (which is what cnpj-lookup's source vetting relied on, see ADR-005) — commercial reuse isn't merely not prohibited, it's the explicit statutory purpose of the open-data format PNCP is legally required to use. No PNCP-specific terms-of-service page, FAQ entry, or manual section found during this build (searched: the public FAQ, the integration manual search results, and a public-interest technical report on PNCP's API) contradicts or narrows this — nothing found restricts commercial use, resale, or bulk automated querying of consulta data.

Access is public and free, confirmed three independent ways (see pncp_lookup/client.py module docstring for the full detail): the live OpenAPI spec defines no security requirement on any consulta operation; PNCP's own FAQ states plainly that access is free/universal and needs no login; and every endpoint this Actor calls was confirmed live to work with zero authentication. The JWT-token authentication documented elsewhere in PNCP's integration manuals is for government bodies submitting data to PNCP — an entirely separate write API this Actor never touches.

What was actually tested vs. not

Tested and passing (57/57 tests, pytest, Python 3.12 venv — see cnpj-lookup/README.md for why 3.12 not the VPS's default 3.10):

  • pncp_lookup/utils.py — date parsing/validation, UF validation (all 27 real Brazilian state codes), CNPJ-filter cleaning, keyword matching.
  • pncp_lookup/modalities.py — the modality name↔code table (see below for how it was verified).
  • pncp_lookup/normalize.py — mapping tested against real API responses (not hand-written fixtures) captured live 2026-08-17 for real, currently-open tenders (tests/fixtures/proposta_sample.json, tests/fixtures/publicacao_sample.json).
  • pncp_lookup/client.py — pagination, retry/backoff, and both of the real non-obvious response shapes discovered live: HTTP 204 with a genuinely empty body for zero matches, and HTTP 429 returning an HTML error page instead of JSON. Tested with httpx.MockTransport (no real network calls in the automated test suite).
  • pncp_lookup/main.py — query planning/validation logic (_plan_queries, _coerce_modalities): correct endpoint selection per mode, correct required-field enforcement per mode, the 31-day range cap, modality resolution/dedup.
  • apify-cli validate-schema passes for both input_schema.json and dataset_schema.json.
  • Ruff lint: clean, zero warnings.

Verified live against the real PNCP API (not mocked) during this build, 2026-08-17:

  • The full OpenAPI spec (/api/consulta/v3/api-docs) fetched and parsed directly — all 12 consulta endpoints, their parameters, and their response schemas came from this, not documentation guesses.
  • All 13 modality codes (1-13) individually confirmed by querying /v1/contratacoes/publicacao with each code and reading back the real modalidadeNome PNCP returned — not taken from memory or a secondary source, since no domain-table endpoint exists in the API.
  • All 4 poderId values (E/L/J/N) and all 4 esferaId values (F/E/M/N) confirmed by sampling ~80 real live records across federal, state, and municipal contracting bodies and reading the pattern off named, recognizable entities (e.g. "MINISTERIO DA SAUDE" → poderId E, esferaId F; "SECRETARIA DE ESTADO DA SAUDE" (SP) → poderId E, esferaId E; "MUNICIPIO DE CAMPINAS" → poderId N, esferaId M).
  • The public PNCP listing URL format (https://pncp.gov.br/app/editais/{cnpj}/{ano}/{sequencial}) confirmed to return HTTP 200 for a real tender.
  • The HTTP 204-empty-body (zero matches) and HTTP 429-HTML-body (rate limited) response shapes — both discovered live, neither documented anywhere found, and both are now specifically handled and unit tested (see client.py).
  • Both consulta endpoints this Actor uses returned correct real data with zero authentication headers sent.

Attempted but NOT cleanly confirmed — full Actor run against the live API, honestly reported: a real end-to-end run was attempted via direct python -m pncp_lookup invocation against the live PNCP API (UF=SP, modality=Pregão Eletrônico, maxResults: 5). By that point in the build session, this developer had already made ~30+ live requests directly against PNCP within roughly 30 minutes (fetching the OpenAPI spec, individually verifying all 13 modality codes, sampling ~80 records for the poderId/esferaId mapping, capturing fixtures, and deliberately probing the rate limiter). The full-Actor run started returning httpx.ReadTimeout on every request; a bare curl against the same endpoint, run completely outside this Actor's code, reproduced the identical timeout, including after a 3-minute cooldown and retry. That rules out a bug in this Actor's own code as the cause (the same failure happens with zero Python/httpx involved) and points to this VPS's IP being temporarily throttled/blocked by PNCP's infrastructure after the session's own request volume — the mechanism wasn't independently confirmed (no response body/header distinguishes it from a generic outage), but a self-inflicted, temporary, IP-scoped block is the best-supported explanation given the earlier ~30 successful requests immediately followed by 100% failures including from outside this Actor entirely.

This was still useful: it caught a real bug (see below) and confirms the Actor's failure handling is correct under a real, sustained upstream outage — the run logged the error per sub-query, did not crash, wrote a "0 matched" summary, and exited cleanly (exit_code: 0), which is exactly the intended graceful-degradation behavior. What it did not confirm is a full successful run that actually returns and charges for real matched tender data end-to-end — that step is still outstanding and should be the first thing re-tried (from a fresh IP/session, with no preceding burst of requests) before or immediately after deploying to Apify's own infrastructure, which will be a different egress IP than this VPS regardless.

A real bug was found and fixed during this attempt: the first attempt crashed the whole Actor with an unhandled httpx.ReadTimeout traceback — client.py's retry loop caught httpx.TimeoutException/httpx.HTTPError internally but re-raised the raw exception after exhausting retries instead of wrapping it in one of this module's own exception types, so it propagated straight past main.py's except PncpRateLimitedError / except PncpRequestError handlers uncaught. Fixed by wrapping both httpx.TimeoutException and httpx.HTTPError into PncpRequestError inside fetch_page (see client.py), with two dedicated regression tests added (tests/test_client.py::TestNetworkErrorsAreWrapped) simulating a raw httpx.ReadTimeout/httpx.ConnectError to confirm they now surface as a caught, per-sub-query error rather than an Actor crash. Re-ran the full suite after the fix: 57/57 passing.

Not tested / could not verify locally:

  • A full Actor run that actually returns real matched tender data end-to-end. This is the single biggest gap in this build's testing, stated plainly: every individual piece (spec, modality codes, poder/esfera mapping, normalization against real fixtures, all client response-shape handling) was verified live and independently, and the orchestration logic is unit tested, but they were never all exercised together in one successful live run that produces output records, because of the timeout situation described above. This should be the first thing re-verified — either by retrying from this VPS later (a fresh session, no preceding request burst) or via the live apify-cli call smoke test during deployment (Apify's platform egress IP will differ from this VPS's regardless).
  • Real pay-per-event charging behavior: outside a real Apify platform run, Actor.push_data(..., charged_event_name=...) correctly no-ops with a log line noting the Actor isn't running under pay-per-event pricing (same behavior documented in cnpj-lookup/README.md). The charging code path is exercised on every match; actual billing can only be confirmed after deploying and monetizing on the real platform.
  • Docker build: this VPS has no Docker installed (no root/sudo available). The Dockerfile follows Apify's official Python Actor template exactly (same one cnpj-lookup uses, already verified to build correctly on Apify's own infrastructure — see cnpj-lookup ADR-006), with only the package name changed (pncp_lookup instead of cnpj_lookup). Apify's build pipeline will build this Dockerfile on apify push / Console upload — not done yet, see Deployment.
  • Behavior under Apify's real platform network conditions/egress (vs. this VPS) — no reason to expect a difference (both are generic outbound HTTPS), but not independently confirmed.
  • No real paid usage, no Apify Store listing yet — this Actor has not been deployed anywhere outside this repo and this VPS.

Deployment

Not yet deployed. Follow the exact same process already verified working for this developer's first Actor — see products/cnpj-lookup/README.md "Deployment" for the full step-by-step (Apify signup, apify-cli login --token, npx apify-cli push, live apify-cli call smoke test, then finishing monetization/publishing via the Apify REST API once the account-level payout/public-profile prerequisites are met — those are one-time, already satisfied by the owner for the cnpj-lookup Actor's account, so this Actor should only need the push + monetize + publish steps, not the account setup again).

Important — do this before monetizing/publishing, not after: the live apify-cli call smoke test step is not just a formality for this Actor the way it was for cnpj-lookup (which had already proven the "resolve a real CNPJ" path live before deploying). PNCP's own build never got a clean confirmed successful run with real returned data (see "What was actually tested vs. not" above) — so this smoke test is the first real chance to confirm the whole pipeline works end-to-end, and should be treated as a genuine test, not a rubber stamp: run it with a real, specific filter (e.g. the exact input from this README's Example 1) and check the output looks like the documented example shape before flipping isPublic: true or setting pricing live.

Once that's confirmed, set pricing from .actor/pay_per_event.json (tender-record event at $0.02) via the same pricingInfos API call pattern already verified working for cnpj-lookup, and set isPublic: true + appropriate Store categories.

For the Aurora project (internal)

Built per ADR-008 (second of two candidate Actors approved that ADR; DataJud, the other candidate, was found infeasible — see ADR-009 — due to CNJ's terms of use explicitly banning commercial resale). PNCP's own legal and terms-of-use situation is materially different and stronger: see "Data source, legal basis & terms of use" above — the open-data obligation is a direct statutory requirement (Lei 14.133/2021 + Decreto 8.777/2016), not merely an absence-of-prohibition read of a third-party API's terms page.

Real, unplanned discoveries during this build worth flagging for future Aurora work on any PNCP-adjacent or similarly slow/rate-limited government API: (1) always fetch the raw OpenAPI spec directly (/api/consulta/v3/api-docs here) rather than relying on the Swagger UI page, which is a client-rendered SPA with no static content reachable by a simple fetch; (2) government API response shapes can defy REST norms in ways worth testing for explicitly rather than assuming — PNCP's 204-empty- body-means-zero-results and 429-returns-HTML-not-JSON were both real surprises that would have caused silent bugs (a crash on .json(), or treating a rate-limit page as valid data) if not specifically probed for and handled; (3) real-world response latency (12-20s/request here) can be a bigger practical constraint on product design than rate limits or auth — it directly shaped this Actor's mode split, date-range cap, and scan-ceiling design.