Unattended checkout invoicing
Issue a paid simplified invoice from a self-service terminal or vending machine in ONE idempotent call, print the VERI*FACTU QR that comes back, and know exactly what to do when a call fails, repeats or is rejected.
A self-service terminal or a vending machine sells, collects and prints in seconds,
with nobody to fix a mistake. This guide shows how such a terminal uses
POST /v1/invoices to
create, issue, collect and register a simplified invoice in one call, and
what comes back so the terminal can print it.
The architecture is the one the AEAT accepts for terminals: a terminal plus a central system that generates the billing record and returns it, so the terminal prints the invoice with its QR (AEAT developers' FAQ of 4 December 2025, section 5). Factuarea is that central system. It generates, numbers, chains, signs when the mode requires it and remits every record; the terminal never does.
The software that runs on the terminal has obligations of its own, among them a declaration of its own. They are in Compliance of the integrator's component.
When this applies
To a company that issues simplified invoices from a device with no operator and has these in place:
- Simplified invoices enabled for the company. Otherwise
type: F2answers422simplified_invoices_disabled. - VeriFactu enabled, in either mode. Check it with
GET /v1/verifactu/config. Without it, an alta cannot be generated and the blockverifactuanswersstatus: failedwitherror_code: verifactu_not_enabled. - An API key with
invoices:write(andverifactu:readto follow the record). Start with afact_test_key: records of a test company are never remitted to the AEAT and their QR points at the AEAT pre-production service. - A series of simplified invoices. You do not need to choose one: without
series_idthe ticket is numbered in your default simplified series, which is created automatically the first time. If you send aseries_id, it must belong to a series of simplified invoices (invoice_kind: simplified); with a series of complete invoices the call answers422series_invoice_kind_mismatch, and no draft is left behind.GET /v1/series/default?document_type=invoice&invoice_kind=simplifiedreturns the series that will be used.
A simplified invoice is only for operations up to 3.000 € VAT included, and never for intra-community, reverse-charge or export operations. See Simplified or full invoices.
The call
Everything goes in one POST /v1/invoices. These are the fields that make it a
checkout:
| Field | What it does |
|---|---|
type: "F2" | Simplified invoice. Without client_id it is an anonymous ticket; with one it is a qualified simplified invoice. |
external_id | The identity of this sale. It never expires: it is what makes a late retry safe. |
prices_include_tax: true | Every unit_price is the final price the customer paid, VAT included. |
payment | Records the payment (method; paid_at and reference optional) for the whole amount after issuing. |
options.register_verifactu: true | Generates the VERI*FACTU alta before responding. |
options.wait_for_pdf: true | Waits up to about 15 seconds for the A4 PDF. |
options.send_automatically and options.send_to | Emails the invoice once issued. |
operation_on | The day the operation took place, when it differs from issued_on. |
curl -X POST https://api.factuarea.com/v1/invoices \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Content-Type: application/json" \
-d '{
"type": "F2",
"issued_on": "2026-06-01",
"due_on": "2026-06-01",
"external_id": "KIOSK-0042-20260601-000187",
"prices_include_tax": true,
"lines": [
{ "description": "Express car wash", "quantity": 1, "unit_price": 5.00, "tax_rate": 21 }
],
"payment": { "method": "credit_card", "reference": "TPV-8841-000187" },
"options": { "register_verifactu": true, "wait_for_pdf": true }
}'In order, the system creates the draft, issues it (definitive number), registers the payment, generates the alta with its fingerprint and QR, emails it if you asked to and prepares the PDF. The response is the invoice plus three blocks:
{
"data": {
"id": "019e5b7a-3c1d-7a52-b4e1-6f2d9c8a1e37",
"type": "F2",
"number": "S-2026-001",
"status": "paid",
"subtotal": 4.13,
"total_vat": 0.87,
"total": 5,
"verifactu": {
"status": "registered",
"error_code": null,
"aeat_status": "pending",
"huella": "98F790A3765977E437FAEBCF1EF9B2B0B3462174D3D36375047185223BDE4308",
"qr_url": "https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR?nif=B12345678&numserie=S-2026-001&fecha=01-06-2026&importe=5.00",
"qr_png_base64": "iVBORw0KGgo…",
"legend": "VERI*FACTU",
"csv": null
},
"pdf": {
"status": "ready",
"url": "https://app.factuarea.com/api/pdf/materialized?company=01931b3e-1111-7a2e-9a8b-3c5d6e7f8a01&expires=1780389734&type=invoice&uuid=019e5b7a-3c1d-7a52-b4e1-6f2d9c8a1e37&signature=67d5ef33797d2d164f32bd6ceedeeefd82ceed8cb2f04023db5a5e5c926fe085",
"expires_at": "2026-06-02T10:42:14+02:00"
},
"public_url": "https://app.factuarea.com/d/019e5b7a-3c1d-7a52-b4e1-6f2d9c8a1e37"
}
}(The invoice resource carries all its usual keys; only the relevant ones are
shown.) The three blocks appear only on a checkout, that is, a request with
type: F2, a payment block or options.register_verifactu. Lists, details
and webhooks never carry them.
verifactuis what you print.statusisregisteredwhen the alta exists andfailedwhen the invoice is issued but the alta could not be generated.aeat_statusis the state of the record before the AEAT: the transmission to the AEAT follows its own course, in batches, after you have answered the customer.csvisnulluntil the AEAT accepts it.pdfis the A4 PDF.readymeans it is materialized andurlserves it at once;pendingmeans the render is queued and the URL answers404for a few seconds. It is never an error.public_urlis the page from which the customer downloads the invoice.
Prices with VAT included
A terminal knows what the customer paid, not the net base. With
prices_include_tax: true each unit_price is the final price and Factuarea
computes the base to the cent so that the total of the invoice equals the sum of
the amounts you sent. A 5,00 € ticket at 21 %, for example, is invoiced as a base of
4,13 € plus 0,87 € of VAT, and the total is exactly the 5,00 € charged.
Rounding rarely allows an exact split: 0,60 € at 21 % has no base that produces 0,60 € as a single line. The rule is any line, and split:
- The line with the largest base absorbs the cents; if it cannot, each other line is tried from the largest base to the smallest, never touching zero-amount lines.
- If no line can, the largest line is split in two: the original, with its quantity, discount and links, and a complementary line of one unit with the same description and the same VAT, whose base is the smallest possible (1 to 3 cents). The total is still exactly what you charged.
- Only a difference larger than a cent per line, which is not rounding, is
rejected with
422amount_reconciliation_failedand nothing is issued. A withholding or an equivalence surcharge can cause it.
Catalog lines (product_id) are not accepted in this mode.
VAT of a line. The line takes the tax_rate you send, then the tax it
references, then the tax of its product and, if none of them exists, the
default VAT of the company for invoices. If the company has none either, the
call answers 422 missing_required_param with error.param: lines.2.tax_rate
and error.line_index: 2, the zero-based index of the line to fix. The API
never guesses a rate. Domain errors that come from one line carry line_index
in the same way (a missing price, an exemption cause outside its catalog…); it is
additive, and param keeps naming the field.
Showing the QR
The QR is the AEAT's tax QR: it must be shown the way the AEAT requires (Order HAC/1177/2024 and the AEAT QR specification). In practice:
- Size and margin. Between 30 × 30 mm and 40 × 40 mm, with at least 2 mm of white margin around it. Factuarea prints 30 mm on tickets.
- Level. Error correction level M.
qr_png_base64is already level M; it has nodata:prefix. If your printer needs another resolution, scale the image by a whole factor or without interpolation so the modules stay sharp. - Label and legend. The label
QR tributario:goes above the code and thelegendthat comes in the response goes below it:VERI*FACTUin verifiable mode, or the sentence «Factura verificable en la sede electrónica de la AEAT» in the other. The legend uses a font no smaller than the rest of the data of the invoice. - Place. At the beginning of the document, together with the invoice data.
- Do not alter it. Print
qr_png_base64andhuellaexactly as received. Never recompute them or change amounts, number or date after the response.
If you prefer not to lay out the receipt yourself, ask Factuarea for it already formatted for a thermal roll:
curl "https://api.factuarea.com/v1/invoices/019e5b7a-3c1d-7a52-b4e1-6f2d9c8a1e37/pdf?format=ticket_80" \
-H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \
--output ticket.pdfformat is a4 (the default, with the company template), ticket_80 (80 mm
roll) or ticket_58 (58 mm roll). A ticket is laid out for a receipt printer:
QR of 30 mm, no header band, no footer, and a height that fits the content. Each
format is rendered and cached separately, so asking for a ticket never changes
the A4 and the signed URL of the A4 keeps working. The ticket has the mandatory
content of a simplified invoice, the date of the operation when it differs and
the recipient when the invoice is qualified. The ETag changes when the alta
is created, so a PDF downloaded before it (without QR) is never revalidated as
current. See GET /v1/invoices/{invoice}/pdf.
Retries, replays and concurrency
The network will drop the response of a call that did succeed. That is what
external_id is for, and it is separate from the Idempotency-Key header: it has
no expiry and is stored on the invoice. See Idempotency.
- Resend the same request, with the same
external_id. If the invoice is already issued, the API answers200withIdempotent-Replayed: trueand that invoice. Before answering it completes what was missing: it issues a draft that was never issued, registers the payment if there is still balance and generates the alta. It creates nothing new and does not consume another number. - The email is not sent twice. A replay does not email again an invoice whose email is already queued or delivered.
- A cancelled or annulled invoice receives no payment and no alta; the replay only reports its state.
- Another sale, another
external_id. If the type or the total of the replay differ from the issued invoice, the answer is409idempotency_key_reusedwithsubcode: unattended_replay_mismatchandparam: external_id. It is a bug in how you build identifiers: never reuse one. - Two simultaneous requests with the same
external_idproduce one invoice, one number and one payment. The second waits up to 20 seconds for the first and then replays it. If the first has not finished, the second answers409resource_lockedwithparam: external_idand has written nothing: send the same request again after a few seconds.
Choose an external_id per sale that is stable and unique in the company, such
as kioskId-date-sequence (KIOSK-0042-20260601-000187), and store it before
the first call.
When verifactu.status is failed
The invoice is issued and numbered, but its alta could not be generated.
verifactu.error_code says why: certificate_missing, certificate_expired,
certificate_revoked, certificate_nif_mismatch, clock_drift_exceeded,
representation_required, system_certificate_unavailable or
verifactu_not_enabled.
The terminal must not hand the document over as a valid invoice yet: treat the
sale as pending, keep its external_id, solve the cause (a certificate, a
representation, the company's VeriFactu activation) and resend the same request.
The replay generates the alta and answers 200. See
VeriFactu auto-submission.
Errors the terminal must handle
| Answer | Meaning | What to do |
|---|---|---|
200 + Idempotent-Replayed: true | The sale was already issued. | Print what comes back, if the first attempt did not print. |
409 idempotency_key_reused (unattended_replay_mismatch) | The external_id belongs to another type or total. | Fix the identifier generator. Do not retry. |
409 resource_locked | Another request of the same external_id is still running. | Send the same request again in a few seconds. |
422 missing_required_param, param: options.send_to | An email was requested with no recipient (no client, or a client with no email). | Nothing was created. Send a send_to or do not ask for the email. |
422 missing_required_param, param: lines.N.tax_rate | A line has no VAT and the company has no default. | Send tax_rate or configure the default VAT. |
422 simplified_invoices_disabled / simplified_invoice_not_allowed | Simplified invoices are not enabled, or the amount exceeds 3.000 € VAT included. | Enable them in the company, or issue a complete invoice (F1). |
422 amount_reconciliation_failed | The amounts with VAT included cannot add up exactly. | Check VAT, discounts, withholding and surcharge of each line. |
422 verifactu_not_eligible | Something does not fit the AEAT record, or the company cannot sign. | See the two tables below. Nothing is issued and no number is consumed. |
429 and 5xx | Limit or transient failure. | Retry with backoff, same external_id. |
422 verifactu_not_eligible: a field does not fit the record
Before confirming the issue, Factuarea checks that the alta the invoice will
generate is valid for the AEAT. If it is not, the invoice is not issued, it
consumes no number and you recover by correcting the field and repeating the call.
error.param names it, with the vocabulary of the API:
error.param | What does not fit |
|---|---|
client_id | Client name missing or longer than 120 characters; a Spanish tax ID that is not 9 characters; a foreign identification longer than 20; a country the AEAT does not accept; a complete invoice with no client. |
series_id | The number of the invoice has more than 60 characters or characters the AEAT does not accept (only printable ASCII, and no ", ', <, > or =). |
original_invoice_id | The number of the invoice being corrected is not admissible. |
simplified_invoice_uuids | The number of a simplified invoice substituted by an F3 is not admissible. |
company_name | The company name has more than 120 characters. A missing company name is 422 business_rule_violation, with this same param. |
lines | More than 12 different tax breakdowns, or a base, tax or surcharge that does not fit 12 integer digits and 2 decimals. |
total | A total, or its tax, that does not fit the format; an F2 above 3.000 €. |
type | The mark of qualified simplified invoice with a type that does not admit it. |
422 verifactu_not_eligible: the company cannot sign
Only for a company that has VeriFactu enabled in NO VERI*FACTU mode and has no
usable certificate. In that mode every billing record is signed, and a record
only counts as generated when it is signed, so the invoice is not issued or
annulled. The error carries subcode: signing_certificate_unavailable and the
reason in error.param:
error.param | What is missing | Who fixes it |
|---|---|---|
certificate | The company certificate is missing, expired, revoked, issued for another tax ID or unreadable. | The company: Settings → Digital certificate. |
representation | The representation that lets Factuarea sign on its behalf is not active (third-party remission modes). | The company: register it, or switch to its own certificate. |
system_certificate | The certificate of Factuarea is unavailable. | Factuarea. Retry in a few minutes; if it persists, contact support. |
The invoice stays exactly as it was (a draft without a number) and nothing is consumed. A company with VeriFactu disabled, or in VERI*FACTU mode, is never blocked by this rule.
Conversions, returns and mistakes
- Return of the goods or the money. A return is a corrective invoice:
POST /v1/invoices/{invoice}/corrective. The corrective of an anonymous simplified invoice is registered asR5; that of a qualified one, asR1toR4with the same mark. See Corrective invoices. - The customer asks for a complete invoice for tickets already issued:
POST /v1/invoices/substitute-simplifiedgroups the simplified invoices under oneF3. - An operation issued by mistake and already paid, such as a duplicated
charge:
POST /v1/invoices/{invoice}/annulwithrevert_collections: truereverts every live payment and annuls the invoice in one atomic operation. If any step fails nothing is reverted. It is an annulment, not a refund: if the customer must get the money back, issue a corrective.can-annultells you beforehand (requires_collection_reversal,active_collections_amount). - Conversion of a quote, a proforma or a delivery note into an invoice answers
with
warningsandwarning_codes(zero_rate_line_without_exemption) when a line stays at 0 % with no exemption cause, because those documents do not model it. It does not block: the invoice is a draft and you set the cause before issuing.
No connection, no invoice
There is no offline mode. A terminal with no connection does not issue: the invoice exists only once the API has answered, because its billing record has to be generated simultaneously with, or immediately before, its issue (RD 1007/2023, art. 9). The terminal must not print a document as an invoice from its own numbering and send it later. What to do with a sale that cannot be invoiced at that moment is a decision of the operator, outside Factuarea.