<?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: BeeL.</title>
    <description>The latest articles on DEV Community by BeeL. (@beel).</description>
    <link>https://dev.to/beel</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%2F4049788%2Fc734ac50-4a66-469a-ae05-a911475eb28b.png</url>
      <title>DEV Community: BeeL.</title>
      <link>https://dev.to/beel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/beel"/>
    <language>en</language>
    <item>
      <title>Designing an API when half your users are agents</title>
      <dc:creator>BeeL.</dc:creator>
      <pubDate>Tue, 28 Jul 2026 08:30:00 +0000</pubDate>
      <link>https://dev.to/beel/designing-an-api-when-half-your-users-are-agents-48dh</link>
      <guid>https://dev.to/beel/designing-an-api-when-half-your-users-are-agents-48dh</guid>
      <description>&lt;p&gt;A growing share of the traffic hitting our API isn't typed by anyone. Not scripts either — agents: a model reading the docs, writing an integration, calling endpoints to check its own work, and retrying whenever something comes back ambiguous.&lt;/p&gt;

&lt;p&gt;Most of that is fine. What it exposes is your defaults, and a lot of ours turned out to have been chosen casually.&lt;/p&gt;

&lt;p&gt;The stakes are unusually literal in our case, because our writes are permanent by law. This is an invoicing API for Spain: every production invoice becomes a fiscal record, hashed and chained to the one before it and submitted to the tax agency. You can't delete one. You can only issue a second record stating the first was wrong, and that one is permanent too.&lt;/p&gt;

&lt;p&gt;So we spent a while rethinking the surface for a caller that never gets tired. Most of what came out has nothing to do with tax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents don't make mistakes once
&lt;/h2&gt;

&lt;p&gt;A developer who runs the wrong command feels it immediately, stops, and thinks. An agent that runs the wrong command has already run it eleven more times while it works out what the error message meant. Whatever your worst-case single call is, an agent will find it and multiply it.&lt;/p&gt;

&lt;p&gt;So the first thing we did was make the dangerous environment opt-in rather than default. Our CLI runs against sandbox unless you pass &lt;code&gt;--live&lt;/code&gt;. And crucially, a live key in the environment doesn't quietly upgrade you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BEEL_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;beel_sk_live_...
beel invoices issue inv_123          &lt;span class="c"&gt;# error, not a silent production write&lt;/span&gt;
beel invoices issue inv_123 &lt;span class="nt"&gt;--live&lt;/span&gt;   &lt;span class="c"&gt;# explicit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That error looks unhelpful if you read it as a human. It's exactly right if you read it as an agent, because the agent's next move is to reason about the flag rather than to discover afterwards that it filed forty invoices with the tax agency.&lt;/p&gt;

&lt;p&gt;The same logic pushed us to attach an &lt;code&gt;Idempotency-Key&lt;/code&gt; to every POST automatically. Retrying is what agents do when something looks ambiguous, and they retry faster than a human can intervene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Machine-readable errors beat readable ones
&lt;/h2&gt;

&lt;p&gt;Prose errors are a trap. An agent parsing "something went wrong, please check your configuration" will guess, and its guess will be confident.&lt;/p&gt;

&lt;p&gt;We ended up with two channels. Data goes to stdout as JSON, so it pipes into &lt;code&gt;jq&lt;/code&gt;. Errors go to stderr, also as JSON, carrying a code, a message, an HTTP status and a request ID. On top of that, exit codes are typed by class rather than being 0-or-1:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Usage or config error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rate limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The distinction that matters is between "retry this" and "stop, you're wrong". A &lt;code&gt;6&lt;/code&gt; means back off. A &lt;code&gt;5&lt;/code&gt; means the payload is wrong and hammering it will never help. Collapsing both into &lt;code&gt;1&lt;/code&gt; is what produces those loops where an agent tries the same broken request twenty times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't make the model guess your surface
&lt;/h2&gt;

&lt;p&gt;The usual failure mode with tooling is a hand-maintained command list that drifts from the actual API. An agent reads it, believes it, and calls something that no longer exists.&lt;/p&gt;

&lt;p&gt;Our commands are derived from the OpenAPI spec at build time, and there's one command that dumps the whole surface as JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beel commands            &lt;span class="c"&gt;# { command, signature, description, method, path } for everything&lt;/span&gt;
beel invoices create &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One call, whole API, no scraping of help text. And because the spec is bundled with the installed version rather than fetched live, there's an obvious hole: an endpoint newer than your CLI. So there's a generic escape hatch that doesn't pretend otherwise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;beel request POST /v1/customers &lt;span class="nt"&gt;--data&lt;/span&gt; @customer.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We've come to treat an escape hatch as a requirement rather than a nicety in agent tooling. The alternative is a model that concludes something is impossible because a wrapper hasn't caught up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins go stale, which makes agents confidently wrong
&lt;/h2&gt;

&lt;p&gt;We ship a Claude Code plugin. The obvious version of that is to bake the API reference into the plugin, and the obvious version is wrong: the day you publish it, it starts decaying, and a stale reference is worse than no reference because it reads authoritative.&lt;/p&gt;

&lt;p&gt;What's local now is only the part that doesn't change: authentication, idempotency, the response envelope, the invoice lifecycle. Endpoints, schemas and event names get fetched from the docs at the moment they're needed. We publish &lt;code&gt;/llms.txt&lt;/code&gt; and &lt;code&gt;/llms-full.txt&lt;/code&gt; for exactly that, plus the raw OpenAPI spec.&lt;/p&gt;

&lt;p&gt;The split turns out to be a decent rule of thumb. Invariants can live in the tool. Anything with a version number should be fetched.&lt;/p&gt;

&lt;p&gt;There's a related detail we like more than it probably deserves: &lt;code&gt;beel docs search&lt;/code&gt; downloads the docs once, caches them, and filters on your machine. The agent gets the matching section, and the query — which often contains the shape of whatever the user is building — never reaches our servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification is the whole game
&lt;/h2&gt;

&lt;p&gt;The part that changed our defect rate wasn't the docs. It was that an agent can prove an integration works before anyone ships it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @beel_es/cli invoices create &lt;span class="nt"&gt;--data&lt;/span&gt; @invoice.json
npx @beel_es/cli invoices get &amp;lt;invoice_id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a test key, a model can create invoices, trigger the resulting events, and confirm a webhook receiver actually handles them, all against a sandbox that's free and unlimited. No throwaway scripts, no "looks right to me" as the last check before production.&lt;/p&gt;

&lt;p&gt;That's the argument for a free, unlimited, genuinely complete sandbox that we'd underrated: it isn't a generosity feature, it's the thing that lets an automated caller close its own feedback loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'd take to any API
&lt;/h2&gt;

&lt;p&gt;If you're building something an agent might call, the questions worth asking are pretty small in number. Can a single call do irreversible damage, and is it reachable by default? Can a caller distinguish "retry" from "stop" without reading English? Can it discover your surface in one call? And can it verify its own work somewhere that doesn't count?&lt;/p&gt;

&lt;p&gt;We had to answer those because our writes are permanent by law. Most APIs get to answer them because a customer will otherwise wake up to a mess. The list is the same either way.&lt;/p&gt;

&lt;p&gt;If you want to see how any of it is wired up, the docs are at &lt;a href="https://docs.beel.es" rel="noopener noreferrer"&gt;docs.beel.es&lt;/a&gt;, sandbox included. And we'd like to hear how other teams are handling the retry-loop problem in particular — it's the one we're least confident we've solved.&lt;/p&gt;

</description>
      <category>api</category>
      <category>ai</category>
      <category>claude</category>
      <category>cli</category>
    </item>
    <item>
      <title>What Spain's Verifactu law actually does to your backend</title>
      <dc:creator>BeeL.</dc:creator>
      <pubDate>Mon, 27 Jul 2026 15:50:42 +0000</pubDate>
      <link>https://dev.to/beel/what-spains-verifactu-law-actually-does-to-your-backend-2b77</link>
      <guid>https://dev.to/beel/what-spains-verifactu-law-actually-does-to-your-backend-2b77</guid>
      <description>&lt;p&gt;Spain is putting a hash chain behind every invoice, and almost everything written about it so far has been written for accountants. This is the version for whoever has to ship it.&lt;/p&gt;

&lt;p&gt;The deadlines are January 1, 2027 for companies and July 1, 2027 for sole traders. If you read something last year that said 2026, that was true until RD-ley 15/2025 moved the whole calendar back twelve months. Software vendors have been on the hook since July 2025, which is a detail worth holding on to if you sell a product that issues invoices for other people.&lt;/p&gt;

&lt;p&gt;At BeeL., we sell an API for this, so read the rest with that in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  The requirement
&lt;/h2&gt;

&lt;p&gt;Each invoice your software issues has to produce a registro de facturación de alta: a record containing a defined set of fields, hashed with SHA-256, where the hash of each record folds in the hash of the one before it. One chain per issuing tax ID, growing forever, never edited.&lt;/p&gt;

&lt;p&gt;Cancelling an invoice is not a delete. It's a second record type, a registro de anulación, which goes into the same chain. Same for corrections, which come in two flavours depending on whether you're amending a difference or replacing the original document. The printed invoice carries a QR code with verification data, plus the string VERI*FACTU if you're in submitting mode.&lt;/p&gt;

&lt;p&gt;Then you either push each record to the tax agency as it happens, or you keep everything locally under stricter signing and retention rules and hand it over when asked.&lt;/p&gt;

&lt;p&gt;Written down like that, it reads like an afternoon of work. A hash function, a &lt;code&gt;previous_hash&lt;/code&gt; column, an HTTP call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the estimate falls apart
&lt;/h2&gt;

&lt;p&gt;The chain is strictly sequential, so two workers issuing invoices for the same tax ID at the same time are racing for the same link. You need a lock per issuer, or a queue, or both, and either way concurrent issuance stops being free.&lt;/p&gt;

&lt;p&gt;Retries are worse than they look. A failed submission that you retry carelessly either duplicates a record or breaks the chain, and a broken chain isn't something you fix with a migration script. It's a tax record. The recovery path is procedural, not technical.&lt;/p&gt;

&lt;p&gt;The transport is a different era of software. XML over web services, digital certificates, environments that go down, error codes you have to classify as retryable or terminal before you can write sensible handling.&lt;/p&gt;

&lt;p&gt;The edge cases are most of the work: multiple VAT rates on one invoice, exemptions, non-subject operations, recargo de equivalencia, intra-community customers, tax ID validation, invoice series.&lt;/p&gt;

&lt;p&gt;And if you're a platform issuing on behalf of your customers, none of the above is singular. You have one chain per client tax ID, each with its own certificate, its own state, its own stuck submissions at 3am.&lt;/p&gt;

&lt;p&gt;One more thing that surprises people: article 201 bis of the general tax law doesn't only point at the business issuing invoices. A vendor selling non-compliant invoicing software is looking at up to €150,000 per year per product. If your SaaS issues invoices for third parties, you are that vendor, whether or not it says so anywhere in your positioning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways out
&lt;/h2&gt;

&lt;p&gt;You can build it yourself against the tax agency. Full control, no per-invoice cost, and a multi-month project followed by permanent maintenance every time a spec changes. Worth it if invoicing is your product rather than a thing your product has to do.&lt;/p&gt;

&lt;p&gt;You can put an off-the-shelf invoicing tool in the loop. Fine when the volume is low and a human is doing the clicking; awkward when your backend needs to emit thousands of invoices a month without anyone opening a browser.&lt;/p&gt;

&lt;p&gt;Or you can hand the fiscal part to an API and keep a POST in your own code. You trade work for a dependency, which is the same trade you already made for payments.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the third option looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BeeL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@beel_es/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;beel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BeeL&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BEEL_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;beel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;nif&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;B12345678&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cliente SL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;facturas@cliente.es&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;lines&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="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pro plan, July 2026&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quantity&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="na"&gt;unitPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;49.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;taxRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;21&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hashing, chaining, QR generation and submission happen behind that call, and the resulting state arrives on a signed webhook. If you already bill through Stripe, the charge itself can be the trigger, so there's even less of your code involved.&lt;/p&gt;

&lt;p&gt;Test keys are prefixed &lt;code&gt;beel_sk_test_&lt;/code&gt; and the sandbox has no limits, so the whole flow is buildable before anyone pays anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things Verifactu is not
&lt;/h2&gt;

&lt;p&gt;It isn't the B2B e-invoicing mandate from the Crea y Crece law. Different regulation, different timeline, and complying with one does nothing for the other.&lt;/p&gt;

&lt;p&gt;It doesn't cover the whole country the same way. The Basque Country and Navarre run on their own regional rules, TicketBAI among them.&lt;/p&gt;

&lt;p&gt;And it isn't SII. If you're already reporting VAT through Suministro Inmediato de Información, your obligations differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 2026 is the good year to do this
&lt;/h2&gt;

&lt;p&gt;Right now, submitting is voluntary. You can integrate, keep the chain clean from invoice number one, and debug in an environment where nothing you get wrong is a sanctionable event.&lt;/p&gt;

&lt;p&gt;The alternative is doing it next December, at the same time as everyone else who waited, with every vendor's support queue full.&lt;/p&gt;

&lt;p&gt;If you're somewhere in this already, I'm curious how you're issuing invoices today and what's blocking you. The sandbox at &lt;a href="https://beel.es" rel="noopener noreferrer"&gt;beel.es&lt;/a&gt; doesn't ask for a card if you want to poke at it first.&lt;/p&gt;

</description>
      <category>api</category>
      <category>saas</category>
      <category>spanish</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
