Versioning

The version is in the path — https://api.essentio.pro/v1 — and it is the only one there is. What v1 answers is a promise: within v1 the API only grows, so a program written against it today keeps working.

v1 only grows

Within v1 these may be added:

  • endpoints, and optional parameters and fields of a request;
  • fields of an answer;
  • values of a field that lists its values — a document’s status, a Line’s vat_category, a Payment’s payment_method;
  • error types, Refusal codes and remedies;
  • Webhook events.

Nothing is renamed or removed: no endpoint, field, value, error type, Refusal code or event. There are no versions pinned by date, and nothing to send to choose one.

So write a program that expects more than it knows:

  • Ignore a field you do not know. Never refuse an answer because it holds one — don’t validate answers against a closed schema.
  • Keep a value you do not know rather than failing on it, and treat it as “something else”: an unknown status is a state your program does not handle yet.
  • Treat an unknown error type by its HTTP status, and an unknown Refusal code as a refusal you do not handle yet, showing its message.
  • Ignore a Webhook event whose type you do not know, and answer it 2xx.

Open value sets

The contract, the OpenAPI file, is written to be read that way. It closes no object — an answer may carry fields the file you downloaded does not list — and it lists an answer’s values as they are today under x-known-values, never as an enum, with each description saying: “Within v1 another value may be added: keep a value you do not know.” A client generated from the file therefore takes a new value as a string instead of failing to parse the answer.

What you send is held to the rules: a request’s parameters and fields keep their enums, and a value the API does not take is refused with a 422 invalid_request.

The API is tested against the file: every answer the test suite receives is checked against it, and a field the server sends that the file does not declare fails the build. Download the file again to see what was added.

A breaking change is a v2

A change that could break a program written against v1 — a field renamed, removed or given another type, a value no longer answered — is never made to v1. It is made in a v2, at https://api.essentio.pro/v2, and v1 keeps answering beside it for at least twelve months after v2 is announced, so you move when it suits you. There is no v2 today.

Changelog

  • 2026-10-03 — a Client’s VAT number is written with its country code. A document’s EN16931 XML to a Client whose VAT number Essentio stores without its country code — as the Client form stores every one — was refused not_exportable since 2026-10-02, saying the number does not start with its country code; it is now exported, the number written as the Client’s VAT identifier (BT-48) with the Client’s country first, in capitals (EL for Greece). A number stored with two letters first is written as stored, as before. Still refused: a Client’s number whose two first letters are not a country code in capitals, or one stored without a code for a Client with no country, in the new words “The Client’s VAT Number has no country code the export can use: enter the number with the Client’s country set, or with its code first in capitals, such as DE123456789. Fix it on the Client.” The JSON answers are unchanged.
  • 2026-10-02 — the EN 16931 check asks for what the standard asks for. A document’s EN16931 XML is refused not_exportable (the same code, in new words) by the standard’s own rule table: a VAT number of the Business only for an intra-community supply (K) or an export (G); for S, Z, E and AE Lines a VAT number or a TAX ID; for a document of O Lines a Registration Number or a TAX ID — so a Business with a TAX ID and no VAT number, or a Non-VAT Payer, now exports where it was refused. Newly refused, before any file is written: an O Line beside another VAT category, a VAT number (the Business’s or the Client’s) not starting with its two-letter country code in capitals, an AE document whose Client has no VAT number, and a K document whose Client has no VAT number or no country. error.message carries each reason as one sentence saying where it is fixed. The JSON answers, and the XML of a document that passed before, are unchanged.
  • 2026-10-02 — a document’s XML follows EN 16931 itself. A document’s EN16931 XML no longer claims Peppol BIS Billing 3.0: its cbc:CustomizationID is urn:cen.eu:en16931:2017 and it carries no cbc:ProfileID. A Business with no bank account is written with the payment means code 1 (not defined) where it was 30 (credit transfer) with no account. A document whose Lines are all not subject to VAT (O) carries neither party’s VAT number, and an O Line or breakdown no cbc:Percent. A Business whose VAT number is not written — a Non-VAT Payer, whose stored number the file no longer carries, or a document of O Lines — is identified by its TAX ID (BT-29, BT-32) when it has one. A document with an intra-community supply (K) carries its delivery: the issue date and the Client’s country. Any other file of a VAT payer changes in its cbc:CustomizationID and cbc:ProfileID only. The JSON answers are unchanged.
  • 2026-10-02 — a Non-VAT Payer’s VAT number is printed nowhere. A Business set to Non-VAT Payer keeps the VAT number it stored and prints none: its documents’ From block and PDFs, the public page, Receipts and Statements leave it out, and the Business setup answers vat_number as null while it is a Non-VAT Payer. A VAT payer’s answers are unchanged. The one place that still carries the stored number is the document’s EN16931 XML, until a later change. The field already allowed null: nothing was added.
  • 2026-10-02 — a Product’s price is read to its last decimal, and held to its currency. A Product’s price — as the API answers it and to the MCP server’s tools — is the price as stored: in a currency of three decimals a price of 10.125 is "10.125", where it read "10.13", and price_without_vat and price_with_vat are computed from it ("10.125" and not "10.130"). A price of two decimals or fewer reads as before. A price sent with more decimals than its currency has was stored and read rounded, and one of more than 15 digits before the point failed; each is now a 422 invalid_request with errors.price, and nothing is written. A currency changed alone to one the Product’s price has more decimals than is refused the same way, with errors.currency. A price, or a Payment’s amount, written with an exponent of more than three digits ("1e1000") is refused in words, a 422 invalid_request too; it was refused later for another reason, or failed. MCP’s tools answer the same.
  • 2026-10-02 — a number or an amount no column can hold is refused in words. An issue, a send or a Payment whose next number is the largest the numbering holds answered a 500; it is now a 422 refusal with the new code numbering_at_its_end, nothing written — a Draft can still be issued with an unused earlier number sent as its document_number. A Client’s credit_limit with a third decimal is refused (invalid_request), where it was rounded to two. And these, which answered a 500, are now a 422 invalid_request naming the field: payment_terms past 999999 days, on a Client and a document; a Product’s stock_quantity or minimum_stock past 2147483647; a Line whose Net amount (quantity × unit price) is not below 1,000,000,000,000,000, under its unit_price; and Lines whose Total is not below it, under lines.
  • 2026-10-01 — a document says when, to whom and how it was last sent. A document — as the API answers it, in a Webhook’s data and to the MCP server’s tools — carries last_sent: {at, recipients, via}, the newest time it was sent while it is marked sent, or null. via is the provider that took the email (essentio, gmail, microsoft, smtp) or marked, a mark by hand with no recipients. Nothing else changed: a field is only ever added within v1.
  • 2026-09-30 — Essentio sends a Business's mail. A Business with no mailbox connected now sends through Essentio's own mail server, so Send is open for it where it was refused no_mail_provider; that code now means a Business with no email address of its own. The new code sending_limit_reached refuses a Send past the Business's daily number of recipients through Essentio, with the remedy connect_mail_provider.
  • 2026-09-30 — a document’s XML carries its Lines’ notes. A Line’s notes, downloaded as the document’s EN16931 XML, is written in that Line as BT-127, Invoice line note: one cbc:Note after the Line’s cbc:ID, in an Invoice’s cac:InvoiceLine and a Credit Note’s cac:CreditNoteLine. A Line with no note exports as before, and nothing is written as the Item’s description (BT-154).
  • 2026-09-30 — a Line takes a note, and a Line’s errors are in plain words. A Line sent to create or update a Draft — through the API or the MCP server’s essentio_create_draft and essentio_update_draft — takes notes, at most 500 characters, printed with the Line on the document; until now a Line’s notes was ignored. A document’s Lines — as the API answers them, in a Webhook’s data and to the MCP server’s tools — carry notes, null when the Line has none. And an error about a Line now names the Line by its place, counted from 1, and its field by name: “Line 1: Unit must not be longer than 50 characters.”, where it read “The lines.0.unit field must not be greater than 50 characters.”. Every Line error has the new words, a decimal sent as a JSON number included; the keys in errors did not change (lines.0.unit).
  • 2026-09-30 — the MCP server's answers no longer link to a plan. A document's actions, as the MCP server's tools answer them, carry no remedy_url with the remedy choose_plan; its code, words and remedy stay. The Public API still sends the link, and connect_mail_provider's link stays on both.
  • 2026-09-30 — a Test business keeps no real books. The Business setup and every document carry is_test. A Test business’s numbers carry TEST- in front of what its number patterns make; its XML is refused with the new code exports_not_in_a_test_business; and it meets the Free plan’s Issuing limit (issuing_limit_reached), which a reset clears. An ordinary Business’s answers are unchanged.
  • 2026-09-30 — a document says whether it is Overdue, and names its conversion. A document — as the API answers it, in a Webhook’s data and to the MCP server’s tools — carries four more fields: is_overdue and days_overdue (null while it is not Overdue), as Essentio shows it, so an overdue Invoice no longer reads only issued; proforma_id, the Proforma Invoice it was converted from; and converted_into_id, the Invoice a Proforma Invoice was converted into. Nothing else changed: a field is only ever added within v1.
  • 2026-09-29 — choose_plan says where to choose a plan. A document’s actions now carry remedy_url with the remedy choose_plan too, as they did with connect_mail_provider: the page in Essentio where the Business’s Owner chooses a plan (Settings → Billing), for a person signed in to Essentio who belongs to the Business.
  • 2026-09-29 — a Draft deleted meanwhile is refused. An issue, a send or an update of a Draft that another request deleted while this one waited for it issued or changed the deleted Draft. It is now a 422 refusal, draft_deleted ("This draft was deleted."): nothing is written and no number is taken. A delete of a Draft that another request issued while this one waited for it deleted the issued document; it is now a 422 refusal, issued_not_deleted, and the document stays. A delete of a Draft that another request deleted meanwhile is answered as before, as done.
  • 2026-09-29 — an amendment that sends no field says there is nothing to change. PATCH /v1/payments/{id} with a body that sends none of a Payment’s fields — {} — answered a 422 refusal saying the amount must be greater than zero, a field it never sent. It is now a 422 invalid_request with errors.payment: “Nothing to change: send at least one field.” Nothing is written, as before. MCP’s essentio_amend_payment given only payment_id answers the same.
  • 2026-09-29 — on the Free plan the Owner’s own MCP Connected app is not cut to reading. The plan cuts a Connected app as it cuts the person who authorised it: on the Free plan the Owner’s own app does what the Role it was given lets it — creating Clients, issuing Invoices up to the Issuing limit, where it is refused issuing_limit_reached — and an Admin’s or a Member’s still only reads (plan_role_viewing). A document’s actions answer the same. A Paused Business and the Public API are unchanged.
  • 2026-09-29 — the Public API needs a paid plan. While a Business is on the Free plan or Paused, every request of its API keys and Connected apps — a read included — is a 403 permission with the code paid_plan_required and the remedy choose_plan, and does nothing. The keys and apps are not revoked: the same credential works again once the Business is on Pro or Pro Plus. The MCP server is not the Public API and keeps working on every plan, so business_paused and plan_role_viewing (the entry below) are now answered there and in Essentio, never by the Public API. message says whether the Business is on the Free plan or paused.
  • 2026-09-29 — a Business’s plan cuts what a credential may do. On the Free plan every credential only reads, and a Paused Business keeps reading and recording Payments. A change the plan does not let is still a 403 permission, now with a code — business_paused or plan_role_viewing — and the remedy choose_plan; a document’s actions answer the same codes.
  • 2026-09-29 — an update of a Draft linked to a Cancelled Proforma Invoice is refused. PATCH /v1/documents/{id} of a Draft still linked to a Proforma Invoice that was cancelled answered 200 and issued the Draft. It is now a 422 refusal, cancelled_not_converted: nothing is written, the Draft keeps its link and takes no number.
  • 2026-09-29 — every refusal carries a Refusal code. Three answers that were a 422 invalid_request in words alone are now a 422 refusal with a code, in the same words: an update of an issued document (issued_not_changed), an update of a Draft linked to a Proforma Invoice that does not convert into it (proforma_deleted, only_into_an_invoice, proforma_other_currency, proforma_other_client), and the XML of a document that is not exported (not_exportable). invalid_request now means only that the request’s fields, parameters or headers were refused. The Refusal codes list every code Essentio has, including the ones no operation answers today.