# France BOAMP Public Tenders Scraper (`knotty_mistveil/france-boamp-tenders`) Actor

French public procurement tenders from the official BOAMP open-data API (DILA/Opendatasoft): buyer, deadline, CPV codes, department, contact email.

- **URL**: https://apify.com/knotty\_mistveil/france-boamp-tenders.md
- **Developed by:** [Jason](https://apify.com/knotty_mistveil) (community)
- **Categories:** Business, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 notice returneds

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

## France BOAMP Public Tenders Scraper

Pulls French public procurement notices from **BOAMP** (Bulletin officiel des annonces de marchés publics), the French government's official tender bulletin, through its open-data API on the DILA / Opendatasoft portal. No API key, no account, no browser automation. You get one flat JSON record per notice with the buyer, the publication date, the response deadline, the market type (works / supplies / services), the notice type, the department code(s), the region (derived from the department), the city, the CPV-style descriptor codes and labels, and the buyer's contact name and e-mail where the notice publishes them - filtered by publication-date window, keyword, market type, department, region, buyer name and response deadline. On 10 September 2026 the dataset held **1,704,443 notices** going back to a publication date of 2 March 2015, and BOAMP publishes roughly **400 notices per business day** (399 on 9 September 2026).

### What you get

Every record contains:

| Field | Description |
| --- | --- |
| `id` | BOAMP notice id (`idweb`), for example `26-87284` |
| `source` | `boamp`, constant |
| `url` | The public notice page, e.g. `https://www.boamp.fr/pages/avis/?q=idweb:26-87284` (the API's own `url_avis`) |
| `title` | The subject (`objet`), trimmed to 200 characters on a word boundary |
| `buyerName` | The contracting authority (`nomacheteur`), e.g. `FRANCE TRAVAIL - IdF` |
| `publicationDate` | Date the notice was published (`dateparution`), `YYYY-MM-DD` |
| `deadline` | Response deadline (`datelimitereponse`) as an ISO timestamp with timezone, or `null` when the notice has none (award and result notices) |
| `marketType` | Array of market types: `TRAVAUX` (works), `FOURNITURES` (supplies), `SERVICES`. Empty when the notice carries none (some corrigenda/results) |
| `natureType` | Notice type in French, e.g. `Avis de marché`, `Rectificatif`, `Résultat de marché`, `Modification` |
| `noticeState` | The notice's state (`etat`), e.g. `INITIAL` |
| `procedureType` | The procurement procedure label (`procedure_libelle`), e.g. `Procédure Adaptée`, or `null` |
| `department` | Array of French department codes (`code_departement`), e.g. `["93","75","77","92","94","95"]` |
| `region` | Region name derived from the department code(s), e.g. `Île-de-France`; multiple regions are joined with `; ` (see *Limits and freshness*) |
| `city` | The buyer's city, read from the notice body (`donnees`); `null` when the notice omits it |
| `cpvCodes` | Array of BOAMP descriptor codes (`descripteur_code`), the CPV-style subject codes, e.g. `["274"]` |
| `cpvLabels` | The matching descriptor labels (`descripteur_libelle`), e.g. `["Prestations de services"]` |
| `contactName` | The buyer's published contact person or service, read from the notice body; `null` when none is published |
| `contactEmail` | The buyer's published contact e-mail, read from the notice body; `null` when none is published or the value is not a well-formed address |
| `description` | The full subject (`objet`), untrimmed |
| `fetchedAt` | ISO timestamp of the run that produced the record |

`city`, `contactName` and `contactEmail` are read out of each notice's source form (`donnees`), whose shape differs by notice family - the national MAPA and FNS forms keep them under `organisme`, and the EU eForms (JOUE) notices reference the buyer by an organisation id that the Actor resolves to that organisation's own address and contact, never the publishing platform's. The dataset has two views: **Overview** (dates, buyer, market type, department, region, city, CPV, link) and **Buyer contacts** (buyer, contact name, e-mail, city, region, deadline, link).

### Input

| Input | Type | Default | Description |
| --- | --- | --- | --- |
| `daysBack` | integer | `3` | Look-back window in days on the publication date, ending today. Ignored when `dateFrom`/`dateTo` are set. Max 3,660. |
| `dateFrom` | string | - | Start of the publication-date window, `YYYY-MM-DD`, inclusive. Overrides `daysBack`. |
| `dateTo` | string | today | End of the publication-date window, `YYYY-MM-DD`, inclusive. |
| `keywords` | string\[] | none | Terms matched against the subject and the buyer name with BOAMP's full-text search, which matches whole words (so `eau` does not match `bureau`). Terms are ORed. |
| `marketTypes` | string\[] | all | Any of `Travaux`, `Fournitures`, `Services`. Notices with no market type are excluded when this is set. |
| `departments` | string\[] | all | French department codes, e.g. `75`, `13`, `2A`, `971`, `973`. A notice is kept when any of its departments matches. |
| `regions` | string\[] | all | Region names, matched as a case-insensitive substring of the derived region, e.g. `Bretagne`, `Occitanie`, `Île-de-France`. Terms are ORed. |
| `buyers` | string\[] | any | Buyer-name terms matched against `nomacheteur` with the full-text search, e.g. `mairie`, `metropole`. Terms are ORed. |
| `deadlineAfter` | string | - | Keep only notices whose response deadline is on or after this date, `YYYY-MM-DD`. Notices with no deadline are excluded when this is set. |
| `maxItems` | integer | `500` | Stop after this many notices. You are charged per notice, so this is also the cost cap. Newest publication date first, so a value below the window's volume drops the **oldest** notices in the window. |

The default of 500 is a little above one business day's volume (about 400), so a daily run with the defaults captures a full day with headroom. Keyword, market-type, department, buyer and deadline filters are applied by the BOAMP API; the region filter is applied locally to the derived region.

Example - open works and supplies tenders in Île-de-France published in the last week, with a deadline still to come:

```json
{ "daysBack": 7, "marketTypes": ["Travaux", "Fournitures"], "regions": ["Île-de-France"], "deadlineAfter": "2026-09-20", "maxItems": 500 }
```

Example - a daily watch on every IT-related notice, run on a schedule:

```json
{ "daysBack": 2, "keywords": ["informatique", "logiciel", "cloud"], "maxItems": 200 }
```

Example - every notice from a set of departments over a quarter:

```json
{ "dateFrom": "2026-06-01", "dateTo": "2026-09-01", "departments": ["75", "92", "93", "94"], "maxItems": 5000 }
```

Bad input fails fast: an unknown market type, a malformed department code or a malformed date stops the run with the validation message as its status, before any request is made.

### Output example

A real record from a live run on 10 September 2026 (`{"daysBack": 2, "maxItems": 5}`), unedited:

```json
{
  "id": "26-87284",
  "source": "boamp",
  "url": "https://www.boamp.fr/pages/avis/?q=idweb:26-87284",
  "title": "PRESTATION DE SERVICE SOCIALE AU BENEFICE DES AGENTS DE FRANCE TRAVAIL ILE DE FRANCE PUBLIC ET PRIVE",
  "buyerName": "FRANCE TRAVAIL - IdF",
  "publicationDate": "2026-09-09",
  "deadline": "2026-10-13T16:00:00+00:00",
  "marketType": ["SERVICES"],
  "natureType": "Avis de marché",
  "noticeState": "INITIAL",
  "procedureType": "Procédure Adaptée",
  "department": ["93", "75", "77", "92", "94", "95"],
  "region": "Île-de-France",
  "city": "MONTREUIL",
  "cpvCodes": ["274"],
  "cpvLabels": ["Prestations de services"],
  "contactName": "Nacéra Djelti",
  "contactEmail": "nacera.djelti@francetravail.fr",
  "description": "PRESTATION DE SERVICE SOCIALE AU BENEFICE DES AGENTS DE FRANCE TRAVAIL ILE DE FRANCE PUBLIC ET PRIVE",
  "fetchedAt": "2026-09-10T00:15:09.032Z"
}
```

An EU-threshold notice (family JOUE, published as eForms) looks the same: for example `26-86644`, buyer `MAIRIE DE GISORS`, `region` `Normandie`, `city` `Gisors`, `contactEmail` `direction.generale@mairie-gisors.fr` resolved from the buyer's own organisation block rather than the publishing platform's.

Every run also writes a `SUMMARY` record to the key-value store with the resolved date window, the filters used, how many records the local region filter dropped, the number of pages read, and the `windowExhausted` completeness flag (see *Limits and freshness*).

### Pricing

Pay per result: **US$0.003 per notice delivered** (US$3 per 1,000 notices). You are charged only for records pushed to the dataset; a run that returns nothing costs nothing. Runs on the Apify free plan are limited by the platform's free-tier usage allowance.

### Data source and licence

- **Endpoint.** `GET https://boamp-datadila.opendatasoft.com/api/explore/v2.1/catalog/datasets/boamp/records`, the Opendatasoft Explore v2.1 API for DILA's BOAMP dataset. Public, no authentication. Dataset page: <https://boamp-datadila.opendatasoft.com/explore/dataset/boamp/> (read 10 September 2026).
- **Licence.** BOAMP open data is published under the **Licence Ouverte / Open Licence 2.0** (Etalab), <https://www.etalab.gouv.fr/licence-ouverte-open-licence/> (read 10 September 2026). It permits reuse for any purpose, including commercial reuse, provided the source and the date of the last update are acknowledged. Attribute as **"Source: DILA / BOAMP, Licence Ouverte 2.0"** and link to the notice page.
- **Personal data.** `contactName` and `contactEmail` are the contact each contracting authority publishes on its own notice so that suppliers can ask questions and submit bids - sometimes a named officer (`Nacéra Djelti`), often a service or a shared mailbox (`marchespublics@…`). BOAMP publishes them lawfully as part of the official bulletin. Using them for anything other than responding to the tender - marketing in particular - is the buyer's responsibility under the applicable regime (the GDPR and the French loi Informatique et Libertés for contacts in France). Some notices also name individuals in the subject text.

### Limits and freshness

- **Volumes, measured 10 September 2026** with this Actor's own date filter: notices with a publication date in the last **1 full day** (9 September) - 399; last **3 days** - 697; last **7 days** - 1,683; last **30 days** - 7,704. That is roughly **400 per business day**. A single keyword narrows it sharply: `informatique` over the last 30 days returned 104; department `75` over the last 7 days, 163. The whole dataset was **1,704,443 notices** back to a publication date of 2 March 2015.
- **Size `maxItems` to the window, and check `windowExhausted`.** `maxItems` counts records delivered and the stream is newest-first on publication date, so a value below the window's volume drops the **oldest** notices in it - but never silently. The run stops when the cap is filled and warns `maxItems (N) was reached before the window was exhausted`, and the `SUMMARY` record carries `limitReached: true` with `windowExhausted: false`. `windowExhausted: true` is the guarantee that you hold the complete window: it is set only when the run read every matching notice and delivered every record it kept.
- **The 10,000-row offset ceiling is handled for you.** The Explore API refuses `offset + limit > 10000` (verified 10 September 2026). A window with more matches than that - about 25 days at the current rate, or fewer with looser filters - is walked in date sub-windows: when the offset ceiling is reached the Actor re-anchors the upper date bound on the oldest publication date seen and continues, de-duplicating the re-read boundary date by `idweb`. So an arbitrarily deep window is delivered complete and in order; only a single day holding more than ~9,900 matching notices (never observed - the busiest day seen was 399) would be unreachable, and the run warns if that ever happens.
- **Speed, measured 10 September 2026.** A complete 3-day window of 697 notices (7 pages of 100) delivered in 11.3 s; a capped run of 300 notices (3 pages) in 6.1 s - roughly **60 notices a second**, paging included. A full 30-day window of about 7,700 notices takes on the order of two to three minutes.
- **Rate limits.** The Actor requests one 100-row page at a time and retries `429` and `5xx` responses with exponential back-off, honouring `Retry-After` (via the shared `fetchWithRetry`). No documented hard quota was hit in testing.
- **Missing values are the notice's, not the Actor's.** `deadline` is `null` for award and result notices, which have no response date; `marketType` is empty on some corrigenda and results; `contactName` is absent on the FNS simplified form, which carries no correspondant; `city` and `contactEmail` are present on most notices but not all. These stay `null`/empty rather than being guessed. `contactEmail` is returned only when the published value is a well-formed address.
- **Freshness.** Notices appear on business days; the newest publication date on 10 September 2026 was 9 September, with nothing yet published for the 10th at run time. Run an overlapping window (`daysBack: 2` daily) rather than one that exactly abuts the previous run. A notice is corrected in place with a `Rectificatif`/`Modification` notice carrying its own `idweb`, so re-run with an overlap to catch changes.

### Use cases

- **Bid pipeline for a supplier or contractor:** a scheduled daily run over your departments or regions and market type, with the `deadline`, `cpvCodes` and the buyer's contact e-mail already on the record, so a bid/no-bid call takes minutes.
- **Lead generation for firms that sell to the public sector:** filter by `keywords` and `buyers`, and use `buyerName`, `city`, `contactName` and `contactEmail` to reach the contracting authority behind each notice.
- **Public-procurement research and dashboards:** pull a full quarter or year by publication date and analyse spend intentions by region, department, CPV code, market type or buyer across France's 1.7-million-notice archive back to 2015.

### Support

Open an issue on the Actor's page on Apify Store. Issues are answered within 14 days, as the Store terms require. Bug reports that name the run id and the input used are fixed fastest.

# Actor input Schema

## `daysBack` (type: `integer`):

Look-back window in days on the publication date (dateparution), ending today. Ignored when Date from / Date to are set. BOAMP publishes roughly 400 notices a day.

## `dateFrom` (type: `string`):

Start of the publication-date window, YYYY-MM-DD (inclusive). Overrides Days back.

## `dateTo` (type: `string`):

End of the publication-date window, YYYY-MM-DD (inclusive). Defaults to today.

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

Terms matched against the subject (objet) and the buyer name (nomacheteur) using BOAMP's full-text search, which matches whole words (so "eau" does not match "bureau"). Terms are ORed. Empty = no keyword filter.

## `marketTypes` (type: `array`):

Keep only notices tagged with at least one of these market types. Notices without a market type (some award/corrigendum notices) are excluded when this is set. Empty = all types.

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

French department codes to keep, e.g. 75 (Paris), 13 (Bouches-du-Rhône), 2A/2B (Corse), 971 (Guadeloupe), 973 (Guyane). A notice is kept when any of its departments matches. Empty = all of France.

## `regions` (type: `array`):

Region names to keep, matched as a case-insensitive substring of the region derived from the notice's department code, e.g. Bretagne, Occitanie, "Île-de-France". Terms are ORed. Empty = all regions.

## `buyers` (type: `array`):

Buyer-name terms matched against nomacheteur with BOAMP's full-text search, e.g. mairie, metropole, "conseil departemental". Terms are ORed. Empty = all buyers.

## `deadlineAfter` (type: `string`):

Keep only notices whose response deadline (datelimitereponse) is on or after this date, YYYY-MM-DD. Notices with no deadline (award and result notices) are excluded when this is set.

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

Stop after this many notices. You are charged per notice delivered, so this is also the cost cap. Newest publication date first, so a value below the window's volume drops the OLDEST notices in the window. About 400 notices are published per day.

## Actor input object example

```json
{
  "daysBack": 3,
  "keywords": [],
  "marketTypes": [],
  "departments": [],
  "regions": [],
  "buyers": [],
  "maxItems": 500
}
```

# Actor output Schema

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

All BOAMP notices returned by the run, one JSON object each. Append ?format=csv or ?format=xlsx to download a spreadsheet.

## `summary` (type: `string`):

Counts, the publication-date window used, the filters applied, how many records each local filter dropped and the windowExhausted completeness flag, written at the end of the run.

# 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 = {
    "daysBack": 3,
    "keywords": [],
    "marketTypes": [],
    "departments": [],
    "regions": [],
    "buyers": [],
    "maxItems": 500
};

// Run the Actor and wait for it to finish
const run = await client.actor("knotty_mistveil/france-boamp-tenders").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 = {
    "daysBack": 3,
    "keywords": [],
    "marketTypes": [],
    "departments": [],
    "regions": [],
    "buyers": [],
    "maxItems": 500,
}

# Run the Actor and wait for it to finish
run = client.actor("knotty_mistveil/france-boamp-tenders").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 '{
  "daysBack": 3,
  "keywords": [],
  "marketTypes": [],
  "departments": [],
  "regions": [],
  "buyers": [],
  "maxItems": 500
}' |
apify call knotty_mistveil/france-boamp-tenders --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,knotty_mistveil/france-boamp-tenders"
        }
    }
}
```

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/k1Pdx9uz0kTG1m7ce/builds/rFd4CpiM3NfJPxH4E/openapi.json
