# Tucuman Tenders Scraper & Monitor (`stefano_seggio/tucuman-compras-monitor`) Actor

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.

- **URL**: https://apify.com/stefano\_seggio/tucuman-compras-monitor.md
- **Developed by:** [Stefano Seggio](https://apify.com/stefano_seggio) (community)
- **Categories:** Business, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 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:

```json
{
    "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:

```json
{ "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):

```json
{
    "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**

```bash
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)**

```python
import os
from apify_client import ApifyClient

client = 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)**

```javascript
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.

# Actor input Schema

## `estados` (type: `array`):

Which procurement states to fetch (estado\_compra on the portal). 1 = upcoming opening, 2 = in adjudication, 3 = already awarded.

## `maxItems` (type: `integer`):

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` (type: `boolean`):

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` (type: `array`):

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` (type: `string`):

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. See README.md 'Delta mode'.

## Actor input object example

```json
{
  "estados": [
    "1"
  ],
  "maxItems": 100,
  "onlyNew": false,
  "eventTypes": [
    "NEW_LISTING",
    "STATUS_CHANGE",
    "UPDATED"
  ]
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("stefano_seggio/tucuman-compras-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("stefano_seggio/tucuman-compras-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call stefano_seggio/tucuman-compras-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,stefano_seggio/tucuman-compras-monitor"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/TdJtze8dfyykMj2qA/builds/ATd2LeOg8PwIyyrpO/openapi.json
