Validation
A-Cube validates every document before it builds the billing record, so that AEAT
does not reject a record A-Cube could have caught. Validation is synchronous: if
anything is wrong, the request fails immediately with a 4xx response, nothing is
stored, and nothing is sent to AEAT.
A request that passes validation is answered 202 Accepted, and AEAT's own verdict is
then reported in the invoice status (see
Invoice lifecycle).
Where validation runs
| Call | What is checked |
|---|---|
POST /invoices | everything: document structure and formats, AEAT rules, duplicate registration |
POST /invoices/simplified | everything, as above |
POST /invoices/draft/{uuid}/send | draft completeness, then everything, as above |
POST /invoices/{uuid}/correction | document structure and formats, AEAT rules, unchanged invoice identity |
POST / PUT on the draft endpoints | document structure and the mandatory identity only: a draft may be incomplete |
The checks run in steps: the document is parsed first, then the structure and the field formats are validated, then the AEAT rules together with the duplicate check. Only the errors of the first failing step are returned, so fixing them can reveal errors of a later step.
Error response format
Errors are returned as RFC 7807 problem
details (Content-Type: application/problem+json):
| Field | Meaning |
|---|---|
status | the HTTP status code |
title | always An error occurred |
detail | a human-readable summary; for validation errors, every violation as propertyPath: message, one per line |
type | /errors/{status} for request errors, /validation_errors/{code} for validation errors |
violations | only on validation errors: the list of individual violations |
Each entry of violations has:
| Field | Meaning |
|---|---|
propertyPath | where the error is, in the terms of the document you sent (see below) |
message | what is wrong |
code | for an AEAT rule, the AEAT error code (for example 1150); otherwise a generic validator identifier or null. Match on it only for AEAT codes |
propertyPath follows the document you sent:
| Input | propertyPath example |
|---|---|
| Facturae (XML or JSON) | Facturae/Invoices/Invoice[0]/TaxesOutputs/Tax[0]/TaxRate |
| native VERI*FACTU or simplified invoice | RegistroAlta/Desglose/DetalleDesglose[0]/CuotaRepercutida |
| XML that cannot be parsed | line 1 (the line of the XML error) |
| draft send (completeness) | the invoice field, for example invoiceNumber |
The AEAT rules are written against the VERI*FACTU record, but they also apply to a Facturae document: A-Cube reports them on the Facturae element the record value comes from.
Error groups
| Status | Group | When |
|---|---|---|
400 | Malformed JSON | the JSON body cannot be parsed |
403 | Permission | you cannot issue invoices for the sender tax id |
409 | Conflict | the operation is not allowed in the current state of the invoice |
415 | Unsupported format | the Content-Type is not accepted by the endpoint |
422 | Malformed XML | the XML body is not well formed |
422 | Structure and formats | a required element is missing or a value has the wrong format |
422 | AEAT rules | the record breaks a VERI*FACTU validation rule, or is already registered |
422 | Incomplete draft | a draft is sent before it is complete |
422 | Correction identity | a correction changes the identity of the invoice it corrects |
Malformed JSON
The JSON body is not valid JSON. Fix the syntax and send it again.
{
"title": "An error occurred",
"detail": "The JSON data provided is invalid: Syntax error",
"status": 400,
"type": "/errors/400"
}
Permission
The sender tax id (SellerParty in Facturae, IDEmisorFactura in a native or
simplified record) must belong to a legal entity
you own with customerInvoiceEnabled. The same 403 is returned when your token lacks
the ROLE_WRITER role.
{
"title": "An error occurred",
"detail": "User is not allowed to send invoices for this tax id",
"status": 403,
"type": "/errors/403"
}
Conflict
The operation does not fit the current state of the invoice. The detail explains
which case applies:
detail | Cause |
|---|---|
Invoice is not in draft state | updating, sending or deleting a draft that was already sent |
Invoice is a simplified invoice draft; update it with PUT /invoices/draft/simplified/{uuid} | PUT /invoices/draft/{uuid} on a simplified draft |
Invoice is not a simplified invoice draft; update it with PUT /invoices/draft/{uuid} | PUT /invoices/draft/simplified/{uuid} on a complete-invoice draft |
Only an invoice accepted with errors can be corrected | a correction of an invoice that is not accepted_with_errors |
{
"title": "An error occurred",
"detail": "Only an invoice accepted with errors can be corrected",
"status": 409,
"type": "/errors/409"
}
Unsupported format
The Content-Type is not one the endpoint accepts: for example a Facturae document
sent to POST /invoices/simplified, or a correction
in a different format from the invoice it corrects. The accepted values are listed in
Send invoice and
Simplified invoices.
{
"title": "An error occurred",
"detail": "Please send the simplified invoice as a native VERI*FACTU xml or as json.",
"status": 415,
"type": "/errors/415"
}
Malformed XML
The XML body is not well formed. Each parser error is a violation whose propertyPath
is the line where it occurred.
{
"status": 422,
"violations": [
{
"propertyPath": "line 1",
"message": "Namespace prefix fe on Facturae is not defined",
"code": null
},
{
"propertyPath": "line 1",
"message": "Premature end of data in tag broken line 1",
"code": null
}
],
"detail": "line 1: Namespace prefix fe on Facturae is not defined\nline 1: Premature end of data in tag broken line 1",
"type": "/validation_errors/0=;1=",
"title": "An error occurred"
}
Structure and formats
A required element is missing, or a value does not have the expected format. For a
Facturae document this covers the Facturae 3.2.2 structure and its business rules (for
example a single invoice per document, matching ResidenceTypeCode and country, IRPF
as the only withheld tax, due dates not earlier than the issue date). For a native or
simplified record it covers the AEAT field formats:
| Element | Rule |
|---|---|
IDEmisorFactura, IDDestinatario/NIF | a valid Spanish NIF, NIE or CIF, including the check digit |
NumSerieFactura | 1 to 60 printable ASCII characters, without " ' < = > |
FechaExpedicionFactura | a real date in dd-mm-yyyy format, not later than today |
NombreRazonEmisor, NombreRazon | up to 120 characters |
DescripcionOperacion | not blank, up to 500 characters |
CuotaTotal, ImporteTotal, BaseImponibleOimporteNoSujeto, CuotaRepercutida | a number with up to 12 integer digits and 2 decimals, optionally negative |
TipoImpositivo | a rate with up to 3 integer digits and 2 decimals |
Impuesto | 01, 02, 03 or 05 |
ClaveRegimen | one of the AEAT codes 01 to 11, 14, 15, 17 to 21 |
CalificacionOperacion | S1, S2, N1 or N2 |
FacturaSinIdentifDestinatarioArt61d | S or N |
IDOtro/IDType | 02 to 07; IDOtro/CodigoPais must be a valid country code |
In the sandbox, the reserved ESSANDBOX* recipient tax ids are exempt from the NIF
check, so you can use them to simulate AEAT outcomes.
{
"status": 422,
"violations": [
{
"propertyPath": "RegistroAlta/Destinatarios/IDDestinatario[0]/NIF",
"message": "The tax identifier \"B12345675\" is not a valid Spanish NIF, NIE or CIF.",
"code": null
}
],
"detail": "RegistroAlta/Destinatarios/IDDestinatario[0]/NIF: The tax identifier \"B12345675\" is not a valid Spanish NIF, NIE or CIF.",
"type": "/validation_errors/00000000000020590000000000000000",
"title": "An error occurred"
}
AEAT rules
The record breaks one of the rules AEAT applies to a VERI*FACTU billing record
(Validaciones y errores, v1.2.2). A-Cube checks these rules itself and returns the
AEAT error code in code, so you can map it exactly as you would map an AEAT
rejection.
code | Rule |
|---|---|
1114 | TipoFactura R1 to R5 requires TipoRectificativa, which is not supported yet |
1111 | IDOtro/CodigoPais is required when IDType is not 02 (NIF-IVA) |
1124 | TipoImpositivo is not an IVA rate in force on the issue date (0, 4, 10, 21, plus 5 from 01-07-2022 to 30-09-2024 and 2 / 7.5 from 01-10-2024 to 31-12-2024) |
1126 | IDOtro/CodigoPais ES is only allowed with IDType 03 (passport) or 07 (not registered in the AEAT census), and 07 requires ES |
1139 | an ImporteTotal of 100,000,000 or more requires Macrodato, which is not supported yet |
1142 | CuotaRepercutida differs from BaseImponibleOimporteNoSujeto × TipoImpositivo / 100 by more than 10 |
1143 | CuotaRepercutida and BaseImponibleOimporteNoSujeto have different signs |
1147 | ClaveRegimen 14 requires FechaOperacion, which is not supported yet |
1150 | a simplified invoice (F2) exceeds 3000 EUR (+10 EUR tolerance) and FacturaSinIdentifDestinatarioArt61d is not S |
1152 | FechaExpedicionFactura is earlier than 28-10-2024, the date VERI*FACTU came into force |
1185 | FacturaSinIdentifDestinatarioArt61d S is only allowed when TipoFactura is F2 or R5 |
1189 | Destinatarios is required when TipoFactura is F1, F3 or R1 to R4 |
1190 | Destinatarios must not be sent when TipoFactura is F2 or R5 |
1193 | the recipient NIF is the same as IDEmisorFactura |
1197 | CalificacionOperacion S2 is only allowed when TipoFactura is F1, F3 or R1 to R4 |
1198 | CalificacionOperacion S2 requires TipoImpositivo and CuotaRepercutida to be 0 |
1200 | ClaveRegimen 03 requires CalificacionOperacion S1 |
1201 | ClaveRegimen 04 requires CalificacionOperacion S2 |
1202 | ClaveRegimen 06 requires BaseImponibleACoste, which is not supported yet |
1203 | ClaveRegimen 07 does not allow CalificacionOperacion S2, N1 or N2 |
1205 | ClaveRegimen 10 requires CalificacionOperacion N1 and TipoFactura F1 |
1206 | ClaveRegimen 11 under IVA requires TipoImpositivo 21 |
1207 | CuotaRepercutida can only be different from 0 when CalificacionOperacion is S1 |
1237 | CalificacionOperacion N1 / N2 (not subject) under IVA is not supported yet |
1246 | ClaveRegimen 21 is only allowed when Impuesto is 03 (IGIC) |
1252 | ClaveRegimen 08 requires CalificacionOperacion N2 |
1260 | ClaveRegimen is only allowed when Impuesto is 01 (IVA), 02 (IPSI) or 03 (IGIC) |
3000 | an invoice with the same issuer, number and issue date already exists; only drafts and rejected invoices do not count |
{
"status": 422,
"violations": [
{
"propertyPath": "RegistroAlta/Desglose",
"message": "The sum of BaseImponibleOimporteNoSujeto and CuotaRepercutida of a simplified invoice cannot exceed 3000 (+10) unless FacturaSinIdentifDestinatarioArt61d is S.",
"code": "1150"
}
],
"detail": "RegistroAlta/Desglose: The sum of BaseImponibleOimporteNoSujeto and CuotaRepercutida of a simplified invoice cannot exceed 3000 (+10) unless FacturaSinIdentifDestinatarioArt61d is S.",
"type": "/validation_errors/1150",
"title": "An error occurred"
}
A duplicate is reported the same way, with code 3000 on the invoice header:
{
"status": 422,
"violations": [
{
"propertyPath": "Facturae/Invoices/Invoice[0]/InvoiceHeader",
"message": "Invoice \"FV-2026-0001\" of 15-01-2026 has already been registered for IDEmisorFactura \"A58818501\".",
"code": "3000"
}
],
"detail": "Facturae/Invoices/Invoice[0]/InvoiceHeader: Invoice \"FV-2026-0001\" of 15-01-2026 has already been registered for IDEmisorFactura \"A58818501\".",
"type": "/validation_errors/3000",
"title": "An error occurred"
}
Incomplete draft
POST /invoices/draft/{uuid}/send
first checks that the draft holds everything an invoice needs. Each missing value is a
violation on the invoice field, as returned by
GET /invoices/draft/{uuid}:
invoiceNumber, invoiceDate, currency, totalAmount, netAmount, taxAmount and
verifactuInvoiceType (the AEAT TipoFactura). The draft is left unchanged: complete
it with PUT and send it again.
{
"status": 422,
"violations": [
{ "propertyPath": "invoiceNumber", "message": "This value should not be null.", "code": "ad32d13f-c3d4-423b-909a-857b961eb720" },
{ "propertyPath": "invoiceDate", "message": "This value should not be null.", "code": "ad32d13f-c3d4-423b-909a-857b961eb720" },
{ "propertyPath": "currency", "message": "This value should not be null.", "code": "ad32d13f-c3d4-423b-909a-857b961eb720" },
{ "propertyPath": "totalAmount", "message": "This value should not be null.", "code": "ad32d13f-c3d4-423b-909a-857b961eb720" },
{ "propertyPath": "netAmount", "message": "This value should not be null.", "code": "ad32d13f-c3d4-423b-909a-857b961eb720" },
{ "propertyPath": "taxAmount", "message": "This value should not be null.", "code": "ad32d13f-c3d4-423b-909a-857b961eb720" },
{ "propertyPath": "verifactuInvoiceType", "message": "This value should not be null.", "code": "ad32d13f-c3d4-423b-909a-857b961eb720" }
],
"detail": "invoiceNumber: This value should not be null.\ninvoiceDate: This value should not be null.\ncurrency: This value should not be null.\ntotalAmount: This value should not be null.\nnetAmount: This value should not be null.\ntaxAmount: This value should not be null.\nverifactuInvoiceType: This value should not be null.",
"type": "/validation_errors/0=ad32d13f-c3d4-423b-909a-857b961eb720;1=ad32d13f-c3d4-423b-909a-857b961eb720;2=ad32d13f-c3d4-423b-909a-857b961eb720;3=ad32d13f-c3d4-423b-909a-857b961eb720;4=ad32d13f-c3d4-423b-909a-857b961eb720;5=ad32d13f-c3d4-423b-909a-857b961eb720;6=ad32d13f-c3d4-423b-909a-857b961eb720",
"title": "An error occurred"
}
Once the draft is complete, the send runs the same structure, AEAT rule and duplicate
checks as POST /invoices.
Correction identity
A correction must keep the issuer, the number and the issue date of the invoice it corrects. The invoice is left unchanged.
{
"title": "An error occurred",
"detail": "A correction must keep the IDEmisorFactura, NumSerieFactura and FechaExpedicionFactura of the invoice it corrects",
"status": 422,
"type": "/errors/422"
}