Changelog
Every change to the BeeL. API, newest first — what moved, when the old way stops working, and how to tell whether it affects you.
Most changes are one entry here. A few are big enough to need the old-to-new route table, the retirement calendar and a way to check your own integration against it — those keep their entry in this list and link to the full note.
Every deprecated route also tells you the same thing in its own responses —
Deprecation: true, a Sunset date and Link: <successor>; rel="successor-version" —
so you never have to trust a page over your own traffic.
Stripe: a buyer with a NIF always gets a standard invoice, and the simplified threshold is deprecated
Three changes to how a connected Stripe account is invoiced. None of them changes an invoice that already exists, and no request or response changes shape.
- A buyer with a NIF (or foreign identifier) and a complete address always receives a
STANDARDinvoice, whatever the amount. Before, a charge below the connection's threshold was issued asSIMPLIFIED, which never carries the buyer's NIF. Without that data the charge still gets aSIMPLIFIEDinvoice up to 3 000 €. - A charge that cannot be invoiced is saved as a draft instead of waiting as a skipped event: a charge above 3 000 € whose buyer has no NIF or address, and a charge whose NIF the AEAT definitively rejects. The draft appears in Needs attention, and on Live connections BeeL. emails the issuer. Complete the recipient and issue it.
simplificada_thresholdis deprecated. The API still accepts and returns it, with the same validation, but its value no longer affects the invoice type. You can stop sending it.- What to do: nothing is required. If you relied on the threshold to keep small sales to identified buyers as simplified invoices, those sales are now standard invoices. To avoid drafts, collect tax IDs and the full billing address in Stripe.
- GET/v1/companies/{company_id}/payment-connections
simplificada_thresholdis returned but deprecated and ignored - PATCH/v1/companies/{company_id}/payment-connections/{connection_id}
simplificada_thresholdis still accepted but deprecated and ignored
Stripe: manual 0 % rates and the buyer's country on paid invoices
Two changes to how a connected Stripe account is invoiced. Neither one changes an invoice that already exists.
- A manual 0 % tax rate in Stripe no longer leaves the charge without an invoice when the buyer is in the peninsula or the Balearic Islands. This is the rate some platforms create by default. BeeL. applies the default exemption reason your company declared, or asks you to declare it. Buyers in the Canary Islands, Ceuta or Melilla, buyers from other countries, and payments with no postal code still wait in Needs attention.
invoice.paidnow sees the buyer's country. When neither the Stripe invoice nor its customer carries one, BeeL. takes it from the billing address of the payment that paid the invoice, so both events of the same charge see the same buyer.- A buyer outside Spain with an incomplete address is no longer invoiced as if the sale were Spanish. If the sale cannot go on a simplified invoice, the charge waits with a draft to complete, as it already did for the payment event.
- What to do: if you sell exempt with a 0 % rate in Stripe, declare your default exemption reason in BeeL. under Settings > Taxes. If you sell outside Spain, collect the full billing address (street, city, postal code and country) at checkout.
A unit_price with more than 4 significant decimals now answers 422 LINE_UNIT_PRICE_TOO_MANY_DECIMALS instead of being rounded silently. Round to 4 decimals, or send the line total.
On the API plan with no agreed price, a second NIF, or going Live with several, answers 403 CONTACT_SALES_REQUIRED with a link to sales.
province is no longer required on an Address outside Spain: a foreign customer or inline recipient without one is accepted. A Spanish address without it still answers 422 PROVINCE_EMPTY.
With VeriFactu, exchanging simplified invoices now issues the full invoice and records it as F3. SIMPLIFIED_EXCHANGE_NOT_RECORDABLE now only means a simplified invoice issued without VeriFactu.
verifactu.status.updated now carries operation: REGISTRATION or VOID. AEAT's answer to a cancellation reaches you by webhook without a lookup.
A corrective can no longer rectify more than was invoiced, has a deadline, and can fix the recipient's data; a sent or paid invoice is voided only if issued in error.
A series numbers only its own type, and an invoice AEAT would reject answers 422 before a number is used. Zero-total invoices are accepted and issued paid.
Exchange simplified invoices for a full invoice
When a customer who paid with a simplified invoice asks for an invoice with their details, POST /v1/companies/{company_id}/invoices/simplified-exchanges issues a STANDARD invoice with the lines of up to 50 simplified invoices and the recipient you send. It is not a corrective invoice: it documents the same operations again, identifying the customer.
The new invoice lists the ones it replaces in replaced_invoice_ids, and each simplified invoice becomes VOIDED with the new void_cause value EXCHANGED, in the same act. The exchange invoice cannot be voided afterwards; if it is wrong, correct it with a corrective invoice.
When the invoice would be recorded with VeriFactu, the request answered 422 SIMPLIFIED_EXCHANGE_NOT_RECORDABLE and nothing was issued.
Update: with VeriFactu the exchange invoice is now issued and recorded as F3, and SIMPLIFIED_EXCHANGE_NOT_RECORDABLE only names a simplified invoice issued without VeriFactu. An F3 exchange invoice cannot be corrected yet. See Simplified invoice exchanges are recorded with VeriFactu.
- Rejections:
422 EXCHANGE_REQUIRES_SIMPLIFIEDwhen an id is not a simplified invoice;422 SIMPLIFIED_NOT_EXCHANGEABLEwhen it is not issued, or was already voided, exchanged or corrected; voiding the exchange invoice answers422 EXCHANGE_INVOICE_NOT_VOIDABLE. - If you read
void_causewith a strict enum, addEXCHANGED;replaced_invoice_idsis a new optional field on every invoice response.
- POST/v1/companies/{company_id}/invoices/simplified-exchangesNew: a full invoice in exchange for simplified invoices
Temporary VAT and surcharge rates only on operations of their period, 0.625 becomes 0.62, companies cannot withhold individuals' IRPF rates, and some regime keys are refused.
A new series needs document_type, several rejections now carry a specific code or status, and an issued invoice's PDF never changes. Plus behaviour changes.
CLI 0.3.0 calls only the routes that name the company, so most commands change name: beel companies list-invoices <company_id> is now beel invoices list.
A failed webhook delivery is now retried 7 times over about 3 days instead of for about a minute, so a short outage of your endpoint no longer loses events.
The history now includes failed runs, skipped periods and pauses, each with a type. For those entries invoice_id is null.
Creating or updating a SIMPLIFIED invoice whose recipient has a NIF or an alternative identifier now answers 400 SIMPLIFIED_INVOICE_FORBIDS_IDENTIFIED_RECIPIENT. Issue it as STANDARD.
A check of the published contract against the live API found a dozen differences. A few were fixed in the API, and the rest in the contract.
A VAT line exempt under art. 21 or 22 that arrives with regime_key: "01" is stored with "02", instead of being refused by AEAT after issue.
Member grants carry the company's name
Each grant in a member's response and in the grants list now carries company_name next to company_id, so you can name the company without resolving every id against the account's companies. It is optional and may be null for a company that has no name yet. It never falls back to the id.
- GET/v1/accounts/{account_id}/members/{member_id}/grantsEach grant carries company_name
The contract stops declaring minLength and pattern on phone in responses; they move to PhoneInput. The server sends exactly what it sent before.
GET …/invoices/{invoice_id}/verifactu-records lists the registration and, after a void, the cancellation — each with its own submission_status, so you can tell whether AEAT accepted the cancellation.
verifactu.chaining_hash was declared but never sent, so it is being withdrawn. No response changes: the server behaves exactly as before.
Deleting a customer that an active or paused recurring template invoices is now rejected, on the single DELETE and row by row in the bulk delete.
Our OSS examples set percentage: 0, which drops the destination VAT from the breakdown. The API never required it. Check your regime_key: "17" lines.
Saving a template with send_automatically: true and no recipient now answers 422 SIN_DESTINATARIO_RESOLUBLE, and malformed recipient addresses are rejected.
is_failing flags a template from its first failed run, and last_generated_scheduled_date gives the latest period it has already invoiced.
Negative line totals on a template, a generated invoice adding up below zero, and editing a draft to a negative total are now rejected.
discard, restore and resolve on a payment event now also apply to the other events of the same charge: its failed attempts, its refunds, its dispute.
Templates gain quarterly and yearly cadences, an end after max_invoices, a completion reason, draft_in_advance, and the amount of the next invoice on every row.
Payment detail without a method, an inactive series, a window with no occurrence and a blank name are now rejected instead of silently discarded.
A card refund gets its corrective at once, disputes and voided credit notes raise a review item, and correctives and shipping add up to the cent.
Resuming keeps next_generation when it has not fallen due yet, instead of recalculating from today, which could issue a period twice or undo a skip.
Two never-documented payment-connection operations are gone today; on 11 December 2026 charges_only defaults to true and three counts fields and four schema aliases retire.
Retired today: GET …/payment-connections/{connection_id} (now 405) and POST …/payment-connections/{connection_id}/reactivations (now 404)No operation is deprecated: listing, opening an authorization and reading a single event stay documented and servedOn 11 December 2026 omitting charges_only on the event listing reads as charges_only=true; until then a request that omits it answers with Deprecation, Sunset and Linkcounts.ignored, counts.needs_action, counts.by_failure_reason and the four *ManagedPaymentEventResponse aliases retire the same day
The retirement date of the deprecated flat routes moves from 10 September to 2026-12-09. Nothing stopped answering, and you have the whole window again.
next_generation now carries only the date the system will honour, and null otherwise: a completed template, or a paused one whose date has passed.
amount, currency and sending_history.sent_at leave required. No response changes — the contract was promising something the server never did.
event_source (PAYMENTS, INVOICES or BOTH) picks which family of Stripe events a connection invoices. Existing connections are BOTH, which is today's behaviour.
invoice.pdf.generated tells you an invoice's PDF is stored and ready, so you can fetch it instead of polling.
A customer update only asks the AEAT census when the fiscal pair changes, the invitation 404 stops saying "pending", and readiness names an incomplete identity.
A customer import row with an incomplete address is now reported as a bad row instead of being created with placeholder values.
The {provider} path segment becomes {connection_id} on nine payment-connection paths. The old paths are gone, with no coexistence window.
issuer.address is optional and is no longer filled with invented values, and one character set governs every address field.
GET /v1/openapi.yaml and /v1/openapi.json serve the public contract — the same document this reference is built from, no API key needed.
GET .../invoices/{id}/pdf now waits for the PDF and answers 200. Prefer: wait=0 keeps the old pure polling behaviour.
Whether an invoice reaches the AEAT is now decided by the NIF's regime at issue time. The per-invoice flag and three response fields are gone.
address.number is now optional: omit it when the address has none, or when street already carries it.
invoice.schedule_failed tells you a scheduled invoice could not be issued, so you find out without opening the dashboard.
A line discounted to zero used to reject the whole invoice. Zero is now judged on the invoice total, with INVOICE_ZERO_AMOUNT.
VeriFactu is always on in sandbox
Creating a company in sandbox is asking for a VeriFactu test bench, so the configuration is now born enabled and the NIF is registered with the provider on the first submission — no explicit step. In Live nothing changes: enabling VeriFactu there is still a deliberate act by the taxpayer.
No existing configuration was touched.
Four collections now paginate — without limit you get the first 20 — and the three branding responses come wrapped in {success, data, meta}.
Issuing with VeriFactu on checks the recipient against the AEAT census first: an uncensused recipient answers 422, and no number is spent.
OAuth2 is gone from the spec. Each operation now states the scope it requires in its own description. API key auth is unchanged.
A literal % in a filter value dropped the filter. series_code matched any series containing the code. Sorting customers ignored the documented field names. Only series_code changes a response you may rely on today.
The published global ceiling drops to 1000 req/60 s to match what was already enforced. Strict and Global count per IP, not per credential.
Bodies above 2 MB answer 413 REQUEST_BODY_TOO_LARGE. 411 LENGTH_REQUIRED is gone — chunked encoding is accepted.
Two overlapping updates: the second now answers 409 CONCURRENT_MODIFICATION instead of silently discarding the first.
Several documented responses and one header were never actually sent. They are out of the contract; the server did not change.
Tax breakdowns travel complete and add up. Series publish the next number they will assign. Recurring invoices declare frequency in the request.
Create managed accounts in bulk from a spreadsheet — template, preview, apply — and see where each claim stands.
A body that parses but breaks a rule now answers 422 with a code and populated details, not an empty 400.
From 2026-09-03 the flat routes reject an undeclared query parameter with 400 instead of ignoring it. Until then they answer 200 with a BeeL-Warning header naming it.
Voided invoices now carry void_cause, void_reason and voided_at, so a direct void and a total corrective are told apart without a second call.
Lists answer data.<plural>, deletes answer 204 unless the body informs, and every create returns Location. DELETE products/bulk takes ?ids= instead of a body.
Lists: data is never a bare array anymore. Six lists that answered data: [...] + root pagination (companies of an account, series, recurring invoices, payment events, request logs, sent emails) now answer data.<plural> + data.pagination — deprecated flat aliases included, since they share the schema. Five unpaginated lists (company stats, email indicators, payment connections, recurring history, series defaults) are wrapped in data.{plural} with no pagination block.Deletes: DELETE .../customers/{id} and DELETE .../representation now answer 204 with no body — their old 200 bodies carried nothing you could not already know. Bulk deletes and DELETE .../activations keep their 200 body (partial results and the computed effective_at are only knowable from the response).DELETE /v1/companies/{company_id}/products/bulk no longer takes a request body — a body on DELETE has no defined semantics and intermediaries may drop it. Send ?ids= (comma-separated, max 100 per request), same as customers/bulk.Every single-resource 201 now carries a Location header pointing at the created resource's canonical GET — including actions that produce a resource (corrective, convert, derivations, generate, payment-event draft). Claim tokens point at their claim_url. Bulk creations carry none: there is no single URI to point at.PUT /v1/configuration/series/{series_id} is now deprecated like the rest of the flat series routes. It has no drop-in successor: the canonical PATCH /v1/companies/{company_id}/series/{series_id} behaves the same for every body except an explicit description: null, which the old PUT ignored and the PATCH honours by clearing the field.Declarative sweep: every live operation now documents 429 and 500, every tag is declared, and the account companies operations moved from the PublicCompanies tag to AccountCompanies.These changes ship without a grace window: measured traffic on the affected shapes was test-mode only. If your integration reads any of the old list shapes, adapt the paths above — everything else about each operation is unchanged.
A webhook URL on a private network is rejected when you register it
Registering or changing a subscription URL that points to a private or internal network address now answers 422 URL_TARGET_NOT_ALLOWED, instead of creating a subscription that would never receive a delivery. A host that does not resolve yet is still accepted, so you can register before your DNS is live.
The invoice field first announced as external_reference is now called external_ref. Responses already emit the new name; the old one is still accepted when you write, as a deprecated alias.
Responses emit external_ref — the old name is no longer returnedWrites still accept external_reference, as a deprecated aliasAffects the request body of the create-invoice routes and the GET /v1/invoices query filterNothing has stopped working, and no removal date is announced yet
A subscription now says how its endpoint is doing, emails you when it keeps failing, and is paused after 25 failed deliveries over more than 48 hours.
New: an account can hold several NIFs, each addressed in the path. As a result the old flat routes are deprecated — 82 operations gain a Sunset date, and all but three a successor route, and keep answering until then.
82 operations are deprecated, each with a Sunset date and, for all but three, a successor routeNothing is removed — every deprecated route keeps answering until its Sunset dateDeprecated routes carry Deprecation: true, Sunset and, where a successor is declared, Link: <successor>; rel="successor-version"The three replacing PUTs (customer, product, recurring invoice) declare no successor: the canonical PATCH merges instead of replacing, so it is not a drop-in
The status filter on GET /v1/invoices accepts a comma-separated list, so one request covers several statuses.
Invoices accept an external_reference — your own order id — that you can filter by and that is enforced as unique.
Invoice lines carry a line_type. Set SUPLIDO to pass through amounts paid in your client’s name, outside the taxable base.
A Request Logs endpoint family lets you inspect what your own API keys have called — method, path, status, timing and the full detail.
Official CLI on npm as @beel_es/cli. Run any endpoint from the terminal, sandbox by default, nothing to install.
Official Node.js / TypeScript SDK on npm as @beel_es/sdk, fully typed from the spec.
Idempotency keys accept any string, country becomes optional, and webhook URLs must be HTTPS.
The header that selects the active company is now Beel-Active-Company, was X-Active-Profile.
Invalid fiscal combinations now fail at request time with specific 422s. Adds a public error-code catalog, metadata filters, the RD-ley rates and BIZUM.
Invoice recipient and payment_info become structured objects, and new series get sensible defaults.
VeriFactu exemptions and document types, plus stricter validation on fiscal fields.
issue_date is now always today, and a new operation_date carries the date the service was actually rendered.
The BeeL. API is publicly available: invoicing, customers, products, tax configuration and VeriFactu, with a 7-day trial.