Novità

Monitoraggio gare d'appalto

Consegna automatica delle nuove gare d'appalto in base ai vostri filtri — regione, tag di settore, parole chiave, intervallo di valore. Pull API + digest email + webhook.

Concetti

Quattro termini che si incontrano in tutti gli endpoint:

  • Tenderuna specifica gara d'appalto proveniente dal portale (NEN, VVZ, E-ZAK, TenderArena, ecc.). Identificata da un ID numerico.
  • Filterinsieme salvato di criteri (regione, settore, parole chiave, valore). Quando una nuova gara soddisfa i criteri, viene generato un match.
  • Matchnuova gara d'appalto rientrata nel tuo filtro. Rappresenta l'associazione (tender × filter) + timestamp + il tuo stato (monitorata / nascosta / letta).
  • Taxonomynomenclatori di regioni (NUTS), settori (industry tag) e codici CPV, in base ai quali classifichiamo le gare d'appalto.

Avvio rapido

I filtri possono essere configurati in due modi — il risultato è lo stesso e il filtro può essere modificato in qualsiasi momento con qualsiasi metodo.

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

In alto è disponibile la ricerca ad-hoc. Se si desidera un monitoraggio continuativo (nuove gare corrispondenti ai propri criteri), utilizzare i filtri + webhook/email digest — vedere di seguito.

Raccomandazione: Per il filtraggio, preferire sempre industryTags rispetto a categories (CPV). I nostri industryTags sono il risultato della combinazione della classificazione LLM della gara e della mappatura CPV — resistenti a codici CPV errati. Le stazioni appaltanti in CZ assegnano spesso codici CPV troppo generici o non pertinenti (tipicamente il generico '45000000-0' per gli appalti di costruzione anziché un prefisso specifico), per cui un filtro basato esclusivamente sul CPV escluderebbe una parte significativa delle gare rilevanti.

Ricerca di gare d'appalto (ad-hoc)

Per la ricerca una tantum di gare d'appalto su tutte le gare attive. Non utilizza un filtro salvato; i parametri vengono passati direttamente nella query string.

GET/api/v2/leads/tenders/search

Parametri di query

ParametroTipoDescrizione
qText*stringTesto ricercato (full-text su titolo + descrizione).
regionsstringCSV di codici NUTS foglia (CZ010,CZ020).
cpvPrefixesstringCSV di prefissi CPV (45,452).
industryTagsstringCSV di ID tag di settore (con_buildings,it_development).
minValuenumberValore stimato minimo (CZK). Le gare prive di valore vengono comunque elaborate.
maxValuenumberValore stimato massimo (CZK). Le gare prive di valore vengono comunque elaborate.
deadlineFromstring (YYYY-MM-DD)Termine di presentazione >= YYYY-MM-DD.
deadlineTostring (YYYY-MM-DD)Termine di presentazione <= YYYY-MM-DD.
sort"newest" | "deadline" | "value"Ordinamento: newest (predefinito), deadline (crescente), value (decrescente).
limitnumberNumero di risultati, max 1000 (predefinito: 50). Per >1k utilizza la paginazione nextCursor o /matches/export.
cursorstringCursore keyset dalla pagina precedente (pagination.nextCursor).

Esempio

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

Response: oggetto data con i campi id, title, description, estimatedValue, deadlineAt, contractingAuthority, documents[], starred, excluded.

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

Il body può contenere recipientEmail per l'inoltro a un altro indirizzo e-mail. Di default viene utilizzato user.email.

GET/api/v2/leads/documents/preview

Query: ?url=:original&kind=docx|xlsx. Allowlist degli host: 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)

Catalog & autocomplete (pubblico, nessuna chiave)

Per i dropdown dell'interfaccia utente e la creazione di filtri. Cache server-side di 1h. Pubblico, nessuna autenticazione necessaria.

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
Search vs. Filter: Search è una query una tantum che restituisce uno snapshot corrente delle gare. Filter (di seguito) è un insieme di criteri salvati — il server notifica continuativamente le nuove gare tramite webhook o email digest.

Cosa si può filtrare e valori consentiti

Il filtro è composto dai campi seguenti. Tutti sono facoltativi ad eccezione di `name`. Campo vuoto = nessuna restrizione nella dimensione corrispondente.

regions string[]

Regioni NUTS per il paese specificato (?country=CZ) con etichette per lingua.

Il filtro è pan-UE: I valori di `regions` sono codici NUTS di qualsiasi paese UE dall'elenco riportato di seguito. Il filtro restituirà solo le regioni coperte dal proprio abbonamento LEADS attivo (subscription scope). Se si dispone di un abbonamento limitato a CZ + SK, i codici NUTS DE verranno ignorati dal filtro.
Paesi supportati (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

Per ottenere l'albero NUTS completo di un paese specifico, utilizzare:

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
Esempio: codici NUTS-3 per CZ (regioni) — espandi ▸
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[] Consigliato

Classificazione settoriale multi-tag. Una gara può avere più tag (es. 'stav_pozemni' + 'stav_remesla' per una ristrutturazione). Il filtro seleziona la gara se presenta almeno uno dei tag specificati (JSON_OVERLAPS).

Perché preferire industryTags rispetto al CPV: I nostri industryTags sono generati dalla combinazione della classificazione LLM del titolo/descrizione della gara + mappatura CPV + hint regex — intercettano le gare rilevanti anche per le stazioni appaltanti che assegnano i codici CPV in modo approssimativo. Il filtro CPV è preciso, ma dipende dalla disciplina della stazione appaltante, che in Italia è spesso carente.

48 tag in 13 aree
🏗️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
Il vostro tag non è presente? Se non trovate il tag per il vostro settore, scriveteci a michal@veritra.io — lo aggiungeremo.

categories string[] Meno preciso

Prefissi CPV (Common Procurement Vocabulary) di lunghezza arbitraria — corrispondenza tramite `LIKE 'prefix%'`. Ovvero '45' intercetta tutte le divisioni edili, '4523' solo le costruzioni di ingegneria civile, '45316110' solo l'illuminazione stradale.

35 divisioni CPV più frequenti (a 2 cifre)
03Agricoltura, silvicoltura e pesca
09Combustibili ed energia
15Alimenti e bevande
18Abbigliamento, calzature, DPI
22Stampati e libri
30Macchine per ufficio e apparecchiature IT
31Macchine elettriche, cavi e apparecchi di illuminazione
32Radio, TV e telecomunicazioni
33Attrezzature medicali e farmaci
34Veicoli e trasporti
35Sicurezza, antincendio e tecnologia militare
38Strumenti di laboratorio e di misura
39Mobili e arredamento per interni
42Macchine industriali
44Materiali e strutture edili
45Lavori edili
48Software e licenze
50Riparazioni e manutenzione
55Ristorazione e alloggio
60Trasporti
64Servizi postali e di telecomunicazione
65Servizi di pubblica utilità (elettricità, acqua)
66Servizi finanziari e assicurativi
70Servizi immobiliari e gestione edifici
71Servizi architettonici, progettuali e ingegneristici
72Servizi IT (sviluppo, integrazione, supporto)
73Ricerca e sviluppo
75Pubblica amministrazione e difesa
77Servizi agricoli, forestali e di giardinaggio
79Servizi professionali (legale, contabilità, HR, marketing)
80Istruzione e formazione
85Assistenza sanitaria e sociale
90Rifiuti, ambiente e pulizie
92Cultura, sport e tempo libero
98Altri servizi alla persona

Il catalogo completo (9.454 codici) è disponibile su /docs/leads/cpv — ricercabile e raggruppato per divisioni.

keywords string[]

Corrispondenza LIKE nel titolo e nella descrizione della gara. Case-insensitive. Tra le voci OR. Utile come rete di sicurezza qualora industryTags/CPV non intercettino tutto (es. una tipologia specifica di appalto presente nel testo).

"keywords": ["ricostruzione", "VO", "illuminazione", "scuola materna"]

minValue / maxValue number | null

Intervallo del valore stimato dell'appalto in CZK. È possibile specificare solo minValue, solo maxValue o entrambi.

Importante: Le gare senza valore stimato compilato (estimatedValue è null o 0) superano il filtro in entrambe le direzioni. Molte stazioni appaltanti non indicano il valore — escluderle comporterebbe la perdita di gare rilevanti.

emailDigest boolean

Se impostato su true, si riceve 1 volta al giorno (ore 5:00 UTC) un'e-mail riepilogativa con le nuove corrispondenze di questo filtro. Valore predefinito: true. Può essere disattivato modificando il filtro. Il webhook si configura separatamente, a livello di account (vedi sezione seguente).

name string · active boolean

`name` — max 120 caratteri, obbligatorio. `active` — se false, il filtro non viene eseguito nel cron (nessuna nuova corrispondenza, digest, webhook). Può essere sospeso senza eliminazione.

Logica di filtraggio

I campi vengono combinati come segue:

MATCH = (stazione_appaltante.NUTS3 ∈ expand(regions))
     AND (minValue ≤ estimatedValue ≤ maxValue  OPPURE  estimatedValue è null o 0)
     AND (industry_or_cpv  OPPURE  keyword_match)

# expand(regions): i codici NUTS vengono espansi fino alle foglie NUTS 3
# (CZ → 14 regioni, CZ01 → CZ010, CZ010 → CZ010).

industry_or_cpv:
   se  industryTags specificati  →  JSON_OVERLAPS(industryTags, tender.industryTags)
   altrimenti categories specificati  →  tender.cpvCode LIKE ANY (categories + "%")
   altrimenti                         →  false   (nessun filtro settoriale applicato)

keyword_match:
   se  keywords specificati      →  tender.title o description LIKE ANY (%kw%)
   altrimenti                    →  false

# Se non vengono specificati industryTags / categories / keywords,
# vengono restituite tutte le gare che soddisfano regions e l'intervallo di valore.

industryTags ha la precedenza su categories — se vengono specificati entrambi, viene utilizzato solo industryTags (categories viene ignorato). keywords funziona in modo indipendente (si trova nel ramo OR insieme a industry_or_cpv).

Endpoint per la gestione dei filtri

Per la gestione dei filtri, utilizzare la chiave di gestione (mrw_live_…). Dispone di limiti di frequenza separati e non consuma crediti LEADS, pertanto la gestione dei filtri non influisce sul recupero giornaliero degli appalti.

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

Parametri body (POST / PUT) — comuni

ParametroTipoDescrizione
name*stringNome del filtro (max 120 caratteri)
regionsstring[]Codici NUTS (es. `CZ010`). È possibile combinare qualsiasi livello. Campo vuoto = tutte le regioni.
industryTagsstring[]Percorso consigliato. ID tag dalla nostra tassonomia (es. 'stav_pozemni', 'it_vyvoj'). Multi-tag — OR tra gli elementi.
categoriesstring[]Prefissi CPV di lunghezza arbitraria (es. '45', '4523', '45316110'). Meno precisi rispetto agli industryTags.
keywordsstring[]Parole chiave — corrispondenza LIKE nel titolo e nella descrizione della gara (OR tra gli elementi).
minValuenumber | nullValore stimato minimo (CZK). Le gare prive di valore vengono comunque elaborate.
maxValuenumber | nullValore stimato massimo (CZK). Le gare prive di valore vengono comunque elaborate.
emailDigestbooleanDigest e-mail giornaliero (default true)
activebooleanFiltro attivo (default true)

Il webhook non si configura più sul filtro — vedere la sezione Webhook di seguito (endpoint a livello di account). Campo webhookUrl viene rifiutato per motivi di sicurezza con HTTP 410.

Esempi

Le parole chiave effettuano la ricerca nel titolo e nella descrizione della gara d'appalto (LIKE %kw%) nella lingua in cui la stazione appaltante pubblica (attualmente sempre in ceco).

Costruzioni a Praga superiori a 500 mila CZK

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

Sviluppo IT e licenze SW (tutte le regioni)

curl -X POST -H "X-Api-Key: mrw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sviluppo IT + licenze SW",
    "industryTags": ["it_development", "it_licensing", "it_data_ai"],
    "keywords": ["sistema informativo", "modulo"]
  }' \
  https://veritra.io/api/v2/leads/filters

Illuminazione pubblica (CZ) — combinazione di tag e parole chiave

curl -X POST -H "X-Api-Key: mrw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Illuminazione pubblica (CZ)",
    "industryTags": ["goods_electrical", "con_civil"],
    "keywords": ["illuminazione", "IP ", "illuminazione pubblica", "lampioni"],
    "minValue": 200000
  }' \
  https://veritra.io/api/v2/leads/filters

Disattiva filtro

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>

Consegna — webhook vs polling

Due modalità per ricevere nuove gare nel proprio ERP. La maggior parte degli integratori le combina entrambe.

Webhook (push)

Veritra invia una POST al tuo endpoint entro ~2 secondi dal match. Evento leads.match.created con il payload completo della gara.

Vantaggi: tempo reale, nessun overhead di polling, gating lato server.

Svantaggi: richiede un endpoint pubblicamente accessibile (HTTPS), verifica HMAC, gestione dell'idempotenza.

Polling (pull)

Il tuo ERP chiama GET /api/v2/leads/matches?since=… periodicamente (es. ogni 5 min). Restituisce i match a partire dal timestamp indicato.

Vantaggi: nessun endpoint pubblico, implementazione semplice, compatibile con i riavvii.

Svantaggi: latenza 5-10 min, vincolo di rate limit (60 req/min mgmt API), risposte vuote superflue.

Consiglio: webhook come canale primario + polling giornaliero (since=yesterday) come rete di sicurezza in caso di mancata consegna del webhook entro il budget di retry.

Recupero dei match

GET/api/v2/leads/matches

Parametri di query

ParametroTipoDescrizione
filterIdstringFiltra per filtro specifico
qTextstringTesto ricercato (full-text su titolo + descrizione).
sincestring (ISO 8601 datetime)Solo match a partire dalla data (ISO datetime)
deliveredbooleanfalse = solo non consegnati, true = solo consegnati
view"starred" | "excluded"Vista speciale: starred (preferiti) | excluded (nascosti).
sort"newest" | "deadline" | "value"Ordinamento: newest (predefinito), deadline (crescente), value (decrescente).
limitnumberNumero di risultati, max 1000 (predefinito: 50). Per >1k utilizza la paginazione nextCursor o /matches/export.
cursorstringCursore keyset dalla pagina precedente (pagination.nextCursor).

I match restituiti vengono contrassegnati automaticamente come delivered=true. Ogni richiesta consuma 1 credito.

Formati di matchId

matchId ha due formati a seconda dell'origine:

  • cm… (prefisso cuid) — restituito dalla tabella LeadMatch pre-calcolata (cron job che abbina le gare ogni giorno). Stabile tra le chiamate, utilizzabile per mark-as-viewed.
  • live-12345 prefisso synthetic per match rilevato in tempo reale tramite search / browse mode (parametro qText, view=starred/excluded). Non presente nella tabella LeadMatch → mark-as-viewed è no-op. tenderId è il suffisso dopo il trattino.

Esempio

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

Risposta

{
  "data": [
    {
      "matchId": "cm…",
      "filterId": "cm…",
      "filterName": "Costruzioni Praga",
      "matchedAt": "2026-04-03T06:00:00.000Z",
      "viewedAt": null,
      "delivered": true,
      "tender": {
        "id": 12345,
        "title": "Ricostruzione del ponte n. 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 }
}

Per la pagina successiva, inviare pagination.nextCursor come parametro ?cursor=.

Azioni sui match

Star (preferito), exclude (nascondi) e view (segna come letto) sono preferenze per-tender salvate in UserTenderPreference.

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

Response: stessa struttura dell'elemento della lista matches.

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

Per i match sintetici live-:id è un no-op (nessuna riga da contrassegnare).

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

Tassonomia

Cataloghi di riferimento statici per i valori dei filtri. Etichette localizzate.

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

Risposta: struttura ad albero divisioni → gruppi → classi → categorie → sottocategorie.

Webhook

Gli endpoint webhook (CRUD, rotazione del secret) sono a livello di account — documentati una sola volta in Account API → Webhook. Qui è descritto solo il payload dell'evento specifico per i lead (leads.match.created) e la verifica HMAC.

Consegna degli eventi

Ad ogni nuovo match inviamo un evento di tipo leads.match.created con firma HMAC-SHA256 nell'intestazione X-Signature-256, chiave di idempotenza in X-Idempotency-Key e tipo in X-Event-Type. In caso di errore, nuovo tentativo con backoff esponenziale fino a ~33 ore.

Policy di retry

Se il tuo endpoint restituisce un codice non-2xx (o non risponde entro 10s), Veritra riprova con exponential backoff: 1min, 5min, 30min, 2h, 12h, 24h. Dopo 6 fallimenti, il webhook viene contrassegnato come failed e lampeggia nella dashboard. L'Idempotency-key è la stessa per ogni tentativo — il tuo endpoint DEVE deduplicare (altrimenti lo stesso match viene elaborato 6 volte).

Gestione degli endpoint tramite API

È possibile gestire gli endpoint webhook senza accedere alla dashboard. Limite di 5 endpoint per account.

GET/api/v2/account/webhooks
POST/api/v2/account/webhooks
Il secret del webhook viene visualizzato una sola volta! Alla chiamata POST /webhooks riceverai nel campo 'secret' della response — salvalo immediatamente nell'ambiente (es. VERITRA_WEBHOOK_SECRET). Non è possibile recuperarlo in seguito. In caso di smarrimento, utilizza /rotate-secret per generarne uno nuovo (il precedente viene invalidato immediatamente).
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

Verifica della firma HMAC

Il server firma il payload con HMAC-SHA256(secret, raw_body) e lo invia nell'intestazione X-Signature-256 nel formato sha256=hex. Verificare con un confronto in constant-time, altrimenti l'integrazione è vulnerabile a timing attack.

// 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": "Costruzioni Praga",
    "matchId": "cm…",
    "tender": { "id": "12345", "title": "…", "estimatedValue": 12500000 }
  }
}

Email digest

Con emailDigest: true riceverete un'email giornaliera con il riepilogo delle nuove gare d'appalto. Il digest viene inviato al mattino (5:00 UTC) all'indirizzo email del vostro account. Può essere disattivato modificando il filtro.

Limiti

ParametroTipoDescrizione
Trial500 req/mese1 giorno gratuito. Senza carta di credito / profilo di fatturazione. Default ApiKey.requestsLimit.
Piano a pagamentoillimitatoIl limite mensile è disabilitato. Si applica solo il rate limit tecnico di 100/h/chiave.
Filtri20Numero massimo di filtri attivi per account.
Endpoint webhook5Numero massimo di URL webhook attivi per account.

Al superamento del limite mensile, l'API restituisce 429 con l'intestazione Retry-After. Il limite viene azzerato il 1° giorno del mese.