# SECOP Colombia Tenders — Alerts & History (`polyglot-data/secop-scraper`) Actor

Open Colombian public tenders from SECOP II with deadlines, award dates, bidders, winners, requested service category and legal context per process. Daily new-only alerts plus history.

- **URL**: https://apify.com/polyglot-data/secop-scraper.md
- **Developed by:** [Polyglot Data](https://apify.com/polyglot-data) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 tenders

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## SECOP Colombia Tenders — Alerts & History

Get Colombian public tenders from **SECOP II** (where new tenders are published today) and the **SECOP I** historical
archive, in one clean, typed feed. Search by keyword, department, city, budget, contract type, entity level, UNSPSC
category and contracting entity. Turn on **alert mode** to receive only the tenders that are new or changed since your
last run.

Built for contractors, consultants, suppliers, analysts and journalists who need to know what the Colombian state is
buying, when bids close, and what rules apply, without reading the portal one process at a time.

The data comes from the official open data of Colombia's public procurement agency (datos.gov.co). The actor uses
plain HTTP requests, so runs are fast and light.

### 🇪🇸 En español

Este actor trae las **licitaciones SECOP** de Colombia (SECOP II para los procesos nuevos y SECOP I para el histórico) en un solo JSON limpio, con valores en pesos, fechas y el enlace directo a cada proceso. Si buscas **alertas de licitaciones en Colombia** o quieres ver los **procesos de contratación abiertos** antes que la competencia, aquí empiezas. Sirve también para consumir la **SECOP II API** y seguir la **contratación pública Colombia** sin entrar proceso por proceso.

Úsalo para:

- Encontrar obras o consultorías abiertas en tu departamento a tiempo para la propuesta.
- Recibir alertas diarias solo con los procesos nuevos o que cambiaron de estado.
- Saber qué cierra en los próximos días con `closingWithinDays`.
- Hacer análisis de mercado: cuánto gasta el Estado por departamento o por familia UNSPSC.
- Seguir las compras de una entidad concreta, por nombre o por NIT.
- Periodismo o veeduría: revisar valores adjudicados y procesos que cambiaron.

Ejemplo para copiar y pegar (construcción y vías en Antioquia, solo lo nuevo):

```json
{
  "keywords": ["construccion", "vias"],
  "departments": ["Antioquia"],
  "statuses": ["open"],
  "publishedWithinDays": 3,
  "onlyNew": true,
  "maxItems": 100
}
```

Para recibir las alertas todos los días, deja `onlyNew` activado, ve a la pestaña **Schedules**, agrega un horario diario y conecta el dataset a Google Sheets, a un webhook o a un correo. Pagas US$0,003 por cada licitación guardada (US$3 por 1.000); las filas que no cambiaron no se cobran.

***

### ✅ Why this actor

- **New tenders and history, same fields.** New tenders are published in SECOP II, which is searched by default. SECOP I
  is the previous platform and holds the history (mostly processes up to 2021). Add it with `sources` and a date range
  (`publishedFrom` / `publishedTo`) for market research. Both platforms are mapped to the same fields and status values,
  so you do not have to merge two exports yourself.
- **Accent-insensitive keywords.** SECOP I text often has broken accents ("CONSTRUCCIoN"), and SECOP II uses
  "construcción". Searching `construccion` finds both spellings.
- **Competitive tenders by default.** The default modalities are public tenders, abbreviated selection, minimum
  amount, merit contests and special-regime processes with offers. Direct contracting is excluded unless you choose it.
- **Dates for planning.** Publication, phase, bid deadline, bid opening and award dates, plus `daysToDeadline`, so you
  can sort by what closes next. See the Dates available section.
- **Who is buying, and from whom.** SECOP II rows include the awarded supplier, the number of views, invited suppliers
  and lots. Turn on `includeBidders` to list the bidders as well. See the Privacy section.
- **Legal context on every row.** A `legal` object summarizes the procurement rules that usually apply to the
  modality (legal basis, award criterion, bid bond, time to fix bid errors). It is informative, not legal advice. See
  Legal context.
- **Demand summary.** A small record ranks the UNSPSC families the run found, with counts and total budget. See
  What services are being requested.
- **Alert mode.** Run it on a daily schedule with `onlyNew` on. You get only tenders that are new, or whose status
  changed, since the previous run with the same filters. Unchanged tenders are skipped and not charged.
- **Clean data.** Budgets are numbers in COP, dates are `YYYY-MM-DD`, every process has a direct link, and missing
  values are `null`, never empty strings.
- **Open data, procurement records only.** The actor asks the open data API only for an allowlist of procurement
  columns. Contact details, bank details and gender are never requested.
- **Pay per tender.** You pay only for tenders saved to the dataset. Error items and unchanged tenders are free.

***

### 📦 What data you get

| Field | Example | Notes |
|---|---|---|
| `source` | `SECOP II` | `SECOP II` or `SECOP I` |
| `processId` | `CO1.BDOS.0000001` | Process id (SECOP I: constancy number) |
| `reference` | `CM-2026-0001` | Reference number of the process |
| `title` | `Mantenimiento de vía urbana ...` | Process name (SECOP I: object to contract) |
| `description` | `Ejemplo de descripción ...` | Long description. SECOP II only, `null` for SECOP I |
| `entity` | `Entidad Ejemplo Distrital` | Contracting entity |
| `entityNit` | `900000001` | Tax ID (NIT) of the entity. An institution, not a person |
| `entityLevel` | `Territorial` | Entity order as published: `Nacional`, `Territorial` or `Corporación Autónoma` (SECOP II) |
| `department` | `Bogotá D.C.` | Canonical department name, same spelling in both platforms |
| `city` | `Bogotá, D.C.` | Municipality of the entity. `null` when not reported |
| `modality` | `Licitación pública` | Procurement modality as published (Spanish) |
| `modalityGroup` | `licitacion` | Normalized group (see the modalities input) |
| `status` | `open` | Normalized: `open`, `evaluation`, `awarded`, `closed`, `cancelled`, `draft`, `suspended` |
| `statusRaw` | `Publicado` | Status as published by SECOP |
| `phase` | `Presentación de oferta` | Process phase (SECOP II only) |
| `contractType` | `Obra` | Type of contract, e.g. Obra, Suministros, Consultoría |
| `contractSubtype` | `null` | Contract subtype as published (SECOP II only) |
| `procuringUnit` | `Unidad Ejemplo de Obras` | Unit of the entity that runs the process (SECOP II only) |
| `unspscCode` | `72141100` | UNSPSC category code. `null` when the process is UNSPECIFIED or has no code yet |
| `unspscFamily` | `7214` | UNSPSC family: first 4 digits of `unspscCode`. `null` without a code |
| `unspscFamilyName` | `Servicios de construcción pesada` | UNSPSC family name (Spanish) |
| `unspscClassName` | `Servicios de pavimentación ...` | UNSPSC class name, from the first 6 digits (Spanish) |
| `unspscGroupName` | `null` | Reserved. Always `null` in this version |
| `unspscName` | `Computadores` | UNSPSC class name as published (SECOP I only) |
| `category` | `Obra civil` | Object to contract (SECOP I only) |
| `inferredCategories` | `[{ "unspscFamily": "7214", ... }]` | Top 3 UNSPSC families guessed from the text, only when `unspscCode` is `null`. See [Category inference](#category-inference) |
| `baseValue` | `1250000000` | Base (estimated) value in COP |
| `awardedValue` | `1190000000` | Awarded value in COP. `null` until awarded |
| `awardedSupplier` | `Constructora Ejemplo S.A.S.` | Name of the awarded supplier, as published by SECOP. `null` until awarded (SECOP II only) |
| `awardedSupplierId` | `900000003` | Tax ID (NIT) of the awarded supplier, as published. Can be a personal ID for a natural person (SECOP II only) |
| `currency` | `COP` | Always COP |
| `publishedAt` | `2026-10-07` | Publication date |
| `phaseDates` | `{ "publicacion": "2026-10-07" }` | Publication dates of each phase: `publicacion`, `fase2`, `fase3`. Only dates that are set. `null` when none (SECOP II only) |
| `bidDeadline` | `2026-10-21` | Date bids close (SECOP II only). `null` for SECOP I |
| `daysToDeadline` | `11` | Whole days from the run date (Bogotá time) to `bidDeadline`. Negative once the deadline has passed (SECOP II only) |
| `bidsOpenedAt` | `null` | Date the bids were opened (SECOP II only) |
| `effectiveOpeningAt` | `null` | Effective date of the bid opening (SECOP II only) |
| `lastUpdatedAt` | `2026-10-08` | Date of the last update on SECOP |
| `awardedAt` | `null` | Award date (SECOP II) or contract signing date (SECOP I) |
| `durationValue` | `120` | Contract duration as a number |
| `durationUnit` | `días` | `días` or `meses` |
| `interestedSuppliers` | `4` | Number of suppliers that expressed interest (SECOP II). Counts only, no names |
| `bidsReceived` | `0` | Number of bids received (SECOP II). Counts only, no names |
| `invitedSuppliers` | `0` | Number of suppliers invited (SECOP II only). Counts only |
| `views` | `152` | Views of the process page on SECOP (SECOP II only) |
| `lots` | `1` | Number of lots (SECOP II only) |
| `modalityJustification` | `null` | Justification of the procurement modality, as published (SECOP II only) |
| `legal` | `{ "legalBasis": ... }` | Procurement rules for this modality. See the Legal context section |
| `bidders` | `[{ "name": "...", "id": "..." }]` | Bidders as published by SECOP. Only with `includeBidders`, SECOP II only. See [Bidders](#bidders) |
| `bidderCount` | `2` | Number of bidders in `bidders`. `null` when `bidders` is `null` |
| `url` | `https://community.secop.gov.co/...` | Direct link to the process on SECOP |
| `matchedKeywords` | `["construccion"]` | Your keywords that matched this tender. `[]` when no keywords were set |
| `changeType` | `new` | Alert mode only: `new` or `updated`. `null` when alert mode is off |
| `scrapedAt` | `2026-10-09T14:00:00.000Z` | UTC timestamp of the run |

**Missing values are `null`.** Some fields only exist for one platform. For example, SECOP I has no bid deadline
column, so `bidDeadline` and `daysToDeadline` are always `null` for SECOP I tenders. Check `publishedAt` and
`lastUpdatedAt` for recency.

#### Category inference

Open tenders often have no UNSPSC code yet. When `unspscCode` is `null`, the actor ranks the most likely UNSPSC
families from the title, description and category text, and puts the top 3 in `inferredCategories`. This is
**approximate**. On a holdout test set the right family was among the top 3 about **60%** of the time, and the first
choice about **41%** of the time. `confidence` is a model score, not a calibrated probability. Use the inferred
families to narrow a search, and combine them with `keywords` for better results.

#### Bidders

With `includeBidders` on, each SECOP II tender gets `bidders` (name and ID, one entry per ID) and `bidderCount`.

- `[]`: the lookup worked and SECOP lists no bidders for this process.
- `null`: the field was not requested (`includeBidders` is off or the source is SECOP I), or the lookup failed. The
  run log says why.

#### Example item: SECOP II (open public tender in Bogotá)

```json
{
  "source": "SECOP II",
  "processId": "CO1.BDOS.0000001",
  "reference": "CM-2026-0001",
  "title": "Mantenimiento de vía urbana en el sector de ejemplo",
  "description": "Ejemplo de descripción del proceso. Datos ilustrativos.",
  "entity": "Entidad Ejemplo Distrital",
  "entityNit": "900000001",
  "entityLevel": "Territorial",
  "department": "Bogotá D.C.",
  "city": "Bogotá, D.C.",
  "modality": "Licitación pública",
  "modalityGroup": "licitacion",
  "status": "open",
  "statusRaw": "Publicado",
  "phase": "Presentación de oferta",
  "contractType": "Obra",
  "contractSubtype": null,
  "procuringUnit": "Unidad Ejemplo de Obras",
  "unspscCode": "72141100",
  "unspscFamily": "7214",
  "unspscFamilyName": "Servicios de construcción pesada",
  "unspscClassName": "Servicios de pavimentación y superficies de edificios de infraestructura",
  "unspscGroupName": null,
  "unspscName": null,
  "category": null,
  "inferredCategories": null,
  "baseValue": 1250000000,
  "awardedValue": null,
  "awardedSupplier": null,
  "awardedSupplierId": null,
  "currency": "COP",
  "publishedAt": "2026-10-07",
  "phaseDates": { "publicacion": "2026-10-07" },
  "bidDeadline": "2026-10-21",
  "daysToDeadline": 11,
  "bidsOpenedAt": null,
  "effectiveOpeningAt": null,
  "lastUpdatedAt": "2026-10-08",
  "awardedAt": null,
  "durationValue": 120,
  "durationUnit": "días",
  "interestedSuppliers": 4,
  "bidsReceived": 0,
  "invitedSuppliers": 0,
  "views": 152,
  "lots": 1,
  "modalityJustification": null,
  "legal": {
    "legalBasis": "Ley 1150 de 2007, art. 2 num. 1; Decreto 1082 de 2015",
    "awardCriterion": "Evaluación objetiva según las reglas del pliego (Ley 1150 art. 2 par. 2 num. 3)",
    "rupRequired": null,
    "bidBondRequired": true,
    "subsanationWindow": "Hasta el traslado del informe de evaluación (5 días hábiles en licitación, CCE C-812 de 2024)",
    "riskHearingRule": "Obligatoria en licitación (art. 2.2.1.2.1.1.2 Decreto 1082); fecha según el pliego",
    "typicalTimeline": "Sin mínimo legal único entre apertura y cierre (CCE); plazos según el pliego",
    "standardBiddingDocs": {
      "likely": true,
      "sectorHint": null,
      "reference": "https://www.colombiacompra.gov.co/normativa-y-relatoria/documentos-tipo"
    },
    "notes": "Regla general de selección. Garantía de seriedad: al menos 10% del valor de la oferta (art. 2.2.1.2.3.1.9 Decreto 1082)."
  },
  "bidders": null,
  "bidderCount": null,
  "url": "https://community.secop.gov.co/Public/Tendering/OpportunityDetail/Index?noticeUID=CO1.NTC.0000001",
  "matchedKeywords": ["mantenimiento"],
  "changeType": null,
  "scrapedAt": "2026-10-09T14:00:00.000Z"
}
```

#### Example item: SECOP I (Antioquia, alert mode)

```json
{
  "source": "SECOP I",
  "processId": "CONSTANCIA-0000002",
  "reference": "PROC-0000002",
  "title": "Suministro de equipos de cómputo para colegios",
  "description": null,
  "entity": "Entidad Ejemplo Departamental",
  "entityNit": "800000002",
  "entityLevel": "TERRITORIAL",
  "department": "Antioquia",
  "city": "Ejemplo",
  "modality": "Licitación Pública",
  "modalityGroup": "licitacion",
  "status": "open",
  "statusRaw": "Convocado",
  "phase": null,
  "contractType": "Suministros",
  "contractSubtype": null,
  "procuringUnit": null,
  "unspscCode": "43211500",
  "unspscFamily": "4321",
  "unspscFamilyName": "Equipo informático y accesorios",
  "unspscClassName": "Computadores",
  "unspscGroupName": null,
  "unspscName": "Computadores",
  "category": "Suministro de equipos de cómputo para colegios",
  "inferredCategories": null,
  "baseValue": 480000000,
  "awardedValue": null,
  "awardedSupplier": null,
  "awardedSupplierId": null,
  "currency": "COP",
  "publishedAt": "2026-10-02",
  "phaseDates": null,
  "bidDeadline": null,
  "daysToDeadline": null,
  "bidsOpenedAt": null,
  "effectiveOpeningAt": null,
  "lastUpdatedAt": "2026-10-08",
  "awardedAt": null,
  "durationValue": 4,
  "durationUnit": "meses",
  "interestedSuppliers": null,
  "bidsReceived": null,
  "invitedSuppliers": null,
  "views": null,
  "lots": null,
  "modalityJustification": null,
  "legal": {
    "legalBasis": "Ley 1150 de 2007, art. 2 num. 1; Decreto 1082 de 2015",
    "awardCriterion": "Evaluación objetiva según las reglas del pliego (Ley 1150 art. 2 par. 2 num. 3)",
    "rupRequired": null,
    "bidBondRequired": true,
    "subsanationWindow": "Hasta el traslado del informe de evaluación (5 días hábiles en licitación, CCE C-812 de 2024)",
    "riskHearingRule": "Obligatoria en licitación (art. 2.2.1.2.1.1.2 Decreto 1082); fecha según el pliego",
    "typicalTimeline": "Sin mínimo legal único entre apertura y cierre (CCE); plazos según el pliego",
    "standardBiddingDocs": {
      "likely": null,
      "sectorHint": null,
      "reference": "https://www.colombiacompra.gov.co/normativa-y-relatoria/documentos-tipo"
    },
    "notes": "Regla general de selección. Garantía de seriedad: al menos 10% del valor de la oferta (art. 2.2.1.2.3.1.9 Decreto 1082)."
  },
  "bidders": null,
  "bidderCount": null,
  "url": "https://www.contratos.gov.co/consultas/detalleProceso.do?numConstancia=0000002",
  "matchedKeywords": [],
  "changeType": "updated",
  "scrapedAt": "2026-10-09T14:00:00.000Z"
}
```

The process ids, names, entities, suppliers and links above are invented for illustration. The `legal` text is
shortened from the real output to the rules it states.

#### ⚠️ Error items

If an input produces nothing or cannot run, the actor saves an error item instead of failing silently.
Error items are **never charged**.

```json
{ "error": "NO_RESULTS", "note": "No tenders matched these filters. Try more days, fewer filters or other keywords.", "filters": { "keywords": ["xyzabc"] } }
```

| error | Meaning | What to do |
|---|---|---|
| `NO_RESULTS` | Nothing matched the filters. The filters used are echoed in `filters` | Widen `publishedWithinDays`, remove a keyword or a department. Note that `closingWithinDays` skips SECOP I |
| `SOURCE_ERROR` | One platform could not be read after 3 retries. The source is named in the item | Run again later. The other platform's results are still saved |
| `INVALID_INPUT` | The input cannot be used: `minValue` greater than `maxValue`, a bad UNSPSC code, an unknown `entityOrders` value, a `closingWithinDays` that is not a whole number, or `categoryKeywords` that match no UNSPSC category | Fix the field named in the note |

***

### 🎯 Use cases

- **Bid radar for contractors:** a daily alert with `keywords` for your trade, `departments` or `city` for the places you
  serve, and `closingWithinDays` for the tenders that close soon.
- **Supplier and consultant pipeline:** track tenders by entity (`entity` accepts the entity's NIT) to plan proposals
  ahead of time.
- **Competitor research:** list who won and who bid on past tenders with `includeBidders`, for a given city or entity.
- **Market sizing:** total `baseValue` by department, city, modality or UNSPSC family over the last 12 months.
- **Public spending journalism:** follow awarded values, contract durations and changed statuses for specific
  entities or categories. Every row links to the official SECOP page.
- **Dashboards and BI:** feed the dataset into Google Sheets, Power BI or your own database through the API.

***

### 🚀 How to use

1. Choose the **sources** (SECOP II for new tenders, the default; add SECOP I for history) and the **status** (open tenders by default).
2. Add **keywords**, **departments**, a **city**, a budget range, **contract types**, **entity levels**, **UNSPSC codes** or
   **category keywords** to narrow the results. Leave them empty to get everything that matches the rest of the filters.
3. Set **Published within (days)**, or a **Published from / to** date range for historical research, and **Max items**.
4. Click **Start**, then download the results as JSON, CSV, Excel or HTML, or connect them to Google Sheets.

#### How to find open tenders for a region and a trade

```json
{
  "keywords": ["construccion", "vias"],
  "departments": ["Bogotá D.C."],
  "statuses": ["open"],
  "publishedWithinDays": 30,
  "maxItems": 200
}
```

Keywords match the title and description, and accents are ignored.

#### How to find tenders that close soon in a city

```json
{
  "city": "Cali",
  "departments": ["Valle del Cauca"],
  "closingWithinDays": 7,
  "statuses": ["open"],
  "maxItems": 100
}
```

`closingWithinDays` looks at the SECOP II bid deadline. It skips SECOP I, which has no deadline column. `city` matches
the entity's city, so `"Cali"` finds Cali processes and does not match `"Calima"`. Accents, capitals and "D.C." are
ignored.

#### How to scan a city's tenders by budget

```json
{
  "keywords": ["software"],
  "city": "Barranquilla",
  "departments": ["Atlántico"],
  "minValue": 50000000,
  "maxValue": 800000000,
  "statuses": ["open", "evaluation"],
  "publishedWithinDays": 60,
  "maxItems": 300
}
```

Budgets are in COP. `minValue` and `maxValue` are compared with the base (estimated) value.

#### How to filter by contract type and entity level

```json
{
  "contractTypes": ["Obra", "Consultoría"],
  "entityOrders": ["territorial"],
  "departments": ["Antioquia"],
  "publishedWithinDays": 30,
  "maxItems": 200
}
```

`entityOrders` accepts `nacional`, `territorial` and `corporacion_autonoma`. The regional environmental authorities
(`corporacion_autonoma`) are not in SECOP I, so that value is ignored for SECOP I rows.

#### How to filter by UNSPSC category

```json
{
  "unspscCodes": ["4321", "7214"],
  "departments": ["Bogotá D.C."],
  "publishedWithinDays": 90,
  "maxItems": 150
}
```

UNSPSC prefixes match from the start of the code: `4321` covers `43211500`, `43211501` and so on.
Processes marked UNSPECIFIED have no category code, so they cannot match `unspscCodes`. Use keywords or `entity` for
those, or `categoryKeywords` below.

#### How to filter by category words

```json
{
  "categoryKeywords": ["construcción"],
  "keywords": ["vías"],
  "departments": ["Antioquia"],
  "statuses": ["open"],
  "publishedWithinDays": 30,
  "maxItems": 150
}
```

`categoryKeywords` matches words against UNSPSC family and class names, so `construcción` finds the construction
families. Tenders with a code are matched on that code. Open tenders with no code yet are matched on their inferred
families, which is approximate. That is why this example also uses `keywords`. The category filter is applied to each
page as it is read, so a run can read more pages before it fills `maxItems`.

#### How to list the bidders of past tenders

```json
{
  "sources": ["secop2"],
  "statuses": ["awarded"],
  "keywords": ["software"],
  "city": "Cali",
  "publishedFrom": "2025-01-01",
  "publishedTo": "2025-12-31",
  "includeBidders": true,
  "maxItems": 200
}
```

`includeBidders` adds `bidders` and `bidderCount` to each SECOP II tender. It does not change which tenders match.
See the Privacy section before you store or share the names and IDs.

#### How to research past contracts (SECOP I + II history)

```json
{
  "sources": ["secop2", "secop1"],
  "statuses": ["awarded", "closed"],
  "keywords": ["interventoría"],
  "departments": ["Antioquia"],
  "publishedFrom": "2018-01-01",
  "publishedTo": "2021-12-31",
  "maxItems": 500
}
```

#### How to get daily alerts for new tenders

1. Create a task with the input below.
2. Turn on the **Schedules** tab and add a daily schedule (for example every morning).
3. Connect the dataset to Google Sheets, a webhook, e-mail or the API. Each row has `changeType` set to `new` or `updated`.

```json
{
  "departments": ["Antioquia"],
  "keywords": ["alcantarillado", "acueducto"],
  "statuses": ["open"],
  "publishedWithinDays": 3,
  "onlyNew": true,
  "maxItems": 100
}
```

#### Input fields

| Field | Type | Default | Description |
|---|---|---|---|
| `keywords` | string\[] | `[]` | Words to find in the title or description. Accents ignored. A tender is kept if any keyword matches. |
| `sources` | select\[] | `["secop2"]` | `secop2` (current platform: new tenders), `secop1` (historical archive) |
| `statuses` | select\[] | `["open"]` | `open`, `evaluation`, `awarded`, `closed`, `cancelled` |
| `modalities` | select\[] | Competitive processes | Eight groups. The default is public tender, abbreviated selection, minimum amount, merit contest and special regime with offers. Direct contracting and special regime without offers are off by default. |
| `departments` | select\[] | `[]` (all) | One or more of the 33 Colombian departments, including Bogotá D.C. Leave empty for all. |
| `city` | string | empty | Exact city or municipality of the contracting entity, e.g. `Cali`. Accents, capitals and "D.C." ignored. The whole name must match. |
| `entity` | string | empty | Part of the entity name, or the entity's NIT digits only |
| `entityOrders` | select\[] | `[]` (all) | Entity level: `nacional`, `territorial`, `corporacion_autonoma`. `corporacion_autonoma` is ignored for SECOP I rows |
| `contractTypes` | select\[] | `[]` (all) | Contract types as SECOP II publishes them, e.g. `Obra`, `Consultoría`, `Prestación de servicios` |
| `minValue` | integer | none | Minimum base value in COP |
| `maxValue` | integer | none | Maximum base value in COP |
| `unspscCodes` | string\[] | `[]` | UNSPSC prefixes, 2 to 8 digits, e.g. `4321` |
| `categoryKeywords` | string\[] | `[]` | Words matched against UNSPSC family and class names, e.g. `construcción`. Approximate for open tenders with no code. Combine with `keywords` |
| `closingWithinDays` | integer | none | SECOP II only: bid deadline between today and today + N days. Skips SECOP I when set |
| `publishedWithinDays` | integer | `30` | Only tenders published in the last N days (1 to 3650) |
| `publishedFrom` / `publishedTo` | date | none | Historical research: an exact publication date range. Replaces `publishedWithinDays` |
| `maxItems` | integer | `100` | Maximum number of tenders to save per run |
| `onlyNew` | boolean | `false` | Alert mode: save only new or changed tenders since the last run with the same filters |
| `includeBidders` | boolean | `false` | SECOP II only. Adds `bidders` (name and ID as published) and `bidderCount` to each tender. Costs one extra lookup per 50 tenders |
| `includeDemandSummary` | boolean | `true` | Writes the `DEMAND_SUMMARY` record to the run's key-value store. Not charged |
| `appToken` | string (secret) | empty | Optional datos.gov.co app token for heavy use |

#### How alert mode works

- The first run with a given set of filters saves every matching tender and marks it as `new`.
- Later runs with the same filters save only tenders that are new, or whose status or last update changed. Those rows
  have `changeType` set to `new` or `updated`. Everything else is skipped and not charged.
- Each filter set keeps its own history. Changing a filter starts a new history, so the next run saves everything again.
  The v2 filters (`contractTypes`, `entityOrders`, `city`, `closingWithinDays`, `categoryKeywords`) count as part of the
  filter set only when you set them, so saved searches from before v2 keep their history. `includeBidders` and
  `includeDemandSummary` do not change the history.
- Use a short **Published within** window (1 to 7 days) with a daily schedule to keep runs small.

***

### 🏛️ What services are being requested

Each run can write a small summary of the demand it found: which UNSPSC families have the most tenders, how many tenders
were inferred rather than coded, and the total base budget per family. It is saved as the record `DEMAND_SUMMARY` in the
run's default key-value store, when `includeDemandSummary` is on (the default). It is not charged.

Example record (invented values):

```json
{
  "generatedAt": "2026-10-10T09:00:00.000Z",
  "totalTenders": 120,
  "withoutCategory": 4,
  "families": [
    {
      "unspscFamily": "7214",
      "unspscFamilyName": "Servicios de construcción pesada",
      "tenders": 34,
      "inferred": 12,
      "totalBaseValue": 4150000000
    },
    {
      "unspscFamily": "4321",
      "unspscFamilyName": "Equipo informático y accesorios",
      "tenders": 21,
      "inferred": 0,
      "totalBaseValue": 980000000
    }
  ]
}
```

How to read it:

- `families` is sorted by `tenders`, most first, and lists at most 50 families.
- `tenders` counts the saved tenders in that family. `inferred` counts how many of them had no code and were placed in the
  family by the text (top-1 guess, so it is approximate). `totalBaseValue` is the sum of `baseValue` in COP.
- `totalTenders` is the number of tenders saved in this run. `withoutCategory` counts tenders with no code and no
  inferred family.
- It covers only the tenders saved in the run, so `maxItems` and the filters limit it. It is written even when nothing
  matched (then `families` is empty).

#### How to read it from the API

1. Get the run with `GET https://api.apify.com/v2/actor-runs/{runId}?token=YOUR_API_TOKEN`. Take `data.defaultKeyValueStoreId` from the response.
2. Get the record with `GET https://api.apify.com/v2/key-value-stores/{storeId}/records/DEMAND_SUMMARY?token=YOUR_API_TOKEN`.

The response is the JSON above.

***

### ⚖️ Legal context

Each tender row has a `legal` object. It summarizes the procurement rules that usually apply to that tender's modality
(`modalityGroup`), and whether the contract type points to a standard bidding document.

| Field | What it says |
|---|---|
| `legalBasis` | The articles that govern the modality, e.g. Ley 1150 de 2007 and Decreto 1082 de 2015 |
| `awardCriterion` | How the award is decided: objective evaluation under the bidding document, lowest price, a reverse auction, or a justified direct award |
| `rupRequired` | Whether registration in the supplier registry (RUP) is required. Always `null` in this version |
| `bidBondRequired` | Whether a bid bond (garantía de seriedad) is usually required. `null` when unknown |
| `subsanationWindow` | How long bidders have to fix errors in the offer, when the rule is set in the law |
| `riskHearingRule` | Whether the risk hearing (audiencia de riesgos) is required. Often says "según el pliego" |
| `typicalTimeline` | Known minimum periods, when the rules set them |
| `standardBiddingDocs` | `likely: true` when the contract type (Obra, Interventoría or Consultoría) and the modality usually use the standard bidding documents of Colombia Compra Eficiente. Otherwise `null`. `reference` links to the standard documents |
| `notes` | Extra points about the modality |

**Sources.** The rules come from Ley 80 de 1993 (Estatuto General de Contratación de la Administración Pública), Ley 1150
de 2007, Decreto 1082 de 2015 (Decreto Único Reglamentario del Sector Administrativo de Planeación Nacional) and the
guidance of the Agencia Nacional de Contratación Pública – Colombia Compra Eficiente (CCE).

**How to use it.** The `legal` object is informative and is **not legal advice**. Many rules depend on the bidding
document of each process, and the text says "según el pliego" (according to the bidding document) where that applies.
Read the bidding document on the process page (`url`) before you bid or rely on a date or a rule.

**Null values.** A `null` means the rule is unknown or not stated for that case. When the modality is not recognized, all
fields are `null` and `notes` says "Modalidad no reconocida".

***

### 📅 Dates available

| Event | Field(s) | Notes |
|---|---|---|
| Publication | `publishedAt` | Both platforms |
| Publication of the process (phase dates) | `phaseDates.publicacion` | SECOP II only |
| Later phase publications | `phaseDates.fase2`, `phaseDates.fase3` | SECOP II, when the process has more phases |
| Bid deadline | `bidDeadline`, `daysToDeadline` | SECOP II only. `daysToDeadline` is negative after the deadline |
| Bids opened | `bidsOpenedAt` | SECOP II only |
| Effective opening | `effectiveOpeningAt` | SECOP II only |
| Last update | `lastUpdatedAt` | Both platforms |
| Award | `awardedAt` | SECOP II: award date. SECOP I: contract signing date |

All dates are day precision, `YYYY-MM-DD`.

**Not in the open data.** The risk hearing date, the observations deadline and the evaluation report date are **not**
in the open data. They appear only on the SECOP II process page, which requires a reCAPTCHA check, so the actor does
not read them. To see them, open the process page with the `url` field of the tender.

***

### 🔒 Privacy

- **Supplier names and IDs are included.** For SECOP II, the actor returns the awarded supplier (`awardedSupplier`,
  `awardedSupplierId`) and, with `includeBidders`, the bidders (`bidders`: name and ID). SECOP publishes these as public
  procurement information under Ley 1712 de 2014 (transparency and access to public information). The actor copies
  them as SECOP publishes them.
- **Some IDs are personal.** When a supplier is a natural person, the ID published by SECOP can be a personal
  identification number, not a company tax ID. The actor does not remove, mask or hash these values. If you store,
  combine or share them, check your obligations under Colombian data protection law (Ley 1581 de 2012) and the terms
  of datos.gov.co.
- **Never requested:** contact data (e-mail, phone, address), bank data, gender, and the IDs of public officials. The
  actor reads only the allowlisted procurement columns described in the Legal section.
- **Entity NIT codes** are the tax IDs of public institutions, not of people.

***

### 🔌 Integrations, API & MCP

- **Schedules:** run it every day from the Schedules tab. Alert mode works best with a daily schedule.
- **Webhooks, Make, Zapier, n8n, Google Sheets:** send results anywhere when a run finishes.
- **API:** start a run and get the items in one call:

```bash
curl -X POST "https://api.apify.com/v2/acts/polyglot-data~secop-scraper/run-sync-get-dataset-items?token=YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"keywords":["construccion"],"departments":["Bogotá D.C."],"publishedWithinDays":7,"maxItems":50}'
```

- **MCP:** the actor can be used from any MCP-compatible AI assistant through the Apify MCP server. Ask the assistant to run `Polyglot-Data/secop-scraper` with your input, and it gets the dataset items back as JSON that it can summarize or compare.

***

### 💰 Pricing

**Pay per result:** you pay a small fixed price for each tender saved to the dataset (see the **Pricing** tab),
plus Apify platform usage. There is no monthly rental. Error items and unchanged tenders in alert mode are never charged.

Platform usage, measured locally before v2 (Oct 2026): a 7-day scan of all open competitive tenders (≈2,300) takes about
3 seconds at 256 MB. No proxy is used. `includeBidders` adds one lookup per 50 SECOP II tenders, and it has not been
measured yet.

***

### ❓ FAQ

**Which platforms are covered?**
SECOP II, where Colombian entities publish new tenders today, is searched by default. SECOP I is the previous platform: add `"secop1"` to `sources` to search its history (mostly processes up to 2021; a few territorial entities still load processes there). For history, use `publishedFrom` / `publishedTo` and a `statuses` value such as `awarded`.

**Does it include direct contracting?**
Not by default. Direct contracting (`contratacion_directa`) and special-regime processes without offers are excluded so that the results are competitive tenders. Select them in `modalities` if you need them.

**Why is `bidDeadline` empty for some results?**
SECOP I has no bid deadline column, so `bidDeadline` is `null` for every SECOP I tender. For SECOP II, it is `null` when the platform does not report one.

**Can I find tenders that have no UNSPSC code?**
Not with `unspscCodes`, because those processes have no code to match. Two options: `categoryKeywords` ranks open tenders by their inferred families (approximate, so combine it with `keywords`), or use `keywords` or `entity` directly.

**How accurate is the category inference?**
On a holdout test set the right UNSPSC family was among the top 3 guesses about 60% of the time, and the first guess about 41% of the time. Treat `inferredCategories` as a starting point, not as the official category.

**Why does `closingWithinDays` return no SECOP I rows?**
SECOP I has no bid deadline column, so the filter cannot apply to it. When `closingWithinDays` is set, SECOP I is skipped.

**Why do I get fewer results than I expected?**
Check the filters. The default status is `open`, the default modalities are competitive processes, and `publishedWithinDays` is 30 days. Widen one of them, or read the `NO_RESULTS` item, which shows the filters that were used.

**How do keywords work?**
Keywords are matched against the title and description, ignoring accents and capitals. A keyword matches a word that starts with it, so `obra` matches `obras` but not `cobra`. The matched words are listed in `matchedKeywords`.

**What is the difference between `status` and `statusRaw`?**
`status` is the normalized value that is the same for both platforms (`open`, `evaluation`, `awarded`, `closed`, `cancelled`, and so on). `statusRaw` is the text SECOP uses, for example `Publicado` or `Convocado`.

**Does the actor collect personal data?**
It collects procurement data, which includes the names and IDs of awarded suppliers and, with `includeBidders`, of bidders, as SECOP publishes them. Some of those IDs can be personal ID numbers. Contact data, bank data, gender and officials' IDs are never requested. See the Privacy section.

**Where do I find the risk hearing or the evaluation report date?**
They are not in the open data. Open the process page with the `url` field. See the Dates available section.

**Where does the data come from?**
From the open data published on datos.gov.co by the national public procurement agency, ANCP–Colombia Compra Eficiente. The data is updated by SECOP as processes change, so a run shows the state of the platform at that moment.

**Something is wrong or a field is missing?**
Open an issue in the **Issues** tab with the input you used. Issues are answered quickly.

**¿Funciona en español?**
Sí. Los campos de entrada aceptan palabras en español, con o sin tildes (por ejemplo, `construccion` o `Bogotá`). Los datos llegan en español tal como los publica SECOP.

***

### ⚖️ Legal

This actor reads public procurement data that SECOP publishes as open data on datos.gov.co. It collects procurement
information: process data, public entities, budgets, dates, counts, and the names and IDs of suppliers and bidders as
published by SECOP. It does not collect contact details, bank details or gender, and it does not request the IDs of
officials. See the Privacy section.

**Attribution and license.** The source data is published by the Agencia Nacional de Contratación Pública – Colombia
Compra Eficiente (ANCP–CCE) under CC BY-SA 4.0. If you publish or redistribute the results, credit ANCP–Colombia
Compra Eficiente as the source, and check the ShareAlike terms for any derived dataset.

You are responsible for making sure your use complies with the terms of datos.gov.co and SECOP and with applicable law,
including Colombian data protection rules (Ley 1581 de 2012) if you combine the data with other records. The
Legal context section is informative and is not legal advice.

# Actor input Schema

## `keywords` (type: `array`):

Words to find in the tender title or description. Accents are ignored, so "construccion" also matches "construcción". A tender is kept if any keyword matches.

## `sources` (type: `array`):

SECOP II is where new tenders are published today. SECOP I is the previous platform: use it for historical research (mostly processes up to 2021); a few territorial entities still load processes there. SECOP II only by default.

## `statuses` (type: `array`):

Process status to include. "open" means accepting bids or expressions of interest. "evaluation" means bids are closed and under review.

## `modalities` (type: `array`):

Procurement method. The default is the competitive processes. Direct contracting and special-regime processes without offers are excluded unless you select them.

## `departments` (type: `array`):

Department of the contracting entity. Leave empty for all of Colombia. For a single municipality use the City field.

## `entity` (type: `string`):

Part of the entity name (e.g. "Bogotá"), or the entity's NIT (tax ID) digits only. Entity NIT is a public institutional ID, not a person's ID.

## `minValue` (type: `integer`):

Keep tenders with a base value of at least this amount, in Colombian pesos (COP). Empty means no minimum.

## `maxValue` (type: `integer`):

Keep tenders with a base value of at most this amount, in Colombian pesos (COP). Empty means no maximum.

## `unspscCodes` (type: `array`):

Product or service category prefixes, 2 to 8 digits, e.g. "4321" (computer equipment) or "72" (construction services). Tenders with no UNSPSC code (UNSPECIFIED) cannot match this filter.

## `contractTypes` (type: `array`):

Contract type as SECOP II publishes it (e.g. Obra, Consultoría, Prestación de servicios). Leave empty for all types. SECOP I uses other spellings, so its rows are compared without accents or case.

## `entityOrders` (type: `array`):

Level of the contracting entity. Leave empty for all levels. corporacion_autonoma (regional environmental authorities, CAR) does not exist in SECOP I, so it is ignored for SECOP I; if it is the only value chosen, SECOP I is skipped.

## `city` (type: `string`):

Exact city or municipality of the contracting entity, e.g. "Cali" or "Bogotá". Accents, capitals and "D.C." are ignored, but the whole name must match ("Cali" does not match "Calima"). This is the entity's city, not the place where the work is done.

## `closingWithinDays` (type: `integer`):

SECOP II only: keep tenders whose bid deadline is between today and today + N days. Setting it skips SECOP I, which has no bid deadline. Leave empty to ignore the deadline.

## `categoryKeywords` (type: `array`):

Words matched against UNSPSC family and class names, accent-insensitive and at the start of a word, e.g. "construcción" or "software". Tenders with a UNSPSC code are matched on that code. Open tenders with no code yet are matched on their top 3 inferred families, which is approximate, so combine this with keywords. If no category matches any word, the run returns an INVALID_INPUT item.

## `publishedWithinDays` (type: `integer`):

Only tenders published in the last N days. Use a small number for alerts (1 to 7) and larger ones for research.

## `publishedFrom` (type: `string`):

Historical research: only processes published on or after this date. When set, it replaces 'Published within (days)'.

## `publishedTo` (type: `string`):

Historical research: only processes published on or before this date.

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

Maximum number of tenders to save in one run.

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

Save only tenders that are new, or whose status or last update changed since the previous run with the same filters. Combine with a daily schedule to get alerts. The first run saves everything. Unchanged tenders are not charged.

## `includeBidders` (type: `boolean`):

SECOP II only. Adds the bidders of each tender (name and ID as published by SECOP) and their count. Costs one extra lookup per 50 tenders. Does not change which tenders match. Leave off unless you need the list.

## `includeDemandSummary` (type: `boolean`):

Writes the DEMAND_SUMMARY record to the run's key-value store: the top UNSPSC families among the saved tenders, with counts and total base value. Not charged.

## `appToken` (type: `string`):

Optional datos.gov.co app token. It raises the rate limit for heavy use. Leave empty for normal runs.

## Actor input object example

```json
{
  "keywords": [],
  "sources": [
    "secop2"
  ],
  "statuses": [
    "open"
  ],
  "modalities": [
    "licitacion",
    "seleccion_abreviada",
    "minima_cuantia",
    "concurso_meritos",
    "con_ofertas"
  ],
  "departments": [],
  "entity": "",
  "unspscCodes": [],
  "contractTypes": [],
  "entityOrders": [],
  "city": "",
  "categoryKeywords": [],
  "publishedWithinDays": 3,
  "maxItems": 10,
  "onlyNew": false,
  "includeBidders": false,
  "includeDemandSummary": true
}
```

# Actor output Schema

## `tenders` (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 = {
    "keywords": [],
    "statuses": [
        "open"
    ],
    "publishedWithinDays": 3,
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("polyglot-data/secop-scraper").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 = {
    "keywords": [],
    "statuses": ["open"],
    "publishedWithinDays": 3,
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("polyglot-data/secop-scraper").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 '{
  "keywords": [],
  "statuses": [
    "open"
  ],
  "publishedWithinDays": 3,
  "maxItems": 10
}' |
apify call polyglot-data/secop-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,polyglot-data/secop-scraper"
        }
    }
}
```

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/XPg5nhVsjrl9CyWTQ/builds/lXNf8KuyWkup7zEOT/openapi.json
