<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[TenderData]]></title><description><![CDATA[TenderData]]></description><link>https://tenderdata.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 15:21:30 GMT</lastBuildDate><atom:link href="https://tenderdata.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to get clean EU tender leads into your CRM daily]]></title><description><![CDATA[Pull EU tenders from the official TED API, merge corrections, flag placeholder values, and push them to your CRM.]]></description><link>https://tenderdata.hashnode.dev/how-to-get-clean-eu-tender-leads-into-your-crm-daily</link><guid isPermaLink="true">https://tenderdata.hashnode.dev/how-to-get-clean-eu-tender-leads-into-your-crm-daily</guid><category><![CDATA[api]]></category><category><![CDATA[Python]]></category><category><![CDATA[open data]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[TenderData]]></dc:creator><pubDate>Sat, 31 Oct 2026 12:34:42 GMT</pubDate><content:encoded><![CDATA[<p>If you sell to the public sector in Europe, the best free lead source you have is <strong>TED (Tenders Electronic Daily)</strong>. It is the online Supplement to the Official Journal of the EU, run by the Publications Office of the European Union. Every tender above the EU thresholds has to be published there. The data is free, and its reuse is allowed, commercially too, as long as you credit the source.</p>
<p>The catch: raw TED data is messy in ways that quietly break a CRM. This guide shows how to pull it yourself with the official API, which two traps to handle before anything reaches your pipeline, and a scheduled shortcut if you'd rather not maintain the code.</p>
<h2>1. The TED Search API in five minutes</h2>
<p>The Search API is public and needs <strong>no API key or account</strong>:</p>
<pre><code>POST https://api.ted.europa.eu/v3/notices/search
Content-Type: application/json
</code></pre>
<p>The body takes an <em>expert query</em> (the same syntax as TED's expert search) and a list of the fields you want back. Here is an example: open IT and software tenders from Portuguese and Spanish buyers.</p>
<pre><code class="language-bash">curl -s -X POST https://api.ted.europa.eu/v3/notices/search \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "buyer-country IN (PRT ESP) AND classification-cpv IN (72* 48*) AND publication-date&gt;=20261023",
    "fields": ["publication-number","notice-type","buyer-name","buyer-country",
               "classification-cpv","estimated-value-proc","estimated-value-cur-proc",
               "deadline-receipt-tender-date-lot","procedure-identifier",
               "change-notice-version-identifier"],
    "limit": 250,
    "scope": "ACTIVE",
    "paginationMode": "ITERATION"
  }'
</code></pre>
<p>Things worth knowing before you build on it:</p>
<ul>
<li><strong>Dates are <code>YYYYMMDD</code>.</strong> In our test on 30 Oct 2026, a malformed date (<code>publication-date=30102026</code>) returned HTTP 400. Earlier reports describe it silently returning zero results, so validate dates in your own code either way.</li>
<li><strong><code>limit</code> is at most 250.</strong> A request with 251 returned HTTP 400.</li>
<li><strong>Use <code>ITERATION</code> paging for anything big.</strong> The response includes an <code>iterationNextToken</code>; send it back to get the next page. The default <code>PAGE_NUMBER</code> mode stops at 15,000 notices per query (TED docs). Don't send a placeholder token on the first call. That mistake causes a puzzling HTTP 500, as one Stack Overflow asker found.</li>
<li><strong>Countries are 3-letter codes</strong> (<code>PRT</code>, <code>ESP</code>). <strong>CPV codes accept prefixes</strong> (<code>72*</code> = all IT services).</li>
<li><strong>Multilingual fields come back as objects</strong>, e.g. <code>"buyer-name": {"eng": ["European Maritime Safety Agency"]}</code>. Pick a language and fall back to any other.</li>
<li><strong>Fair use:</strong> TED's fair-usage policy sets a limit of 700 HTTP requests per minute.</li>
</ul>
<p>That's enough to get data out. Now for the part most tutorials skip.</p>
<h2>2. Trap one: the same tender, several times</h2>
<p>On TED, a correction (corrigendum) or change to a tender is <strong>not an edit to the original notice</strong>. It is published as a <em>new notice</em> for the same procedure. Feed the raw results to a CRM and you get duplicate deals.</p>
<p>How common is this? We counted standard contract notices (<code>notice-type=cn-standard</code>) published from 1 January to 30 October 2026:</p>
<table>
<thead>
<tr>
<th></th>
<th>Notices</th>
</tr>
</thead>
<tbody><tr>
<td>All <code>cn-standard</code> contract notices</td>
<td>362,402</td>
</tr>
<tr>
<td>…of which are change notices (<code>change-notice-version-identifier</code> present)</td>
<td>101,185</td>
</tr>
</tbody></table>
<p>That is <strong>about 28%</strong>, more than one in four competition notices.</p>
<p>A real example: a Lithuanian procurement for used diesel M3 buses (procedure <code>efe077af-a00d-4fc1-b990-3df7109d914b</code>) appears as three notices:</p>
<table>
<thead>
<tr>
<th>Notice</th>
<th>Published</th>
<th>Change notice?</th>
</tr>
</thead>
<tbody><tr>
<td>662865-2026</td>
<td>2026-09-25</td>
<td>No (original)</td>
</tr>
<tr>
<td>674230-2026</td>
<td>2026-10-01</td>
<td>Yes</td>
</tr>
<tr>
<td>718151-2026</td>
<td>2026-10-16</td>
<td>Yes</td>
</tr>
</tbody></table>
<p><strong>Fix:</strong> request <code>procedure-identifier</code>, group by it, and keep only the most recent notice. Keep the IDs you dropped for traceability. Award data has a related subtlety. When a contract is modified, the Publications Office has confirmed that modification notices carry <em>updated</em> amounts, not deltas (<a href="https://github.com/OP-TED/eForms-SDK/discussions/1388">eForms-SDK discussion #1388</a>). Take the latest value; never sum them.</p>
<h2>3. Trap two: values that aren't values</h2>
<p>Buyers sometimes have to fill in an estimated value they don't want to disclose, so they type something that passes validation. Counting notices published from 1 January to 30 October 2026:</p>
<table>
<thead>
<tr>
<th>Query</th>
<th>Notices</th>
</tr>
</thead>
<tbody><tr>
<td><code>estimated-value-proc=0</code></td>
<td>8,580</td>
</tr>
<tr>
<td><code>estimated-value-proc=1</code></td>
<td>2,974</td>
</tr>
</tbody></table>
<p>Put a few thousand "€1 tenders" into a pipeline report or a market-sizing chart and your averages are wrong. Treat these as <strong>unknown, not zero</strong>:</p>
<pre><code class="language-python">PLACEHOLDERS = {0, 1, -1}

def clean_value(v):
    try:
        v = float(v)
    except (TypeError, ValueError):
        return None, "missing"
    if v in PLACEHOLDERS:
        return None, "placeholder"
    if str(int(v)).strip("9") == "":   # 99999, 999999999 ...
        return None, "nines_placeholder"
    return v, None
</code></pre>
<p>Judge plausibility in the notice's <strong>currency</strong>, not in euros. 50 billion HUF is a big but real number; 50 billion EUR almost certainly isn't.</p>
<p>Two smaller clean-ups make the data CRM-ready:</p>
<ul>
<li><strong>Normalise organisation names into a matching key.</strong> Remove accents, punctuation and legal forms (S.A., GmbH, Lda, Sp. z o.o.) so that "Infraestruturas de Portugal, SA" matches the "Infraestruturas de Portugal" account you already have.</li>
<li><strong>Leave out personal data.</strong> Notices can name contact persons. A bid pipeline usually only needs organisation-level data, and leaving the rest out keeps your GDPR exposure small.</li>
</ul>
<h2>4. The shortcut: a scheduled Apify Actor</h2>
<p>If you'd rather not maintain this, <em>disclosure: I built</em> <strong><a href="https://apify.com/hazel_bedstead/ted-tenders-clean">EU Tenders (TED) — Clean, Deduplicated &amp; GDPR-safe</a></strong>. It is an Apify Actor that runs the steps above against the same official API:</p>
<ul>
<li>one row per procedure (corrections merged, superseded IDs kept);</li>
<li>placeholder values set to <code>null</code>, with the raw figure and a <code>dataQualityFlags</code> reason;</li>
<li><code>buyerNameKey</code> and <code>winners[].nameKey</code> matching keys;</li>
<li>no contact persons, emails or phones.</li>
</ul>
<p>The Actor itself is free for now. You pay only Apify's platform usage, and you need an Apify account.</p>
<p>Input for the PT/ES IT example:</p>
<pre><code class="language-json">{
  "cpvPrefixes": ["72", "48"],
  "countries": ["PT", "ES"],
  "noticeCategories": ["competition"],
  "onlyOpen": true,
  "lastDays": 7,
  "maxItems": 200
}
</code></pre>
<p>Sample output from a run on 30 Oct 2026 (five of 40 rows, columns shortened):</p>
<table>
<thead>
<tr>
<th>noticeId</th>
<th>buyerName</th>
<th>buyerNameKey</th>
<th>cpvMain</th>
<th>estimatedValue</th>
<th>deadline</th>
</tr>
</thead>
<tbody><tr>
<td>734675-2026</td>
<td>Infraestruturas de Portugal, SA</td>
<td>INFRAESTRUTURAS DE PORTUGAL</td>
<td>72246000</td>
<td>250,000 EUR</td>
<td>2026-11-23</td>
</tr>
<tr>
<td>734645-2026</td>
<td>Oficina Española de Patentes y Marcas, O.A.</td>
<td>OFICINA ESPANOLA DE PATENTES Y MARCAS OA</td>
<td>72500000</td>
<td>1,144,793.36 EUR</td>
<td>2026-11-23</td>
</tr>
<tr>
<td>733660-2026</td>
<td>Director Gerente de FREMAP</td>
<td>DIRECTOR GERENTE DE FREMAP</td>
<td>48300000</td>
<td>265,770.45 EUR</td>
<td>2026-11-27</td>
</tr>
<tr>
<td>734054-2026</td>
<td>Departamento de Sanidad</td>
<td>DEPARTAMENTO DE SANIDAD</td>
<td>48000000</td>
<td>238,164.40 EUR</td>
<td>2026-11-19</td>
</tr>
<tr>
<td>733691-2026</td>
<td>Mercados de Abastecimientos de Barcelona, S.A</td>
<td>MERCADOS DE ABASTECIMIENTOS DE BARCELONA</td>
<td>48500000</td>
<td>null (none published)</td>
<td>2026-11-20</td>
</tr>
</tbody></table>
<p>Each row also carries <code>noticeUrl</code>, <code>procedureId</code>, <code>isChangeNotice</code>, <code>title</code>, <code>cpvCodes</code>, <code>submissionDeadline</code> and a <code>source</code> attribution to TED.</p>
<h3>Run it every morning, new notices only</h3>
<ol>
<li>Turn on <strong>Incremental</strong> and give it a <strong>State name</strong> (e.g. <code>pt-es-it</code>). Each run then returns only notices you haven't received before.</li>
<li>In Apify Console, create a <strong>Schedule</strong> with a cron expression. Schedules support time zones and daylight saving time. For example, <code>0 8 * * 1-5</code> runs at 08:00 on weekdays in your time zone.</li>
</ol>
<p>Use a different state name for each saved search.</p>
<h2>5. Into the CRM</h2>
<p>Every run writes to an Apify dataset. Pick whichever of these fits your stack. All of them are standard Apify features:</p>
<ul>
<li><strong>CSV or Excel pull.</strong> The dataset items endpoint <code>GET https://api.apify.com/v2/datasets/{datasetId}/items?format=csv</code> also supports <code>xlsx</code>, <code>json</code>, <code>jsonl</code>, <code>xml</code>, <code>html</code> and <code>rss</code>. Most CRMs can import a CSV directly.</li>
<li><strong>Webhook.</strong> Add a webhook on the <code>ACTOR.RUN.SUCCEEDED</code> event. Your endpoint receives the run details, fetches the dataset and upserts.</li>
<li><strong>Zapier.</strong> Use the Apify trigger <em>Finished Actor run</em> (or <em>Finished task run</em>), fetch the dataset items, then add your CRM's "create or update" action.</li>
<li><strong>Make.</strong> Use the Apify module <em>Watch Actor Runs</em>, then <em>Get Dataset Items</em>, then your CRM module.</li>
</ul>
<p>Whichever route you choose, set the mapping up once:</p>
<ul>
<li><strong>Upsert key = <code>procedureId</code>.</strong> A later correction then updates the existing deal instead of creating a new one. Rows with <code>isChangeNotice: true</code> are exactly those updates.</li>
<li><strong>Account match = <code>buyerNameKey</code>.</strong> Link tenders to the public-sector accounts you already track.</li>
<li><strong>Award notices are competitor intelligence.</strong> Run a second saved search with <code>noticeCategories: ["award"]</code> and group by <code>winners[].nameKey</code> per CPV code to see who keeps winning in your niche.</li>
<li><strong>Always open <code>noticeUrl</code> before bidding.</strong> The cleaned row is a lead, not the legal notice.</li>
</ul>
<h2>Limits worth knowing</h2>
<ul>
<li>TED covers above-threshold procurement only. Smaller tenders appear only on national portals.</li>
<li>Values are reported in the notice currency and are not converted.</li>
<li>Merging corrections works within each run's time window. Later corrections arrive as updated rows, which is why upserting by <code>procedureId</code> matters.</li>
</ul>
<hr />
<p><em>Source: TED (Tenders Electronic Daily), published by the Publications Office of the European Union, © European Union, <a href="https://ted.europa.eu">https://ted.europa.eu</a>. TED notices may be reused for commercial and non-commercial purposes (Commission Decision 2011/833/EU). The data in this article has been reformatted and abridged. Neither this article nor the Actor is affiliated with or endorsed by the Publications Office or the European Commission. Figures from the TED Search API, retrieved 30 October 2026.</em></p>
]]></content:encoded></item></channel></rss>