FacturaE 3.2.2 from JSON: pitfalls of XAdES-EPES signing
FacturaE is the XML invoice format required for billing Spanish public administrations. The format is documented. The signing is not trivial. Here's what you actually encounter building a FacturaE API: XAdES-EPES canonicalization traps, why we chose FirmaXadesNet over a custom implementation, FACe submission idempotency, and how a JSON-in / signed-XML-out model keeps integrations clean.
FacturaE in one page
FacturaE (format version 3.2.2, defined in RD 1619/2012) is an XML invoice format mandatory for electronic invoicing with Spanish public administrations since 2015. Some large private buyers also request it as a condition of their payment terms, but it does not satisfy the AEAT B2B e-invoicing mandate, which requires UBL.
The XML structure covers:
- FileHeader: schema version, modality (individual invoice vs. batch), batch information if applicable.
- Parties: SellerParty and BuyerParty, each with fiscal data (NIF, address, contact details).
- Invoices — one or more invoice records, each with:
- InvoiceHeader: number, series, type (F1 for standard, F2 for simplified, R1–R5 for corrective).
- InvoiceIssueData: dates, place of issue, currency.
- TaxesOutputs: VAT breakdown by rate, with taxable base, rate, and tax amount per entry.
- InvoiceTotals: gross amount, taxes, total payable, amounts already paid.
- Items: line items with description, quantity, unit price, taxes, totals.
- PaymentDetails: method, due date, IBAN for bank transfer.
The schema is XSD-validated. A conformant XML that fails XSD validation will be rejected by FACe (the Spanish government's invoice submission platform) immediately, before even reaching signature verification.
The signing problem: XAdES-EPES is not trivial
FacturaE requires XAdES-EPES (XML Advanced Electronic Signatures, Explicit Policy Electronic Signature). This is a profile of XAdES, which is itself a profile of XMLDSig. Each layer adds requirements on top of the base standard.
The traps that catch most implementations:
Canonicalization (C14N)
XML canonicalization normalizes the XML before hashing to ensure that logically equivalent documents produce the same digest. FacturaE requires Canonical XML 1.0 (C14N). The common mistake is using Exclusive C14N (EXC-C14N, the variant used in WS-Security and SOAP signing), which produces a different output. FACe rejects signatures computed over EXC-C14N without a useful error message.
The transform URI to use is http://www.w3.org/TR/2001/REC-xml-c14n-20010315, not
http://www.w3.org/2001/10/xml-exc-c14n#.
Signature policy
XAdES-EPES requires a SignaturePolicyIdentifier element referencing the policy under
which the signature is made. For FacturaE, this is the FACTURAE policy, with a specific identifier
OID and a hash of the policy document. The hash must match exactly — and the policy document the
AEAT publishes has had minor revisions that change the hash. Using the hash from an outdated
example is a common source of rejection.
Signing certificate inclusion
The signing certificate (the customer's FNMT certificate) must be included in the
KeyInfo element of the signature. The certificate must be included as a base64-encoded
DER blob, not as a PEM string. The order of elements within KeyInfo is specified —
deviating from it produces valid XML that some validators accept and others reject.
Timestamp
XAdES-EPES at the level required by FacturaE includes a SigningTime element. This must
be an ISO 8601 datetime with timezone offset. UTC-only timestamps (ending in Z) are technically
valid but have caused interoperability issues with some FACe validation services. Using the local
Spanish timezone offset (+01:00 or +02:00 for DST) is safer.
Why we use FirmaXadesNet
When building the initial version of Kreyo FacturaE, we evaluated two options: a custom XAdES
implementation using .NET's System.Security.Cryptography.Xml, or using
FirmaXadesNet — a .NET library maintained by the Spanish government's Ministerio de Hacienda
for exactly this purpose.
The custom implementation was abandoned after two days. The issues:
- Getting the C14N transform order correct in
System.Security.Cryptography.Xmlrequires precise configuration that isn't documented in a way that makes the FacturaE-specific requirements clear. - The
SignaturePolicyIdentifierstructure is not part of the .NET standard library — it requires custom XML construction that is easy to get wrong. - Testing requires generating a signature and submitting it to FACe's validation service, which has rate limits and a turnaround time that makes rapid iteration slow.
FirmaXadesNet solves all of these:
- It produces C14N-normalized signatures that FACe accepts.
- It includes the correct FacturaE policy identifier out of the box.
- It has been validated against FACe in production by multiple vendors over several years.
The decision to use a government-maintained library for a government-mandated signature format is clear-cut. The cost is that it's a .NET 4.x-era library that requires some care to integrate cleanly into a modern .NET 9 project, but that's a one-time setup cost.
XSD validation before signing
Always validate the generated XML against the official FacturaE 3.2.2 XSD before signing. Signing an invalid document costs time: the signature is irreversible, the FACe rejection response doesn't always pinpoint the XSD violation, and you have to regenerate and re-sign.
The official XSD is published by the Spanish government at the URL referenced in the schema. In .NET, the validation setup is simple:
var settings = new XmlReaderSettings();
settings.Schemas.Add("http://www.facturae.gob.es/formato/Version3.2.2/Facturae_v3_2_2.xsd",
"Facturae_v3_2_2.xsd");
settings.ValidationType = ValidationType.Schema;
settings.ValidationEventHandler += (s, e) => {
if (e.Severity == XmlSeverityType.Error)
throw new InvalidOperationException($"XSD violation: {e.Message}");
};
using var reader = XmlReader.Create(new StringReader(xmlContent), settings);
while (reader.Read()) { }
Common XSD violations:
- Decimal precision exceeding the specified number of digits (FacturaE uses up to 8 decimal places for some fields).
- Missing required elements for the invoice type (e.g., corrective invoice type R1 requires
CorrectiveDetails, which some generators omit). - NIF format not matching the allowed pattern — the XSD constrains this via a regular expression.
- Date format issues — FacturaE uses
YYYY-MM-DDnot ISO 8601 datetime.
FACe submission: idempotency and retries
Once the FacturaE XML is generated and signed, it's submitted to FACe (or directly to the buyer's receiving platform for B2B). FACe is a web service with its own submission protocol.
FACe responses on submission include a status code:
| Code | Meaning | Action |
|---|---|---|
| 0 | Invoice registered successfully | Record the FACe identifier returned |
| -1 | Service error | Retry with backoff; safe to retry |
| -2 | Duplicate invoice | Invoice already registered; treat as success if your identifier matches |
| -3 | Invalid invoice format | XSD or signature validation failed; fix before retrying |
| -4 | Unauthorized | Certificate issue; check FNMT certificate and trust chain |
The idempotency key for FacturaE submissions is the combination of invoice_number +
issuer_nif + invoice_series. If the submission returns -2 (duplicate),
the invoice was already registered. Retrieve the existing registration record by querying FACe
with these identifiers.
Do not retry on -3 or -4. These indicate problems with the document or authentication that a retry won't fix. Log the full FACe response body — it contains validation details that point to the specific issue.
Network timeouts from FACe should be treated as unknown state, not failure. FACe may have registered the invoice successfully before the timeout. Query FACe for the invoice status using the identifiers before retrying the submission.
The JSON-in / signed-XML-out model
Kreyo FacturaE accepts the same InvoiceData JSON schema used by the other products
and returns a signed FacturaE XML:
curl -X POST https://api.kreyo.io/facturae/v1/sign \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"issuer": {
"nif": "B82371940",
"name": "Acme Servicios S.L.",
"address": { "street": "Calle Gran Vía 1", "city": "Madrid", "postal_code": "28013" }
},
"recipient": {
"nif": "S2800155J",
"name": "Ayuntamiento de Madrid"
},
"invoice_number": "2026-0042",
"invoice_series": "A",
"invoice_date": "2026-04-15",
"line_items": [
{
"description": "Consultoría técnica — abril 2026",
"quantity": 1,
"unit_price": 5000.00,
"vat_rate": 0.21
}
],
"certificate": {
"pfx_base64": "MIIJ...",
"password": "cert-password"
}
}'
The response contains the signed XML, ready for submission to FACe or to the buyer's receiving endpoint. The customer's certificate is used for signing and is not stored by Kreyo beyond the duration of the request.
The InvoiceData schema is shared with Kreyo Extract, VeriFactu, and PDF. An invoice
that arrives via Extract can be passed directly to the FacturaE signing endpoint without
transformation — the same JSON describes the invoice across all four products.
A note on XAdES vs VeriFactu signing
The signing in FacturaE (XAdES-EPES) and the signing in VeriFactu (plain XMLDSig) are structurally different and serve different purposes. A FacturaE XAdES signature provides non-repudiation for a specific document. A VeriFactu XMLDSig signature authenticates the submission to the AEAT real-time reporting service.
Both use the same customer certificate — the FNMT-issued certificate for the business — but through different cryptographic paths and producing different output formats. They are not interchangeable. Having one does not satisfy the requirement for the other.