# French Public Tenders Scraper | BOAMP & DECP (`rlbrt/marches-publics-veille`) Actor

Get French public tenders from BOAMP and contracts nearing renewal from DECP open data: buyer, incumbent supplier, amount, CPV code, estimated end date and a relevance score. Filter by keyword or CPV, get only new results on daily runs, and export to JSON, CSV or Excel via API.

- **URL**: https://apify.com/rlbrt/marches-publics-veille.md
- **Developed by:** [Raphael Lambert](https://apify.com/rlbrt) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 contract or notice delivereds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## French Public Tenders Scraper | BOAMP & DECP

Get French public tenders from BOAMP and contracts nearing renewal from DECP open data: buyer, incumbent supplier, amount, CPV code, estimated end date and a relevance score. Filter by keyword or CPV, get only new results on daily runs, and export to JSON, CSV or Excel via API.

### Use cases

- **Suppliers preparing a re-tender**: as the incumbent, see your contracts coming up for renewal; as a challenger, spot the contracts held by competitors months before the call for tenders.
- **Sales teams**: target the public buyers of your sector (CPV codes, departments) with the amount and end date of their current contracts.
- **Consultants**: map a client's competitors — who wins which contracts, for which buyers, at what amounts.

$4 per 1,000 results (cheaper on higher Apify plans) — you only pay for results delivered.

**En français** : veille marchés publics — renouvellement de marché à venir avec le titulaire sortant, chaque appel d'offres ouvert noté de 0 à 100, à partir des données ouvertes officielles DECP et BOAMP. Recherche par codes CPV ou mots-clés, filtre par département, mode incrémental pour une veille planifiée.

### What you get

- **Contracts due for renewal (DECP)**: estimated end date (notification date + duration), incumbent supplier (name, SIREN, size), amount, number of bids received, buyer — sorted by nearest end date, deduplicated, so you can prepare 6 to 12 months ahead.
- **Open tenders (BOAMP)**: open calls published on BOAMP matching your CPV codes or keywords, with a 0-100 relevance score and the reason for each point (CPV match, keywords, department, time left to answer).
- **Watch mode**: run it on a schedule with `incremental: true` and you only get (and pay for) what is new since the last run.

### Output examples

Real results from 2026-10-06.

Contract due for renewal (input `{"mode": "renouvellements", "queries": ["90911"], "departements": ["69"]}`):

```json
{
    "id": "decp:21690282500019:814167b3",
    "type": "renouvellement",
    "nature": "Marché",
    "objet": "Nettoyage des vitres dans les bâtiments de la ville de Meyzieu",
    "cpv": "90911300",
    "acheteurNom": "COMMUNE DE MEYZIEU",
    "acheteurSiret": "21690282500019",
    "departement": "69",
    "montant": 150000,
    "procedure": "Procédure adaptée",
    "dateNotification": "2023-07-11",
    "dureeMois": 48,
    "dateEcheanceEstimee": "2027-07-11",
    "moisAvantEcheance": 9,
    "titulaireSiret": "48249150300033",
    "titulaireSiren": "482491503",
    "titulaireNom": "GARIC",
    "titulaireCategorie": "PME",
    "titulaireMasque": false,
    "motifMasquage": null,
    "offresRecues": 5,
    "typeMarche": "Services",
    "query": "90911",
    "source": "DECP (data.economie.gouv.fr) et Recherche d’entreprises (DINUM) — Licence Ouverte 2.0",
    "sourceUrl": "https://data.economie.gouv.fr/explore/dataset/decp-2022-marches-valides/table/?refine.id=2314&refine.acheteur_id=21690282500019",
    "scrapedAt": "2026-10-06T09:25:35.834Z"
}
```

Open tender (`mode: "opportunites"`):

```json
{
    "id": "boamp:26-90717",
    "type": "opportunite",
    "objet": "Nettoyage des biens immobiliers de la Métropole de Lyon",
    "cpv": "90911200",
    "acheteurNom": "Métropole de Lyon",
    "departement": "69",
    "procedure": "Procédure Ouverte",
    "datePublication": "2026-09-20",
    "dateLimiteReponse": "2026-10-23T15:00:00+00:00",
    "joursRestants": 18,
    "typeMarche": "Services",
    "score": 74,
    "scoreDetails": ["CPV 90911200 ⊂ 90911 : 30/40", "département 69 : 10/10", "délai de réponse 18 j : 12/20"],
    "query": "90911",
    "source": "BOAMP (DILA) — Licence Ouverte",
    "sourceUrl": "https://www.boamp.fr/pages/avis/?q=idweb:26-90717"
}
```

Every result has the same fields; fields that belong to the other mode are `null` (omitted above for readability).

### Pricing

**$4.00 per 1,000 results**, plus a small start fee per run. You only pay for results delivered: duplicates,
excluded contracts and, in watch mode, results already delivered are never charged. The run stops cleanly
when your spending limit is reached.

### Input and options

| Field | Type | Description |
| --- | --- | --- |
| `queries` | list of strings, **required** | Keywords searched in the contract subject (e.g. `restauration collective`) and/or CPV codes, full or partial (2-8 digits, e.g. `90911` = office cleaning). Each entry is a separate search; results are deduplicated. |
| `mode` | `renouvellements` (default) or `opportunites` | Contracts due for renewal (DECP) or open tenders (BOAMP). |
| `cpvPrefixes` | list of strings, optional | Keep only contracts whose CPV code starts with one of these prefixes, **in addition to** the search (AND). |
| `excludeKeywords` | list of strings, optional | Drop contracts whose subject contains one of these words (whole words, accents and case ignored). |
| `departements` | list of strings | French department codes (`69`, `2A`, `974`…). Empty = all of France. |
| `horizonMois` | integer, default 12 | Renewals: estimated end date between today and N months from now (1-36). |
| `minMoisAvantEcheance` | integer, default 3 | Renewals: skip contracts ending in less than N months (too late to prepare). |
| `minDureeMois` | integer, default 12 | Renewals: skip contracts shorter than N months (one-off purchases, rarely renewed as such). |
| `includeSubsequents` | boolean, default `false` | Renewals: also return orders placed under a framework agreement (*marchés subséquents*, « BDC n° … »). By default only the framework agreement itself is returned, since that is what gets re-tendered. |
| `scoreMin` | integer, default 0 | Open tenders: minimum relevance score (0-100). |
| `maxPerBuyer` | integer, default 5 | At most N results per buyer, so that a buyer with many lots does not fill the whole output. `0` = unlimited. |
| `maxResults` | integer, default 100 | Maximum number of results (1-10,000). Each result is charged. |
| `incremental` | boolean, default `false` | Watch mode: only new results since the last run with the same parameters. |
| `proxyConfiguration` | object, optional | Not needed: the sources are public APIs. |

Example: `{"queries": ["restauration collective"], "cpvPrefixes": ["555", "158", "153"], "excludeKeywords": ["vaisselle"], "departements": ["69", "38"], "horizonMois": 9}`

All options except `queries` are optional and have safe defaults.

### Data and privacy (RGPD)

- Only company data is returned. When the incumbent supplier is a **sole trader** (*entrepreneur
  individuel*), has an unknown legal form or cannot be found in the public company register, or has a
  **company name that is a person's name** (e.g. « DUPONT JEAN » SARL, or a name matching one of its
  directors), the contract is still returned but `titulaireNom`, `titulaireSiret` and `titulaireSiren` are
  `null`, `titulaireMasque` is `true` and `motifMasquage` says why: `entrepreneur_individuel`,
  `denomination_nominative` or `inconnu`. `titulaireCategorie` stays a real INSEE category (PME, ETI, GE) or
  `null`.
- Directors' names from the register are only used, in memory, to detect such company names; they are never
  stored, returned or logged. The item `id` never contains nor derives from the supplier's SIRET: it is a
  fingerprint of the DECP identifier, the buyer, the lot, the notification date and the amount.
- Buyers are public bodies and are returned as published. Buyer contact names, phones and e-mails present in
  BOAMP notices are never returned.

### Limits and tips

- **Combine a keyword with CPV prefixes.** A keyword alone also matches contracts that merely mention it.
  For school and staff catering, use `"queries": ["restauration collective"]` with
  `"cpvPrefixes": ["555", "158", "153"]` (catering services, various food products, fruit and vegetables).
- **Negations are handled**: a subject such as « traiteurs et réceptions *hors* restauration collective »
  does not match `restauration collective` (also « sauf », « à l'exclusion de »). Use `excludeKeywords`
  for the remaining noise.
- **Sorted by nearest end date** (renewals): `maxResults` keeps the most urgent contracts. All matching
  contracts in the window are read first, so very broad searches (e.g. `travaux` over 36 months, all of
  France) take longer: about 2 minutes. Searches above 10,000 contracts are split automatically by contract
  duration. Collection stops at 20,000 contracts; when that happens, or when the public API cannot return
  everything, the run status and the log say so and advise narrowing with `cpvPrefixes` or `departements`.
- **Real renewals only, by default**: contracts ending in less than 3 months, shorter than 12 months, and
  orders placed under a framework agreement are skipped (`minMoisAvantEcheance`, `minDureeMois`,
  `includeSubsequents`). Each result has a `nature` field (Marché, Accord-cadre, Marché subséquent…).
- **Duplicates removed**: the same contract published twice (same buyer, supplier, notification date and
  amount, and either the same subject or one identifier containing the other, including across both DECP
  datasets) is returned once, keeping the most complete record. Separate lots (« LOT 16 », « LOT 18 »…) are
  never merged.
- **Republished tenders returned once**: when a BOAMP notice is republished (new notice linked to the earlier
  one), only the latest version is returned and charged.
- **Number of bids (`offresRecues`)** is filled in by the buyer in only about 60 % of DECP records; when it
  is missing the field is `null` (unknown), not 0.
- **Department**: place of performance as declared by the buyer (department, postal or municipality code; a
  postal code entered as a department code is handled). When the buyer only gave a region, a country or a
  canton (about 4 % of records), the department of the buyer's head office is used instead.
- **Contract type (`typeMarche`)** is derived from the CPV division in both modes: 45 = Travaux, 03 to 44
  and 48 = Fournitures, 50 to 98 = Services. These ranges cover every CPV division (03 to 98); a mixed
  contract is classified by its main CPV. `null` only when a record has no CPV (about 0.3 % of DECP v3
  records); BOAMP's own field is used only when a notice has no CPV.
- **Estimated end date ignores renewals and amendments**: it is the notification date plus the initial
  duration. Renewal options (*reconductions*) and amendments (*avenants*) that extend or shorten a contract are
  not taken into account, so the actual end date may be later.
- **Amounts are as declared by the buyer**: for a framework agreement (*accord-cadre*) the amount is often
  the maximum over its whole duration, not the amount actually spent (e.g. 200 M€ declared for network
  equipment by a mid-sized town). Compare amounts between contracts with care.
- **Open tenders without a CPV code**: BOAMP notices published under the simplified procedure (*MAPA*, about
  7 % of open notices) carry no CPV code; they are found by keywords only, not by CPV codes or `cpvPrefixes`.
- **Diversity**: `maxPerBuyer` (default 5) stops a buyer with 19 lots from taking the whole output; the run
  log says how many results were dropped for that reason.

### En français

Veille des marchés publics français : **renouvellements à venir** (marchés attribués dont l'échéance estimée
approche, avec le titulaire sortant, le montant et le nombre d'offres reçues, d'après les DECP) et **appels
d'offres ouverts** du BOAMP notés de 0 à 100 avec le détail du score. Recherche par codes CPV et/ou mots-clés,
filtre par département, mode incrémental pour une veille planifiée. Données publiques sous Licence Ouverte,
aucune donnée personnelle : pour les marchés attribués à des entrepreneurs individuels, le titulaire est masqué
(`titulaireMasque: true`).

### Technical notes

- **Sources**: DECP (*données essentielles de la commande publique*, data.economie.gouv.fr, datasets
  `decp-2022-marches-valides` and `decp-v3-marches-valides`, Licence Ouverte 2.0); BOAMP (DILA, Licence
  Ouverte); supplier and buyer names from the Recherche d'entreprises API (DINUM, Licence Ouverte 2.0).
  Built on official open-data APIs: no fragile HTML parsing, no browser, no proxy needed.
- **Estimated end date** = notification date + initial duration; the window is pre-filtered on the server by
  duration band, then checked exactly. Amendments (*avenants*) that extend a
  contract are not taken into account yet; DECP data quality varies by buyer (implausible amounts are
  returned as `null`).
- **Coverage**: contracts notified in the last 10 years and published in the DECP; open BOAMP notices.
  CPV filtering on BOAMP relies on eForms notices (about three quarters of open notices in a 100-notice sample); use keywords as well to
  catch the others.
- **Relevance score**: CPV match (40), keywords found in the subject (40), department (10), time left to
  answer (20), normalised to 100 over the criteria that apply to your input. Deterministic, no AI.
- **Privacy**: see « Data and privacy » above. Contract subjects are cleaned (HTML tags, broken entities
  such as `et#8211,`, literal `\r\n`, technical `INX` prefix).
- **Reliability**: retries on 403/429/5xx and network errors, error counters per category in the run log
  and in the `RUN_STATS` record; the run fails with a clear message if the data format changes.
- **Speed** (measured on Apify, 256 MB): about 5 minutes per 1,000 renewals (supplier lookups at
  5 requests/s), about 2 minutes per 1,000 open tenders.
- **Limited permissions**: the Actor only uses its own storages (plus one named key-value store for watch mode).

# Actor input Schema

## `queries` (type: `array`):

Mots-clés cherchés dans l'objet du marché (ex. « restauration collective ») et/ou codes CPV complets ou partiels (2 à 8 chiffres, ex. « 90911 » = nettoyage de locaux). Chaque entrée est une recherche distincte ; les résultats sont dédoublonnés. Conseil : combinez un mot-clé avec « Préfixes CPV » pour éliminer les faux positifs.

## `mode` (type: `string`):

« renouvellements » : marchés attribués dont l'échéance estimée (notification + durée) tombe dans l'horizon choisi, avec titulaire sortant, montant et nombre d'offres reçues (DECP). « opportunites » : appels d'offres ouverts au BOAMP avec un score de pertinence 0-100 expliqué.

## `cpvPrefixes` (type: `array`):

Facultatif. Ne garde que les marchés dont le code CPV commence par l'un de ces préfixes (2 à 8 chiffres), en plus de la recherche. Ex. restauration : « 555 » (services de cantine et traiteur), « 158 » (produits alimentaires divers), « 153 » (fruits et légumes).

## `excludeKeywords` (type: `array`):

Facultatif. Écarte les marchés dont l'objet contient l'un de ces mots (mots entiers, sans accents ni casse), ex. « vaisselle », « traiteur ».

## `departements` (type: `array`):

Codes de département (ex. « 69 », « 2A », « 974 »). Vide = toute la France.

## `horizonMois` (type: `integer`):

Mode renouvellements : échéances estimées entre aujourd'hui et dans N mois.

## `minMoisAvantEcheance` (type: `integer`):

Renouvellements : écarte les marchés qui expirent dans moins de N mois (trop tard pour se préparer).

## `minDureeMois` (type: `integer`):

Renouvellements : écarte les marchés plus courts (achats ponctuels, rarement renouvelés à l'identique).

## `includeSubsequents` (type: `boolean`):

Par défaut, les commandes passées sur un accord-cadre (marchés subséquents, « BDC n° … ») sont écartées : seul l'accord-cadre se renouvelle.

## `scoreMin` (type: `integer`):

Mode opportunités : n'afficher que les avis dont le score de pertinence atteint ce seuil (0-100).

## `maxPerBuyer` (type: `integer`):

Évite qu'un acheteur à lots multiples monopolise la sortie. 0 = illimité.

## `maxResults` (type: `integer`):

Arrête le run après ce nombre de résultats (toutes recherches confondues). Chaque résultat est facturé.

## `incremental` (type: `boolean`):

Ne renvoie (et ne facture) que les marchés ou avis nouveaux depuis le dernier run avec les mêmes paramètres. Idéal pour un run planifié.

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

Facultatif. Les sources sont des API publiques : aucun proxy n'est nécessaire en temps normal.

## Actor input object example

```json
{
  "queries": [
    "restauration collective"
  ],
  "mode": "renouvellements",
  "cpvPrefixes": [
    "555"
  ],
  "horizonMois": 12,
  "minMoisAvantEcheance": 3,
  "minDureeMois": 12,
  "includeSubsequents": false,
  "scoreMin": 0,
  "maxPerBuyer": 5,
  "maxResults": 20,
  "incremental": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `renouvellements` (type: `string`):

Marchés à renouveler (mode renouvellements)

## `opportunites` (type: `string`):

Appels d'offres ouverts (mode opportunités)

## `runStats` (type: `string`):

Compteurs d'items valides, invalides, écartés et d'erreurs par catégorie

# 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 = {
    "queries": [
        "restauration collective"
    ],
    "cpvPrefixes": [
        "555"
    ],
    "maxResults": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("rlbrt/marches-publics-veille").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 = {
    "queries": ["restauration collective"],
    "cpvPrefixes": ["555"],
    "maxResults": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("rlbrt/marches-publics-veille").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 '{
  "queries": [
    "restauration collective"
  ],
  "cpvPrefixes": [
    "555"
  ],
  "maxResults": 20
}' |
apify call rlbrt/marches-publics-veille --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rlbrt/marches-publics-veille"
        }
    }
}
```

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/FNywdXbykc8uTrS7Q/builds/GFjHQkqKQvqB061gT/openapi.json
