<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Fast Data</title>
    <description>The latest articles on DEV Community by Fast Data (@fast_data_asap).</description>
    <link>https://dev.to/fast_data_asap</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4195812%2F8a9221d7-81fc-4a74-89ca-73193b447a79.png</url>
      <title>DEV Community: Fast Data</title>
      <link>https://dev.to/fast_data_asap</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fast_data_asap"/>
    <language>en</language>
    <item>
      <title>Your AI agent found a new IBAN on an invoice. Should it pay? (IBAN checks in Python, and their limits)</title>
      <dc:creator>Fast Data</dc:creator>
      <pubDate>Thu, 12 Nov 2026 07:45:33 +0000</pubDate>
      <link>https://dev.to/fast_data_asap/your-ai-agent-found-a-new-iban-on-an-invoice-should-it-pay-iban-checks-in-python-and-their-p53</link>
      <guid>https://dev.to/fast_data_asap/your-ai-agent-found-a-new-iban-on-an-invoice-should-it-pay-iban-checks-in-python-and-their-p53</guid>
      <description>&lt;p&gt;An invoice arrives. Same supplier, same amount pattern, but the IBAN is different. If an AI agent prepares your payments, what should it do?&lt;/p&gt;

&lt;p&gt;Bank-detail changes are a classic invoice-fraud pattern, and agents make it more interesting: they read every PDF, never get tired and will happily "fix" vendor master data if the instructions say so. So I tested what an IBAN check actually catches.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built the &lt;a href="https://apify.com/fast_data_asap/iban-validator-bank-lookup" rel="noopener noreferrer"&gt;IBAN Validator &amp;amp; Bank Lookup&lt;/a&gt; on Apify, which I use below. The lesson applies to any validator.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;Nine inputs: a known-good German IBAN, the same IBAN with one typo, three IBANs where I changed the account number &lt;strong&gt;and recomputed the two check digits&lt;/strong&gt; (what a fraudster would do), plus Dutch, French, Austrian and a truncated one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="no"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://api.apify.com/v2/acts/fast_data_asap~iban-validator-bank-lookup"&lt;/span&gt;
       &lt;span class="s2"&gt;"/run-sync-get-dataset-items"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Bearer {os.environ['APIFY_TOKEN']}"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;ibans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"DE89370400440532013000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# original&lt;/span&gt;
    &lt;span class="s2"&gt;"DE89 3704 0044 0532 0130 01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# one digit typo&lt;/span&gt;
    &lt;span class="s2"&gt;"DE62370400440532013001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# changed account + recomputed check digits&lt;/span&gt;
    &lt;span class="s2"&gt;"DE40370400440532013009"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"DE18370400440532013017"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"NL91ABNA0417164300"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"FR1420041010050500013M02606"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"AT611904300234573201"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"DE12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"ibans"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ibans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"includeBankInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"checksumValid"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"nationalChecksumValid"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bankName"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Valid&lt;/th&gt;
&lt;th&gt;MOD-97&lt;/th&gt;
&lt;th&gt;National check&lt;/th&gt;
&lt;th&gt;Bank&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DE89 …013000 (original)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;Commerzbank&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DE89 …013001 (typo)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;fails&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;Commerzbank&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DE62 …013001 (changed, recomputed)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;Commerzbank&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DE40 …013009 (changed, recomputed)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;Commerzbank&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DE18 …013017 (changed, recomputed)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;Commerzbank&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NL91 ABNA…&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;ABN AMRO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FR14 2004…&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;La Banque Postale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AT61 1904…&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;(no bank data)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DE12345&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;fails&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;wrong length&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Real output, run on 12 November 2026. Malformed rows are not charged.&lt;/p&gt;

&lt;p&gt;Two takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Typos are caught.&lt;/strong&gt; The IBAN check digits (ISO 7064 MOD-97) exist exactly for that, and the one-digit typo fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliberate changes are not.&lt;/strong&gt; Recompute the two check digits and the IBAN is mathematically perfect. For these three accounts the German national account check passed as well. To every format rule, a fraudulent IBAN at the same bank looks exactly like a real one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A validator answers "is this a well-formed IBAN, and which bank is it?". It cannot answer "does this account belong to my supplier?".&lt;/p&gt;

&lt;h2&gt;
  
  
  What the validator &lt;em&gt;is&lt;/em&gt; good for in an agent
&lt;/h2&gt;

&lt;p&gt;The useful signal is not &lt;code&gt;valid&lt;/code&gt;, it is the &lt;strong&gt;comparison with what you already know&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;What the agent should do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;valid: false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;validator&lt;/td&gt;
&lt;td&gt;reject, ask for corrected invoice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IBAN differs from vendor master data&lt;/td&gt;
&lt;td&gt;your ERP&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;hold payment&lt;/strong&gt;, never update master data automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;bankName&lt;/code&gt; / &lt;code&gt;bic&lt;/code&gt; differs from the known bank&lt;/td&gt;
&lt;td&gt;validator + ERP&lt;/td&gt;
&lt;td&gt;hold, flag as high risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;country of the bank differs from the supplier's country&lt;/td&gt;
&lt;td&gt;validator + ERP&lt;/td&gt;
&lt;td&gt;hold, flag as high risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;new IBAN confirmed by the supplier through a known channel&lt;/td&gt;
&lt;td&gt;human&lt;/td&gt;
&lt;td&gt;release&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is that rule as code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;iban_decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_iban&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"""vendor = your master data, check = one row from the validator."""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"reject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"iban"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"iban"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"pay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"IBAN matches master data"&lt;/span&gt;
    &lt;span class="n"&gt;reasons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"IBAN differs from master data"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bic"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bic"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bic"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bic"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"bank changed: {vendor.get('bank')} -&amp;gt; {check.get('bankName')}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"countryCode"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"iban"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bank country changed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"hold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"; "&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;" - confirm with supplier via known phone number"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important design choice: &lt;strong&gt;the agent can never move from "hold" to "pay" on its own.&lt;/strong&gt; It can prepare the evidence (old vs new bank, invoice, sender address), but the release needs a human or a bank-side confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification of Payee does the part a validator cannot
&lt;/h2&gt;

&lt;p&gt;Since October 2025, EU payment service providers have to offer &lt;strong&gt;Verification of Payee&lt;/strong&gt; for euro credit transfers: before you send money, your bank checks whether the payee name matches the account holder of that IBAN and tells you "match", "close match" or "no match". That is the check that would expose the fraudulent IBANs above, because the name on the account would not be your supplier's.&lt;/p&gt;

&lt;p&gt;So the full chain for an accounts-payable agent looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Format and bank&lt;/strong&gt; (validator): cheap, offline, catches typos and tells you which bank.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Master-data comparison&lt;/strong&gt; (your system): catches changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification of Payee&lt;/strong&gt; (your bank, at payment time): catches wrong account holders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human release&lt;/strong&gt; for every change.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://apify.com/fast_data_asap/iban-validator-bank-lookup" rel="noopener noreferrer"&gt;IBAN Validator &amp;amp; Bank Lookup on Apify&lt;/a&gt;: bulk IBAN validation with MOD-97 and national checksums, SEPA flag, bank name and BIC (German banks from the Bundesbank file), $0.0005 per IBAN, runs offline inside the actor. For the full supplier check (company register, VAT ID, IBAN) see our free Agent Skill &lt;a href="https://github.com/ichkifokkus/data-recipes/blob/main/skills/verify-supplier-de/SKILL.md" rel="noopener noreferrer"&gt;verify-supplier-de&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is about payment hygiene, not legal advice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question:&lt;/strong&gt; if you let an agent prepare payments, where do you draw the line: may it ever update vendor bank details, or is that always a human step?&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>fintech</category>
      <category>security</category>
    </item>
    <item>
      <title>Do countries that shop online also run their firms on AI? I checked 27 EU countries (Eurostat 2025)</title>
      <dc:creator>Fast Data</dc:creator>
      <pubDate>Wed, 11 Nov 2026 17:47:58 +0000</pubDate>
      <link>https://dev.to/fast_data_asap/do-countries-that-shop-online-also-run-their-firms-on-ai-i-checked-27-eu-countries-eurostat-2025-4lgm</link>
      <guid>https://dev.to/fast_data_asap/do-countries-that-shop-online-also-run-their-firms-on-ai-i-checked-27-eu-countries-eurostat-2025-4lgm</guid>
      <description>&lt;p&gt;I pulled two fresh Eurostat datasets for a short-video series and noticed they look suspiciously alike:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI in firms:&lt;/strong&gt; share of enterprises with 10+ employees (excluding the financial sector) that use at least one AI technology, 2025 (&lt;code&gt;isoc_eb_ai&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online shopping:&lt;/strong&gt; share of people who bought something online in the last 12 months, 2025 (&lt;code&gt;isoc_ec_ib20&lt;/code&gt;, indicator &lt;code&gt;I_BLT12&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Across the 27 EU countries, the two correlate with &lt;strong&gt;r = 0.70&lt;/strong&gt; (Spearman rank correlation also 0.70). Countries where almost everyone shops online tend to be the countries whose companies use AI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F92u4p9ohhavv47qlmwk8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F92u4p9ohhavv47qlmwk8.png" alt="AI use in firms vs online shopping, 27 EU countries, Eurostat 2025" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Firms using AI&lt;/th&gt;
&lt;th&gt;People shopping online&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Denmark&lt;/td&gt;
&lt;td&gt;42.0%&lt;/td&gt;
&lt;td&gt;91.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finland&lt;/td&gt;
&lt;td&gt;37.8%&lt;/td&gt;
&lt;td&gt;82.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sweden&lt;/td&gt;
&lt;td&gt;35.0%&lt;/td&gt;
&lt;td&gt;88.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Belgium&lt;/td&gt;
&lt;td&gt;34.5%&lt;/td&gt;
&lt;td&gt;78.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netherlands*&lt;/td&gt;
&lt;td&gt;33.2%&lt;/td&gt;
&lt;td&gt;94.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Germany&lt;/td&gt;
&lt;td&gt;26.0%&lt;/td&gt;
&lt;td&gt;81.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EU average&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;73.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ireland&lt;/td&gt;
&lt;td&gt;19.6%&lt;/td&gt;
&lt;td&gt;95.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Czechia&lt;/td&gt;
&lt;td&gt;17.6%&lt;/td&gt;
&lt;td&gt;83.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Italy&lt;/td&gt;
&lt;td&gt;16.4%&lt;/td&gt;
&lt;td&gt;56.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poland&lt;/td&gt;
&lt;td&gt;8.4%&lt;/td&gt;
&lt;td&gt;69.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Romania&lt;/td&gt;
&lt;td&gt;5.2%&lt;/td&gt;
&lt;td&gt;60.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*Eurostat flags the Netherlands' 2025 AI value as a break in series. Dropping it changes r from 0.70 to 0.68.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things that stood out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The Nordic block sits near the top on both.&lt;/strong&gt; Denmark, Finland and Sweden are in the top right corner. Denmark's 42.0% means more than two in five firms use some AI technology, roughly double the EU average of 20.0%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Ireland is the big outlier.&lt;/strong&gt; 95.1% of people bought online in 2025, the highest share in the EU, but only 19.6% of firms use AI, just below the EU average. Without Ireland the correlation rises from 0.70 to 0.77. My guess (and it is only a guess): Irish consumers buy heavily from platforms based abroad, while the AI question is asked of firms located in Ireland. The two indicators measure different populations, and Ireland makes that visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Germany is middle of the pack on AI, above average on shopping.&lt;/strong&gt; 26.0% of German firms use AI (about one in four) and 81.4% of people shop online. Both are above the EU average, but well behind the Nordics.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correlation, not causation.&lt;/strong&gt; A third factor (income, digital skills, broadband, company size structure) likely drives both. I did not control for anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Uses AI" is a yes/no question.&lt;/strong&gt; A firm that uses one chatbot counts the same as one that runs its whole planning on machine learning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No trend claims.&lt;/strong&gt; I only compare 2025. The list of AI technologies in the survey can change between years, so year-over-year jumps need care.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Eurostat data into Python without pain
&lt;/h2&gt;

&lt;p&gt;Eurostat's API returns JSON-stat: values come as a flat, sparse dictionary keyed by a single index, and you have to unfold that index into the dimensions yourself. This is the helper I used (standard library only):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="no"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://ec.europa.eu/eurostat/api/dissemination/statistics/1.0/data/"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;eurostat_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;API&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"?"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"JSON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"lang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"EN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;codes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"dimension"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# last dimension changes fastest&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;//=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"flag"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;

&lt;span class="n"&gt;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;eurostat_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"isoc_eb_ai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indic_is&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"E_AI_TANY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size_emp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"GE10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"PC_ENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nace_r2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"C10-S951_X_K"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sinceTimePeriod&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2025"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;shop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;eurostat_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"isoc_ec_ib20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indic_is&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"I_BLT12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ind_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"IND_TOTAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"PC_IND"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sinceTimePeriod&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2025"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two gotchas cost me time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gotcha&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A wrong filter code&lt;/td&gt;
&lt;td&gt;the response has a dimension of size 0 and no values, no error&lt;/td&gt;
&lt;td&gt;check &lt;code&gt;d["dimension"][dim]["category"]["label"]&lt;/code&gt;; for firm size it is &lt;code&gt;GE10&lt;/code&gt;, not the label text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ignoring &lt;code&gt;status&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;you silently compare provisional or broken series&lt;/td&gt;
&lt;td&gt;keep the &lt;code&gt;flag&lt;/code&gt; column (&lt;code&gt;p&lt;/code&gt; provisional, &lt;code&gt;e&lt;/code&gt; estimated, &lt;code&gt;b&lt;/code&gt; break in series) and decide per row&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full helper is in our public repo: &lt;a href="https://github.com/ichkifokkus/data-recipes/blob/main/snippets/eurostat_table.py" rel="noopener noreferrer"&gt;snippets/eurostat_table.py&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I build data tools for AI agents as &lt;a href="https://apify.com/fast_data_asap" rel="noopener noreferrer"&gt;Fast Data on Apify&lt;/a&gt;; this article uses only Eurostat's free API and needs none of them.&lt;/em&gt; If you work with European business data, our open &lt;a href="https://github.com/ichkifokkus/data-recipes" rel="noopener noreferrer"&gt;data-recipes repo&lt;/a&gt; has the snippet above plus n8n workflows and Agent Skills for public tenders, supplier checks and e-invoices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question:&lt;/strong&gt; what would you control for first to see if the AI/online-shopping link survives: income per head, broadband coverage, or the share of large companies? And does anyone have a better explanation for Ireland?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>dataviz</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Your invoice is dated on a Saturday. Which ECB exchange rate do you use? (Python)</title>
      <dc:creator>Fast Data</dc:creator>
      <pubDate>Wed, 11 Nov 2026 12:55:00 +0000</pubDate>
      <link>https://dev.to/fast_data_asap/your-invoice-is-dated-on-a-saturday-which-ecb-exchange-rate-do-you-use-python-1jfg</link>
      <guid>https://dev.to/fast_data_asap/your-invoice-is-dated-on-a-saturday-which-ecb-exchange-rate-do-you-use-python-1jfg</guid>
      <description>&lt;p&gt;Here is a USD 1,250 invoice dated Saturday, 7 November 2026. What is it worth in euros?&lt;/p&gt;

&lt;p&gt;The ECB publishes no reference rate on Saturdays. So your code either crashes, silently uses some other day, or asks a human. And the day matters more than people expect: between 2 and 10 November, the same USD 1,250 was worth between &lt;strong&gt;€1,109.43 and €1,119.57&lt;/strong&gt; depending on which daily ECB rate you pick.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftcus7hgbgb7lri78jkm8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftcus7hgbgb7lri78jkm8.png" alt="EUR value of a USD 1,250 invoice per ECB rate day, 2 to 10 November 2026" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is up to €10.14 on one mid-sized invoice, from rate-day choice alone, and even the two neighbouring fixing days (Friday vs Monday) differ by €2.80. Across a few hundred invoices a month, "we just used the rate from somewhere" gets hard to reconcile for whoever closes the books.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built the &lt;a href="https://apify.com/fast_data_asap/ecb-exchange-rates-invoice-converter" rel="noopener noreferrer"&gt;ECB Exchange Rates – Invoice Currency Converter&lt;/a&gt; on Apify, which the code below uses. The rules in this post apply to any implementation.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The four decisions behind every conversion
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Options&lt;/th&gt;
&lt;th&gt;Actor default (your policy may differ)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Which rate source?&lt;/td&gt;
&lt;td&gt;ECB reference rate, bank rate, a "mid-market" API&lt;/td&gt;
&lt;td&gt;ECB reference rate (public, reproducible)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which day?&lt;/td&gt;
&lt;td&gt;invoice date, delivery date, payment date&lt;/td&gt;
&lt;td&gt;invoice date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No rate that day?&lt;/td&gt;
&lt;td&gt;previous fixing day, next fixing day, fail&lt;/td&gt;
&lt;td&gt;previous fixing day, &lt;strong&gt;flagged&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which rounding?&lt;/td&gt;
&lt;td&gt;per line, per invoice, half-up, banker's&lt;/td&gt;
&lt;td&gt;half-up to 2 decimals, keep the unrounded value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third row is where homemade scripts often go wrong. The ECB publishes its euro reference rates for about 30 currencies on TARGET working days, around 16:00 CET. No rate on weekends, none on TARGET holidays such as 1 January or Good Friday, and none for today until the afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting a batch of invoices
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="no"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://api.apify.com/v2/acts/fast_data_asap~ecb-exchange-rates-invoice-converter"&lt;/span&gt;
       &lt;span class="s2"&gt;"/run-sync-get-dataset-items"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"INV-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1250.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-11-07"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"INV-1002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;480.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s2"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"GBP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-11-05"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"INV-1003"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2999.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"CHF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-10-30"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="no"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Bearer {os.environ['APIFY_TOKEN']}"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"convert"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"targetCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"fallback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"previous"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ERROR:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"  &amp;lt;- rate of {row['rateDate']}"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"fallbackUsed"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{row['id']}: {row['amount']} {row['currency']} = {row['convertedAmount']} {row['targetCurrency']} "&lt;/span&gt;
          &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"(rate {row['rate']}){flag}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real output of that input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INV-1001: 1250 USD = 1119.57 EUR (rate 1.1165)  &amp;lt;- rate of 2026-11-06
INV-1002: 480.5 GBP = 576.19 EUR (rate 0.83393)
INV-1003: 2999.9 CHF = 3262.89 EUR (rate 0.9194)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row also carries &lt;code&gt;daysShifted&lt;/code&gt; (here &lt;code&gt;-1&lt;/code&gt;), &lt;code&gt;convertedAmountUnrounded&lt;/code&gt; (&lt;code&gt;1119.57008509&lt;/code&gt;) and a &lt;code&gt;sourceUrl&lt;/code&gt; that returns exactly the ECB observation that was used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://data-api.ecb.europa.eu/service/data/EXR/D.USD.EUR.SP00.A?format=csvdata&amp;amp;startPeriod=2026-11-06&amp;amp;endPeriod=2026-11-06
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That URL is the audit trail. Store it next to the booking and anyone can check the number later without trusting your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the three fallback rules give you
&lt;/h2&gt;

&lt;p&gt;Same invoice, USD 1,250 on Saturday 7 November, three settings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;fallback&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Rate day used&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;th&gt;EUR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;previous&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fri 6 Nov&lt;/td&gt;
&lt;td&gt;1.1165&lt;/td&gt;
&lt;td&gt;1,119.57&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;next&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mon 9 Nov&lt;/td&gt;
&lt;td&gt;1.1193&lt;/td&gt;
&lt;td&gt;1,116.77&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;none&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;error row, so a human decides&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;previous&lt;/code&gt; is the actor's default because that rate was already published on the invoice date. &lt;code&gt;next&lt;/code&gt; can make sense if your policy is "first rate published after the document". &lt;code&gt;none&lt;/code&gt; is right when you would rather stop than guess. The bad option is the one many scripts implement by accident: no rule at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three gotchas I hit while building it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The ECB quotes "units of foreign currency per 1 EUR".&lt;/strong&gt; &lt;code&gt;1.1165&lt;/code&gt; means 1 EUR = 1.1165 USD, so USD → EUR is a division: &lt;code&gt;1250 / 1.1165&lt;/code&gt;. Multiplying gives you €1,395.63 and a very confused accountant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cross rates go through the euro.&lt;/strong&gt; There is no ECB USD/GBP rate. USD → GBP is &lt;code&gt;amount / rate(USD) * rate(GBP)&lt;/code&gt;, with both rates from the same fixing day. Mix a cached rate for one currency with a fresh one for the other and your cross rate quietly comes from two different days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Fetch per currency, not per invoice.&lt;/strong&gt; One request per invoice is slow and impolite to a free public API. Group by currency, request the whole date range once, then look up each invoice locally. The actor sends one ECB data request per currency per run (plus retries on errors), however many invoices you pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you only need the rates
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;rates&lt;/code&gt; mode returns the daily series instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"rates"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"currencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"JPY"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="s2"&gt;"startDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-11-02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"endDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-11-10"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For 2 to 10 November that is 7 fixing days per currency (the weekend is simply missing), for example USD 1.1216 on 2 November and 1.1214 on 10 November. An unknown code such as &lt;code&gt;XYZ&lt;/code&gt; comes back as one error row instead of crashing the run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not tax advice.&lt;/strong&gt; The ECB itself says its reference rates are for information purposes only. For VAT, some countries prescribe other rates (in Germany, for example, the Federal Ministry of Finance publishes monthly average rates, based on the ECB rates, under § 16 (6) UStG). Ask whoever signs your tax return which rate applies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not affiliated with the ECB.&lt;/strong&gt; The actor only reads the ECB's public Data Portal API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a trading rate.&lt;/strong&gt; Your bank converted the actual payment at its own rate. The difference is usually booked separately as an FX gain or loss; your accountant decides how.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://apify.com/fast_data_asap/ecb-exchange-rates-invoice-converter" rel="noopener noreferrer"&gt;ECB Exchange Rates – Invoice Currency Converter on Apify&lt;/a&gt;: $0.001 per result row (converted amount or daily rate), error rows are free, no API key for the ECB needed. It pairs with our &lt;a href="https://apify.com/fast_data_asap/xrechnung-zugferd-einvoice-parser" rel="noopener noreferrer"&gt;XRechnung &amp;amp; ZUGFeRD parser&lt;/a&gt;: take &lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;grandTotal&lt;/code&gt; and &lt;code&gt;issueDate&lt;/code&gt; from a parsed e-invoice and convert it here. AI agents can call it as an MCP tool: &lt;code&gt;https://mcp.apify.com?tools=fast_data_asap/ecb-exchange-rates-invoice-converter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question:&lt;/strong&gt; which rate day does your team use for foreign-currency invoices: invoice date, delivery date or payment date? And has an auditor ever asked you to prove where a rate came from?&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>fintech</category>
      <category>accounting</category>
    </item>
    <item>
      <title>How to screen company names and domains against BaFin warnings in Python, and the false positive you will hit first</title>
      <dc:creator>Fast Data</dc:creator>
      <pubDate>Wed, 11 Nov 2026 07:48:26 +0000</pubDate>
      <link>https://dev.to/fast_data_asap/how-to-screen-company-names-and-domains-against-bafin-warnings-in-python-and-the-false-positive-b2f</link>
      <guid>https://dev.to/fast_data_asap/how-to-screen-company-names-and-domains-against-bafin-warnings-in-python-and-the-false-positive-b2f</guid>
      <description>&lt;p&gt;If you onboard merchants, pay suppliers or approve affiliates in Germany, one cheap check is: &lt;em&gt;is this firm or website on a current warning from BaFin&lt;/em&gt;, the German financial regulator?&lt;/p&gt;

&lt;p&gt;BaFin publishes these warnings as news items: unauthorised banking, clone websites, fake job offers, plus fines and other measures. There is no screening API. So I wrapped the public feeds into one, and this post shows how to call it from Python, and what the results look like on real data.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built the &lt;a href="https://apify.com/fast_data_asap/bafin-investor-warnings" rel="noopener noreferrer"&gt;BaFin Warnings actor&lt;/a&gt; on Apify. The routing logic below works with any warning source.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the data looks like
&lt;/h2&gt;

&lt;p&gt;I keep a daily snapshot of BaFin's German RSS feeds. The four snapshots from 8 to 11 November contain 36 notices, published between 9 October and 10 November:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Notices&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supervisory measures (fines, own-funds requirements, orders)&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer warnings (unauthorised firms, clone sites, fake jobs)&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;... of which name at least one website domain&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The measures are mostly against &lt;em&gt;licensed&lt;/em&gt; institutions, so they are a different signal from the warnings. For screening new counterparties, the 14 consumer warnings are the interesting part. They break down into unauthorised business (7), identity theft (4), fake job offers (2) and one general warning.&lt;/p&gt;

&lt;p&gt;Two of the 14 had no usable firm name in the feed text alone: one is about a WhatsApp group, the other title quotes the scam sites' advertising slogan. The actor opens each warning page to fill these gaps; my offline archive only has the feed, which is why the numbers here are a lower bound.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;Screen mode takes names, domains or full URLs and returns one row per hit, or one &lt;code&gt;matched: false&lt;/code&gt; row per name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="no"&gt;ACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"fast_data_asap~bafin-investor-warnings"&lt;/span&gt;
&lt;span class="no"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"APIFY_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"screen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"queries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"minScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;min_score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"matched"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"pass"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"matchedOn"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"domain:"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"matchScore"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"block"&lt;/span&gt;          &lt;span class="c1"&gt;# the exact website is named in a warning&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"warningType"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"identity_theft"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"review"&lt;/span&gt;         &lt;span class="c1"&gt;# see the next section&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"review"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"matchScore"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;"block"&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://www.meinallianz.com/login"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Landshypotek Bank"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Example Trading Ltd"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"matchScore"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"matchedOn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Names are normalised before matching: legal forms like GmbH, Ltd or AG are dropped, &lt;code&gt;example(.)com&lt;/code&gt; obfuscation is resolved and URLs are reduced to their domain. Exact domain hits score 100.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real results
&lt;/h2&gt;

&lt;p&gt;I ran the same matching code offline against the 36 archived notices:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Matched on&lt;/th&gt;
&lt;th&gt;Warning type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;meinallianz.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;domain &lt;code&gt;meinallianz.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;identity theft&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://www.klever-beratung.de/jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;domain &lt;code&gt;klever-beratung.de&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;fake job offers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Klever Beratung GmbH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;name &lt;code&gt;Klever Beratung&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;fake job offers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;klever-beratung.com&lt;/code&gt; (other TLD)&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;name &lt;code&gt;Klever Beratung&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;fake job offers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Landshypotek Bank&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;97&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;name &lt;code&gt;landshypotek-bnk&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;identity theft&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allianz.de&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;name &lt;code&gt;meinallianz&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;identity theft&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Example Trading Ltd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;(no hit)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first four rows behave the way you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  The false positive you will hit first
&lt;/h2&gt;

&lt;p&gt;Look at &lt;code&gt;Landshypotek Bank&lt;/code&gt;. Landshypotek is a real Swedish bank. The BaFin warning is about &lt;code&gt;landshypotek-bnk.com&lt;/code&gt;, a website that &lt;strong&gt;pretends to be&lt;/strong&gt; that bank. Screening the real bank's name scores 97, above the default threshold of 85.&lt;/p&gt;

&lt;p&gt;This is not a matching bug. It is how identity-theft warnings work: the fraudulent site copies a legitimate name on purpose. The better your fuzzy matching, the more reliably the real firm matches the warning about its impostor. 4 of the 14 warnings in my sample are of this type, so you will see it early.&lt;/p&gt;

&lt;p&gt;The fix is in the routing, not in the threshold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact domain hit&lt;/strong&gt;: block or escalate. The website itself is named by the regulator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name hit on an &lt;code&gt;identity_theft&lt;/code&gt; warning&lt;/strong&gt;: send to a human, and check which &lt;em&gt;domain&lt;/em&gt; the counterparty actually uses. Here, &lt;code&gt;landshypotek.se&lt;/code&gt; scores only 81 against the fake domain, under the threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name hit on other warning types&lt;/strong&gt;: review. A different TLD (&lt;code&gt;.com&lt;/code&gt; vs &lt;code&gt;.de&lt;/code&gt;) can be the same operation or an unrelated firm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Raising &lt;code&gt;minScore&lt;/code&gt; to 98 would hide the Landshypotek row, but it would also hide the &lt;code&gt;.com&lt;/code&gt; variant, which is exactly the kind of near-miss you want a person to look at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage, said plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The feeds cover recent weeks, not a full archive. In my snapshots, the oldest notice was about a month old. If you need history, run list mode daily and store the rows.&lt;/li&gt;
&lt;li&gt;A missing hit means "not in current warnings", not "licensed". Licences are checked in BaFin's company database, which this does not query.&lt;/li&gt;
&lt;li&gt;Natural persons are never returned: names of people in warnings are replaced with &lt;code&gt;[name redacted]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It is a technical lookup, not legal advice. Every hit links to the BaFin page, which is the authoritative source.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Screen one name or domain&lt;/td&gt;
&lt;td&gt;$0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily list of new warnings (20 rows)&lt;/td&gt;
&lt;td&gt;$0.04&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A weekly re-screen of 200 active merchants costs $2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://apify.com/fast_data_asap/bafin-investor-warnings" rel="noopener noreferrer"&gt;BaFin Warnings Scraper on Apify&lt;/a&gt;: list mode for monitoring, screen mode for your own names and domains. Also usable from n8n or as an MCP tool for AI agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question:&lt;/strong&gt; if you run counterparty checks, how do you handle impersonation warnings: by domain only, manual review, or an allow-list of known legitimate firms?&lt;/p&gt;

</description>
      <category>python</category>
      <category>fintech</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I added 10 validation rules to an e-invoice parser without changing one existing warning. Here is the test setup</title>
      <dc:creator>Fast Data</dc:creator>
      <pubDate>Tue, 10 Nov 2026 17:48:55 +0000</pubDate>
      <link>https://dev.to/fast_data_asap/i-added-10-validation-rules-to-an-e-invoice-parser-without-changing-one-existing-warning-here-is-3l05</link>
      <guid>https://dev.to/fast_data_asap/i-added-10-validation-rules-to-an-e-invoice-parser-without-changing-one-existing-warning-here-is-3l05</guid>
      <description>&lt;p&gt;The validation output of my e-invoice parser had a design flaw: it was a list of English sentences.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"validationWarnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Tax basis + VAT = 1728.70, but grand total is 1729 (BR-CO-15)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fine for a human. Bad for an accounts-payable workflow that wants to block on real errors, only log hints, and show the user &lt;em&gt;which field&lt;/em&gt; is wrong. The only way to do that was to parse the strings. And once output is published, it is an API, whether you like it or not.&lt;/p&gt;

&lt;p&gt;So this week's job was: add machine-readable findings and more rules &lt;strong&gt;without changing a single existing string&lt;/strong&gt;, because any workflow built on the old output has to keep working.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built the &lt;a href="https://apify.com/fast_data_asap/xrechnung-zugferd-einvoice-parser" rel="noopener noreferrer"&gt;XRechnung &amp;amp; ZUGFeRD Parser&lt;/a&gt; on Apify. This post is about the testing approach, which works for any parser.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The new shape
&lt;/h2&gt;

&lt;p&gt;Every finding now has a rule ID, a severity and the business term it refers to. The message is exactly the old string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"validationFindings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"BR-04"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"BT-3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Missing required field BT-3 type code"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"BR-CO-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"BT-112"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Tax basis + VAT = 1728.70, but grand total is 1729 (BR-CO-15)"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"BR-CO-16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"BT-115"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Grand total - prepaid + rounding = 729.30, but amount due is 729 (BR-CO-16)"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nl"&gt;"errorCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nl"&gt;"warningCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is real output for a UBL credit-note sample from the Mustang project's test files. &lt;code&gt;validationWarnings&lt;/code&gt; is still there and still a list of strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safety net: old code vs new code on public invoices
&lt;/h2&gt;

&lt;p&gt;I did not write new unit tests first. I froze the old module and compared it with the new one on real files, a golden-master test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;importlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;importlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spec_from_file_location&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;importlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;module_from_spec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec_module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mod&lt;/span&gt;

&lt;span class="nf"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"old"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"new"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;".xml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;".pdf"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"rb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. every old warning must still be there, same text, same order&lt;/span&gt;
    &lt;span class="n"&gt;old_w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"validationWarnings"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"validationWarnings"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;prefix_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old_w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;old_w&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. every other old field must be unchanged&lt;/span&gt;
    &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"validationWarnings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"validationWarningCount"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;prefix_ok&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"prefix_ok ="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix_ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"diff ="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"files with differences:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules do the work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Old warnings are a prefix of the new list.&lt;/strong&gt; New checks may only append. This forces the code structure too: the historical checks stay in one block in their original order, new checks come after it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every old field is equal.&lt;/strong&gt; New fields are allowed, changed ones are not.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The test data is public
&lt;/h2&gt;

&lt;p&gt;No customer invoices needed. Two open sources cover most real-world shapes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;KoSIT XRechnung test suite&lt;/strong&gt;: 66 standard cases in UBL and CII syntax (invoices, corrections, self-billing),&lt;/li&gt;
&lt;li&gt;sample files from the &lt;strong&gt;Mustang project&lt;/strong&gt;: 7 files including a ZUGFeRD PDF, a Factur-X MINIMUM file and a UBL credit note.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the run showed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result on 73 public files&lt;/th&gt;
&lt;th&gt;Files&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Old output identical, no findings&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old output identical, findings unchanged (now with rule IDs)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old output differs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one difference was a good catch. For CII invoices I had added &lt;code&gt;contactDepartment&lt;/code&gt; to the existing &lt;code&gt;seller&lt;/code&gt; object, because rule BR-DE-5 accepts a department name as contact point. A new key inside an existing object is still a diff. It is additive, so I kept it, but I only knew about it because the script compares whole objects and not just the fields I had in mind.&lt;/p&gt;

&lt;p&gt;The two files with findings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Findings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mustang &lt;code&gt;ubl-creditnote.xml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;BR-04, BR-CO-15, BR-CO-16 (all error)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mustang &lt;code&gt;facturFrMinimum.xml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;BR-CO-16 (warning)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All 66 KoSIT standard cases produce zero findings, which is what you want from a rule set: valid invoices stay quiet.&lt;/p&gt;

&lt;p&gt;Quiet files prove nothing about new rules though, so I derived four variants from one KoSIT case, three of them deliberately broken:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Own test file&lt;/th&gt;
&lt;th&gt;New findings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Credit note (381) without reference&lt;/td&gt;
&lt;td&gt;FD-PRECEDING (warning)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credit note (381) with reference&lt;/td&gt;
&lt;td&gt;none, &lt;code&gt;precedingInvoice&lt;/code&gt; = &lt;code&gt;{number: "123456XX", date: "2016-04-04"}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deliberately broken XRechnung (UBL)&lt;/td&gt;
&lt;td&gt;BR-CO-16, BR-DE-6, BR-DE-9, BR-DE-23-a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XRechnung CII without seller contact&lt;/td&gt;
&lt;td&gt;BR-DE-2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Three decisions that mattered more than the code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Severity depends on the profile.&lt;/strong&gt; A Factur-X MINIMUM file is not a full EN 16931 invoice by design. Reporting its missing fields as errors would make every such file "broken". So missing EN fields are errors for full profiles and warnings for MINIMUM and BASIC WL. That is why the same rule (BR-CO-16) appears as error in one row above and as warning in the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never invent an official rule number.&lt;/strong&gt; The IDs were checked against the public KoSIT and EN 16931 Schematron files. For my own plausibility checks, such as "credit note without a preceding invoice reference", I use a private prefix (&lt;code&gt;FD-...&lt;/code&gt;) instead of borrowing a BR number that says something slightly different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Say what it is not.&lt;/strong&gt; This is a set of plausibility checks, not a Schematron validation. An invoice with &lt;code&gt;errorCount: 0&lt;/code&gt; can still be rejected by the official validator. That sentence is now in the README, next to the rule table.&lt;/p&gt;

&lt;h2&gt;
  
  
  One gap the test did not close
&lt;/h2&gt;

&lt;p&gt;On the CII file without a contact, BR-DE-7 (seller e-mail) stays silent: the parser has always filled the e-mail from the electronic address when its scheme is &lt;code&gt;EM&lt;/code&gt;. Changing that would alter an existing field, so rule 2 of the test says no. It is in the backlog as a documented limitation instead of a silent behaviour change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Published strings are an API. Freeze them and add structure next to them.&lt;/li&gt;
&lt;li&gt;A short old-vs-new script over public sample files finds what you did not think of.&lt;/li&gt;
&lt;li&gt;Valid files must stay quiet; you need deliberately broken files to see new rules fire.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The parser turns XRechnung (UBL/CII) and ZUGFeRD/Factur-X PDFs into JSON with the findings above, for $0.003 per invoice: &lt;a href="https://apify.com/fast_data_asap/xrechnung-zugferd-einvoice-parser" rel="noopener noreferrer"&gt;XRechnung &amp;amp; ZUGFeRD Parser on Apify&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question:&lt;/strong&gt; how do you keep old output stable when you extend a parser or an API: golden files, contract tests, something else? And have you been bitten by "additive" changes that still broke a consumer?&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>api</category>
      <category>fintech</category>
    </item>
    <item>
      <title>How to check a new supplier's company data (LEI, register number, VAT) with Python before the first payment</title>
      <dc:creator>Fast Data</dc:creator>
      <pubDate>Tue, 10 Nov 2026 12:44:57 +0000</pubDate>
      <link>https://dev.to/fast_data_asap/how-to-check-a-new-suppliers-company-data-lei-register-number-vat-with-python-before-the-first-3924</link>
      <guid>https://dev.to/fast_data_asap/how-to-check-a-new-suppliers-company-data-lei-register-number-vat-with-python-before-the-first-3924</guid>
      <description>&lt;p&gt;Before a new supplier gets paid for the first time, someone in finance usually does the same three checks by hand: &lt;em&gt;Does this company officially exist? Is the VAT number valid? Who is behind it?&lt;/em&gt; Each answer lives in a different place, and none of them is fun to copy-paste.&lt;/p&gt;

&lt;p&gt;Two of those places are official and free to query:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GLEIF&lt;/strong&gt;, the global registry of &lt;strong&gt;Legal Entity Identifiers (LEI)&lt;/strong&gt;: legal name, legal form, register number, addresses, status and parent companies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VIES&lt;/strong&gt;, the European Commission's service that confirms whether an EU VAT number is valid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post shows a short script that runs both checks for a whole supplier list and returns &lt;strong&gt;one row per company with warning flags&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built the &lt;a href="https://apify.com/fast_data_asap/dach-company-kyb-official" rel="noopener noreferrer"&gt;EU Company Check (KYB)&lt;/a&gt; actor used here. It only queries the official GLEIF and VIES APIs: no register scraping, no data about people.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother: one in five German LEIs has lapsed
&lt;/h2&gt;

&lt;p&gt;I asked the official GLEIF API today (10 November 2026):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;LEIs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Worldwide&lt;/td&gt;
&lt;td&gt;3,489,248&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Germany (legal address)&lt;/td&gt;
&lt;td&gt;259,636&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Germany, status ISSUED (renewed)&lt;/td&gt;
&lt;td&gt;186,894&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Germany, status LAPSED (not renewed)&lt;/td&gt;
&lt;td&gt;55,763&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A lapsed LEI is not proof of anything bad, but it means the company stopped confirming its data every year. Combined with a VAT number that VIES does not recognise, it is a good reason to pick up the phone before you pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install apify-client&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"APIFY_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;suppliers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"SAP SE; DE143454214"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# name + VAT number after a semicolon&lt;/span&gt;
    &lt;span class="s2"&gt;"Siemens Healthineers AG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# name only&lt;/span&gt;
    &lt;span class="s2"&gt;"HRB 719915"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;# German register number&lt;/span&gt;
    &lt;span class="s2"&gt;"Nonexistent Testfirma XYZ GmbH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"fast_data_asap/dach-company-kyb-official"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"companies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;suppliers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# filter matches to German entities&lt;/span&gt;
    &lt;span class="s2"&gt;"includeParents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# add direct and ultimate parent&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"defaultDatasetId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"supplier_check.csv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"legalName"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"registeredAs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"lei"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"registrationStatus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ultimateParent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"vatValid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"flags"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ultimateParent"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"legalName"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;vat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"vat"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"legalName"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"registeredAs"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"lei"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"registrationStatus"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"; "&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"flags"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;needs_review&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"flags"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{len(rows)} checked, {len(needs_review)} need a human look"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The input accepts a &lt;strong&gt;name&lt;/strong&gt;, an &lt;strong&gt;LEI&lt;/strong&gt; or a &lt;strong&gt;register number&lt;/strong&gt; and detects which one it is. A VAT number can follow a semicolon.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came back
&lt;/h2&gt;

&lt;p&gt;This is the real result of a test run on 10 November 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Legal name&lt;/th&gt;
&lt;th&gt;Register no.&lt;/th&gt;
&lt;th&gt;LEI status&lt;/th&gt;
&lt;th&gt;Ultimate parent&lt;/th&gt;
&lt;th&gt;VAT&lt;/th&gt;
&lt;th&gt;Flags&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SAP SE; DE143454214&lt;/td&gt;
&lt;td&gt;SAP SE&lt;/td&gt;
&lt;td&gt;HRB 719915&lt;/td&gt;
&lt;td&gt;ISSUED&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;Multiple matches (2) - showing 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Siemens Healthineers AG&lt;/td&gt;
&lt;td&gt;Siemens Healthineers AG&lt;/td&gt;
&lt;td&gt;HRB 237558&lt;/td&gt;
&lt;td&gt;ISSUED&lt;/td&gt;
&lt;td&gt;Siemens Aktiengesellschaft&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HRB 719915&lt;/td&gt;
&lt;td&gt;SAP SE&lt;/td&gt;
&lt;td&gt;HRB 719915&lt;/td&gt;
&lt;td&gt;ISSUED&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nonexistent Testfirma XYZ GmbH&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;No match&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things worth noticing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The parent company comes for free.&lt;/strong&gt; Siemens Healthineers shows Siemens AG as direct and ultimate parent, with its LEI. That is the question procurement teams ask most often and the hardest one to answer by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"No match" is a result, not an error.&lt;/strong&gt; The made-up company returns a row flagged &lt;code&gt;No match&lt;/code&gt; (and is not charged), so your list stays complete and the gap is visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Even big names get flagged.&lt;/strong&gt; GLEIF has two records with the exact legal name "SAP SE". The actor picks the active, issued one and tells you it had a choice. Register numbers are not unique across German register courts either, which is why the &lt;code&gt;country&lt;/code&gt; filter matters.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The flags, and what to do with them
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Typical next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;No match&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nothing in GLEIF for this name/number&lt;/td&gt;
&lt;td&gt;Ask the supplier for its LEI or register extract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LEI lapsed (not renewed)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Company stopped confirming its data&lt;/td&gt;
&lt;td&gt;Check if the company is still active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Fuzzy name match - verify&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No exact name, closest full-text hit shown&lt;/td&gt;
&lt;td&gt;Compare address and register number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VAT invalid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VIES does not confirm the VAT number&lt;/td&gt;
&lt;td&gt;Do not pay before clarifying&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Name differs from VAT name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VAT number belongs to a differently named entity&lt;/td&gt;
&lt;td&gt;Check for typos or a different group company&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Multiple matches (n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Several records fit&lt;/td&gt;
&lt;td&gt;Add the register number or LEI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The point is not to block payments automatically. It is to let the 90% of clean rows through and put the rest in front of a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it on every new supplier
&lt;/h2&gt;

&lt;p&gt;Two easy ways to make this routine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In your ERP export:&lt;/strong&gt; run the script nightly on suppliers created that day and post rows with flags to Slack or e-mail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For AI agents:&lt;/strong&gt; the actor is available through Apify's MCP server, so an accounts-payable agent can call it before it proposes a payment. Our &lt;a href="https://github.com/ichkifokkus/data-recipes/tree/main/mcp" rel="noopener noreferrer"&gt;MCP setup&lt;/a&gt; lists it in the finance pack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a fuller check, combine it with an &lt;a href="https://apify.com/fast_data_asap/iban-validator-bank-lookup" rel="noopener noreferrer"&gt;IBAN validation&lt;/a&gt; and a screen against &lt;a href="https://apify.com/fast_data_asap/bafin-investor-warnings" rel="noopener noreferrer"&gt;BaFin warnings&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Only companies &lt;strong&gt;with an LEI&lt;/strong&gt; are in GLEIF. Many small businesses never needed one, so &lt;code&gt;No match&lt;/code&gt; for a local craftsman is normal.&lt;/li&gt;
&lt;li&gt;This is not a commercial register extract, a credit check or legal advice. It confirms identity data from official sources; the decision stays with you.&lt;/li&gt;
&lt;li&gt;VIES depends on national systems and is sometimes unavailable; the actor flags &lt;code&gt;VAT check unavailable&lt;/code&gt; instead of guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;$0.004 per company found; rows without a match are free. Checking 100 new suppliers a month costs at most $0.40.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://apify.com/fast_data_asap/dach-company-kyb-official" rel="noopener noreferrer"&gt;EU Company Check (KYB) on Apify&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question for finance and procurement folks:&lt;/strong&gt; which check do you do first when a new supplier shows up, and which one has actually caught a problem for you?&lt;/p&gt;

</description>
      <category>python</category>
      <category>fintech</category>
      <category>api</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to get EU public tenders from TED as JSON in Python (without learning expert-query syntax)</title>
      <dc:creator>Fast Data</dc:creator>
      <pubDate>Tue, 10 Nov 2026 07:44:38 +0000</pubDate>
      <link>https://dev.to/fast_data_asap/how-to-get-eu-public-tenders-from-ted-as-json-in-python-without-learning-expert-query-syntax-3pig</link>
      <guid>https://dev.to/fast_data_asap/how-to-get-eu-public-tenders-from-ted-as-json-in-python-without-learning-expert-query-syntax-3pig</guid>
      <description>&lt;p&gt;Every public tender above the EU thresholds, from a German city's software project to a French hospital's equipment purchase, is published in &lt;strong&gt;TED (Tenders Electronic Daily)&lt;/strong&gt;, the EU's official procurement journal. TED has a free, official search API. The catch is its &lt;em&gt;expert query&lt;/em&gt; syntax, its nested multilingual fields and a single notice that can contain several lots with different deadlines and values.&lt;/p&gt;

&lt;p&gt;This post shows a script that answers one practical question: &lt;strong&gt;"Which open tenders match what we sell, in the countries we serve, and are still biddable?"&lt;/strong&gt; You get flat JSON and a CSV.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built the &lt;a href="https://apify.com/fast_data_asap/eu-tenders-ted-api" rel="noopener noreferrer"&gt;EU Tenders (TED) API&lt;/a&gt; actor used below. It calls the official TED Search API v3 and only returns organisations, never contact persons.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 20-line version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install apify-client&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"APIFY_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;run_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"software"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"countries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"DEU"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"AUT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# ISO alpha-3 buyer countries&lt;/span&gt;
    &lt;span class="s2"&gt;"cpvCodes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"72"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"48"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# IT services, software packages&lt;/span&gt;
    &lt;span class="s2"&gt;"noticeTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"competition"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# open calls for tenders&lt;/span&gt;
    &lt;span class="s2"&gt;"publishedWithinDays"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"deadlineAfter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"today"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# skip expired tenders&lt;/span&gt;
    &lt;span class="s2"&gt;"maxResults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"fast_data_asap/eu-tenders-ted-api"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;notices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"defaultDatasetId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;notices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"deadline"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;"9999"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"deadline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"buyerName"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"buyerCity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"buyerCountry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"estimatedValue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"tenders.csv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extrasaction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ignore"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"open tenders written to tenders.csv"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No browser, no HTML parsing. The actor turns the inputs into a TED expert query, follows pagination and flattens each notice into one row.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes back
&lt;/h2&gt;

&lt;p&gt;These are real rows from test runs on 8 and 9 November 2026 (shortened):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deadline&lt;/th&gt;
&lt;th&gt;Buyer&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Est. value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;02.12.2026&lt;/td&gt;
&lt;td&gt;MD-IT GmbH, Berlin&lt;/td&gt;
&lt;td&gt;Framework agreement: AI for hospital billing review, compliance, AI governance (3 lots)&lt;/td&gt;
&lt;td&gt;€121,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07.12.2026&lt;/td&gt;
&lt;td&gt;Stadt Brandis&lt;/td&gt;
&lt;td&gt;Development of OpenCore modules for the PartheCloud&lt;/td&gt;
&lt;td&gt;€625,600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;08.12.2026&lt;/td&gt;
&lt;td&gt;BWI GmbH, Bonn&lt;/td&gt;
&lt;td&gt;Framework contract, notice "1961-JC-NoCodeDB" (software packages)&lt;/td&gt;
&lt;td&gt;not published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row has the same flat fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"noticeId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"779062-2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Germany – Programming services of application software – Entwicklungsleistungen für OpenCore-Module der PartheCloud"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"buyerName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Stadt Brandis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"buyerCountry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"DEU"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"buyerCity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Brandis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"cpvMain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"72212000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"cpvCodes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"72212000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"72262000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"72263000"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"formType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"competition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"procedureType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"estimatedValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;625600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"publishedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-11-09"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"deadline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-12-07T12:00:00+01:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"lotsCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"https://ted.europa.eu/en/notice/-/detail/779062-2026"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that save you work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;deadline&lt;/code&gt;&lt;/strong&gt; is the &lt;em&gt;earliest&lt;/em&gt; lot deadline as an ISO timestamp, so sorting and "days left" are one line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;estimatedValue&lt;/code&gt;&lt;/strong&gt; is summed across lots. It is &lt;code&gt;null&lt;/code&gt; when the buyer did not publish a value (BWI above), which is common.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Choosing filters that actually match your business
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CPV prefixes&lt;/strong&gt; are the most useful filter. CPV is the EU's product vocabulary, and a prefix matches everything below it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Covers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;72&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IT services (consulting, development, support)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;48&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Software packages and information systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;79&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Business services (consulting, marketing, translation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;45&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Construction work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;90&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cleaning, waste and environmental services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;33&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Medical equipment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keywords are matched in full text in any language, so &lt;code&gt;["software", "Software", "logiciel"]&lt;/code&gt; widens the net across countries. Start broad, look at a week of results, then add &lt;code&gt;minValueEur&lt;/code&gt; if small contracts are noise for you. Notices without a published value are kept, so you do not silently lose them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want to know who wins?&lt;/strong&gt; Switch &lt;code&gt;noticeTypes&lt;/code&gt; to &lt;code&gt;["result"]&lt;/code&gt;. Contract award notices show which buyer bought what, and often for how much: useful for competitor research and pricing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it every morning
&lt;/h2&gt;

&lt;p&gt;For a daily alert set &lt;code&gt;publishedWithinDays&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt; (one day overlap as a safety net) and de-duplicate on &lt;code&gt;noticeId&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;

&lt;span class="n"&gt;seen_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"seen.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seen_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;notices&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"noticeId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;seen_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"noticeId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;'{n["deadline"][:10]}  {n["buyerName"]}: {n["title"][:90]}\n  {n["url"]}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule the script with cron or a GitHub Action, or use an Apify schedule and pipe the dataset into Slack or a sheet. If you prefer no-code, our free &lt;a href="https://github.com/ichkifokkus/data-recipes/tree/main/9-sales-signals-daily-feed" rel="noopener noreferrer"&gt;n8n sales-signal workflow&lt;/a&gt; does the same for German tenders and can take TED as a third branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TED lists notices &lt;strong&gt;above the EU thresholds&lt;/strong&gt; (plus voluntary ones). Smaller national tenders are on national portals; for Germany there is our &lt;a href="https://apify.com/fast_data_asap/german-public-tenders-bund" rel="noopener noreferrer"&gt;German Public Tenders actor&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Titles and summaries come in the notice language. The actor prefers your &lt;code&gt;language&lt;/code&gt; (English by default) where the notice is multilingual.&lt;/li&gt;
&lt;li&gt;This is a search tool, not procurement or legal advice. Always read the official notice before bidding.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;$0.002 per notice returned, and empty runs are free. The 50-notice daily run above costs at most $0.10; a typical "last two days, two countries, two CPV codes" run returns far fewer rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://apify.com/fast_data_asap/eu-tenders-ted-api" rel="noopener noreferrer"&gt;EU Tenders (TED) API on Apify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Agent users: the free &lt;a href="https://github.com/ichkifokkus/data-recipes/tree/main/skills/find-public-tenders" rel="noopener noreferrer"&gt;&lt;code&gt;find-public-tenders&lt;/code&gt; skill&lt;/a&gt; lets an AI agent run this search for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question for bid managers:&lt;/strong&gt; which filter do you rely on most, CPV codes or keywords, and how many false positives do you accept per day before an alert becomes noise?&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>govtech</category>
      <category>sales</category>
    </item>
    <item>
      <title>How long do you get to bid on a German public tender? I checked 187 notices</title>
      <dc:creator>Fast Data</dc:creator>
      <pubDate>Mon, 09 Nov 2026 17:45:42 +0000</pubDate>
      <link>https://dev.to/fast_data_asap/how-long-do-you-get-to-bid-on-a-german-public-tender-i-checked-187-notices-1j04</link>
      <guid>https://dev.to/fast_data_asap/how-long-do-you-get-to-bid-on-a-german-public-tender-i-checked-187-notices-1j04</guid>
      <description>&lt;p&gt;If you sell to the public sector in Germany, the clock starts the moment a tender is published. So I wanted a simple number: &lt;strong&gt;how many days are there between publication and the bid deadline?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I took two daily snapshots (8 and 9 November 2026) of the public tender RSS feed on &lt;strong&gt;service.bund.de&lt;/strong&gt;, the federal portal that also aggregates many state and municipal contracting bodies. After de-duplication that is &lt;strong&gt;195 tenders&lt;/strong&gt; published between 6 and 9 November. &lt;strong&gt;187&lt;/strong&gt; of them state an offer deadline (&lt;em&gt;Angebotsfrist&lt;/em&gt;) in the feed.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built the &lt;a href="https://apify.com/fast_data_asap/german-public-tenders-bund" rel="noopener noreferrer"&gt;German Public Tenders&lt;/a&gt; actor mentioned at the end. The analysis below uses only the free public RSS feed.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxwtowl3d8yuf73yagllx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxwtowl3d8yuf73yagllx.png" alt="Distribution of days between publication and bid deadline" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Days to bid deadline&lt;/th&gt;
&lt;th&gt;Tenders&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;up to 14&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15–21&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22–30&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;td&gt;47%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31–45&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;over 45&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Median: 30 days.&lt;/strong&gt; Shortest: 5 days. Longest: 64 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;40 tenders (21%) give you three weeks or less.&lt;/strong&gt; That is roughly one in five.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What this means in practice: if you check the portal once a month, you will miss most of the short ones entirely, and you will see the typical 30-day tender with only a week or two left. If public customers matter to you, the check has to be daily, or at least a few times a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is construction faster than IT?
&lt;/h2&gt;

&lt;p&gt;I grouped the titles with a crude keyword match (construction terms like &lt;em&gt;Sanierung&lt;/em&gt;, &lt;em&gt;Dach&lt;/em&gt;, &lt;em&gt;Elektro&lt;/em&gt;, &lt;em&gt;Los 3&lt;/em&gt;; IT terms like &lt;em&gt;Software&lt;/em&gt;, &lt;em&gt;Lizenz&lt;/em&gt;, &lt;em&gt;Server&lt;/em&gt;). It is rough, so treat it as a hint, not a finding:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group (by title keywords)&lt;/th&gt;
&lt;th&gt;Tenders with deadline&lt;/th&gt;
&lt;th&gt;Median days&lt;/th&gt;
&lt;th&gt;≤ 21 days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Construction&lt;/td&gt;
&lt;td&gt;77&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supplies&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Services&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IT / software&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;29.5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other / unclear&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The medians are almost identical. In this sample, the type of work does not predict the deadline; the individual contracting body does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the tenders come from
&lt;/h2&gt;

&lt;p&gt;German postal codes start with a zone digit, which gives a quick regional picture without parsing addresses:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Postal zone&lt;/th&gt;
&lt;th&gt;Rough region&lt;/th&gt;
&lt;th&gt;Tenders&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Cologne, Bonn, Aachen, Koblenz, Trier&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Saxony, East Thuringia, south Saxony-Anhalt&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Ruhr area, Düsseldorf, Münsterland&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Hanover, North Hesse, parts of Saxony-Anhalt&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Berlin, Brandenburg, Mecklenburg-Vorpommern&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Nuremberg, North Bavaria, most of Thuringia&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Hamburg, Bremen, Schleswig-Holstein&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Frankfurt, Saarland, Mannheim&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Stuttgart, Karlsruhe&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Munich, South Bavaria&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Careful with this one. Several German states run their own procurement portals, so a low count here means "few of this region's tenders flow into this feed", not "few tenders in this region". If you only watch one portal, you only see part of the market.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repeat it yourself
&lt;/h2&gt;

&lt;p&gt;The feed is public and needs no key. This script fetches the newest 100 entries and computes the same distribution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;FEED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.service.bund.de/Content/DE/Ausschreibungen/Suche/Formular.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?nn=4641482&amp;amp;type=0&amp;amp;resultsPerPage=100&amp;amp;sortOrder=dateOfIssue_dt+desc&amp;amp;jobsrss=true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tender-deadline-study/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;item&amp;gt;(.*?)&amp;lt;/item&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;pubDate&amp;gt;(.*?)&amp;lt;/pubDate&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unescape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;description&amp;gt;(.*?)&amp;lt;/description&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Angebotsfrist:\s*&amp;lt;strong&amp;gt;(\d\d\.\d\d\.\d{4})&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;published&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parsedate_to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%d.%m.%Y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenders, median&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3 weeks or less:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it once a day and store the result, and after a month you have a much better sample than my four days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small sample.&lt;/strong&gt; Four publication days, one portal. The shape (most deadlines around 30 days, a fat tail of short ones) is plausible, the exact percentages are not a statistic for all of Germany.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feed dates.&lt;/strong&gt; &lt;code&gt;pubDate&lt;/code&gt; is when the notice entered the feed, which can be a day after the official publication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing deadlines.&lt;/strong&gt; 8 of 195 entries had no offer deadline in the feed, only an end of publication.&lt;/li&gt;
&lt;li&gt;This is not legal or procurement advice. The official notice is what counts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you want the deadline, contracting authority, state, procedure type and CPV codes as clean JSON instead of parsing HTML in RSS descriptions, the &lt;a href="https://apify.com/fast_data_asap/german-public-tenders-bund" rel="noopener noreferrer"&gt;German Public Tenders actor&lt;/a&gt; does that for $0.002 per tender. For EU-wide notices there is the &lt;a href="https://apify.com/fast_data_asap/eu-tenders-ted-api" rel="noopener noreferrer"&gt;EU Tenders (TED) API&lt;/a&gt; at the same price, and a ready-made n8n workflow that turns both into a daily Slack digest lives in our &lt;a href="https://github.com/ichkifokkus/data-recipes/tree/main/9-sales-signals-daily-feed" rel="noopener noreferrer"&gt;data-recipes repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question for anyone who bids on public contracts:&lt;/strong&gt; what is the shortest realistic time you need from "we found it" to a complete bid? I am trying to decide where to set the "urgent" threshold in the alert.&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>sales</category>
      <category>govtech</category>
    </item>
    <item>
      <title>How to build a daily B2B sales-signal feed from public tenders and hiring data with n8n</title>
      <dc:creator>Fast Data</dc:creator>
      <pubDate>Mon, 09 Nov 2026 12:45:43 +0000</pubDate>
      <link>https://dev.to/fast_data_asap/how-to-build-a-daily-b2b-sales-signal-feed-from-public-tenders-and-hiring-data-with-n8n-3f77</link>
      <guid>https://dev.to/fast_data_asap/how-to-build-a-daily-b2b-sales-signal-feed-from-public-tenders-and-hiring-data-with-n8n-3f77</guid>
      <description>&lt;p&gt;Most "intent data" tools sell you a score without telling you where it comes from. But two of the strongest B2B buying signals are public and free to read:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A public body publishes a tender.&lt;/strong&gt; It has a budget, a need and a deadline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A company opens several jobs at once.&lt;/strong&gt; It is growing, reorganising or starting a project, and it will buy tools and services for it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post is a ready-to-import n8n workflow that collects both every weekday morning, turns them into one feed with a simple score, and sends only the new signals to Slack, a Google Sheet and, if you want, Pipedrive or HubSpot.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built the two Apify actors used here, &lt;a href="https://apify.com/fast_data_asap/german-public-tenders-bund" rel="noopener noreferrer"&gt;German Public Tenders&lt;/a&gt; and &lt;a href="https://apify.com/fast_data_asap/career-site-jobs-api" rel="noopener noreferrer"&gt;Career Site Jobs API&lt;/a&gt;. The workflow is free in our &lt;a href="https://github.com/ichkifokkus/data-recipes/tree/main/9-sales-signals-daily-feed" rel="noopener noreferrer"&gt;data-recipes repo&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the feed looks like
&lt;/h2&gt;

&lt;p&gt;This is a real run from 8 November 2026 (keyword "Software", three target companies), passed through the workflow's own Code nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sales signals: 6 new (1 high, 3 medium, 2 low)
2 public tender(s), 4 job posting(s)

[high] Tender: Hochschule Zittau/Görlitz - Bereitstellung einer virtuellen Plattform ... - due 2026-11-13
    5 day(s) to end of publication
[medium] Tender: Stadt Regensburg - Einfuehrung und Nutzung eines Archivinformationssystems ... - due 2026-12-07
    30 day(s) to offer deadline
[medium] Hiring: Tibber - Pricing &amp;amp; Project Lead
    2 role(s) posted at Tibber in the last 7 day(s)
...and 2 more in the sheet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every signal also lands as one row in a sheet: &lt;code&gt;signalId, found_at, signalType, organisation, title, location, date, deadline, url, score, reason&lt;/code&gt;. Only organisations appear, never people or contact details.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Weekdays 7:00 → Settings ─┬─ Get public tenders ─┐
                          └─ Get hiring signals ─┴─ Combine → Normalize &amp;amp; score
  → Read signal log → Keep new signals only ─┬─ Log new signals (Sheets)
                                             ├─ Build Slack digest → Post to Slack
                                             └─ High score only → Create Pipedrive activity (disabled)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All nodes are built in. The two data calls are plain HTTP Request nodes against Apify's synchronous endpoint, which runs the actor and returns its results in one step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://api.apify.com/v2/acts/fast_data_asap~german-public-tenders-bund/run-sync-get-dataset-items
Authorization: Bearer &amp;lt;your Apify token&amp;gt;   (Header Auth credential in n8n)

{"keywords": ["Software"], "states": [], "excludeKeywords": [], "lastDays": 2, "maxItems": 50, "includeDetails": true}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://api.apify.com/v2/acts/fast_data_asap~career-site-jobs-api/run-sync-get-dataset-items

{"companies": ["teamtailor:tibber", "ashby:n8n", "greenhouse:celonis"], "postedWithinDays": 2, "maxJobsPerCompany": 25, "includeDescription": false}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tender actor reads service.bund.de, the German federal portal that aggregates many federal, state and municipal publishers, and adds details such as contracting authority, place of performance, procedure type, CPV codes and the offer deadline. The jobs actor reads the public job-board APIs behind company career sites (Greenhouse, Lever, Ashby, Personio, Teamtailor, Workday and others), so there is no fragile HTML scraping.&lt;/p&gt;

&lt;p&gt;All inputs live in one &lt;strong&gt;Settings&lt;/strong&gt; node: keywords, optional German states, words to exclude, and your list of target companies. The lookback window is automatic: four days on Mondays to cover the weekend, two days otherwise. The extra day is a safety net if a run fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  One schema, two simple scores
&lt;/h2&gt;

&lt;p&gt;The &lt;em&gt;Normalize &amp;amp; score&lt;/em&gt; Code node maps both sources to the same fields and applies two rules you can explain to any salesperson:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tender&lt;/strong&gt;: if the offer deadline (or, when none is published, the end of publication) is less than 21 days away, it is &lt;strong&gt;high&lt;/strong&gt;. Someone has to make a bid/no-bid decision this week. Otherwise &lt;strong&gt;medium&lt;/strong&gt;. Tenders whose deadline has already passed are dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hiring&lt;/strong&gt;: three or more roles at the same company in the window is &lt;strong&gt;high&lt;/strong&gt;. Two roles, or one leadership role (Head of, Director, VP), is &lt;strong&gt;medium&lt;/strong&gt;. A single other role is &lt;strong&gt;low&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;High&lt;/th&gt;
&lt;th&gt;Medium&lt;/th&gt;
&lt;th&gt;Low&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tender&lt;/td&gt;
&lt;td&gt;deadline &amp;lt; 21 days away&lt;/td&gt;
&lt;td&gt;deadline 21+ days away&lt;/td&gt;
&lt;td&gt;– (expired tenders are dropped)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hiring&lt;/td&gt;
&lt;td&gt;3+ roles at one company&lt;/td&gt;
&lt;td&gt;2 roles, or 1 leadership role&lt;/td&gt;
&lt;td&gt;1 other role&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both thresholds are constants at the top of the node. In practice you will want to tune them: count only Sales or Engineering titles, or give extra weight to your core regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deduplication with a Google Sheet
&lt;/h2&gt;

&lt;p&gt;Instead of n8n's internal "seen before" memory, the workflow reads the &lt;code&gt;signalId&lt;/code&gt; column of the sheet and drops anything that is already there. The ID is simply the type plus the source URL (&lt;code&gt;tender:https://...&lt;/code&gt;, &lt;code&gt;job:https://...&lt;/code&gt;). The advantage: the memory is visible. If a rep wants to see a signal again, they delete the row.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the test showed
&lt;/h2&gt;

&lt;p&gt;I ran each actor once with small limits (256 MB, a few results) on a Sunday:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tenders&lt;/strong&gt;: the keyword "Software" with a 2-day window returned &lt;strong&gt;2 tenders&lt;/strong&gt; (the search page had 100 results, 2 inside the window). One had a published offer deadline 30 days out; the other had no deadline, only an end of publication 5 days later, which is why the score falls back to that date. Weekdays bring more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jobs&lt;/strong&gt;: Tibber had &lt;strong&gt;13 open roles&lt;/strong&gt;, n8n &lt;strong&gt;48&lt;/strong&gt; and Celonis &lt;strong&gt;233&lt;/strong&gt;; with &lt;code&gt;postedWithinDays: 7&lt;/code&gt; and a cap of 1-2 jobs per company the run returned 4 postings. &lt;code&gt;personio:personio&lt;/code&gt; returned 0, a reminder to check each company's career-site URL once.&lt;/li&gt;
&lt;li&gt;Both calls finished in under 6 seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Both actors charge per result: &lt;strong&gt;$0.002 per tender&lt;/strong&gt; and &lt;strong&gt;$0.002 per job posting&lt;/strong&gt; on the Free and Starter plans. The test run returned 6 results, so $0.012. A realistic day with 10 tenders and 15 new jobs, each returned twice because of the overlap day, is about $0.10, or roughly &lt;strong&gt;$2 per month&lt;/strong&gt; for 21 weekdays. You control the ceiling with &lt;code&gt;maxTenders&lt;/code&gt; and &lt;code&gt;maxJobsPerCompany&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tenders cover &lt;strong&gt;Germany via one portal&lt;/strong&gt;. Not every contracting body publishes there, and some titles are truncated by the portal. For EU-wide notices, our &lt;a href="https://apify.com/fast_data_asap/eu-tenders-ted-api" rel="noopener noreferrer"&gt;EU Tenders (TED) API&lt;/a&gt; ($0.002 per notice) can be added as a third HTTP branch with the same schema mapping.&lt;/li&gt;
&lt;li&gt;Hiring data is only as good as the career site. Custom career pages without a supported job board return nothing, and a re-posted job counts as new.&lt;/li&gt;
&lt;li&gt;This is a lead feed, not procurement or legal advice. Read the official notice before you bid.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup in ten minutes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Import &lt;code&gt;workflow.json&lt;/code&gt; from the &lt;a href="https://github.com/ichkifokkus/data-recipes/tree/main/9-sales-signals-daily-feed" rel="noopener noreferrer"&gt;repo folder&lt;/a&gt; into n8n.&lt;/li&gt;
&lt;li&gt;Create a Header Auth credential (&lt;code&gt;Authorization&lt;/code&gt; = &lt;code&gt;Bearer YOUR_APIFY_TOKEN&lt;/code&gt;) and select it in both HTTP nodes.&lt;/li&gt;
&lt;li&gt;Edit the Settings node.&lt;/li&gt;
&lt;li&gt;Create a sheet with a tab &lt;code&gt;Signals&lt;/code&gt; and the header row from the README, then connect Google Sheets and Slack.&lt;/li&gt;
&lt;li&gt;Optionally enable the Pipedrive node, run once, and activate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The workflow, the full README with the scoring table and the limits are in the &lt;a href="https://github.com/ichkifokkus/data-recipes/tree/main/9-sales-signals-daily-feed" rel="noopener noreferrer"&gt;data-recipes repo&lt;/a&gt;. Pull requests welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://apify.com/fast_data_asap/german-public-tenders-bund" rel="noopener noreferrer"&gt;German Public Tenders on Apify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/fast_data_asap/career-site-jobs-api" rel="noopener noreferrer"&gt;Career Site Jobs API on Apify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ichkifokkus/data-recipes/tree/main/9-sales-signals-daily-feed" rel="noopener noreferrer"&gt;Workflow + README on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question for the sales and RevOps people here:&lt;/strong&gt; which public signal has actually led to a meeting for you – tenders, hiring, funding news, or something else entirely? I would like to add the one that really works as the next branch.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>productivity</category>
      <category>sales</category>
    </item>
    <item>
      <title>How to build an AI accounts-payable check for German e-invoices with n8n</title>
      <dc:creator>Fast Data</dc:creator>
      <pubDate>Mon, 09 Nov 2026 07:44:03 +0000</pubDate>
      <link>https://dev.to/fast_data_asap/how-to-build-an-ai-accounts-payable-check-for-german-e-invoices-with-n8n-1844</link>
      <guid>https://dev.to/fast_data_asap/how-to-build-an-ai-accounts-payable-check-for-german-e-invoices-with-n8n-1844</guid>
      <description>&lt;p&gt;Germany is switching B2B invoicing to structured e-invoices. Since January 2025 every business must be able to receive them, and by 2028 most invoices between companies will be XRechnung XML or ZUGFeRD / Factur-X PDFs with embedded XML. For accounts payable that is good news: the IBAN, the supplier's VAT ID and the amounts are no longer pixels in a PDF, they are fields.&lt;/p&gt;

&lt;p&gt;Fields can be checked automatically. This post is a ready-to-import n8n workflow that does a first-line check on every incoming e-invoice and gives it a traffic light, before anyone presses "pay". It is also a good building block for an AI finance agent: the agent gets clean, verified facts instead of guessing from a PDF.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built the four Apify actors used here. The workflow is free in our &lt;a href="https://github.com/ichkifokkus/data-recipes/tree/main/8-ai-finance-assistant-einvoice-check" rel="noopener noreferrer"&gt;data-recipes repo&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You drop an invoice file into a Google Drive folder (or POST its URL to a webhook). Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parse&lt;/strong&gt; the invoice: XRechnung (UBL or CII) or ZUGFeRD / Factur-X PDF to flat JSON with seller, buyer, IBAN, totals, VAT breakdown and plausibility warnings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the IBAN&lt;/strong&gt;: checksum, length, SEPA flag, and the bank behind the bank code from the official Bundesbank file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the seller's VAT ID&lt;/strong&gt; against the EU Commission's VIES service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen the seller name&lt;/strong&gt; against current warnings of BaFin, the German financial regulator (unauthorised firms, scam websites, identity clones).&lt;/li&gt;
&lt;li&gt;A Code node turns the results into &lt;strong&gt;green / yellow / red with reasons&lt;/strong&gt;, appends a row to a Google Sheet &lt;code&gt;Eingangsrechnungen&lt;/code&gt;, and posts red invoices to Slack.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;All nodes are built in: Drive Trigger / Webhook → HTTP Request (parse) → three HTTP Requests (IBAN, VAT, BaFin) → Code (traffic light) → Google Sheets and Slack, with small Code nodes in between.&lt;/p&gt;

&lt;p&gt;Every HTTP Request calls Apify's synchronous endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://api.apify.com/v2/acts/fast_data_asap~xrechnung-zugferd-einvoice-parser/run-sync-get-dataset-items
Authorization: Bearer &amp;lt;your Apify token&amp;gt;   (Header Auth credential in n8n)

{ "fileUrls": ["https://.../invoice.xml"], "includeLineItems": false }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three check calls look the same with their own input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ibans"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"DE79000000001234567890"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"includeBankInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"vatNumbers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"DE123456789"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxRetries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"screen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"queries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Lieferant GmbH"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxMatchesPerQuery"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two design choices matter in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One call per actor per execution.&lt;/strong&gt; A Code node collects the unique IBANs, VAT IDs and seller names of all invoices in the run, and the check nodes use &lt;em&gt;Execute Once&lt;/em&gt;. Ten invoices from the same supplier cost one VAT check, not ten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checks never block the run.&lt;/strong&gt; The check nodes use &lt;em&gt;Always Output Data&lt;/em&gt; and &lt;em&gt;On Error: Continue&lt;/em&gt;. If VIES is down for Italy, that invoice turns yellow with "VIES could not answer"; the rest go through.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The traffic-light rules
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Red&lt;/strong&gt; (do not pay before a human looked at it): the IBAN is invalid, VIES says the VAT ID is invalid, the VAT ID has an impossible format, or the seller name matches a BaFin warning with a score of 85 or more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yellow&lt;/strong&gt; (could not verify): not a structured e-invoice, plausibility warnings from the parser (for example totals that do not add up), no IBAN on the invoice, bank code unknown, IBAN country differs from the seller country, VIES unavailable, VIES name differs from the invoice name, or a BaFin near-match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Green&lt;/strong&gt;: no automatic check found anything.&lt;/p&gt;

&lt;p&gt;Each rule is a few lines in one Code node, so you can make it stricter or looser. One catch: German VAT IDs return no company name from VIES, so the name comparison only helps with suppliers from countries that share it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the real test showed
&lt;/h2&gt;

&lt;p&gt;I ran the four actors on the two public test invoices from the parser's example input: KoSIT's official XRechnung test case &lt;code&gt;01.01a&lt;/code&gt; (UBL) and &lt;code&gt;EN16931_Einfach.pdf&lt;/code&gt; from the Mustang project (a ZUGFeRD PDF).&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;parser&lt;/strong&gt; read both in 3 seconds: invoice &lt;code&gt;123456XX&lt;/code&gt; over 336.90 EUR at 7 % VAT, and invoice &lt;code&gt;471102&lt;/code&gt; over 529.87 EUR with a 7 % and a 19 % VAT line. Zero plausibility warnings. The ZUGFeRD PDF has no IBAN at all in its XML.&lt;/p&gt;

&lt;p&gt;Then the checks, and this is the interesting part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;IBAN&lt;/strong&gt; &lt;code&gt;DE79 0000 0000 1234 5678 90&lt;/code&gt; passed the checksum and length test, so a simple validator would call it valid. But its bank code is &lt;code&gt;00000000&lt;/code&gt;, and the actor answered &lt;code&gt;"Bank code not found in Bundesbank list"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;VAT ID&lt;/strong&gt; &lt;code&gt;DE123456789&lt;/code&gt;, used by both test invoices, has a valid format. VIES said &lt;code&gt;invalid&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BaFin&lt;/strong&gt;: &lt;code&gt;Lieferant GmbH&lt;/code&gt; was screened against 60 current warnings and measures, no match (best score 50). (33 seconds, it reads the live warning pages).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So both official test invoices end up &lt;strong&gt;red&lt;/strong&gt;. That is the correct result: test files use placeholders that are formally perfect but do not exist. It is also the best argument for this workflow. A format check would have passed both invoices; the registry checks did not.&lt;/p&gt;

&lt;p&gt;With real supplier invoices you will see mostly green, and yellow when a supplier leaves out the IBAN or VIES has a bad day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;The actors charge per result, and failed items are free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parse: $0.003 per invoice&lt;/li&gt;
&lt;li&gt;IBAN: $0.0005&lt;/li&gt;
&lt;li&gt;VAT ID: $0.001&lt;/li&gt;
&lt;li&gt;BaFin screening: $0.01 per name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is &lt;strong&gt;$0.0145 per invoice&lt;/strong&gt;, about $1.45 for 100 invoices a month. BaFin is the largest part; screen long-standing suppliers once a month instead and a repeat invoice costs $0.0045.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;This is a technical plausibility check, not tax or legal advice. It does not tell you whether an invoice satisfies § 14 UStG or whether you may deduct the input VAT.&lt;/p&gt;

&lt;p&gt;The BaFin data covers roughly the last five weeks of warnings, because that is what BaFin publishes in machine-readable form. It is a check against current warnings, not a historical archive.&lt;/p&gt;

&lt;p&gt;And it does not catch the most common invoice fraud: a real supplier's invoice with a changed IBAN that is perfectly valid, just not the supplier's. For that, add a supplier master-data tab and a rule "IBAN differs from the known IBAN". It is the first extension I would build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup in ten minutes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Import &lt;code&gt;workflow.json&lt;/code&gt; from the &lt;a href="https://github.com/ichkifokkus/data-recipes/tree/main/8-ai-finance-assistant-einvoice-check" rel="noopener noreferrer"&gt;repo folder&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Create a Header Auth credential (&lt;code&gt;Authorization&lt;/code&gt; / &lt;code&gt;Bearer YOUR_APIFY_TOKEN&lt;/code&gt;) and select it in the four HTTP nodes.&lt;/li&gt;
&lt;li&gt;Create a Drive folder, share it by link, and pick it in the trigger (or use the webhook with presigned URLs).&lt;/li&gt;
&lt;li&gt;Connect Google Sheets (tab &lt;code&gt;Eingangsrechnungen&lt;/code&gt;) and Slack.&lt;/li&gt;
&lt;li&gt;Activate and drop the KoSIT test invoice into the folder: you should get a red row.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The workflow, the README with all rules and the sheet layout are in the &lt;a href="https://github.com/ichkifokkus/data-recipes/tree/main/8-ai-finance-assistant-einvoice-check" rel="noopener noreferrer"&gt;data-recipes repo&lt;/a&gt;. If you would rather give these checks to Claude or Cursor directly, the same actors are available as MCP tools in the repo's Finance &amp;amp; Sales pack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try the actors directly:&lt;/strong&gt; &lt;a href="https://apify.com/fast_data_asap/xrechnung-zugferd-einvoice-parser" rel="noopener noreferrer"&gt;E-Invoice Parser&lt;/a&gt; · &lt;a href="https://apify.com/fast_data_asap/iban-validator-bank-lookup" rel="noopener noreferrer"&gt;IBAN Validator&lt;/a&gt; · &lt;a href="https://apify.com/fast_data_asap/eu-vat-number-validator" rel="noopener noreferrer"&gt;EU VAT Validator&lt;/a&gt; · &lt;a href="https://apify.com/fast_data_asap/bafin-investor-warnings" rel="noopener noreferrer"&gt;BaFin Warnings&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How do you check incoming invoices today: fully manual, rules in your accounting tool, or already with an agent?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is a technical check, not tax or legal advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>fintech</category>
      <category>ai</category>
    </item>
    <item>
      <title>I counted every server in the official MCP Registry: 59,603 entries, and October alone added 16,282</title>
      <dc:creator>Fast Data</dc:creator>
      <pubDate>Sun, 08 Nov 2026 17:46:56 +0000</pubDate>
      <link>https://dev.to/fast_data_asap/i-counted-every-server-in-the-official-mcp-registry-59603-entries-and-october-alone-added-16282-1jlj</link>
      <guid>https://dev.to/fast_data_asap/i-counted-every-server-in-the-official-mcp-registry-59603-entries-and-october-alone-added-16282-1jlj</guid>
      <description>&lt;p&gt;The &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; is the standard that lets AI agents such as Claude, ChatGPT or Cursor call external tools. Its &lt;strong&gt;official registry&lt;/strong&gt; at &lt;code&gt;registry.modelcontextprotocol.io&lt;/code&gt; is where server authors publish their metadata, and it has a public, paginated API.&lt;/p&gt;

&lt;p&gt;On 8 November 2026 I walked through the entire registry: 2,295 pages, 229,461 published versions. Here is what that says about where the agent ecosystem stands.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: we publish one entry there ourselves (&lt;code&gt;io.github.ichkifokkus/fast-data&lt;/code&gt;, Germany/EU business data tools via Apify). That is 1 of almost 60,000, so it does not move any of the numbers below.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Published versions (all)&lt;/td&gt;
&lt;td&gt;229,461&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distinct servers (latest version)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;59,603&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;… with a hosted remote endpoint&lt;/td&gt;
&lt;td&gt;39,789 (67%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;… with an npm package&lt;/td&gt;
&lt;td&gt;16,350&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;… with a PyPI package&lt;/td&gt;
&lt;td&gt;5,280&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;… with an MCP bundle (&lt;code&gt;mcpb&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;2,250&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;… with a container image (OCI)&lt;/td&gt;
&lt;td&gt;1,713&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names under &lt;code&gt;io.github.*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;34,156 (57%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A server can have both a remote endpoint and packages, so the package rows overlap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Growth: first publication per month
&lt;/h2&gt;

&lt;p&gt;I took the earliest &lt;code&gt;publishedAt&lt;/code&gt; of each server name as its "first seen" date:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Month&lt;/th&gt;
&lt;th&gt;New servers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sep 2025&lt;/td&gt;
&lt;td&gt;456&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dec 2025&lt;/td&gt;
&lt;td&gt;276&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mar 2026&lt;/td&gt;
&lt;td&gt;1,938&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 2026&lt;/td&gt;
&lt;td&gt;3,022&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 2026&lt;/td&gt;
&lt;td&gt;5,072&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 2026&lt;/td&gt;
&lt;td&gt;6,518&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 2026&lt;/td&gt;
&lt;td&gt;11,849&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Oct 2026&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16,282&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nov 2026 (1–8)&lt;/td&gt;
&lt;td&gt;5,728&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;October added more than &lt;strong&gt;five times&lt;/strong&gt; as many new servers as May. The first eight days of November are already above the whole of July. The curve has not flattened.&lt;/p&gt;

&lt;p&gt;Two caveats: the registry launched in preview in September 2025, so the early months partly reflect adoption of the registry itself, not of MCP. And "first published" only counts servers that are still listed today; anything deleted in between is invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote first
&lt;/h2&gt;

&lt;p&gt;Two thirds of all servers advertise a &lt;strong&gt;hosted remote endpoint&lt;/strong&gt; (streamable HTTP or SSE). Local packages are the minority: npm leads with 16,350, PyPI follows with 5,280, and Rust (&lt;code&gt;cargo&lt;/code&gt;, 122) and .NET (&lt;code&gt;nuget&lt;/code&gt;, 175) are rounding errors.&lt;/p&gt;

&lt;p&gt;For anyone building tools for agents, this matters more than it looks. Remote servers need no install, work in browser-based clients and can authenticate with OAuth. The ecosystem has clearly voted for "add a URL" over "install a package".&lt;/p&gt;

&lt;h2&gt;
  
  
  What the servers are about
&lt;/h2&gt;

&lt;p&gt;I searched the latest descriptions for a few business keywords (case-insensitive substring match, so "search" also counts "research", and one server can match several words):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Keyword&lt;/th&gt;
&lt;th&gt;Servers mentioning it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;search&lt;/td&gt;
&lt;td&gt;7,277&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;email&lt;/td&gt;
&lt;td&gt;1,211&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;google&lt;/td&gt;
&lt;td&gt;1,059&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;payment&lt;/td&gt;
&lt;td&gt;1,004&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;invoice&lt;/td&gt;
&lt;td&gt;695&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;database&lt;/td&gt;
&lt;td&gt;502&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;calendar&lt;/td&gt;
&lt;td&gt;413&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sales&lt;/td&gt;
&lt;td&gt;397&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crm&lt;/td&gt;
&lt;td&gt;333&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;finance&lt;/td&gt;
&lt;td&gt;298&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;europe&lt;/td&gt;
&lt;td&gt;219&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;german / germany&lt;/td&gt;
&lt;td&gt;195 / 71&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shopify&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e-commerce / ecommerce&lt;/td&gt;
&lt;td&gt;73 / 48&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Search and retrieval dominate by a wide margin. &lt;strong&gt;Payments and invoices&lt;/strong&gt; are surprisingly strong: over 1,000 servers mention payments and almost 700 mention invoices. Agents that pay and bill are clearly a thing people are building for.&lt;/p&gt;

&lt;p&gt;At the other end, &lt;strong&gt;regional business data is thin&lt;/strong&gt;. Fewer than 200 descriptions mention "German", fewer than 250 mention "Europe", and e-commerce shows up in barely more than 100. Given how much of the economy runs on country-specific rules (tax IDs, e-invoicing formats, public tenders, company registers), that gap is where I expect a lot of the next wave of useful servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repeat it yourself
&lt;/h2&gt;

&lt;p&gt;The API is public and needs no key. This script walks all pages, keeps the latest version of each server and counts first appearances per month:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://registry.modelcontextprotocol.io/v0.1/servers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;servers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;io.modelcontextprotocol.registry/official&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;published&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publishedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9999&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9999&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isLatest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nextCursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;

&lt;span class="n"&gt;per_month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;servers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_month&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per_month&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;with remote:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;remotes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A full pass takes a few minutes. Please be gentle with the registry: one pass a day is plenty for trend tracking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The agent tool layer is growing faster than anyone can curate.&lt;/strong&gt; At more than 500 new servers a day, discovery and trust (who maintains this, is it alive, is it safe) become the real problem, not supply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote is the default.&lt;/strong&gt; If you build a tool for agents in 2026, ship a URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic is crowded, specific is open.&lt;/strong&gt; Search, email and Google integrations are everywhere. Country-specific business data, compliance checks and e-commerce rules are not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point is why we keep a small, curated &lt;a href="https://github.com/ichkifokkus/data-recipes/tree/main/catalog" rel="noopener noreferrer"&gt;catalog of agent tools for German and EU business data&lt;/a&gt;, re-checked weekly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Data: official MCP Registry API, full pass on 8 November 2026. Keyword counts are simple substring matches on descriptions and only indicate rough topic size.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>opensource</category>
      <category>datascience</category>
    </item>
    <item>
      <title>How to get German job listings from the Arbeitsagentur as JSON or CSV (Python and n8n)</title>
      <dc:creator>Fast Data</dc:creator>
      <pubDate>Sun, 08 Nov 2026 12:44:54 +0000</pubDate>
      <link>https://dev.to/fast_data_asap/how-to-get-german-job-listings-from-the-arbeitsagentur-as-json-or-csv-python-and-n8n-34og</link>
      <guid>https://dev.to/fast_data_asap/how-to-get-german-job-listings-from-the-arbeitsagentur-as-json-or-csv-python-and-n8n-34og</guid>
      <description>&lt;p&gt;The &lt;strong&gt;Bundesagentur für Arbeit&lt;/strong&gt; (Federal Employment Agency) runs Germany's largest job board, the &lt;em&gt;Jobsuche&lt;/em&gt;. Its search is also reachable as data, which makes it the best free starting point if you need German job listings for a job board, a market report, a recruiting dashboard or just a daily alert for yourself.&lt;/p&gt;

&lt;p&gt;This guide shows how to get those listings filtered by keyword, city, radius and date, as JSON or as a CSV file, and how to turn that into a daily alert. All numbers below are from real runs on 8 November 2026.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built the tool used below, the &lt;a href="https://apify.com/fast_data_asap/job-search-api" rel="noopener noreferrer"&gt;Job Search API&lt;/a&gt; on Apify. It calls the official public job APIs; you don't need an Arbeitsagentur account.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: one city, one role, last 7 days
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/fast_data_asap~job-search-api/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "keywords": "Data Analyst",
    "location": "Hamburg",
    "radiusKm": 25,
    "postedWithinDays": 7,
    "sources": ["arbeitsagentur"],
    "maxJobsPerSource": 20
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This took &lt;strong&gt;6.3 seconds&lt;/strong&gt; and returned &lt;strong&gt;7 jobs&lt;/strong&gt; published in the Hamburg area during the previous week. Each one is a flat row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Data Analyst (m/w/d)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fabplus GmbH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"21129, Hamburg, Germany"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"remote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"employmentType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"full-time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-03T00:00:00+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.arbeitsagentur.de/jobsuche/jobdetail/…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arbeitsagentur"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceJobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryMin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things stood out in those 7 rows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two of the seven were the same client job, advertised by service providers.&lt;/strong&gt; "Data Analyst (d/f/m) for AIRBUS" and "Data Analyst (d/m/w) für AIRBUS" were posted by two different companies. If you count jobs per employer, you need to account for ads that name the actual client only in the title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword matching is broader than the title.&lt;/strong&gt; The search also returned a "Client Partner Banking" and a "Senior Business Analyst" role, presumably because the agency's own search matches tags and descriptions. If you need strict title matches, filter afterwards (see step 3).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: all of Germany, as CSV
&lt;/h2&gt;

&lt;p&gt;Leave &lt;code&gt;location&lt;/code&gt; empty to search the whole country. Add &lt;code&gt;&amp;amp;format=csv&lt;/code&gt; to the endpoint and you get a CSV file instead of JSON, ready for Excel or Google Sheets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/fast_data_asap~job-search-api/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN&amp;amp;format=csv"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"keywords": "Python", "location": "", "postedWithinDays": 7, "sources": ["arbeitsagentur"], "maxJobsPerSource": 50}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; python-jobs.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My run hit the limit of &lt;strong&gt;50 jobs in 7.5 seconds&lt;/strong&gt;, so the real number of new Python listings that week is higher. Raise &lt;code&gt;maxJobsPerSource&lt;/code&gt; for a complete pull. A few facts from those 50 rows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Marked as remote or home office&lt;/td&gt;
&lt;td&gt;6 of 50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-time&lt;/td&gt;
&lt;td&gt;48 of 50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With a salary figure&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 of 50&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Most frequent cities&lt;/td&gt;
&lt;td&gt;Berlin 5, Hamburg 4, Rostock 4, Bremen 3, Kassel 3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The salary row is the real gotcha: &lt;strong&gt;the Arbeitsagentur feed does not include salary data&lt;/strong&gt;, so &lt;code&gt;salaryMin&lt;/code&gt; and &lt;code&gt;salaryMax&lt;/code&gt; stay empty for this source. If salary matters for your use case, add the &lt;code&gt;arbeitnow&lt;/code&gt;, &lt;code&gt;remoteok&lt;/code&gt; or &lt;code&gt;himalayas&lt;/code&gt; sources, which sometimes include it, or read it from the description text with &lt;code&gt;includeDescription: true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: clean it up in Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.apify.com/v2/acts/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fast_data_asap~job-search-api/run-sync-get-dataset-items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;german_jobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keywords&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;radiusKm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postedWithinDays&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arbeitsagentur&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxJobsPerSource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;german_jobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data Analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hamburg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# strict title match: the agency search is broader than the title
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="c1"&gt;# drop the same job advertised by several service providers
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\(.*?\)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_duplicates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Hamburg example the title filter leaves &lt;strong&gt;4 Data Analyst jobs&lt;/strong&gt; instead of 7. Two of those are still the same Airbus role (one ad says "for", the other "für"), so for serious counting add a client-name rule or a fuzzy match on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: a daily job alert in n8n
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Schedule Trigger&lt;/strong&gt;: every weekday at 07:00.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP Request&lt;/strong&gt; node: &lt;code&gt;POST&lt;/code&gt; the input from step 1 to the endpoint, with &lt;code&gt;postedWithinDays: 1&lt;/code&gt; and your Apify token as a Header Auth credential (&lt;code&gt;Authorization: Bearer …&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code&lt;/strong&gt; node: the strict title filter and duplicate removal from step 3 (a few lines of JavaScript).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IF&lt;/strong&gt; node: stop when there are no rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gmail&lt;/strong&gt;, &lt;strong&gt;Slack&lt;/strong&gt; or &lt;strong&gt;Telegram&lt;/strong&gt; node: send one message listing title, company and link.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Swap step 5 for a &lt;strong&gt;Google Sheets&lt;/strong&gt; append and you have a growing log of every matching job, which after a few weeks shows you which companies hire most in your field and city.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other filters worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;remoteOnly: true&lt;/code&gt; keeps only remote or home-office jobs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;filterByLocation: true&lt;/code&gt; drops jobs whose location text does not contain your city, useful when the radius pulls in too much.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;companies&lt;/code&gt;: add careers-page URLs on Greenhouse, Lever, Ashby or SmartRecruiters to watch specific employers in the same run.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sources&lt;/code&gt;: combine the Arbeitsagentur with Arbeitnow, Remote OK and Himalayas for remote roles.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;The actor charges &lt;strong&gt;$2.50 per 1,000 jobs&lt;/strong&gt; returned. The 57 jobs in this article cost about 14 cents at that rate. A daily alert that returns 10–20 new jobs costs a few cents per month.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Data: Bundesagentur für Arbeit Jobsuche, queried 8 November 2026. Company names are as published in the job ads.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>automation</category>
      <category>career</category>
    </item>
  </channel>
</rss>
