Factuarea APIDevelopers

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: F2 answers 422 simplified_invoices_disabled.
  • VeriFactu enabled, in either mode. Check it with GET /v1/verifactu/config. Without it, an alta cannot be generated and the block verifactu answers status: failed with error_code: verifactu_not_enabled.
  • An API key with invoices:write (and verifactu:read to follow the record). Start with a fact_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_id the ticket is numbered in your default simplified series, which is created automatically the first time. If you send a series_id, it must belong to a series of simplified invoices (invoice_kind: simplified); with a series of complete invoices the call answers 422 series_invoice_kind_mismatch, and no draft is left behind. GET /v1/series/default?document_type=invoice&invoice_kind=simplified returns 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:

FieldWhat it does
type: "F2"Simplified invoice. Without client_id it is an anonymous ticket; with one it is a qualified simplified invoice.
external_idThe identity of this sale. It never expires: it is what makes a late retry safe.
prices_include_tax: trueEvery unit_price is the final price the customer paid, VAT included.
paymentRecords the payment (method; paid_at and reference optional) for the whole amount after issuing.
options.register_verifactu: trueGenerates the VERI*FACTU alta before responding.
options.wait_for_pdf: trueWaits up to about 15 seconds for the A4 PDF.
options.send_automatically and options.send_toEmails the invoice once issued.
operation_onThe 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.

  • verifactu is what you print. status is registered when the alta exists and failed when the invoice is issued but the alta could not be generated. aeat_status is 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. csv is null until the AEAT accepts it.
  • pdf is the A4 PDF. ready means it is materialized and url serves it at once; pending means the render is queued and the URL answers 404 for a few seconds. It is never an error.
  • public_url is 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:

  1. 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.
  2. 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.
  3. Only a difference larger than a cent per line, which is not rounding, is rejected with 422 amount_reconciliation_failed and 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_base64 is already level M; it has no data: 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 the legend that comes in the response goes below it: VERI*FACTU in 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_base64 and huella exactly 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.pdf

format 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 answers 200 with Idempotent-Replayed: true and 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 is 409 idempotency_key_reused with subcode: unattended_replay_mismatch and param: external_id. It is a bug in how you build identifiers: never reuse one.
  • Two simultaneous requests with the same external_id produce 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 answers 409 resource_locked with param: external_id and 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

AnswerMeaningWhat to do
200 + Idempotent-Replayed: trueThe 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_lockedAnother 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_toAn 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_rateA 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_allowedSimplified 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_failedThe amounts with VAT included cannot add up exactly.Check VAT, discounts, withholding and surcharge of each line.
422 verifactu_not_eligibleSomething 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 5xxLimit 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.paramWhat does not fit
client_idClient 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_idThe number of the invoice has more than 60 characters or characters the AEAT does not accept (only printable ASCII, and no ", ', <, > or =).
original_invoice_idThe number of the invoice being corrected is not admissible.
simplified_invoice_uuidsThe number of a simplified invoice substituted by an F3 is not admissible.
company_nameThe company name has more than 120 characters. A missing company name is 422 business_rule_violation, with this same param.
linesMore than 12 different tax breakdowns, or a base, tax or surcharge that does not fit 12 integer digits and 2 decimals.
totalA total, or its tax, that does not fit the format; an F2 above 3.000 €.
typeThe 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.paramWhat is missingWho fixes it
certificateThe company certificate is missing, expired, revoked, issued for another tax ID or unreadable.The company: Settings → Digital certificate.
representationThe 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_certificateThe 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 as R5; that of a qualified one, as R1 to R4 with the same mark. See Corrective invoices.
  • The customer asks for a complete invoice for tickets already issued: POST /v1/invoices/substitute-simplified groups the simplified invoices under one F3.
  • An operation issued by mistake and already paid, such as a duplicated charge: POST /v1/invoices/{invoice}/annul with revert_collections: true reverts 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-annul tells you beforehand (requires_collection_reversal, active_collections_amount).
  • Conversion of a quote, a proforma or a delivery note into an invoice answers with warnings and warning_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.

On this page

Need a hand?Contact support