Nouveau

Surveillance des marchés

Livraison automatique des nouveaux marchés selon vos filtres — région, tags sectoriels, mots-clés, plage de valeurs. API Pull + digest e-mail + webhook.

Concepts

Quatre notions que vous rencontrerez dans tous les endpoints :

  • Tenderun appel d'offres spécifique issu d'un portail (NEN, VVZ, E-ZAK, TenderArena, etc.). Identifié par un ID numérique.
  • Filterun ensemble de critères enregistrés (région, secteur, mots-clés, valeur). Lorsqu'un nouvel appel d'offres satisfait ces critères, une correspondance est créée.
  • Matchun nouvel appel d'offres correspondant à votre filtre. Représente la liaison (tender × filtre) + timestamp + votre statut (suivi / masqué / lu).
  • Taxonomyréférentiels de régions (NUTS), de secteurs (tags sectoriels) et de codes CPV selon lesquels nous classifions les marchés publics.

Démarrage rapide

Les filtres peuvent être configurés de deux manières — le résultat est identique et le filtre peut être modifié à tout moment par l'une ou l'autre méthode.

curl -H "X-Api-Key: mrw_live_…" \
  "https://veritra.io/api/v2/leads/tenders/search?qText=rekonstrukce%20mostu&limit=5"

La barre supérieure permet une recherche ad hoc. Pour un monitoring continu (nouveaux appels d'offres correspondant à vos critères), utilisez les filtres + webhook/email digest — voir ci-dessous.

Recommandation : Pour le filtrage, privilégiez toujours les industryTags plutôt que les categories (CPV). Nos industryTags résultent de la combinaison de la classification LLM des appels d'offres et du mapping CPV — robustes face aux CPV incorrects. Les acheteurs publics en CZ attribuent souvent des codes CPV trop génériques ou sans rapport (typiquement le générique « 45000000-0 » pour les marchés de travaux au lieu d'un préfixe spécifique), si bien qu'un filtre purement CPV manque une partie des marchés pertinents.

Recherche d'appels d'offres (ad hoc)

Pour une recherche ponctuelle d'appels d'offres parmi tous les marchés actifs. N'utilise pas de filtre enregistré ; les paramètres sont passés directement dans la chaîne de requête.

GET/api/v2/leads/tenders/search

Paramètres de requête

ParamètreTypeDescription
qText*stringTexte recherché (recherche plein texte dans le titre + la description).
regionsstringCSV de codes feuilles NUTS (CZ010,CZ020).
cpvPrefixesstringCSV de préfixes CPV (45,452).
industryTagsstringCSV d'identifiants de tags sectoriels (con_buildings,it_development).
minValuenumberValeur estimée minimale (CZK). Les appels d'offres sans valeur sont acceptés.
maxValuenumberValeur estimée maximale (CZK). Les appels d'offres sans valeur sont acceptés.
deadlineFromstring (YYYY-MM-DD)Date limite de dépôt >= AAAA-MM-JJ.
deadlineTostring (YYYY-MM-DD)Date limite de dépôt <= AAAA-MM-JJ.
sort"newest" | "deadline" | "value"Tri : newest (par défaut), deadline (croissant), value (décroissant).
limitnumberNombre de résultats, max 1000 (défaut : 50). Pour >1k, utilisez la pagination nextCursor ou /matches/export.
cursorstringCurseur keyset de la page précédente (pagination.nextCursor).

Exemple

curl -H "X-Api-Key: mrw_live_…" \
  "https://veritra.io/api/v2/leads/tenders/search?qText=rekonstrukce&regions=CZ010,CZ020&minValue=500000&limit=10"
GET/api/v2/leads/tenders/:id

Réponse : objet data avec les champs id, title, description, estimatedValue, deadlineAt, contractingAuthority, documents[], starred, excluded.

POST/api/v2/leads/tenders/:id/email

Le corps peut contenir recipientEmail pour transférer vers une autre adresse e-mail. Par défaut : user.email.

GET/api/v2/leads/documents/preview

Query : ?url=:original&kind=docx|xlsx. Liste blanche des hôtes : NEN, E-ZAK, Tender Arena, Gemin, ProfilZadavatele.

GET /api/v2/leads/documents/preview?url=https://nen.nipez.cz/…/Vyzva.docx&kind=docx
→ 302 Location: https://rwx-storage…/Tendero/doc-cache-v8/<hash>.html (signed, TTL 10 min)

Catalogue & autocomplétion (public, aucune clé requise)

Pour les menus déroulants de l'interface et la création de filtres. Cache côté serveur : 1 h. Public, aucune authentification requise.

GET/api/v2/leads/zadavatele?q=praha
GET/api/v2/leads/countries
GET/api/v2/leads/regions?country=CZ
GET/api/v2/leads/regions/catalog?country=CZ&locale=cs
GET/api/v2/leads/taxonomy/industry
GET/api/v2/leads/taxonomy/cpv
Recherche vs. Filtre : La recherche est une requête ponctuelle qui retourne un snapshot courant des appels d'offres. Le filtre (ci-dessous) correspond à des critères enregistrés — le serveur vous notifie en continu des nouveaux appels d'offres via webhook ou email digest.

Éléments filtrables et valeurs autorisées

Le filtre est composé des champs ci-dessous. Tous sont optionnels sauf `name`. Un champ vide = aucune restriction sur cette dimension.

regions string[]

Régions NUTS pour le pays donné (?country=CZ) avec libellés par locale.

Le filtre est pan-UE : Les valeurs `regions` sont des codes NUTS de n'importe quel pays de l'UE figurant dans la liste ci-dessous. Le filtre ne retournera que les régions couvertes par votre abonnement LEADS actif (périmètre d'abonnement). Si vous avez souscrit uniquement à CZ + SK, les codes NUTS DE seront ignorés par le filtre.
Pays pris en charge (NUTS-0)
CZČeská republika
SKSlovensko
PLPolsko
DENěmecko
ATRakousko
FRFrancie
ESŠpanělsko
ITItálie
NLNizozemsko
BEBelgie
PTPortugalsko
SEŠvédsko
FIFinsko
DKDánsko
NONorsko
IEIrsko
GRŘecko
RORumunsko
BGBulharsko
HUMaďarsko
HRChorvatsko
SISlovinsko
LTLitva
LVLotyšsko
EEEstonsko
LULucembursko
CYKypr
MTMalta
CHŠvýcarsko
ISIsland
MKSeverní Makedonie

Pour obtenir l'arborescence NUTS complète d'un pays donné, utilisez :

GET /api/v2/leads/regions/catalog?country=CZ&locale=cs
GET /api/v2/leads/regions/catalog?country=DE&locale=de
GET /api/v2/leads/regions/catalog?country=FR&locale=en
Exemple : codes NUTS-3 pour CZ (régions) — développer ▸
CZ010Hlavní město Praha
CZ020Středočeský kraj
CZ031Jihočeský kraj
CZ032Plzeňský kraj
CZ041Karlovarský kraj
CZ042Ústecký kraj
CZ051Liberecký kraj
CZ052Královéhradecký kraj
CZ053Pardubický kraj
CZ063Kraj Vysočina
CZ064Jihomoravský kraj
CZ071Olomoucký kraj
CZ072Zlínský kraj
CZ080Moravskoslezský kraj

industryTags string[] Recommandé

Classification sectorielle multi-tags. Un appel d'offres peut avoir plusieurs tags (ex. : « stav_pozemni » + « stav_remesla » pour une rénovation). Le filtre correspond à un appel d'offres s'il possède au moins un des tags indiqués (JSON_OVERLAPS).

Pourquoi préférer les industryTags au CPV : Nos industryTags sont générés par la combinaison de la classification LLM du titre/description de l'appel d'offres + du mapping CPV + d'indices regex — ils capturent les marchés pertinents même chez les acheteurs qui attribuent les CPV de façon négligente. Le filtre CPV est précis, mais dépend de la rigueur de l'acheteur, qui est faible en République tchèque.

48 tags dans 13 domaines
🏗️Construction
con_buildingsBuilding construction and renovation
con_civilCivil engineering (roads, bridges, water)
con_tradesConstruction trades and subworks
con_energy_efficiencyEnergy savings & renewables
con_materialsConstruction materials
📐Design & supervision
des_documentationDesign documentation and studies
des_supervision_ohsSite supervision and OHS
des_surveyingSurveying and land consolidation
💻IT & software
it_developmentSoftware development and integration
it_licensingSoftware licences and subscriptions
it_hardwareHardware and infrastructure
it_cybersecurityCybersecurity
it_data_aiData, analytics, AI/ML
📡Telecommunications
telecom_internetTelecom, internet, mobile
🧑‍💼Professional services
prof_marketingMarketing, PR, advertising
prof_legalLegal services
prof_accountingAccounting, grants, audit
prof_hrHR, recruitment
prof_translationTranslation and interpreting
prof_insuranceInsurance and finance
🛡️Operations & maintenance
ops_cleaningCleaning services
ops_securitySecurity and reception
ops_maintenanceEquipment maintenance and service
ops_wasteWaste management
ops_facilityProperty management
🍽️Catering & accommodation
cat_cateringCatering services
cat_accommodationAccommodation and conferences
cat_foodFood and beverages
🚚Transport & vehicles
trans_transportPassenger and freight transport
trans_vehiclesVehicles, parts, leasing
🏥Healthcare
health_pharmaPharmaceuticals
health_devicesMedical devices and supplies
health_careHealth and social care
📦Goods & equipment
goods_furnitureFurniture and interiors
goods_clothingClothing, PPE, uniforms
goods_electricalElectrical material
goods_machineryIndustrial machinery
Energy & water
energy_fuelsFuels
energy_power_heatElectricity and heat
energy_waterWater utilities
🌳Nature, forestry, safety
nat_forestryForestry
nat_greeneryGreenery maintenance and gardening
nat_agricultureAgriculture
defense_safetyFire, military, defence
🔬Science & education
sci_labLab and measurement equipment
sci_researchResearch and development
edu_trainingEducation and training
culture_mediaCulture, books, media
Votre tag est manquant ? Si vous ne trouvez pas de tag pour votre secteur d'activité, écrivez-nous à michal@veritra.io — nous l'ajouterons.

categories string[] Moins précis

Préfixes CPV (Common Procurement Vocabulary) de longueur quelconque — correspondance via `LIKE 'prefix%'`. Ainsi, « 45 » capture toutes les divisions de travaux, « 4523 » uniquement les travaux de génie civil, « 45316110 » uniquement l'éclairage public.

35 divisions CPV les plus fréquentes (à 2 chiffres)
03Agriculture, sylviculture, pêche
09Combustibles et énergie
15Produits alimentaires et boissons
18Vêtements, chaussures, équipements de protection individuelle
22Imprimés, livres
30Machines de bureau, équipements informatiques
31Machines électriques, câbles, éclairage
32Radio, télévision, télécommunications
33Équipements médicaux, médicaments
34Véhicules, transport
35Sécurité, matériel de pompiers et militaire
38Instruments de laboratoire et de mesure
39Mobilier, aménagement intérieur
42Machines industrielles
44Matériaux et structures de construction
45Travaux de construction
48Logiciels et licences
50Réparations et maintenance
55Restauration, hébergement
60Transport (acheminement)
64Services postaux, télécommunications
65Services publics (électricité, eau)
66Services financiers et d'assurance
70Services immobiliers et gestion de bâtiments
71Services d'architecture, de conception et d'ingénierie
72Services informatiques (développement, intégration, support)
73Recherche et développement
75Administration publique, défense
77Services agricoles, forestiers et horticoles
79Services aux entreprises (juridique, comptabilité, RH, marketing)
80Éducation, formation
85Soins de santé et services sociaux
90Déchets, environnement, nettoyage
92Culture, sport, loisirs
98Autres services à la population

Le catalogue complet (9 454 codes) est disponible sur /docs/leads/cpv — consultable et regroupé par divisions.

keywords string[]

Correspondance LIKE sur le titre et la description du marché. Non sensible à la casse. Opérateur OR entre les éléments. Utile comme filet de sécurité si les industryTags/CPV ne capturent pas tout (ex. : type de marché spécifique mentionné dans le texte).

"keywords": ["rekonstrukce", "VO", "osvětlení", "MŠ"]

minValue / maxValue number | null

Plage de la valeur estimée du marché en CZK. Il est possible de renseigner uniquement minValue, uniquement maxValue, ou les deux.

Important : Les appels d'offres sans valeur estimée renseignée (estimatedValue est null ou 0) passent le filtre dans les deux sens. De nombreux acheteurs n'indiquent pas cette valeur — les exclure vous ferait manquer ces marchés.

emailDigest boolean

Si true, vous recevez 1× par jour (05:00 UTC) un e-mail récapitulatif des nouveaux résultats correspondant à ce filtre. Valeur par défaut : true. Désactivable en modifiant le filtre. Le webhook se configure séparément, au niveau du compte (voir la section ci-dessous).

name string · active boolean

`name` — 120 caractères max, obligatoire. `active` — si false, le filtre ne s'exécute pas dans le cron (aucun nouveau résultat, digest ou webhook). Permet de mettre en pause sans supprimer.

Logique de filtrage

Les champs se combinent comme suit :

MATCH = (acheteur.NUTS3 ∈ expand(regions))
     AND (minValue ≤ estimatedValue ≤ maxValue  OU  estimatedValue est null ou 0)
     AND (industry_or_cpv  OU  keyword_match)

# expand(regions) : les codes NUTS sont développés jusqu'aux feuilles NUTS 3
# (CZ → 14 régions, CZ01 → CZ010, CZ010 → CZ010).

industry_or_cpv :
   si  industryTags renseignés  →  JSON_OVERLAPS(industryTags, tender.industryTags)
   sinon categories renseignées →  tender.cpvCode LIKE ANY (categories + "%")
   sinon                        →  false   (aucun filtre industry appliqué)

keyword_match :
   si  keywords renseignés      →  tender.title ou description LIKE ANY (%kw%)
   sinon                        →  false

# Si vous ne renseignez aucun industryTags / categories / keywords,
# tous les appels d'offres satisfaisant regions et la plage de valeur sont retournés.

industryTags a la priorité sur categories — si vous renseignez les deux, seul industryTags est utilisé (categories est ignoré). keywords fonctionne indépendamment (il se trouve dans la branche OR avec industry_or_cpv).

Points de terminaison pour la gestion des filtres

Pour gérer les filtres, utilisez la clé de gestion (mrw_live_…). Elle dispose de limites de débit (rate limits) indépendantes et ne consomme pas de crédits LEADS, de sorte que la gestion des filtres n'affecte pas votre collecte quotidienne de marchés.

GET/api/v2/leads/filters
POST/api/v2/leads/filters
GET/api/v2/leads/filters/:id
PATCH/api/v2/leads/filters/:id
DELETE/api/v2/leads/filters/:id
GET/api/v2/leads/filters/:id/matches/export?format=csv|xlsx|json&view=all|starred|excluded

Paramètres body (POST / PUT) — communs

ParamètreTypeDescription
name*stringNom du filtre (120 caractères max)
regionsstring[]Codes NUTS (ex. `CZ010`). Il est possible de mélanger n'importe quel niveau. Tableau vide = toutes les régions.
industryTagsstring[]Approche recommandée. Identifiants de tags issus de notre taxonomie (ex. 'stav_pozemni', 'it_vyvoj'). Multi-tag — OR entre les éléments.
categoriesstring[]Préfixes CPV de longueur quelconque (ex. '45', '4523', '45316110'). Moins précis qu'industryTags.
keywordsstring[]Mots-clés — correspondance LIKE dans le titre et la description du marché (OR entre les éléments).
minValuenumber | nullValeur estimée minimale (CZK). Les appels d'offres sans valeur sont acceptés.
maxValuenumber | nullValeur estimée maximale (CZK). Les appels d'offres sans valeur sont acceptés.
emailDigestbooleanDigest e-mail quotidien (par défaut true)
activebooleanFiltre actif (par défaut true)

Le webhook ne se configure plus au niveau du filtre — voir la section Webhook ci-dessous (points de terminaison au niveau du compte). Le champ webhookUrl est refusé pour des raisons de sécurité avec HTTP 410.

Exemples

Les mots-clés sont recherchés dans le titre et la description de l'appel d'offres (LIKE %kw%) dans la langue de publication du pouvoir adjudicateur (actuellement toujours en tchèque).

Travaux à Prague supérieurs à 500 000 CZK

curl -X POST -H "X-Api-Key: mrw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Travaux Prague 500k+",
    "regions": ["CZ010"],
    "industryTags": ["con_buildings", "con_trades"],
    "minValue": 500000
  }' \
  https://veritra.io/api/v2/leads/filters

Développement IT et licences logicielles (toutes les régions)

curl -X POST -H "X-Api-Key: mrw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Dév. IT + licences SW",
    "industryTags": ["it_development", "it_licensing", "it_data_ai"],
    "keywords": ["système d'information", "module"]
  }' \
  https://veritra.io/api/v2/leads/filters

Éclairage public (CZ) — combinaison de tags + mots-clés

curl -X POST -H "X-Api-Key: mrw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Éclairage public (CZ)",
    "industryTags": ["goods_electrical", "con_civil"],
    "keywords": ["éclairage", "EP ", "éclairage public", "lampadaires"],
    "minValue": 200000
  }' \
  https://veritra.io/api/v2/leads/filters

Désactiver le filtre

curl -X PATCH -H "X-Api-Key: mrw_live_…" \
  -H "Content-Type: application/json" \
  -d '{"active": false}' \
  https://veritra.io/api/v2/leads/filters/<id>

Distribution — webhook vs polling

Deux façons de recevoir les nouveaux marchés dans votre ERP. La plupart des intégrateurs combinent les deux.

Webhook (push)

Veritra envoie un POST à votre endpoint en ~2 secondes après le match. Événement leads.match.created avec le payload complet du marché.

Avantages : temps réel, aucun overhead de polling, contrôle côté serveur.

Inconvénients : nécessite un endpoint publicly accessible (HTTPS), vérification HMAC, gestion de l'idempotence.

Polling (pull)

Votre ERP appelle GET /api/v2/leads/matches?since=… périodiquement (par ex. toutes les 5 min). Retourne les correspondances depuis le timestamp indiqué.

Avantages : aucun endpoint public requis, implémentation simple, compatible redémarrage.

Inconvénients : latence de 5 à 10 min, contrainte de rate-limit (60 req/min API de gestion), réponses vides superflues.

Notre recommandation : webhook en priorité + polling quotidien (since=yesterday) comme filet de sécurité en cas d'échec de livraison du webhook au-delà du budget de retry.

Récupération des correspondances

GET/api/v2/leads/matches

Paramètres de requête

ParamètreTypeDescription
filterIdstringFiltrer selon un filtre spécifique
qTextstringTexte recherché (recherche plein texte dans le titre + la description).
sincestring (ISO 8601 datetime)Uniquement les correspondances à partir de cette date (datetime ISO)
deliveredbooleanfalse = non livrées uniquement, true = livrées uniquement
view"starred" | "excluded"Vue spéciale : starred (favoris) | excluded (masqués).
sort"newest" | "deadline" | "value"Tri : newest (par défaut), deadline (croissant), value (décroissant).
limitnumberNombre de résultats, max 1000 (défaut : 50). Pour >1k, utilisez la pagination nextCursor ou /matches/export.
cursorstringCurseur keyset de la page précédente (pagination.nextCursor).

Les correspondances retournées sont automatiquement marquées comme delivered=true. Chaque requête consomme 1 crédit.

Formats de matchId

Le matchId se présente sous deux formats selon l'origine :

  • cm… (préfixe cuid) — retourné depuis la table LeadMatch pré-calculée (tâche cron qui apparie les marchés chaque jour). Stable d'un appel à l'autre, utilisable pour mark-as-viewed.
  • live-12345 Préfixe synthetic pour un match détecté en direct via le mode recherche / navigation (paramètre qText, view=starred/excluded). Absent de la table LeadMatch → mark-as-viewed est sans effet. tenderId est le suffixe après le tiret.

Exemple

curl -H "X-Api-Key: mrw_leads_…" \
  "https://veritra.io/api/v2/leads/matches?delivered=false&limit=10"

Réponse

{
  "data": [
    {
      "matchId": "cm…",
      "filterId": "cm…",
      "filterName": "Stavby Praha",
      "matchedAt": "2026-04-03T06:00:00.000Z",
      "viewedAt": null,
      "delivered": true,
      "tender": {
        "id": 12345,
        "title": "Reconstruction du pont réf. 123",
        "estimatedValue": 12500000,
        "deadlineAt": "2026-05-15T22:00:00.000Z",
        "publishedAt": "2026-04-01T08:00:00.000Z",
        "firstSeenAt": "2026-04-01T08:30:00.000Z",
        "url": "https://nen.nipez.cz/...",
        "portalType": "NEN",
        "cpvCode": "45000000",
        "tenderType": "OFFERS",
        "contractingAuthority": {
          "ico": "12345678",
          "name": "Město Praha",
          "region": "Praha",
          "district": "Praha 1"
        },
        "documents": [
          { "name": "Výzva.pdf", "url": "https://nen.nipez.cz/…", "fileType": "pdf", "fileSizeBytes": 320000 }
        ],
        "starred": false,
        "excluded": false
      }
    }
  ],
  "pagination": { "nextCursor": "eyJmaXJzdFNlZW5BdC…", "totalCount": 42 }
}

Pour la page suivante, envoyez pagination.nextCursor en tant que paramètre ?cursor=.

Actions sur les matchs

Star (favori), exclude (masquer) et view (marquer comme lu) sont des préférences par appel d'offres stockées dans UserTenderPreference.

GET/api/v2/leads/matches/:matchId

Réponse : même structure que l'élément de la liste des matchs.

POST/api/v2/leads/matches/:matchId/star
{ "starred": true }
POST/api/v2/leads/matches/:matchId/exclude
{ "excluded": true }
POST/api/v2/leads/matches/:matchId/view

Pour les matchs live-:id synthétiques, il s'agit d'une opération sans effet (no-op) (aucune ligne à marquer).

GET/api/v2/leads/preferences
{
  "data": {
    "starred": [12345, 67890],
    "excluded": [54321]
  }
}

Taxonomie

Catalogues de référence statiques pour les valeurs de filtre. Libellés adaptés à la locale.

GET/api/v2/leads/taxonomy/industry?locale=cs
{
  "data": {
    "locale": "cs",
    "areas": [{ "id": "construction", "icon": "🏗️", "label": "Stavebnictví" }, …],
    "tags": [
      { "id": "con_buildings", "area": "construction", "label": "Pozemní stavby", "cpvPrefixes": ["452"] },
      …
    ]
  }
}
GET/api/v2/leads/taxonomy/cpv?locale=cs

Réponse : structure arborescente divisions → groupes → classes → catégories → sous-catégories.

Webhook

Les endpoints Webhook (CRUD, rotation du secret) sont au niveau du compte — documentés une seule fois dans API Compte → Webhooks. Seul le payload d'événement spécifique aux leads (leads.match.created) et la vérification HMAC sont décrits ici.

Livraison des événements

Lors d'un nouveau match, nous envoyons un événement de type leads.match.created avec une signature HMAC-SHA256 dans l'en-tête X-Signature-256, la clé d'idempotence dans X-Idempotency-Key et le type dans X-Event-Type. En cas d'échec, nouvelle tentative avec backoff exponentiel jusqu'à ~33 heures.

Politique de retry

Si votre endpoint retourne un code non-2xx (ou ne répond pas dans les 10 s), Veritra effectue des retries avec backoff exponentiel : 1 min, 5 min, 30 min, 2 h, 12 h, 24 h. Après 6 échecs, le webhook est marqué comme failed et une alerte s'affiche dans le tableau de bord. L'Idempotency-key reste identique pour chaque tentative — votre endpoint DOIT dédupliquer les requêtes (sans quoi la même correspondance sera traitée 6 fois).

Gestion des endpoints via l'API

Vous pouvez gérer les endpoints webhook sans passer par le tableau de bord. Limite de 5 endpoints par compte.

GET/api/v2/account/webhooks
POST/api/v2/account/webhooks
Le secret webhook ne s'affiche qu'une seule fois ! Lors d'un POST /webhooks, le champ 'secret' est retourné dans la réponse — enregistrez-le immédiatement dans votre environnement (par ex. VERITRA_WEBHOOK_SECRET). Il ne pourra pas être récupéré ultérieurement. En cas de perte, utilisez /rotate-secret pour en générer un nouveau (l'ancien est immédiatement invalidé).
GET/api/v2/account/webhooks/:id
PATCH/api/v2/account/webhooks/:id
DELETE/api/v2/account/webhooks/:id
POST/api/v2/account/webhooks/:id/rotate-secret
POST/api/v2/account/webhooks/:id/test
GET/api/v2/account/webhooks/:id/deliveries
POST/api/v2/account/webhooks/:id/deliveries/:deliveryId/replay

Vérification de signature HMAC

Le serveur signe le payload avec HMAC-SHA256(secret, raw_body) et l'envoie dans l'en-tête X-Signature-256 au format sha256=hex. Vérifiez en comparaison en temps constant, sans quoi l'intégration est vulnérable à une attaque par timing.

// Node.js (Express)
import crypto from "node:crypto";

const WEBHOOK_SECRET = process.env.MRICKWOOD_WEBHOOK_SECRET!;

function verify(rawBody: string, sig: string | undefined): boolean {
  if (!sig) return false;
  const expected = "sha256=" + crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(rawBody)
    .digest("hex");
  // Constant-time comparison (timing-safe)
  const a = Buffer.from(sig);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhooks/veritra",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const raw = req.body.toString("utf8");
    if (!verify(raw, req.header("X-Signature-256"))) {
      return res.status(401).send("Invalid signature");
    }
    const idempKey = req.header("X-Idempotency-Key")!;
    const evt = JSON.parse(raw);
    // Idempotency: store idempKey, skip if already processed
    if (await alreadyProcessed(idempKey)) return res.status(200).send("ok");
    await processEvent(evt);
    await markProcessed(idempKey);
    res.status(200).send("ok"); // Must be 2xx within 10s
  },
);
# Python (Flask)
import hmac, hashlib, os
from flask import Flask, request, abort

WEBHOOK_SECRET = os.environ["MRICKWOOD_WEBHOOK_SECRET"]

def verify(raw: bytes, sig: str | None) -> bool:
    if not sig: return False
    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(), raw, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(sig, expected)

@app.post("/webhooks/mrickwood")
def webhook():
    raw = request.get_data()
    if not verify(raw, request.headers.get("X-Signature-256")):
        abort(401)
    # ... idempotency check + process
    return "ok", 200
{
  "id": "evt_…",
  "type": "leads.match.created",
  "createdAt": "2026-04-03T06:00:00.000Z",
  "data": {
    "filterId": "cm…",
    "filterName": "Stavby Praha",
    "matchId": "cm…",
    "tender": { "id": "12345", "title": "…", "estimatedValue": 12500000 }
  }
}

Digest e-mail

Avec emailDigest: true, vous recevrez un e-mail quotidien récapitulant les nouveaux appels d'offres. Le digest est envoyé le matin (5h00 UTC) à l'adresse e-mail de votre compte. Il peut être désactivé en modifiant le filtre.

Limites

ParamètreTypeDescription
Essai500 req/mois1 jour gratuit. Sans carte / profil de facturation. ApiKey.requestsLimit par défaut.
Plan payantillimitéLa limite mensuelle est désactivée. Seule la limite technique de 100/h/clé s'applique.
Filtres20Nombre maximum de filtres actifs par compte.
Endpoints Webhook5Nombre maximum d'URL webhook actives par compte.

En cas de dépassement de la limite mensuelle, l'API renvoie 429 avec l'en-tête Retry-After. La limite se réinitialise le 1er du mois.