Brazil Government Tenders Search (PNCP)
Under maintenancePricing
$20.00 / 1,000 matched tender records
Brazil Government Tenders Search (PNCP)
Under maintenanceSearch 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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
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.
| Field | Required | Notes |
|---|---|---|
mode | no (default open_for_proposals) | open_for_proposals or published — see "Search modes" below, they map to two different PNCP endpoints with different required fields. |
closingBefore | yes, if mode = open_for_proposals | YYYY-MM-DD. Proposal deadline cutoff. |
publishedFrom / publishedTo | yes, if mode = published | YYYY-MM-DD. Max 31-day span per run (see below). |
modalities | no | Array 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). |
uf | no | Two-letter Brazilian state code. |
municipalityIbgeCode | no | IBGE municipality code. |
contractingBodyCnpj | no | CNPJ of the contracting body, punctuation optional. |
keyword | no | All space-separated words must (case-insensitively) appear in the tender's object/description. Applied by this Actor, not PNCP (see "Search semantics"). |
minEstimatedValueBrl | no | Minimum estimated value in BRL. Applied by this Actor, not PNCP. |
maxResults | no (default 100, max 500) | Caps matched+charged records per run. |
maxConcurrency | no (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_proposals→GET /v1/contratacoes/proposta. OnlydataFinal(→closingBefore) is required; modality is optional. Best for "what can I bid on right now."published→GET /v1/contratacoes/publicacao. RequiresdataInicial+dataFinal(→publishedFrom/publishedTo) andcodigoModalidadeContratacao— modality is not optional for this endpoint. If you don't specifymodalities, this Actor queries all 13 modalities in turn to give you full coverage, which is real extra time and (bounded bymaxResults) 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'sobjetoCompra(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 againstvalorTotalEstimado. 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":
- Higher operational cost per record. PNCP's own API is measurably
slower than the community APIs
cnpj-lookupwraps (12-20 seconds per page observed live, vs. sub-second forcnpj-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.publishedmode 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. - 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.
Data source, legal basis & terms of use
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 withhttpx.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-schemapasses for bothinput_schema.jsonanddataset_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/publicacaowith each code and reading back the realmodalidadeNomePNCP returned — not taken from memory or a secondary source, since no domain-table endpoint exists in the API. - All 4
poderIdvalues (E/L/J/N) and all 4esferaIdvalues (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 callsmoke 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 incnpj-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-lookupuses, already verified to build correctly on Apify's own infrastructure — seecnpj-lookupADR-006), with only the package name changed (pncp_lookupinstead ofcnpj_lookup). Apify's build pipeline will build this Dockerfile onapify 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.