# PBA Tenders Scraper & Monitor (`stefano_seggio/pba-tenders-monitor`) Actor

Extracts public tender listings from PBAC, the official procurement portal of Buenos Aires Province, Argentina: upcoming openings, tenders from the last 30 days, and awarded processes, with optional full-detail enrichment.

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

## PBA Tenders Scraper & Monitor — Apify Store Overview

### Executive Summary & Business Use Case

PBA Tenders Scraper & Monitor extracts public tender listings from **PBAC** (`pbac.cgp.gba.gov.ar`), the official procurement portal of the Province of Buenos Aires, Argentina — the country's largest and most populous jurisdiction. The actor reads all three of PBAC's summary grids directly from the server-rendered homepage: **upcoming openings** (`apertura_proxima`), **tenders from the last 30 days** (`ultimos_30_dias`), and **awarded processes** (`adjudicados`), turning them into clean, structured records. No login and no browser are required — PBAC renders these tables as plain Bootstrap HTML on the first GET — and an optional full-detail mode can additionally simulate the site's per-process postback (see the Known Limitation note in the Technical Features section for exactly what that does and does not return today).

No Argentina national procurement actor covers this at the provincial level, and Buenos Aires Province runs its own separate portal from the national `comprar.gob.ar` system. That gap is the actor's core value: anyone who needs to track this specific province's public tenders has no other structured-data source to buy or build against. On top of the raw extraction, the actor ships a delta engine that reports only what genuinely changed between runs — a tender that is new, moved between lifecycle stages, was amended, or disappeared from the site entirely — so recurring monitoring doesn't require re-downloading and re-diffing the full grid yourself.

Three concrete, real-world use cases this data supports: (1) **suppliers to Buenos Aires Province organisms** (health authorities, general-services agencies, etc.) who need to know the moment a tracked tender moves to evaluation or gets awarded, so they can prepare or withdraw an offer at the right moment; (2) **compliance and business-development teams** who want a feed of newly published tenders (`event_type=NEW_LISTING`) filtered by `organismo`, so new opportunities get routed to the right owner without a person manually re-checking the portal; and (3) **bid consultants and gestores managing several clients' tenders at once**, who need a single feed of exactly what changed — `event_type`, `previousVistaOrigen`, `previousEstado` — since yesterday, across every tender they track, so they can notify each client with the specific change rather than a full re-read of the site.

### Technical Features & V2 Architecture Highlights

**Delta engine with cross-run persistence.** This is version 2.0 of the actor, and per its own `CHANGELOG.md` it is "the first delta engine this actor has ever had, built directly to this fleet's v2 standard" — unlike sibling actors in the same portfolio that went through a v1-then-v2 retrofit, PBA Tenders Monitor shipped with `STATUS_CHANGE`/`UPDATED`/`CLOSED` detection from day one. State is tracked in a **named key-value store scoped to this actor** (per the `onlyNew` input field's own description), so a scheduled run can compare against what a prior run last observed without you managing any external database.

**Real, domain-specific event types — not a generic set.** The actor's `event_type` field (confirmed in `.actor/dataset_schema.json`) uses exactly five values: `NEW_LISTING` (never seen before), `STATUS_CHANGE` (the tender's `vistaOrigen` — which of the three PBAC grids it's in — or its `estado` field changed since it was last seen), `UPDATED` (same grid and status, but some other field changed, detected via a sha1 content fingerprint stored in `contentHash`), `UNCHANGED` (delivered only when `onlyNew` is off), and `CLOSED` (no longer listed in any of the three grids). `STATUS_CHANGE` is worth calling out specifically: because a tender moving from "upcoming openings" to "awarded" is a real lifecycle transition that both grids already expose for free, the actor tracks `vistaOrigen` exactly like `estado` and reports whichever one actually changed in `previousVistaOrigen`/`previousEstado`.

**`onlyNew` (delta mode).** Per the actor's own input schema, enabling `onlyNew` means recurring/scheduled runs "return only tenders that are new, changed vistaOrigen/estado, amended or closed since a prior run of this actor." When `onlyNew` is off, every tender in the requested views is returned every run, including ones that are `UNCHANGED`. The companion `eventTypes` input then narrows which of the four delta categories (`NEW_LISTING`/`STATUS_CHANGE`/`UPDATED`/`CLOSED`) get delivered when delta mode is on; it's ignored entirely when `onlyNew` is off.

**`CLOSED` has a real, disclosed gating condition.** `CLOSED` — a tender absent from all three grids — is only ever computed when the `views` input covers all three default views. Requesting a narrower selection turns the run's fetch into a genuine subset of the site, so a previously tracked tender missing from a partial fetch might simply live in a grid you didn't request rather than actually be gone; in that case `CLOSED` is skipped and logged rather than guessed at. Separately, `ultimos_30_dias` is a genuinely rolling 30-day window, so a tender can age out of that one grid purely because time passed — `CLOSED` only ever means "absent from all three grids," never "removed from one specific view."

**Output schema field count.** `.actor/dataset_schema.json` defines 16 fields per record: 9 domain fields describing the tender itself (`numeroProceso`, `descripcion`, `tipoProcedimiento`, `fechaApertura`, `estado`, `organismo`, `vistaOrigen`, `detalleCompleto`, `scrapedAt`) plus the 7-field standardized envelope the v2 changelog introduced (`record_id`, `event_type`, `previousVistaOrigen`, `previousEstado`, `is_new`, `source_url`, `contentHash`). `is_new` is always populated — even on a plain non-delta run — as `true` when the record's id wasn't in the persisted seen-set at the start of the run.

**Required residential proxy.** PBAC blocks non-residential-Argentina traffic — verified live: both no proxy and a standard datacenter proxy time out identically on every request, while Residential proxy with country set to Argentina succeeds consistently. The actor's `proxyConfiguration` input defaults to `{ useApifyProxy: true, apifyProxyGroups: ["RESIDENTIAL"], apifyProxyCountry: "AR" }` and this should not be switched to datacenter.

### Input Schema & JSON Configuration Example

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `views` | array (enum: `apertura_proxima`, `ultimos_30_dias`, `adjudicados`) | all three | Which PBAC grids to extract. Defaults to all three. |
| `fetchFullDetail` | boolean | `false` | When enabled, simulates the postback for each process. Known limitation: this currently returns only a short status label, not the full Pliego document text - that content lives in a nested panel not yet mapped. Leave off until this is resolved; it adds cost with limited extra value today. |
| `maxItems` | integer | `200` | Hard cap on the number of tender processes returned this run, across all selected views. |
| `proxyConfiguration` | object | Residential + Argentina | Required: pbac.cgp.gba.gov.ar blocks non-residential-Argentina traffic. Verified live - datacenter proxy (any provider) times out identically to no proxy at all; only Residential + Argentina succeeds. Defaults to that; do not switch to datacenter. |
| `onlyNew` | boolean | `false` | For recurring/scheduled runs: return only tenders that are new, changed vistaOrigen/estado, amended or closed since a prior run of this actor (tracked in a named key-value store scoped to this actor). CLOSED (a tender no longer in any of the 3 grids) is only ever computed when 'Views to extract' covers all 3 default views - a narrower selection makes this run's fetch a subset of the site, so CLOSED is skipped (and logged) in that case. |
| `eventTypes` | array (enum: `NEW_LISTING`, `STATUS_CHANGE`, `UPDATED`, `CLOSED`) | all four | 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 = vistaOrigen or estado changed - e.g. a tender moving from upcoming to awarded. UPDATED = same vistaOrigen/estado, a field changed. CLOSED = no longer listed in any of the 3 grids (only computed on an unfiltered 'Views to extract'). |
| `dateRange` | string (enum: `24h`, `7d`, `30d`) | (none) | Restricts results to tenders whose fechaApertura falls within this window ending now. Independent of onlyNew. Note: fechaApertura is WHEN BIDS OPEN, not when the tender was published - for the 'Upcoming openings' view specifically that date is routinely in the future, so this filter will not match those rows (see AGENTS.md/README). |

A valid configuration for a daily monitoring run, watching all three grids and receiving only what changed since the previous run:

```json
{
  "views": ["apertura_proxima", "ultimos_30_dias", "adjudicados"],
  "fetchFullDetail": false,
  "maxItems": 200,
  "onlyNew": true,
  "eventTypes": ["NEW_LISTING", "STATUS_CHANGE", "UPDATED", "CLOSED"],
  "dateRange": "30d",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "AR"
  }
}
```

A minimal configuration for a one-off pull of just the two forward-looking grids, no delta tracking:

```json
{
  "views": ["apertura_proxima", "ultimos_30_dias"],
  "maxItems": 100
}
```

### Output Dataset Sample & Data Dictionary

| Field | Type | Description |
| --- | --- | --- |
| `numeroProceso` | string | PBAC process number, e.g. 2026-338-99-265 |
| `descripcion` | string | Short description of the tender |
| `tipoProcedimiento` | string | Licitacion Publica, Licitacion Privada, or Procedimiento Abreviado |
| `fechaApertura` | string | Opening date and time as shown by PBAC |
| `estado` | string | Publicado, En Apertura, En Evaluacion, Disponible Para Adjudicar, etc. |
| `organismo` | string | Contracting unit/agency |
| `vistaOrigen` | string | Which PBAC grid this row came from: apertura\_proxima, ultimos\_30\_dias or adjudicados |
| `detalleCompleto` | object | null | Full document preview text, only populated when fetchFullDetail is enabled |
| `scrapedAt` | string | ISO timestamp of extraction |
| `record_id` | string | Same value as numeroProceso |
| `event_type` | string | NEW\_LISTING (never seen before), STATUS\_CHANGE (vistaOrigen or estado changed), UPDATED (a field changed), UNCHANGED (only when Only new records is off) or CLOSED (no longer in any of the 3 grids). |
| `previousVistaOrigen` | string | null | Set only when event\_type=STATUS\_CHANGE and vistaOrigen changed. |
| `previousEstado` | string | null | Set only when event\_type=STATUS\_CHANGE and estado changed. |
| `is_new` | boolean | True if this id was not in the persisted seen-set when this run started. |
| `source_url` | string | The PBAC homepage - this portal has no per-tender deep link. |
| `contentHash` | string | sha1 fingerprint used to detect UPDATED between runs. |

A realistic single dataset record, as delivered by a delta-mode run for a tender that just moved from "upcoming openings" to a new status:

```json
{
  "numeroProceso": "2026-338-99-265",
  "descripcion": "Adquisicion de insumos de bioseguridad para hospitales de la Region Sanitaria VI",
  "tipoProcedimiento": "Licitacion Publica",
  "fechaApertura": "15/10/2026 10:00",
  "estado": "En Evaluacion",
  "organismo": "Ministerio de Salud de la Provincia de Buenos Aires",
  "vistaOrigen": "ultimos_30_dias",
  "detalleCompleto": null,
  "scrapedAt": "2026-09-08T14:32:07.118Z",
  "record_id": "2026-338-99-265",
  "event_type": "STATUS_CHANGE",
  "previousVistaOrigen": null,
  "previousEstado": "Publicado",
  "is_new": false,
  "source_url": "https://pbac.cgp.gba.gov.ar/",
  "contentHash": "3f9a2b7c4d1e8f0561a29b3c7d4e8f9012ab34cd"
}
```

### Multi-language Integration Snippets

#### cURL

```bash
curl "https://api.apify.com/v2/acts/stefano_seggio~pba-tenders-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "views": ["apertura_proxima", "ultimos_30_dias", "adjudicados"],
    "onlyNew": true,
    "eventTypes": ["NEW_LISTING", "STATUS_CHANGE", "UPDATED", "CLOSED"],
    "maxItems": 200
  }'
```

#### Python (apify-client)

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

run_input = {
    "views": ["apertura_proxima", "ultimos_30_dias", "adjudicados"],
    "onlyNew": True,
    "eventTypes": ["NEW_LISTING", "STATUS_CHANGE", "UPDATED", "CLOSED"],
    "maxItems": 200,
}

run = client.actor("stefano_seggio/pba-tenders-monitor").call(run_input=run_input)

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

#### 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/pba-tenders-monitor').call({
  views: ['apertura_proxima', 'ultimos_30_dias', 'adjudicados'],
  onlyNew: true,
  eventTypes: ['NEW_LISTING', 'STATUS_CHANGE', 'UPDATED', 'CLOSED'],
  maxItems: 200,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();

for (const item of items) {
  console.log(`[${item.event_type}] ${item.numeroProceso} - ${item.organismo}`);
}
```

### Pricing Model Explanation

This actor bills per event, on top of standard Apify platform usage:

| Event | Price | When it fires on this actor |
| --- | --- | --- |
| `result` | $0.003 per record | Every `NEW_LISTING`, `STATUS_CHANGE`, or `UPDATED` record — a tender whose full row content was actually parsed from a PBAC grid this run. |
| `result-summary` | $0.001 per record | Every `CLOSED` record — a derived absence signal (the tender is inferred no longer listed in any of the 3 grids); nothing new was fetched for it, so it bills at the lower tier. |
| `apify-actor-start` | $0.00005 | Once per run, regardless of how many tenders are found. |

The two-tier split is about what was actually fetched, not about `fetchFullDetail`. `result` and `result-summary` are priced differently because a `CLOSED` record reflects an inference from what's already been scraped in a run (the tender simply isn't in any grid anymore) rather than new content being pulled from the site, while `NEW_LISTING`/`STATUS_CHANGE`/`UPDATED` all correspond to a genuinely parsed row. Enabling `fetchFullDetail` does **not** move a record into a different pricing tier: today it only returns a short status label rather than the full Pliego document text (a known, disclosed limitation — see the Technical Features section), and charging more for a fetch that isn't yet meaningfully richer would be misleading, so `result` stays $0.003 whether or not `fetchFullDetail` is on.

If you turn on `onlyNew` (delta mode), an unchanged tender is simply never turned into a dataset record at all — it is not delivered and not billed at $0, it just never becomes a chargeable event in the first place. Only tenders whose event type is `NEW_LISTING`, `STATUS_CHANGE`, `UPDATED`, or `CLOSED` (and is included in your `eventTypes` selection) generate a billable event. A daily monitor that finds 5 changes across all 3 grids costs about $0.02/day (roughly $0.45/month) plus the fixed $0.00005 actor-start charge per run.

# Actor input Schema

## `views` (type: `array`):

Which PBAC grids to extract. Defaults to all three.

## `fetchFullDetail` (type: `boolean`):

When enabled, simulates the postback for each process. Known limitation: this currently returns only a short status label, not the full Pliego document text - that content lives in a nested panel not yet mapped. Leave off until this is resolved; it adds cost with limited extra value today.

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

Hard cap on the number of tender processes returned this run, across all selected views.

## `proxyConfiguration` (type: `object`):

Required: pbac.cgp.gba.gov.ar blocks non-residential-Argentina traffic. Verified live - datacenter proxy (any provider) times out identically to no proxy at all; only Residential + Argentina succeeds. Defaults to that; do not switch to datacenter.

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

For recurring/scheduled runs: return only tenders that are new, changed vistaOrigen/estado, amended or closed since a prior run of this actor (tracked in a named key-value store scoped to this actor). CLOSED (a tender no longer in any of the 3 grids) is only ever computed when 'Views to extract' covers all 3 default views - a narrower selection makes this run's fetch a subset of the site, so CLOSED is skipped (and logged) in that case.

## `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 = vistaOrigen or estado changed - e.g. a tender moving from upcoming to awarded. UPDATED = same vistaOrigen/estado, a field changed. CLOSED = no longer listed in any of the 3 grids (only computed on an unfiltered 'Views to extract').

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

Restricts results to tenders whose fechaApertura falls within this window ending now. Independent of onlyNew. Note: fechaApertura is WHEN BIDS OPEN, not when the tender was published - for the 'Upcoming openings' view specifically that date is routinely in the future, so this filter will not match those rows (see AGENTS.md/README).

## Actor input object example

```json
{
  "views": [
    "apertura_proxima",
    "ultimos_30_dias",
    "adjudicados"
  ],
  "fetchFullDetail": false,
  "maxItems": 200,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "AR"
  },
  "onlyNew": false,
  "eventTypes": [
    "NEW_LISTING",
    "STATUS_CHANGE",
    "UPDATED",
    "CLOSED"
  ]
}
```

# 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 = {
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "AR"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("stefano_seggio/pba-tenders-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 = { "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "AR",
    } }

# Run the Actor and wait for it to finish
run = client.actor("stefano_seggio/pba-tenders-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 '{
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "AR"
  }
}' |
apify call stefano_seggio/pba-tenders-monitor --silent --output-dataset

```

## MCP server setup

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