# Mendoza Tenders Scraper & Monitor (`stefano_seggio/mendoza-compras-monitor`) Actor

Extracts public procurement processes (licitaciones, contrataciones directas) from the Province of Mendoza, Argentina COMPR.AR-based portal, with type, status, organism and budget per process.

- **URL**: https://apify.com/stefano\_seggio/mendoza-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

## Mendoza Tenders Scraper & Monitor — Apify Store Overview

**Store URL:** https://apify.com/stefano\_seggio/mendoza-compras-monitor
**Actor ID:** bb4cRgt1i27hvr9Ug
**Version:** 2.0

***

### Executive Summary & Business Use Case

Mendoza Tenders Scraper & Monitor extracts public procurement processes — *licitaciones* (public tenders) and *contrataciones directas* (direct-award contracts) — from the Province of Mendoza, Argentina's official COMPR.AR-based procurement portal, `comprar.mendoza.gov.ar`. For every process it walks, the actor returns the process number, name, type, status, executing organism, administrative/financial service, and budget amount, pulled from a real backlog of 25,000+ processes on the portal's results grid. Since the 2.0 release, it also runs a delta engine: on repeat runs it can report not just processes it has never seen before, but ones whose status changed (e.g. "Pendiente Análisis" → "Adjudicado") or whose content was amended (budget, opening date, executing unit), turning a one-off scrape into an ongoing monitor.

The most direct use case is **supplier and bidder monitoring**: companies that sell to Mendoza's provincial organisms (construction, health, IT, general services firms) can track specific processes and get notified the moment a tracked tender's `estado` moves to "Adjudicado" or its `monto` changes, instead of manually re-checking the portal. A second use case is **bid consultants and gestores** who manage tender pipelines for several clients simultaneously — a daily delta run surfaces exactly which of their clients' tracked processes changed status or were amended since yesterday, with a direct link to hand the client. A third is **transparency and spending-pattern analysis**: journalists, researchers, and public-accountability groups can use `tipoProceso`, `unidadEjecutora`, and `monto` across a large `maxItems` pull to see which organisms rely most heavily on direct-award contracting (which typically has less competitive oversight than open tenders) versus formal public tenders. A fourth, adjacent use is regional tender-data resellers and LATAM procurement platforms who would rather consume a structured, change-aware feed of a real ASP.NET postback-driven session flow than build and maintain that scraping logic themselves.

All of these use cases are grounded directly in what the source portal actually exposes: process type, status, organism, and a raw budget figure. The actor deliberately does not claim capabilities the source doesn't support — for example, it does not report a `CLOSED` event (a process disappearing from the register), because with 25,784+ total processes only a full sequential census could trustworthily prove a process is gone, and that census would take hours, not seconds — so that event was considered and explicitly rejected rather than faked.

***

### Technical Features & V2 Architecture Highlights

**Cross-run delta persistence.** The actor opens a *named* Apify key-value store (not the run's own default, isolated-per-run store) to persist state between scheduled runs. As of v2.0, that state shape is `entries: Record<numeroProceso, {estado, hash}>` — for every process it has ever walked, it remembers the process's last-known `estado` and a content fingerprint. This is a breaking change from the v1 shape (a bare list of seen ids): a v1-shaped state is treated as absent rather than migrated, so an existing scheduled task's first v2 run re-baselines from scratch.

**Real event types — three, not the generic four-event set.** When `onlyNew` is on, every delivered record carries one of exactly three event types, read directly from `event_type` in the dataset schema:

- `NEW_LISTING` — the process has never been seen by this actor before.
- `STATUS_CHANGE` — the process was seen before and its `estado` changed since then (e.g. "Pendiente Análisis" → "Adjudicado"). This is detected for free: `estado` is already present in every grid row the actor walks, so no extra request is needed.
- `UPDATED` — the process was seen before, `estado` is unchanged, but some other field changed (monto, fecha, unidad ejecutora, etc.), detected via a sha1 content fingerprint (`contentHash`) computed over the row's mutable fields.

A fourth value, `UNCHANGED`, appears only when `onlyNew` is off (full-mode runs) — it is not a delta-mode delivery option. Note what this actor's event set does **not** include: there is no `CLOSED` event, unlike some sibling actors in this actor family that operate on much smaller procurement registers. That omission is a deliberate design decision (documented in this actor's own CHANGELOG and AGENTS notes), not an oversight — proving a process has closed would require a complete sequential census of the ~25,784-process backlog, which is infeasible within a normal run's time budget.

**`onlyNew` — a safe post-filter, not an early-stop optimization.** Per this actor's own input schema, `onlyNew` persists which `numeroProceso` ids have already been returned — along with their last-known `estado` and content fingerprint — in the named key-value store, and keeps only records that are new, status-changed, or amended. Critically, and unlike a typical "delta mode," this does **not** make the actor stop paginating early. The portal's listing is sorted by `numeroProceso` ascending (each organism's own sequential counter, cycling through years), verified live to mix processes from 2019 through 2026 on page one of a blank search — not newest-first. So `onlyNew` fetches up to `maxItems` raw processes exactly as a normal run would, and only after each row is walked does it check the seen-set to decide whether to keep it. A brand-new process is not guaranteed to appear within a small `maxItems` window; raising `maxItems` is the way to cover more of the backlog on a delta run.

**One genuinely expensive step, made optional.** Unlike sources with a plain per-row link, Mendoza's results grid links are session-bound `javascript:__doPostBack(...)` calls with no plain href. Getting a real, stable, cookie-independent permalink (`source_url`) for a process means replaying that row's own postback — a real, measured cost of roughly 1.5 seconds per row. The `resolveSourceUrl` input (default `true`) makes this optional: disabling it skips the extra request per record, producing a much faster run when a process-specific link isn't needed, at the cost of `source_url` falling back to the generic search page. Every record's `sourceUrlResolved` field records whether the permalink was genuinely resolved this run or is the fallback.

**Field count.** The dataset schema defines 15 fields per record: 7 standardized B2B integration-envelope fields (`record_id`, `event_type`, `scraped_at`, `is_new`, `contentHash`, `sourceUrlResolved`, `source_url`) plus 8 domain-specific fields (`numeroProceso`, `nombreProceso`, `tipoProceso`, `fechaApertura`, `estado`, `unidadEjecutora`, `servicioAdministrativoFinanciero`, `monto`).

***

### Input Schema & JSON Configuration Example

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `maxItems` | integer | `100` | Hard cap on the number of RAW processes walked this run (10 per page, 25,000+ total at audit time). This is the same cap as before - onlyNew/dateRange are applied on top of this raw walk as post-filters, so the number of records actually returned can be lower than maxItems when either is active. This actor does not yet expose the advanced-search form's own filters (date range, organism, process type) as actor input; start small, raise gradually. |
| `onlyNew` | boolean | `false` | Persists which numeroProceso ids this actor has already returned - and their last-known estado + content fingerprint - in its own private key-value store that survives between scheduled runs, and keeps only the ones that are new, status-changed or amended. IMPORTANT for this source: comprar.mendoza.gov.ar's process listing is sorted by numero de proceso ascending (each organism's own sequential counter, cycling through years) - NOT by date and NOT newest-first. Verified live: a blank-form search's first page mixes processes from 2019 through 2026 rather than showing only the most recent ones. Because of this, onlyNew is a safe POST-FILTER (fetch up to maxItems raw processes exactly as a normal run does, then drop the ones already seen and unchanged) rather than an early-stop optimization - it does NOT make pagination stop sooner, and it does NOT guarantee a newly created process will appear within a small maxItems window, since its position in the list depends on its own organism's counter, not on when it was published. Raise maxItems to cover more of the backlog if a delta run keeps returning fewer new records than expected. See AGENTS.md for the full live evidence. |
| `eventTypes` | array (enum: `NEW_LISTING`, `STATUS_CHANGE`, `UPDATED`) | `["NEW_LISTING", "STATUS_CHANGE", "UPDATED"]` | Which kinds of change to deliver when 'Only new records' is on (ignored, everything delivered, when it is off). NEW\_LISTING = never seen before. STATUS\_CHANGE = seen before, estado changed (e.g. Pendiente Analisis -> Adjudicado) - free to detect, no extra request. UPDATED = seen before, same estado, a field changed (monto, fecha, unidad ejecutora...). |
| `resolveSourceUrl` | boolean | `true` | When enabled (default), resolves each delivered record's own permalink via one extra postback per row (~1.5s each - a real, measured cost, see AGENTS.md). Disable for a much faster run when you don't need a process-specific link: source\_url falls back to the plain search page, and delivery is charged at the cheaper result-summary rate instead of result. See README 'How much does it cost'. |
| `dateRange` | string (enum: `24h`, `7d`, `30d`) | *(none)* | Restricts the (raw, up-to-maxItems) results to processes whose own 'Fecha de apertura' (scheduled bid-opening date/time) falls within this window ending now. Independent of onlyNew. Note: Fecha de apertura is WHEN BIDS OPEN, not when the process was published on the portal - a currently-open tender is routinely dated in the future relative to when it first appeared, so this answers 'opens/opened in the last N', not 'listed in the last N'. A future-dated process never matches any window until its own opening date falls inside it. See README for detail. |

#### Example configuration — plain pull

```json
{
  "maxItems": 100
}
```

#### Example configuration — recurring delta monitor, permalinks resolved

```json
{
  "maxItems": 500,
  "onlyNew": true,
  "eventTypes": ["STATUS_CHANGE", "UPDATED"],
  "resolveSourceUrl": true
}
```

#### Example configuration — large backlog sweep, fast and cheap

```json
{
  "maxItems": 1000,
  "onlyNew": true,
  "resolveSourceUrl": false,
  "dateRange": "30d"
}
```

***

### Output Dataset Sample & Data Dictionary

| Field | Type | Description |
| --- | --- | --- |
| `numeroProceso` | string | e.g. `10201-0001-CDI20` |
| `nombreProceso` | string | Process name/subject |
| `tipoProceso` | string | e.g. Contratacion Directa, Licitacion Publica |
| `fechaApertura` | string | Scheduled bid-opening date and time |
| `estado` | string | Process status, e.g. "Pendiente Análisis", "Adjudicado", "Desierto" |
| `unidadEjecutora` | string | Executing unit |
| `servicioAdministrativoFinanciero` | string | Administrative/financial service |
| `monto` | string | Budget amount, raw source format (Argentine comma decimal) |
| `record_id` | string | Same value as numeroProceso - stable across runs |
| `event_type` | string | NEW\_LISTING (never seen before), STATUS\_CHANGE (estado changed since last seen), UPDATED (a field changed, same estado) or UNCHANGED (only when Only new records is off). |
| `scraped_at` | string | ISO-8601 timestamp of this extraction (same for every record in one run) |
| `is_new` | boolean | true if not seen in a prior run (delta mode) |
| `contentHash` | string | sha1 fingerprint of this record's changeable fields - used to detect UPDATED between runs. |
| `sourceUrlResolved` | boolean | True when source\_url is a genuine process-specific permalink; false when it's the generic search-page fallback. |
| `source_url` | string | Direct, cookie-independent link to the official process page (or the generic search page - see sourceUrlResolved) |

#### Sample record

```json
{
  "numeroProceso": "10201-0034-CDI24",
  "nombreProceso": "Adquisicion de insumos de bioseguridad para hospitales publicos",
  "tipoProceso": "Contratacion Directa",
  "fechaApertura": "12/11/2026 10:00 Hrs.",
  "estado": "Adjudicado",
  "unidadEjecutora": "Ministerio de Salud, Desarrollo Social y Deportes",
  "servicioAdministrativoFinanciero": "SAF Central Salud",
  "monto": "1869000,00",
  "record_id": "10201-0034-CDI24",
  "event_type": "STATUS_CHANGE",
  "scraped_at": "2026-09-08T09:14:00.000Z",
  "is_new": false,
  "contentHash": "8f2a1c9e7b6d4a0f3e5c1b9d7a6f4e2c1b0a9d8e",
  "sourceUrlResolved": true,
  "source_url": "https://comprar.mendoza.gov.ar/PLIEGO/VistaPreviaPliegoCiudadano.aspx?qs=9f7a2e1c0d8b6a4f2e1c0d9b8a7f6e5d"
}
```

***

### Multi-language Integration Snippets

#### cURL

```bash
curl "https://api.apify.com/v2/acts/stefano_seggio~mendoza-compras-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "maxItems": 500,
    "onlyNew": true,
    "eventTypes": ["STATUS_CHANGE", "UPDATED"],
    "resolveSourceUrl": true
  }'
```

#### Python (apify-client)

```python
from apify_client import ApifyClient

client = ApifyClient(token="YOUR_APIFY_TOKEN")

run_input = {
    "maxItems": 500,
    "onlyNew": True,
    "eventTypes": ["STATUS_CHANGE", "UPDATED"],
    "resolveSourceUrl": True,
}

run = client.actor("stefano_seggio/mendoza-compras-monitor").call(run_input=run_input)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(f"[{item['event_type']}] {item['numeroProceso']} - {item['estado']} - {item['source_url']}")
```

#### Node.js (apify-client)

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

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });

const runInput = {
    maxItems: 500,
    onlyNew: true,
    eventTypes: ['STATUS_CHANGE', 'UPDATED'],
    resolveSourceUrl: true,
};

const run = await client.actor('stefano_seggio/mendoza-compras-monitor').call(runInput);

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) {
    console.log(`[${item.event_type}] ${item.numeroProceso} - ${item.estado} - ${item.source_url}`);
}
```

***

### Pricing Model Explanation

This actor bills per result event ("Pay per event"), with platform usage costs already included in each event's price:

| Event | Price | When it fires for this actor |
| --- | --- | --- |
| `result` | $0.003 per record | The delivered record's `source_url` was genuinely resolved this run — i.e. `resolveSourceUrl` was `true` (the default) and the per-row detail postback succeeded. |
| `result-summary` | $0.001 per record | Either `resolveSourceUrl` was set to `false`, or (with it left on) the per-row detail postback failed or the row's link markup was missing — in both cases `source_url` falls back to the generic search-page URL rather than a genuine permalink, and `sourceUrlResolved` is `false`. |
| `apify-actor-start` | $0.00005 | Once per run, regardless of configuration. |

The two-tier pricing exists specifically because of the one genuinely expensive step this actor performs: resolving a process-specific permalink for `source_url` costs one extra sequential postback per record (~1.5 seconds each), because Mendoza's results grid exposes only session-bound `javascript:__doPostBack(...)` links with no plain href to build a URL from. When that extra request happens and succeeds, the record is billed at the full `result` rate ($0.003) because it carries a genuine, stable, shareable link. When that step is skipped entirely (`resolveSourceUrl: false`) or it fails, the record still contains every domain field and every envelope field — it is simply missing a real permalink — and is billed at the lower `result-summary` rate ($0.001) to reflect that reduced value. This is not a "new vs. old record" distinction; it is a "was the one expensive lookup performed and did it succeed" distinction, and it applies identically to `NEW_LISTING`, `STATUS_CHANGE`, and `UPDATED` records alike.

For delta monitoring, enabling `onlyNew: true` means the actor still walks up to `maxItems` raw rows every run (this source's listing is not sorted newest-first, so there is no early-stop), but only rows found to be new, status-changed, or amended are actually pushed to the dataset — an already-seen, unchanged row is filtered out after being walked and is never created as a dataset item at all. It is not delivered and not billed at $0; it simply never becomes a chargeable event in the first place. This also means `onlyNew` reduces cost indirectly: rows filtered out by the seen-set never trigger the `resolveSourceUrl` detail postback either, so a warm delta run only pays the higher `result` rate for rows that genuinely changed.

**Example run cost:** A daily monitor that finds 5 changed processes with `resolveSourceUrl: true` costs about 5 × $0.003 + $0.00005 ≈ $0.015–0.02 per run (roughly $0.45–0.60/month at one run/day). A `resolveSourceUrl: false` sweep over a much larger `maxItems` window (e.g. covering more of the 25,784-process backlog) is both faster and cheaper per record, at the cost of every `source_url` being the generic search page rather than a process-specific link.

# Actor input Schema

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

Hard cap on the number of RAW processes walked this run (10 per page, 25,000+ total at audit time). This is the same cap as before - onlyNew/dateRange are applied on top of this raw walk as post-filters, so the number of records actually returned can be lower than maxItems when either is active. This actor does not yet expose the advanced-search form's own filters (date range, organism, process type) as actor input; start small, raise gradually.

## `onlyNew` (type: `boolean`):

Persists which numeroProceso ids this actor has already returned - and their last-known estado + content fingerprint - in its own private key-value store that survives between scheduled runs, and keeps only the ones that are new, status-changed or amended. IMPORTANT for this source: comprar.mendoza.gov.ar's process listing is sorted by numero de proceso ascending (each organism's own sequential counter, cycling through years) - NOT by date and NOT newest-first. Verified live: a blank-form search's first page mixes processes from 2019 through 2026 rather than showing only the most recent ones. Because of this, onlyNew is a safe POST-FILTER (fetch up to maxItems raw processes exactly as a normal run does, then drop the ones already seen and unchanged) rather than an early-stop optimization - it does NOT make pagination stop sooner, and it does NOT guarantee a newly created process will appear within a small maxItems window, since its position in the list depends on its own organism's counter, not on when it was published. Raise maxItems to cover more of the backlog if a delta run keeps returning fewer new records than expected. See AGENTS.md for the full live evidence.

## `eventTypes` (type: `array`):

Which kinds of change to deliver when 'Only new records' is on (ignored, everything delivered, when it is off). NEW\_LISTING = never seen before. STATUS\_CHANGE = seen before, estado changed (e.g. Pendiente Analisis -> Adjudicado) - free to detect, no extra request. UPDATED = seen before, same estado, a field changed (monto, fecha, unidad ejecutora...).

## `resolveSourceUrl` (type: `boolean`):

When enabled (default), resolves each delivered record's own permalink via one extra postback per row (~1.5s each - a real, measured cost, see AGENTS.md). Disable for a much faster run when you don't need a process-specific link: source\_url falls back to the plain search page, and delivery is charged at the cheaper result-summary rate instead of result. See README 'How much does it cost'.

## `dateRange` (type: `string`):

Restricts the (raw, up-to-maxItems) results to processes whose own 'Fecha de apertura' (scheduled bid-opening date/time) falls within this window ending now. Independent of onlyNew. Note: Fecha de apertura is WHEN BIDS OPEN, not when the process was published on the portal - a currently-open tender is routinely dated in the future relative to when it first appeared, so this answers 'opens/opened in the last N', not 'listed in the last N'. A future-dated process never matches any window until its own opening date falls inside it. See README for detail.

## Actor input object example

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

# 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/mendoza-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/mendoza-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/mendoza-compras-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,stefano_seggio/mendoza-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/bb4cRgt1i27hvr9Ug/builds/qu1AdwgfE6uQrihjd/openapi.json
