Tucuman Tenders Scraper & Monitor
Pricing
Pay per event
Tucuman Tenders Scraper & Monitor
Extracts public tenders (licitaciones publicas, privadas, contrataciones directas) from the Province of Tucuman, Argentina official procurement portal, across upcoming/in-adjudication/awarded states, with direct PDF pliego links.
Pricing
Pay per event
Rating
0.0
(0)
Developer
Stefano Seggio
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
15 hours ago
Last modified
Categories
Share
Tucuman Tenders Scraper & Monitor — Store Overview
Actor: stefano_seggio/tucuman-compras-monitor · Actor ID: TdJtze8dfyykMj2qA · Version: 2.0
Store URL: https://apify.com/stefano_seggio/tucuman-compras-monitor
Executive Summary & Business Use Case
Tucuman Tenders Scraper & Monitor extracts every public tender — Licitacion Publica, Licitacion Privada, Concurso de Precios, and Contratacion Directa — from the Province of Tucuman, Argentina's official procurement portal, comprasbys.tucuman.gob.ar (the ver_llamados_compras_avanzado.php listing endpoint). It covers all three stages of a tender's real lifecycle — apertura proxima (upcoming opening), en adjudicacion (in adjudication), and adjudicada (awarded) — and pulls the full record in a single request per page of results: buying organism, procurement type, category, expediente (file) number, the complete multi-renglon line-item breakdown, montos (bid document price, official budget, required bid bond where published), bid-opening date and location, and a direct link to the pliego PDF. There is no separate detail-page fetch anywhere in this actor's design, because the source itself has no usable detail page — every field a consumer needs is already inline in the listing response.
What makes this more than a one-off scrape is its delta engine: when onlyNew is enabled, each run tracks every idCompra across all three estados in a persistent, named key-value store, and reports not just brand-new tenders but real lifecycle transitions (a tender that moved from "upcoming" to "awarded") and content amendments (a changed monto, a corrected opening date, an added renglon) as distinct, classified events — without ever re-fetching anything, since every field used for change detection is already part of the one listing fetch this actor always makes.
Three concrete buyers for this data: (1) Suppliers to Tucuman's provincial ministries and organisms — construction, IT, security, health, general services firms — who need to know who's buying what, at what budget, and whether an opening date moved, so they can make a bid/no-bid call before a deadline slips past them. (2) Bid consultants and gestores managing several client accounts, who need a single feed telling them which of their tracked tenders was awarded or amended since yesterday, so they can notify the client and pull the updated pliego. (3) Regional tender-data resellers and LATAM procurement platforms, who would rather consume a structured, change-aware Tucuman feed (with a standardized envelope: record_id, event_type, scraped_at, is_new, source_url) than build and maintain one more of the dozens of provincial-government scrapers a pan-Argentina or pan-LATAM product needs. Journalists, researchers, and transparency groups are a fourth realistic user for spending-pattern analysis by organism and process type, using tipoCompra, reparticion, estadoCompraLabel, and fechaAdjudicacion — but note the source has no single "value awarded" field beyond presupuestoOficial (official budget, populated only "when published"), so this actor supports budget-pattern analysis, not a guaranteed final-contract-value dataset.
Technical Features & V2 Architecture Highlights
Cross-run delta persistence. When onlyNew: true, state is tracked cross-estado, per idCompra, in this actor's own named key-value store, tucuman-compras-monitor-delta-state — distinct from a run's default key-value store, which is created fresh every run and would lose this state immediately. The v2.0 state shape (shipped 2026-09-08) replaced the earlier per-estado { seenIds, lastRunAt } map with one flat, cross-estado map (idCompra -> { estado, hash }), specifically because a per-estado seen-set could tell you an id was new to that estado but could not distinguish "genuinely new" from "just moved here from a different estado" — exactly the distinction STATUS_CHANGE needed. This is a breaking, non-migrated change: a v1-shaped state blob is treated as absent rather than reinterpreted, so an existing scheduled task's first v2 run re-baselines (every currently-known id is reported once more, classified fresh) rather than silently misreading old state.
Real, domain-specific event types. This actor's event_type field uses NEW_LISTING, STATUS_CHANGE, UPDATED, and UNCHANGED — not a generic listing-monitor set. NEW_LISTING means the idCompra was never seen before, under any estado. STATUS_CHANGE means the id was seen before under a different estadoCompra — a real lifecycle transition (e.g. moved from upcoming to awarded) — and carries previousEstado, the estado it was last seen under. UPDATED means the id was seen before under the same estado, but its content fingerprint changed (a changed monto, renglon, date, or any other field), detected via a sha1 contentHash of the tender's changeable fields — computed at zero extra HTTP cost, since every field is already inline in the one listing fetch this actor makes. UNCHANGED is only ever delivered on a plain, non-delta run (onlyNew: false), where "everything" now genuinely means everything, including records with no real change since last seen.
onlyNew semantics, exactly as documented in this actor's own input schema. It is a delta-mode flag for recurring/scheduled monitoring that returns only tenders that are new, moved to a different estadoCompra (a real lifecycle transition), or amended (a changed monto, renglon, or date) since a previous run — implemented as a post-filter, not early-stop pagination. This is a deliberate design decision, not a missed optimization: this portal's own listing order was live-verified (2026-09-06) to be sorted by each tender's own opening-date field — ascending for estado 1, descending for estado 3 — not by when the tender was added to the site. That means a later page can hold a genuinely new tender while an earlier page is full of ones already seen, so the actor still fetches up to maxItems as usual and only then drops unchanged, already-seen ids, rather than stopping early once N pages come back empty.
Standardized envelope fields. Every record carries five fleet-standardized envelope fields, per this actor's own README: record_id (same value as idCompra), event_type, scraped_at (ISO-8601, same for every record in one run), is_new (true if this id was never returned by any previous run, under any estado — computed even when onlyNew=false), and source_url (the listing page this record was read from, with a fragment identifying its own detail modal, e.g. #myModal8900). This actor also adds two delta-specific fields beyond that base envelope — previousEstado (set only on STATUS_CHANGE) and contentHash (the sha1 fingerprint used to detect UPDATED) — that are specific to Tucuman's lifecycle/amendment tracking rather than part of the shared envelope every fleet actor carries.
No separate detail-page fetch, by design. The site's only per-tender detail link (detalle_llamado.php?id_compra=N) is dead — commented out of the live HTML. The one link that does work ("Imprimir") points to a different domain and, when live-tested, returned a byte-for-byte duplicate of the same modal content already embedded in the listing page. So this actor makes exactly one HTTP request per page of 5 tenders and never needs a fetchDetail toggle, because there is no second, richer source to optionally skip.
Input Schema & JSON Configuration Example
| Field | Type | Default | Description |
|---|---|---|---|
estados | array of string (enum "1", "2", "3") | ["1"] | Which procurement states to fetch (estado_compra on the portal). 1 = upcoming opening, 2 = in adjudication, 3 = already awarded. |
maxItems | integer | 100 | Hard cap on the number of tenders returned this run, across all selected states. The portal lists only 5 tenders per page, so a high value across multiple states means many sequential requests - estado 2 alone has 3700+ historical records. Start small. |
onlyNew | boolean | false | Delta mode for recurring/scheduled monitoring: returns only tenders that are new, changed to a different estado_compra (a real lifecycle transition, e.g. upcoming -> awarded) or amended (a changed monto, renglon or date) since a previous run. Tracked cross-estado, per idCompra, in this actor's own named key-value store (tucuman-compras-monitor-delta-state), so it survives between scheduled runs - unlike the run's default key-value store, which is created fresh per run. Implemented as a POST-FILTER: this actor still fetches up to maxItems as usual and only then drops unchanged already-seen ids, rather than stopping pagination early once N pages come back with nothing new. Early-stop is NOT used here because this portal's own listing order was verified live (2026-09-06) to be sorted by each tender's own opening-date field - ascending for estado 1, descending for estado 3 - not by when the tender was added to the site, so a later page can hold a genuinely new tender while an earlier page is full of ones already seen. See AGENTS.md for the page-by-page evidence. |
eventTypes | array of string (enum NEW_LISTING, STATUS_CHANGE, UPDATED) | ["NEW_LISTING", "STATUS_CHANGE", "UPDATED"] | Which kinds of change to deliver when 'Only new' is on (ignored, everything delivered, when it is off). NEW_LISTING = never seen before. STATUS_CHANGE = seen before under a different estado_compra (a real lifecycle transition). UPDATED = seen before, same estado, but a monto, renglon, date or other field changed. |
dateRange | string (enum "24h", "7d", "30d") | (none) | Filters results to tenders whose fechaAperturaSobres (bid-opening date - the only per-tender date this source publishes) falls within the last 24 hours / 7 days / 30 days. Independent of onlyNew - combine both for 'new to me AND recently opened'. Gotcha: for estado 1 (apertura proxima) this date is always in the FUTURE, since the opening has not happened yet, so it can never fall inside this backward-looking window - dateRange only has a real effect on estado 2/3 tenders, whose opening has already occurred. |
Example configuration — recurring monitor of all three lifecycle stages, delta mode on, only genuinely new or changed tenders delivered:
{"estados": ["1", "2", "3"],"maxItems": 200,"onlyNew": true,"eventTypes": ["NEW_LISTING", "STATUS_CHANGE", "UPDATED"],"dateRange": "7d"}
A minimal one-off pull of upcoming-opening tenders only:
{ "estados": ["1"], "maxItems": 100 }
Output Dataset Sample & Data Dictionary
| Field | Type | Description |
|---|---|---|
idCompra | string | Tender ID (the site's own unique identifier). |
estadoCompra | string | 1, 2 or 3. |
estadoCompraLabel | string | Human-readable label for the estado (upcoming, in adjudication, awarded). |
reparticion | string | Buying organism. |
tipoCompra | string | Procurement type, e.g. "LICITACION PUBLICA", "CONCURSO DE PRECIOS", "CONTRATACION DIRECTA". |
rubro | string | Category. |
numeroExpediente | string | File (expediente) number. |
numeroConvocatoria | string or null | Human-readable call number, e.g. "CONCURSO DE PRECIOS Nº 012/2026". |
primerRenglon | string | First line item, as shown on the listing card. |
valorPliego | string | Bid document price. |
presupuestoOficial | string or null | Official budget, when published. |
garantiaOfertaExigida | string or null | Required bid bond, when published. |
fechaAperturaSobres | string or null | Bid-opening date/time. |
fechaAdjudicacion | string or null | Award date - populated only for awarded (estado 3) tenders. |
lugarApertura | string or null | Bid-opening location. |
informesAdquisicionPliegos | string or null | Where to get more info / buy the pliego. |
autorizadoPor | string or null | Authorizing resolution, when published. |
objetoLibre | string or null | Free-text note, when the organism added one. |
renglones | array of { renglon, descripcion } | Full line-item breakdown - can be 1-4+ items per tender. |
pliegoPdfUrl | string or null | Direct downloadable pliego PDF link, when linked. |
record_id | string | Standardized envelope field: same value as idCompra. |
event_type | string | NEW_LISTING, STATUS_CHANGE, UPDATED or UNCHANGED (see Technical Features above). |
scraped_at | string | ISO-8601 timestamp of this run's extraction, same for every record from one run. |
is_new | boolean | True if this record's id was not already returned by a previous run at all, under any estado (computed even when onlyNew=false). |
previousEstado | string or null | Set only when event_type=STATUS_CHANGE: the estadoCompra this id was last seen under. |
contentHash | string | sha1 fingerprint of this record's changeable fields as of this run - used to detect UPDATED between runs. |
source_url | string | The listing page this record was read from, with a fragment identifying its own modal. |
Sample dataset record (values below are realistic examples for documentation; field names are exact):
{"idCompra": "8920","estadoCompra": "1","estadoCompraLabel": "Apertura proxima","reparticion": "Ministerio de Obras y Servicios Publicos","tipoCompra": "LICITACION PUBLICA","rubro": "Construccion y obra vial","numeroExpediente": "12345/2026-J","numeroConvocatoria": "LICITACION PUBLICA Nº 044/2026","primerRenglon": "Repavimentacion Ruta Provincial 301, tramo km 12-18","valorPliego": "$ 15.000,00","presupuestoOficial": "$ 48.500.000,00","garantiaOfertaExigida": "$ 485.000,00","fechaAperturaSobres": "10/09/2026, 10:00:00","fechaAdjudicacion": null,"lugarApertura": "Direccion Provincial de Vialidad - San Miguel de Tucuman","informesAdquisicionPliegos": "Direccion Provincial de Vialidad, Av. Sarmiento 1050, hasta el dia 09/09/2026","autorizadoPor": "Resolucion Nº 0891/2026","objetoLibre": null,"renglones": [{ "renglon": "1", "descripcion": "Repavimentacion asfaltica, tramo km 12-15" },{ "renglon": "2", "descripcion": "Sealing y bacheo, tramo km 15-18" }],"pliegoPdfUrl": "https://comprasbys.tucuman.gob.ar/aplicacion/a_pdf/8920.pdf","record_id": "8920","event_type": "NEW_LISTING","scraped_at": "2026-09-08T14:32:07.512Z","is_new": true,"previousEstado": null,"contentHash": "a3f1c9d8e7b6021f4c5d8a9b0e1f2c3d4a5b6c7d","source_url": "https://comprasbys.tucuman.gob.ar/ver_llamados_compras_avanzado.php?n=1&pagina_actual=3&estado_compra=1#myModal8920"}
Multi-language Integration Snippets
cURL
curl "https://api.apify.com/v2/acts/stefano_seggio~tucuman-compras-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"estados": ["1", "2", "3"],"maxItems": 200,"onlyNew": true,"eventTypes": ["NEW_LISTING", "STATUS_CHANGE", "UPDATED"],"dateRange": "7d"}'
Python (apify-client)
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("stefano_seggio/tucuman-compras-monitor").call(run_input={"estados": ["1", "2", "3"],"maxItems": 200,"onlyNew": True,"eventTypes": ["NEW_LISTING", "STATUS_CHANGE", "UPDATED"],"dateRange": "7d",})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["record_id"], item["event_type"], item["previousEstado"], item["is_new"])
Node.js (apify-client)
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('stefano_seggio/tucuman-compras-monitor').call({estados: ['1', '2', '3'],maxItems: 200,onlyNew: true,eventTypes: ['NEW_LISTING', 'STATUS_CHANGE', 'UPDATED'],dateRange: '7d',});const { items } = await client.dataset(run.defaultDatasetId).listItems();items.forEach((item) => {console.log(item.record_id, item.event_type, item.previousEstado, item.is_new);});
Pricing Model Explanation
This actor bills on two named events, platform usage included:
| Event | Price | When it fires |
|---|---|---|
result | $0.003 per record | Once for every tender delivered to the dataset. |
apify-actor-start | $0.00005 | Once per run. |
Unlike sibling actors in this portfolio, there is no cheaper result-summary tier on this actor, and that is a deliberate pricing decision, not an oversight. Those sibling actors (e.g. the Australia/UK HSE, Florida, and Santa Fe monitors) can charge less for a listing-only record because fetching full detail on those sources costs a second HTTP request that a fetchDetail: false mode can skip. Tucuman's source never had that second request to skip: the portal's only would-be "detail page" link is dead, and the one working alternate link returned a byte-for-byte duplicate of data already present in the listing response. Every Tucuman record — whether it is NEW_LISTING, STATUS_CHANGE, or UPDATED — already carries full modal-level detail (renglones, montos, pliego link) at identical extraction cost, one request per 5 tenders regardless. Introducing a cheaper tier here would not reflect any real cost difference, so every delivered record is charged the same single result event at $0.003.
If onlyNew is enabled, an unchanged tender (one whose estadoCompra and contentHash both match what was already stored from a previous run) is filtered out entirely before it ever becomes a dataset item — it is simply never created as a result event, not billed at $0. In practice this means a daily monitor of estado 1+3 that finds only 3 real changes on a given day costs roughly $0.01 for that run (about $0.30/month at that pace), while a one-off full pull of all currently-awarded tenders (313 live records at the time this actor was verified) costs roughly $0.94, plus the one-time $0.00005 actor-start charge per run in both cases.