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.

XML code structure on a developer screen
Photo by Markus Spiske on Unsplash
Note: XAdES signing is required for FacturaE. It is entirely separate from VeriFactu, which uses plain XMLDSig. Completing a FacturaE signing does not satisfy VeriFactu obligations and vice versa. See XAdES is not VeriFactu for a full comparison.

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:

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:

FirmaXadesNet solves all of these:

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:


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:

CodeMeaningAction
0Invoice registered successfullyRecord the FACe identifier returned
-1Service errorRetry with backoff; safe to retry
-2Duplicate invoiceInvoice already registered; treat as success if your identifier matches
-3Invalid invoice formatXSD or signature validation failed; fix before retrying
-4UnauthorizedCertificate 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.