Salta Tenders Scraper & Monitor
Pricing
Pay per event
Salta Tenders Scraper & Monitor
Extracts public tenders and contract awards (contrataciones, adjudicaciones) from the Province of Salta, Argentina official procurement portal, with organism, expediente and opening-date detail per process.
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
Salta Tenders Scraper & Monitor — Apify Store Overview
Actor ID: Tx9wBKZyySZa5WcsE · Store URL: https://apify.com/stefano_seggio/salta-compras-monitor · Version: 2.0
Executive Summary & Business Use Case
Salta Tenders Scraper & Monitor extracts every public tender and contract award (contrataciones, adjudicaciones) listed on the Province of Salta, Argentina's official public procurement portal, compras.salta.gob.ar. It walks the portal's currently-open ("vigentes") publications listing — a plain server-rendered PHP page, no browser or proxy required — and, on request, follows each publication through to its own detail page for a richer field set: the buying organism, the expediente (file number), the exact opening date and time, where to consult or purchase the bid documents, and direct links to attached documents such as the pliego (bid specification) and cotización forms. Because the source portal has no historical changelog or amendment feed of its own, this actor also runs a delta engine that persists what it has seen across runs and reports which publications are genuinely new, which were amended, and which have quietly closed.
The data supports three concrete groups of buyers. First, suppliers to provincial organisms — construction, health, IT, and general-services vendors who sell into the Salta public sector — use the organismo and objeto fields to see who is buying what, and rely on event_type=UPDATED to catch a changed deadline or amended bid document before they submit an offer that is already out of date. Second, bid consultants and gestores who track tenders on behalf of several clients simultaneously use event_type=CLOSED and event_type=UPDATED as a daily monitoring feed, so they can tell a client a process has closed (rather than continuing to prepare an offer for something no longer open) or that terms changed since the last check. Third, regional tender-data resellers and LATAM procurement-intelligence platforms use the actor as a structured, change-aware substitute for building and maintaining their own screen-scraper against one of dozens of Argentine provincial procurement sites — the full envelope (record_id, event_type, scraped_at, is_new, source_url, contentHash) gives them exactly the fields needed to merge Salta into a larger multi-province feed. Note that this source portal exposes no monetary "amount" or "budget" field on the listing or detail page as a first-class field, so this data is best suited to tracking who is buying what and when, and whether a tracked process has changed or closed — not to value-based spend analytics.
Technical Features & V2 Architecture Highlights
Architecture. The actor is a plain fetch + Cheerio scraper — no headless browser, no proxy. This was verified live against compras.salta.gob.ar: the homepage, a paginated listing page, and a detail page were all grepped for ASPxGridView|dxgv|DevExpress|UpdatePanel|ScriptManager|__VIEWSTATE|react|angular|vue|id="root" and returned zero matches — the tender data arrives fully formed in the initial server-rendered HTML, served as Content-Type: text/html; charset=UTF-8 with correct round-tripping of accented Spanish characters. The only frontend JavaScript present drives the filter form's UX, not the data itself.
Cross-run delta persistence. When onlyNew is enabled, the actor opens a named key-value store, salta-compras-monitor-delta-state — deliberately not the run's own default key-value store, which is isolated per run and would not survive between scheduled runs. This store holds, per publication id, a fingerprint hash plus enough display fields (titulo, tipoPublicacion, numeroPublicacion, organismo) to describe a record even after it has left the source's vigentes list and can no longer be re-fetched.
Event types are domain-specific, not the fleet's generic default. This actor's event_type field takes one of four real values, per dataset_schema.json: NEW_LISTING (never seen before), UPDATED (a fingerprinted field changed since the last run), UNCHANGED (delivered only on a plain run with onlyNew off), and CLOSED (a previously-seen publication is absent from a complete walk this run — no longer vigente; the source does not say why). Notably, this actor's build history explicitly considered and rejected an HSE-style two-way split (e.g. a "SANCTION"-equivalent category driven off tipoPublicacion, which distinguishes Licitación Pública, Contratación Abreviada, and Adjudicación Simple publications) — reasoned out in AGENTS.md as an unjustified taxonomy, since every publication type here comes from the one listing endpoint and shares one schema; an Adjudicación Simple appearing in the currently-open feed is still an open, actionable listing, not a resolved legal outcome the way a closed conviction record is.
What onlyNew actually does, per the actor's own input_schema.json: "Delta mode for daily/recurring monitoring: returns only publications that are new, changed (a fingerprinted amendment — a new monto, document, date...) or closed since a previous run. Seen ids are persisted across runs in a named key-value store unique to this actor... Pagination is NOT stopped early once already-seen records are reached: this actor's listing is sorted by Fecha/Hora Apertura (the bid-opening deadline), not by when a publication was created, so a genuinely new publication can appear on any page — onlyNew filters after a full fetch up to maxItems instead of short-circuiting it." This is a deliberate, verified design choice: a live check found publications with lower source ids sitting on the last page of the walk simply because their bid-opening date falls years in the future, which rules out any early-stop optimization based on "N consecutive already-seen pages."
CLOSED detection is gated on a complete walk. Because a partial walk (one truncated by maxItems) cannot prove a missing id actually closed rather than simply sitting past where the walk stopped, CLOSED is only ever computed when this run's listing walk was not truncated — maxItems needs to be raised above the real register size (currently roughly 250–300 open publications; the actor's own description notes "the portal serves ~250 currently-open (vigentes) publications at a time, 5 per page") to enable it on a recurring monitor.
Fingerprinting. contentHash is a sha1 fingerprint (computed in src/fingerprint.ts) over every listing-card field, plus — when fetchDetail is on — the detail page's field bag, pdfUrl, and attachments. This is what makes UPDATED detection possible, and it comes at a real, disclosed cost: detecting an amendment on an already-known publication requires re-fetching that publication's detail page again, not just a new one's — so fetchDetail: true re-fetches detail for every walked item this run (new and previously seen), bounded by maxItems, not only the items that ultimately get delivered after onlyNew/eventTypes/dateRange filtering. fetchDetail: false remains a lighter, listing-only path that never pays this cost.
Engineering rigor worth noting for a technical buyer: the CHANGELOG records a double-charge bug caught and fixed before shipping v2.0 — an earlier draft called both Actor.pushData(record, eventName) (which performs the PPE charge itself) and a separate Actor.charge(...) afterward, which would have billed every delivered record twice. This was caught by checking the installed apify SDK's own .d.ts rather than assumed from memory.
The dataset schema itself defines 17 top-level fields (record_id, titulo, tipoPublicacion, numeroPublicacion, fechaApertura, horaApertura, objeto, organismo, expediente, consultaPliego, consultas, event_type, is_new, contentHash, source_url, detail, scraped_at), where detail is itself a nested object (fields, pdfUrl, archivosAdjuntos) carrying the richer, per-publication detail-page data when fetchDetail is enabled.
Input Schema & JSON Configuration Example
| Field | Type | Default | Description |
|---|---|---|---|
fetchDetail | boolean | true | "When enabled (default), fetches each publication's detail page for the full field set (organismo gestor, costo pliego, lugar de entrega, when present) plus attached document links. Disable for a faster, listing-only run." |
maxItems | integer | 100 | "Hard cap on the number of publications returned this run. The portal serves ~250 currently-open (vigentes) publications at a time, 5 per page." (minimum: 1) |
onlyNew | boolean | false | "Delta mode for daily/recurring monitoring: returns only publications that are new, changed (a fingerprinted amendment - a new monto, document, date...) or closed since a previous run. Seen ids are persisted across runs in a named key-value store unique to this actor ("salta-compras-monitor-delta-state"), not the run's own default store (which is isolated per run and would not survive between scheduled runs). Every record still carries is_new, so a normal (onlyNew=false) run also tells you which of its results are new. Pagination is NOT stopped early once already-seen records are reached..." |
eventTypes | array (enum, multi-select) | ["NEW_LISTING", "UPDATED", "CLOSED"] | "Which kinds of change to deliver when 'Only new' is on (ignored, everything delivered, when it is off). NEW_LISTING = never seen before. UPDATED = seen before, a field changed (fingerprinted). CLOSED = a previously-seen publication is absent from a complete walk this run - no longer vigente. CLOSED detection only runs when maxItems does not truncate the walk..." |
dateRange | string (enum: "24h" | "7d" | "30d") | (none) | "Filters results to publications whose own Fecha/Hora Apertura (bid-opening deadline - the only date field this source exposes anywhere, listing or detail; there is no separate publication date) falls within the given window. For the "vigentes" (currently open) publications this actor covers, that date is almost always in the FUTURE relative to when this actor runs, so this is an upcoming-deadline filter ("opens within the next 24h/7d/30d"), not a "published in the last N" filter. Independent of onlyNew." |
A full recurring-monitor configuration, valid against input_schema.json:
{"fetchDetail": true,"maxItems": 300,"onlyNew": true,"eventTypes": ["NEW_LISTING", "UPDATED", "CLOSED"],"dateRange": "30d"}
A minimal one-off pull (all defaults except a lower cap):
{"fetchDetail": true,"maxItems": 100}
Output Dataset Sample & Data Dictionary
| Field | Type | Description |
|---|---|---|
record_id | string | Publication id — the source portal's own id, reused as-is. |
titulo | string | Full header as shown on the portal (e.g. "Adjudicación Simple N° 98/2026"). |
tipoPublicacion | string | Publication type, parsed from the header (e.g. "Adjudicación Simple", "Contratación Abreviada", "Licitación Pública"). |
numeroPublicacion | string | Number as published — raw free text, not a structured number/year pair. |
fechaApertura | string | Bid-opening date (DD/MM/YYYY as printed by the source). |
horaApertura | string | Bid-opening time. |
objeto | string | Subject / short description of the process. |
organismo | string | Organismo Originante y Destino — the buying organism. |
expediente | string | File number (expediente). |
consultaPliego | string | Where to consult or purchase the bid documents (Consulta y Adquisición Pliego). |
consultas | string | Contact information for questions about the process. |
event_type | string | NEW_LISTING, UPDATED, UNCHANGED (only when onlyNew is off), or CLOSED (no longer vigente). |
is_new | boolean | true if record_id was not seen by a previous run — computed even when onlyNew is off. |
contentHash | string | sha1 fingerprint of this record's changeable fields, used to detect UPDATED between runs. |
source_url | string | Link to the official detail page for this publication. |
detail | object | null | Present when fetchDetail is on. Contains fields (an object of labeled detail-page fields — e.g. Organismo Gestor, Costo Pliego — whose exact set varies per publication), pdfUrl (the portal's own auto-generated PDF of the publication, or null), and archivosAdjuntos (an array of { nombre, url } uploaded attachments). |
scraped_at | string | ISO timestamp of this run's extraction — the same value for every record from one run. |
A realistic dataset record (illustrative values; every field name above is drawn directly from dataset_schema.json):
{"record_id": "148437","titulo": "Adjudicación Simple N° 98/2026","tipoPublicacion": "Adjudicación Simple","numeroPublicacion": "98/2026","fechaApertura": "12/09/2026","horaApertura": "10:00","objeto": "Adquisición de insumos de librería para Hospital Público Materno Infantil","organismo": "Ministerio de Salud Pública","expediente": "EXP-2026-148437","consultaPliego": "Dirección General de Contrataciones, Av. Belgrano 1349, Salta Capital","consultas": "compras@salta.gob.ar","event_type": "UPDATED","is_new": false,"contentHash": "7f3a9c1e5b8d2046f9a3c7e1b5d9f204a8c6e3b1","source_url": "https://compras.salta.gob.ar/publico/publicacionactual/verpublicacion1/148437/0","detail": {"fields": {"Organismo Gestor": "Ministerio de Salud Pública","Costo Pliego": "$0,00","Expediente": "EXP-2026-148437"},"pdfUrl": "https://compras.salta.gob.ar/publico/publicacionactual/verpublicacionpdf/148437","archivosAdjuntos": [{ "nombre": "Pliego de Condiciones.pdf", "url": "https://compras.salta.gob.ar/adjuntos/148437/pliego.pdf" }]},"scraped_at": "2026-09-08T14:32:10.000Z"}
Multi-language Integration Snippets
cURL
curl "https://api.apify.com/v2/acts/stefano_seggio~salta-compras-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"fetchDetail": true,"maxItems": 300,"onlyNew": true,"eventTypes": ["NEW_LISTING", "UPDATED", "CLOSED"],"dateRange": "30d"}'
Python (apify-client)
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("stefano_seggio/salta-compras-monitor").call(run_input={"fetchDetail": True,"maxItems": 300,"onlyNew": True,"eventTypes": ["NEW_LISTING", "UPDATED", "CLOSED"],"dateRange": "30d",})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["record_id"], item["event_type"], item["organismo"], item["objeto"])
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/salta-compras-monitor').call({fetchDetail: true,maxItems: 300,onlyNew: true,eventTypes: ['NEW_LISTING', 'UPDATED', 'CLOSED'],dateRange: '30d',});const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const item of items) {console.log(item.record_id, item.event_type, item.organismo, item.objeto);}
Pricing Model Explanation
This actor bills per event ("pay-per-event"), with platform usage included in the event price — there is no separate compute charge on top.
| Event | Price | When it fires on this actor |
|---|---|---|
result | $0.003 per record | A record this run fetched fresh detail for — either a brand-new publication or an amended one, whenever fetchDetail: true. |
result-summary | $0.001 per record | A listing-only record (run with fetchDetail: false), or a CLOSED record — a CLOSED publication has nothing left to re-fetch (it's gone from the source), so it is always billed at the lower tier regardless of the fetchDetail setting. |
apify-actor-start | $0.00005 | Once per run. |
The two-tier split here is specifically about how much work went into producing that record, not about "new vs. old": a record only bills at the higher $0.003 result tier when this run actually reached out to that publication's own detail page and pulled its full field set (organism, expediente, attached documents). A record produced from the listing page alone — because fetchDetail was turned off, or because the publication was determined CLOSED and there is no detail page left to fetch — bills at the lower $0.001 result-summary tier. A daily monitor of the roughly 250-publication register that finds 5 changes costs about $0.02/day (~$0.60/month); a one-off full pull with detail costs about $0.75, per the actor's own documented pricing example.
On onlyNew and unchanged records: when onlyNew: true, a publication that is still open and has not changed since the last run is classified UNCHANGED internally — and UNCHANGED is only ever a deliverable event_type when onlyNew is off. In delta mode, an unchanged record is simply never pushed to the dataset at all. Concretely, that means it is never billed: not as a $0.001 result-summary "unchanged" event, not at any reduced rate — it is filtered out before Actor.pushData() is ever called for it, so no chargeable event is created for it in the first place. You only pay for records this run actually delivers: new listings, real amendments, and confirmed closures (narrowed further by whichever subset of eventTypes you selected).