Neu

Ausschreibungsmonitor

Automatische Lieferung neuer Ausschreibungen nach Ihren Filtern — Region, Branchen-Tags, Schlagworte, Wertbereich. Pull-API + E-Mail-Digest + Webhook.

Konzepte

Vier Begriffe, die in allen Endpoints vorkommen:

  • Tendereine konkrete Ausschreibung von einem Portal (NEN, VVZ, E-ZAK, TenderArena, etc.). Mit numerischer ID identifiziert.
  • Filtergespeicherte Kriterien-Set (Region, Branche, Schlüsselwörter, Wert). Wenn eine neue Ausschreibung die Kriterien erfüllt, entsteht ein Match.
  • Matchneue Ausschreibung, die in Ihren Filter gefallen ist. Repräsentiert die Verknüpfung (tender × filter) + Timestamp + Ihren Status (markiert / ausgeblendet / gesehen).
  • Taxonomykontrollierte Vokabulare für Regionen (NUTS), Branchen (Industry-Tags) und CPV-Codes zur Klassifikation der Ausschreibungen.

Schnellstart

Filter können auf zwei Wegen angelegt werden — das Ergebnis ist identisch und ein Filter lässt sich jederzeit über beide Wege bearbeiten.

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

Oben ist eine Ad-hoc-Suche. Für kontinuierliches Monitoring (neue Ausschreibungen passend zu Ihren Kriterien) nutzen Sie Filter + Webhook/E-Mail-Digest — siehe unten.

Empfehlung: Bevorzugen Sie für die Filterung stets industryTags gegenüber categories (CPV). Unsere industryTags sind eine Vereinigung aus LLM-Klassifikation der Ausschreibung und CPV-Mapping — robust gegenüber falschen CPV-Codes. Tschechische Auftraggeber vergeben CPV-Codes häufig zu generisch oder unpassend (typisch das generische '45000000-0' für Bau statt eines spezifischen Präfixes), sodass ein reiner CPV-Filter einen großen Teil relevanter Ausschreibungen verpasst.

Ausschreibungssuche (Ad-hoc)

Für einmalige Suchen über alle aktiven Ausschreibungen. Nutzt keinen gespeicherten Filter — Parameter gehen direkt in den Query-String.

GET/api/v2/leads/tenders/search

Query-Parameter

ParameterTypBeschreibung
qText*stringSuchtext (Volltext in title + description).
regionsstringNUTS-Leaf-Codes CSV (CZ010,CZ020).
cpvPrefixesstringCPV-Prefixes CSV (45,452).
industryTagsstringIndustry-Tag-IDs CSV (con_buildings,it_development).
minValuenumberMin. geschätzter Wert (CZK). Ausschreibungen ohne Wertangabe gehen durch.
maxValuenumberMax. geschätzter Wert (CZK). Ausschreibungen ohne Wertangabe gehen durch.
deadlineFromstring (YYYY-MM-DD)Abgabefrist >= YYYY-MM-DD.
deadlineTostring (YYYY-MM-DD)Abgabefrist <= YYYY-MM-DD.
sort"newest" | "deadline" | "value"Sortierung: newest (Default), deadline (aufsteigend), value (absteigend).
limitnumberErgebnisanzahl, max. 1000 (Standard: 50). Für >1k Ergebnisse nextCursor-Paginierung oder /matches/export verwenden.
cursorstringKeyset-Cursor aus vorheriger Seite (pagination.nextCursor).

Beispiel

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: { data: { id, title, description, estimatedValue, deadlineAt, contractingAuthority, documents[], starred, excluded } }.

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

Body kann recipientEmail enthalten für Weiterleitung an andere E-Mail. Default user.email.

GET/api/v2/leads/documents/preview

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

Katalog & Autocomplete (öffentlich, ohne Schlüssel)

Für UI-Dropdowns und Filter-Erstellung. Server-Cache 1h. Öffentlich, keine Auth erforderlich.

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
Suche vs. Filter: Search ist eine einmalige Abfrage, gibt den aktuellen Snapshot zurück. Ein Filter (unten) ist gespeichertes Kriterien-Set — der Server benachrichtigt kontinuierlich über neue Ausschreibungen per Webhook oder E-Mail-Digest.

Filter-Felder und erlaubte Werte

Ein Filter besteht aus den folgenden Feldern. Alle sind optional außer `name`. Ein leeres Feld bedeutet keine Einschränkung in dieser Dimension.

regions string[]

NUTS-Regionen für das gegebene Land (?country=CZ) mit Locale-Labels.

Filter ist EU-weit: Die `regions`-Werte sind NUTS-Codes aus jedem unten gelisteten EU-Land. Der Filter liefert nur Regionen, für die Sie ein aktives LEADS-Abo haben (subscription scope). Wenn Sie nur CZ + SK abonniert haben, werden DE-NUTS-Codes vom Filter ignoriert.
Unterstützte Länder (NUTS-0)
CZTschechien
SKSlowakei
PLPolen
DEDeutschland
ATÖsterreich
FRFrankreich
ESSpanien
ITItalien
NLNiederlande
BEBelgien
PTPortugal
SESchweden
FIFinnland
DKDänemark
NONorwegen
IEIrland
GRGriechenland
RORumänien
BGBulgarien
HUUngarn
HRKroatien
SISlowenien
LTLitauen
LVLettland
EEEstland
LULuxemburg
CYZypern
MTMalta
CHSchweiz
ISIsland
MKNordmazedonien

Für den vollen NUTS-Baum eines konkreten Landes:

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
Beispiel: NUTS-3-Codes für CZ (Regionen) — aufklappen ▸
CZ010Hauptstadt Prag
CZ020Mittelböhmische Region
CZ031Südböhmische Region
CZ032Pilsner Region
CZ041Karlsbader Region
CZ042Aussiger Region
CZ051Reichenberger Region
CZ052Königgrätzer Region
CZ053Pardubitzer Region
CZ063Region Vysočina
CZ064Südmährische Region
CZ071Olmützer Region
CZ072Zliner Region
CZ080Mährisch-Schlesische Region

industryTags string[] Empfohlen

Multi-Tag-Branchenklassifikation. Eine Ausschreibung kann mehrere Tags haben (z.B. 'stav_pozemni' + 'stav_remesla' bei einer Sanierung). Der Filter trifft eine Ausschreibung, wenn mindestens einer der gewünschten Tags gesetzt ist (JSON_OVERLAPS).

Warum industryTags besser sind als CPV: Unsere industryTags kombinieren LLM-Klassifikation des Titels/Beschreibung der Ausschreibung mit CPV-Mapping und Regex-Hinweisen — sie erfassen relevante Ausschreibungen auch bei Auftraggebern, die CPV-Codes nachlässig vergeben. Ein reiner CPV-Filter ist exakt, hängt aber von der Disziplin der Auftraggeber ab, die in CZ schwach ist.

48 Tags in 13 Bereichen
🏗️Bauwesen
con_buildingsHochbau und Sanierung
con_civilTiefbau (Straßen, Brücken, Wasser)
con_tradesBaugewerke und Teilarbeiten
con_energy_efficiencyEnergieeinsparung & erneuerbare Energien
con_materialsBaumaterialien
📐Planung & Bauüberwachung
des_documentationPlanungsdokumentation und Studien
des_supervision_ohsBauüberwachung und Arbeitsschutz
des_surveyingVermessung und Flurneuordnung
💻IT & Software
it_developmentSoftwareentwicklung und Integration
it_licensingSoftwarelizenzen und Abonnements
it_hardwareHardware und Infrastruktur
it_cybersecurityCybersicherheit
it_data_aiDaten, Analytik, KI/ML
📡Telekommunikation
telecom_internetTelekom, Internet, Mobilfunk
🧑‍💼Professionelle Dienstleistungen
prof_marketingMarketing, PR, Werbung
prof_legalRechtsdienstleistungen
prof_accountingBuchhaltung, Förderungen, Audit
prof_hrHR, Personalbeschaffung
prof_translationÜbersetzung und Dolmetschen
prof_insuranceVersicherung und Finanzen
🛡️Betrieb & Wartung
ops_cleaningReinigungsdienste
ops_securitySicherheit und Empfang
ops_maintenanceWartung und Service von Anlagen
ops_wasteAbfallwirtschaft
ops_facilityImmobilienverwaltung
🍽️Verpflegung & Unterkunft
cat_cateringVerpflegung, Catering
cat_accommodationUnterkunft und Konferenzen
cat_foodLebensmittel und Getränke
🚚Transport & Fahrzeuge
trans_transportPersonen- und Güterverkehr
trans_vehiclesFahrzeuge, Teile, Leasing
🏥Gesundheitswesen
health_pharmaArzneimittel und Pharma
health_devicesMedizingeräte und -material
health_careGesundheits- und Sozialwesen
📦Waren & Ausstattung
goods_furnitureMöbel und Innenausstattung
goods_clothingBekleidung, PSA, Uniformen
goods_electricalElektromaterial
goods_machineryIndustriemaschinen
Energie & Wasser
energy_fuelsKraftstoffe und Brennstoffe
energy_power_heatStrom und Wärme
energy_waterWasserversorgung
🌳Natur, Forst, Sicherheit
nat_forestryForstwirtschaft
nat_greeneryGrünpflege und Gartenbau
nat_agricultureLandwirtschaft
defense_safetyFeuerwehr, Militär, Verteidigung
🔬Wissenschaft & Bildung
sci_labLabor- und Messgeräte
sci_researchForschung und Entwicklung
edu_trainingBildung und Schulung
culture_mediaKultur, Bücher, Medien
Tag fehlt? Falls Sie keinen Tag für Ihre Branche finden, schreiben Sie an michal@veritra.io — wir fügen ihn hinzu.

categories string[] Weniger genau

CPV-Präfixe (Common Procurement Vocabulary) beliebiger Länge — Match über `LIKE 'präfix%'`. Also '45' erfasst alle Bau-Hauptgruppen, '4523' nur Tiefbau, '45316110' nur Straßenbeleuchtung.

35 häufigste CPV-Hauptgruppen (2-stellig)
03Landwirtschaft, Forstwirtschaft, Fischerei
09Brennstoffe und Energie
15Lebensmittel und Getränke
18Kleidung, Schuhe, PSA
22Druckerzeugnisse, Bücher
30Büromaschinen, IT-Ausrüstung
31Elektrische Maschinen, Kabel, Beleuchtung
32Radio, TV, Telekommunikation
33Medizintechnik, Arzneimittel
34Fahrzeuge, Transport
35Sicherheit, Feuerwehr- und Militärtechnik
38Labor- und Messgeräte
39Möbel, Innenausstattung
42Industriemaschinen
44Baumaterialien und Konstruktionen
45Bauarbeiten
48Software und Lizenzen
50Reparatur und Wartung
55Gastronomie, Unterkunft
60Transport (Beförderung)
64Post, Telekommunikationsdienste
65Versorgungsleistungen (Strom, Wasser)
66Finanz- und Versicherungsdienste
70Immobilien und Facility Management
71Architektur, Planung, Ingenieurdienste
72IT-Dienste (Entwicklung, Integration, Support)
73Forschung und Entwicklung
75Öffentliche Verwaltung, Verteidigung
77Land-, forst- und gartenbauliche Dienste
79Unternehmensdienste (Recht, Buchhaltung, HR, Marketing)
80Bildung und Schulung
85Gesundheits- und Sozialfürsorge
90Abfall, Umwelt, Reinigung
92Kultur, Sport, Freizeit
98Sonstige Dienste für die Öffentlichkeit

Vollständiger Katalog (9.454 Codes) verfügbar unter /docs/leads/cpv — durchsuchbar und nach Abteilungen gruppiert.

keywords string[]

LIKE-Match in Titel und Beschreibung der Ausschreibung. Case-insensitive. OR zwischen Einträgen. Sinnvoll als Sicherheitsnetz, falls industryTags/CPV nicht alles erfasst (z.B. spezifischer Ausschreibungstyp, der nur im Text auftaucht).

"keywords": ["Sanierung", "Beleuchtung", "Kindergarten"]

minValue / maxValue number | null

Geschätzter Wertbereich der Ausschreibung in CZK. Sie können nur minValue, nur maxValue oder beides setzen.

Wichtig: Ausschreibungen ohne angegebenen geschätzten Wert (estimatedValue ist null oder 0) durchlaufen den Filter in beide Richtungen. Viele Auftraggeber geben keinen Wert an — würde man sie abschneiden, gingen Ihnen diese Ausschreibungen verloren.

emailDigest boolean

Wenn true, erhalten Sie 1× täglich (5:00 UTC) eine E-Mail mit den neuen Treffern dieses Filters. Standard true. Deaktivierung durch Bearbeiten des Filters. Webhooks werden separat auf Konto-Ebene konfiguriert (siehe unten).

name string · active boolean

`name` — max. 120 Zeichen, Pflichtfeld. `active` — wenn false, wird der Filter aus dem Cron ausgenommen (keine neuen Treffer, kein Digest, kein Webhook). Pausieren ohne Löschen.

Filterlogik

Felder kombinieren wie folgt:

MATCH = (Auftraggeber.NUTS3 ∈ expand(regions))
     AND (minValue ≤ estimatedValue ≤ maxValue  ODER  estimatedValue ist null oder 0)
     AND (industry_or_cpv  ODER  keyword_match)

# expand(regions): NUTS-Codes werden auf NUTS-3-Blätter erweitert
# (CZ → 14 Regionen, CZ01 → CZ010, CZ010 → CZ010).

industry_or_cpv:
   wenn  industryTags gesetzt  →  JSON_OVERLAPS(industryTags, tender.industryTags)
   sonst categories gesetzt    →  tender.cpvCode LIKE ANY (categories + "%")
   sonst                       →  false   (kein Industry-Filter angewendet)

keyword_match:
   wenn  keywords gesetzt      →  tender.title oder description LIKE ANY (%kw%)
   sonst                       →  false

# Wenn Sie keine industryTags / categories / keywords angeben,
# werden alle Ausschreibungen zurückgegeben, die regions und value range erfüllen.

industryTags hat Vorrang vor categories — wenn Sie beide angeben, wird nur industryTags verwendet (categories wird ignoriert). keywords funktionieren unabhängig (sie sind im OR-Zweig mit industry_or_cpv).

Endpunkte zur Filterverwaltung

Verwenden Sie für die Filterverwaltung den Management-Key (mrw_live_…). Er hat eigene Rate-Limits und verbraucht keine LEADS-Credits, sodass die Filterverwaltung Ihren täglichen Lead-Abruf nicht beeinflusst.

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

Body-Parameter (POST / PUT) — gemeinsam

ParameterTypBeschreibung
name*stringFiltername (max. 120 Zeichen)
regionsstring[]NUTS-Codes (z. B. `CZ010`). Beliebige Ebenen mischbar. Leeres Array = alle Regionen.
industryTagsstring[]Empfohlener Weg. Tag-IDs aus unserer Taxonomie (z.B. 'stav_pozemni', 'it_vyvoj'). Multi-Tag — OR zwischen Einträgen.
categoriesstring[]CPV-Präfixe beliebiger Länge (z.B. '45', '4523', '45316110'). Weniger genau als industryTags.
keywordsstring[]Schlagworte — LIKE-Match in Titel und Beschreibung (OR zwischen Einträgen).
minValuenumber | nullMin. geschätzter Wert (CZK). Ausschreibungen ohne Wertangabe gehen durch.
maxValuenumber | nullMax. geschätzter Wert (CZK). Ausschreibungen ohne Wertangabe gehen durch.
emailDigestbooleanTäglicher E-Mail-Digest (Standard: true)
activebooleanAktiver Filter (Standard: true)

Webhooks werden nicht mehr pro Filter konfiguriert — siehe Abschnitt Webhook unten (Konto-Endpunkte). Das Feld webhookUrl wird aus Sicherheitsgründen mit HTTP 410 abgelehnt.

Beispiele

Die folgenden Beispiele sind zur besseren Lesbarkeit auf Deutsch. Im Produktivbetrieb werden Keywords (LIKE %kw%) im Titel und der Beschreibung der Ausschreibung in der Sprache des Veröffentlichers gesucht — derzeit immer Tschechisch, daher den Filter mit tschechischen Begriffen speichern (z. B. "osvětlení", "veřejné osvětlení").

Bauarbeiten in Prag über 500 Tsd. CZK

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

IT-Entwicklung und SW-Lizenzen (alle Regionen)

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

Öffentliche Beleuchtung (CZ) — Tags + Keywords Kombination

curl -X POST -H "X-Api-Key: mrw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Öffentliche Beleuchtung (CZ)",
    "industryTags": ["goods_electrical", "con_civil"],
    "keywords": ["Beleuchtung", "Straßenbeleuchtung", "öffentliche Beleuchtung", "Lampen"],
    "minValue": 200000
  }' \
  https://veritra.io/api/v2/leads/filters

Filter deaktivieren

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>

Zustellung — Webhook vs. Polling

Zwei Wege, neue Ausschreibungen ins ERP zu bekommen. Die meisten Integratoren kombinieren beide.

Webhook (Push)

Tendero schickt POST an deinen Endpoint innerhalb von ~2 Sekunden nach Match. Event leads.match.created mit voller Tender-Payload.

Plus: Echtzeit, kein Polling-Overhead, Server-side Gating.

Minus: erfordert öffentlich erreichbaren Endpoint (HTTPS), HMAC-Verifikation, Idempotency-Handling.

Polling (Pull)

Dein ERP ruft GET /api/v2/leads/matches?since=… periodisch auf (z. B. alle 5 min). Gibt Matches seit dem Timestamp zurück.

Plus: kein öffentlicher Endpoint, einfache Implementierung, restart-freundlich.

Minus: Latenz 5-10 min, Rate-Limit (60 req/min Mgmt-API), unnötige Leer-Antworten.

Empfehlung: primär Webhook + tägliches Polling (since=yesterday) als Safety-Net, falls Webhook-Delivery das Retry-Budget aufgebraucht hat.

Treffer abrufen

GET/api/v2/leads/matches

Query-Parameter

ParameterTypBeschreibung
filterIdstringAuf einen bestimmten Filter einschränken
qTextstringSuchtext (Volltext in title + description).
sincestring (ISO 8601 datetime)Nur Treffer seit diesem Datum (ISO-Datetime)
deliveredbooleanfalse = nur unzugestellte, true = nur zugestellte
view"starred" | "excluded"Spezielle View: starred (Favoriten) | excluded (versteckt).
sort"newest" | "deadline" | "value"Sortierung: newest (Default), deadline (aufsteigend), value (absteigend).
limitnumberErgebnisanzahl, max. 1000 (Standard: 50). Für >1k Ergebnisse nextCursor-Paginierung oder /matches/export verwenden.
cursorstringKeyset-Cursor aus vorheriger Seite (pagination.nextCursor).

Zurückgegebene Treffer werden automatisch markiert als delivered=true. Jeder Request verbraucht 1 Credit.

matchId-Formate

matchId hat zwei Formate je nach Herkunft:

  • cm… (cuid-Präfix) — zurückgegeben aus vorberechneter LeadMatch-Tabelle (täglicher Cron). Stabil über Calls hinweg, verwendbar für mark-as-viewed.
  • live-12345 synthetischer Präfix für live-erkannte Matches via Search / Browse-Modus (qText, view=starred/excluded). Nicht in LeadMatch-Tabelle → mark-as-viewed ist no-op. tenderId ist der Bindestrich-Suffix.

Beispiel

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

Antwort

{
  "data": [
    {
      "matchId": "cm…",
      "filterId": "cm…",
      "filterName": "Bauarbeiten Prag",
      "matchedAt": "2026-04-03T06:00:00.000Z",
      "viewedAt": null,
      "delivered": true,
      "tender": {
        "id": 12345,
        "title": "Brückensanierung Reg.-Nr. 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 }
}

Für nächste Seite, übergib pagination.nextCursor als ?cursor= Parameter.

Match-Aktionen

Star (Favorit), exclude (ausblenden) und view (als gelesen markieren) sind Per-Tender-Preferences in UserTenderPreference.

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

Response: gleiche Struktur wie Matches-List-Item.

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

No-op für synthetische live-:id-Matches (keine Zeile zum Markieren).

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

Taxonomy

Statische Referenzkataloge für Filter-Values. Locale-aware Labels.

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

Response: Baumstruktur Divisions → Groups → Classes → Categories → Subcategories.

Webhook

Webhook-Endpoints (CRUD, Secret-Rotation) sind account-level — einmalig dokumentiert in Account-API → Webhooks. Dieser Abschnitt behandelt nur das leads-spezifische Event-Payload (leads.match.created) und HMAC-Verifizierung.

Event-Zustellung

Bei einem neuen Match senden wir ein Event vom Typ leads.match.created mit HMAC-SHA256-Signatur im Header X-Signature-256, Idempotency-Key in X-Idempotency-Key und Typ in X-Event-Type. Bei Fehlschlag erfolgen Retries mit exponentiellem Backoff bis zu ~33 Stunden.

Retry-Policy

Wenn dein Endpoint non-2xx zurückgibt (oder nicht innerhalb von 10s antwortet), retry Tendero mit exponential backoff: 1min, 5min, 30min, 2h, 12h, 24h. Nach 6 Fehlschlägen wird der Webhook als failed markiert und im Dashboard markiert. Idempotency-Key bleibt für jeden Retry gleich — dein Endpoint MUSS deduplizieren (sonst wird derselbe Match 6× verarbeitet).

Endpoints per API verwalten

Webhook-Endpoints können ohne Dashboard verwaltet werden. Limit: 5 Endpoints pro Konto.

GET/api/v2/account/webhooks
POST/api/v2/account/webhooks
Webhook-Secret wird nur einmal angezeigt! Bei POST /webhooks enthält die Antwort ein 'secret'-Feld — sofort in die Umgebung speichern (z. B. MRICKWOOD_WEBHOOK_SECRET). Kann nicht später abgerufen werden. Bei Verlust /rotate-secret für ein neues (altes sofort ungültig).
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

HMAC-Signaturverifikation

Server signiert Payload als HMAC-SHA256(secret, raw_body) und schickt im X-Signature-256-Header im sha256=hex-Format. Verifiziere in Constant-Time, sonst ist die Integration anfällig für Timing-Angriffe.

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

E-Mail-Digest

Mit emailDigest: true erhalten Sie eine tägliche E-Mail mit den neuen Aufträgen. Der Digest wird morgens (5:00 UTC) an Ihre Konto-E-Mail gesendet. Lässt sich durch Bearbeiten des Filters deaktivieren.

Limits

ParameterTypBeschreibung
Testphase500 req/Monat1 Tag kostenlos. Keine Karte / Abrechnungsprofil nötig. Default ApiKey.requestsLimit.
Bezahlter PlanunbegrenztMonatslimit aufgehoben. Nur das technische Rate Limit 100/h/Schlüssel gilt.
Filter20Max. Anzahl aktiver Filter pro Konto.
Webhook-Endpoints5Max. Anzahl aktiver Webhook-URLs pro Konto.

Bei Überschreitung des Monatslimits antwortet die API mit 429 und einem Retry-After-Header. Das Limit wird am 1. des Monats zurückgesetzt.