FACe invoicing (B2G)
Submit FacturaE 3.2.2 invoices to FACe — DIR3 codes, signed XAdES-EPES XML retained per attempt, processing states, uncertain transmissions and how to recover, cancellation, sandbox simulation and the facturae:write scope.
Invoicing a Spanish public administration (B2G) is mandatory through FACe, the general entry point for electronic invoices (Ley 25/2013). Factuarea generates the FacturaE 3.2.2 XML for any issued invoice, signs it XAdES-EPES with your company certificate and presents it to the FACe web service — then keeps tracking the processing status reported by FACe until the invoice is paid (or rejected).
The whole lifecycle is covered by the five operations of the FacturaE group in the API Reference:
- Download the FacturaE XML of an invoice — signed or unsigned, with or without FACe.
- Submit an invoice to FACe.
- List the submissions of an invoice.
- Retrieve a submission to track its processing status.
- Request the cancellation of a submission.
Reads use the facturae:read scope; submitting and cancelling require
facturae:write. The FacturaE module is included from the Profesional plan.
All the routes in this guide hang off /v1/companies/{company}/…: {company} is the id (UUID v7)
of the tax ID you operate on, and in the curl examples it is $COMPANY_ID.
Reach a tax ID explains how to resolve it.
Before you submit
Set the client's DIR3 codes. Every public-administration client
carries three codes from the DIR3 directory, each matching
^[A-Z][A-Z0-9]{8,9}$ (e.g. L01280796), and optionally a fourth, the
proponent. Set them when you create or
update the contact’s customer profile:
curl -X PATCH https://api.factuarea.com/v1/companies/$COMPANY_ID/contacts/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/customer-profile \
-H "Idempotency-Key: $(uuidgen)" \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Content-Type: application/json" \
-d '{
"dir3_accounting_office": "L01280796",
"dir3_managing_body": "L01280796",
"dir3_processing_unit": "L01280796"
}'| Field | DIR3 role |
|---|---|
dir3_accounting_office | Oficina contable (01) |
dir3_managing_body | Órgano gestor (02) |
dir3_processing_unit | Unidad tramitadora (03) |
dir3_proponent | Proponente (04), optional |
The administration tells you the codes (the first three often coincide); you can also look them up in the public DIR3 directory. The operation replaces the profile, so a code you leave out is cleared: send all you want to keep.
The invoice captures these codes when it is created, and you can override
them per invoice or correct them after issuing with an
administrative revision. The routing that
counts is the invoice's, not the live contact's; roles 01 to 03 are required to
submit and 04 is optional. If the invoice has a known routing with no centres,
the submission fails with client_missing_dir3_codes even if the contact has codes.
Upload an active signing certificate. FACe only accepts signed
invoices, so submission requires the FNMT (PKCS#12) certificate your company
already uses for VeriFactu
(POST /v1/companies/{company}/verifactu/certificates). Without an active
certificate the submission fails with signing_certificate_required.
Issue the invoice. Draft invoices cannot travel to FACe — sending or
emitting the invoice first is what freezes its legal content. Drafts and scheduled invoices (they have no fiscal number yet) answer invoice_not_emittable_for_facturae.
Downloading the FacturaE XML
You can download the XML at any time — for manual presentation, archiving or
validation — without involving FACe. Any invoice that already has a fiscal number
is exportable (it works for paid, annulled or cancelled invoices too); only
draft and scheduled invoices are not. An older invoice with no captured payment
schedule exports without the payment-instalments block: Factuarea never invents it.
curl -OJ https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices/0197b1c2-89ab-7def-8123-456789abcdef/facturae \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW"With an active certificate the body is signed XAdES-EPES (Facturae v3.1
signature policy) and the file is named .xsig; without one, the XML comes
back unsigned as .xml. The X-Facturae-Signed: true|false response header
tells both cases apart. See the
endpoint reference.
Downloading tolerates a missing certificate (you get unsigned XML); submitting to FACe does not — FACe requires the signature.
To get the exact file that was presented in one attempt, add
submission_id:
curl -OJ "https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices/0197b1c2-89ab-7def-8123-456789abcdef/facturae?submission_id=0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d" \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW"That returns the retained bytes of that attempt: it never regenerates, signs again
or reads the current certificate or contact, so it is the evidence of what the
administration received, even if the invoice, the DIR3 codes or the certificate
changed afterwards. The response adds X-Facturae-Sha256 (and the same value as
ETag) when the hash of the retained file is known. A file that is missing or
corrupt answers 500 facturae_artifact_unavailable; it is not regenerated.
The invoice and the attempt must match, otherwise 404.
Submitting to FACe
The submit operation needs no request body: the invoice travels in the path. Each call prepares an attempt: it freezes the invoice, the effective DIR3 routing (including the proponent), the payment schedule, the certificate that signs and the exact signed XML, and only then talks to FACe:
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices/0197b1c2-89ab-7def-8123-456789abcdef/face-submissions \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Idempotency-Key: $(uuidgen)"Response (201 for a new attempt):
{
"data": {
"id": "0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"object": "face_submission",
"invoice_id": "0197b1c2-89ab-7def-8123-456789abcdef",
"status": "submitted",
"registry_number": "202612345678",
"dir3_accounting_office": "L01280796",
"dir3_managing_body": "L01280796",
"dir3_processing_unit": "L01280796",
"error_code": null,
"error_message": null,
"status_updated_at": "2026-06-12T10:15:00Z",
"last_polled_at": null,
"created_at": "2026-06-12T10:15:00Z",
"dir3_proponent": null,
"administrative_version": 1,
"administrative_revision_id": null,
"payment_terms_version": 1,
"source_fingerprint": "6d2f8a4c1b73e5b0a9c4d7e1f3a6b8c2d5e9f0a1b4c7d8e2f3a5b6c9d0e1f2a3",
"artifact_sha256": "b4c7d8e2f3a5b6c9d0e1f2a36d2f8a4c1b73e5b0a9c4d7e1f3a6b8c2d5e9f0a1",
"prepared_at": "2026-06-12T10:14:58Z",
"transmission_started_at": "2026-06-12T10:14:59Z",
"can_transmit": false,
"requires_reconciliation": false,
"signing_certificate_fingerprint": "e5b0a9c4d7e1f3a6b8c2d5e9f0a1b4c7d8e2f3a5b6c9d0e1f2a36d2f8a4c1b73",
"artifact_status": "ready"
}
}registry_number is the FACe registry entry that proves the presentation —
keep it for any dispute with the administration. The extra fields identify what
the attempt froze: source_fingerprint, artifact_sha256 (hash of the retained
XML), the administrative_version and payment_terms_version used, and the
certificate fingerprint. artifact_status is ready, unverified (an attempt
that predates the hash), unavailable or corrupt.
The attempt is the unit, not the call
The body is optional. With none, Factuarea creates the attempt identity and answers
201. You can also send your own submission_id (a UUID v7) in the body:
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices/0197b1c2-89ab-7def-8123-456789abcdef/face-submissions \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "submission_id": "0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d" }'- A new
submission_idcreates a new attempt:201. - An existing one of that invoice continues it:
200, with the same retained XML and the same certificate, without looking at the current certificate or the contact again. If FACe had already accepted it, you get the stored result and nothing is sent twice. submission_id: nullis rejected, and an id of another invoice or company is a404.- The
Idempotency-Keydoes not replacesubmission_id: the key replays an HTTP request, the id identifies the durable attempt. The submit and cancel operations are declared as never to be retried automatically (x-no-retry): the SDKs do not retry them, so handle failures as described below instead of wrapping the call in a blind retry loop.
If the call stops with 502 face_transmission_failed (FACe down, timeout,
malformed answer) or 500 face_submission_completion_failed, the attempt
exists and error.details.attempt_id gives you its id. Never send a new
submission blindly: list the attempts of the invoice
(history)
or retrieve that one, and look at its status:
status after the failure | What it means | What you do |
|---|---|---|
prepared | Frozen and signed, never transmitted. | Retry with the same submission_id. |
transmission_uncertain | The request left for FACe and the outcome is unknown. A timeout does not prove that FACe did not receive it. | Do not resubmit. Calls answer 409 face_transmission_uncertain with the same attempt_id until the attempt is reconciled, which Factuarea does with audited evidence (requires_reconciliation: true); contact support. |
submitted and later | FACe has it. | Track it like any submission. |
error | The attempt failed. It is released only when FACe explicitly rejected it (error_code: face_rejected) or non-reception was confirmed (confirmed_not_received). | In those cases fix the cause and submit again with a new identity; otherwise it stays blocked. |
A prepared attempt or any non-final processing state blocks a new one for the
same invoice (409 face_submission_already_exists), and an uncertain one answers
409 face_transmission_uncertain; rejected and cancelled free it. A 409 face_submission_attempt_conflict
means another request already holds or finished that attempt: read it again.
Tracking the processing status
FACe reports how the administration processes the invoice. Factuarea polls FACe periodically and updates each submission — retrieving the submission detail (or the invoice's submission history) is the way to track progress; there is no refresh endpoint in v1:
curl https://api.factuarea.com/v1/companies/$COMPANY_ID/face-submissions/0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW"status | Meaning |
|---|---|
prepared | Signed XML retained, not yet transmitted. Continue it with its submission_id. |
transmission_uncertain | Transmission started and the result is unknown. Blocks new submissions until it is reconciled. |
submitted | Presented to FACe; registry number assigned. |
registered_rcf | Registered in the RCF (the administration's accounting registry of invoices). |
accounted | Recognized as an accounting obligation by the administration. |
paid | The administration reports the invoice as paid. |
rejected | Rejected by the administration — check the reason in FACe and issue a corrected invoice. |
cancellation_requested | You requested cancellation; awaiting FACe confirmation. |
cancelled | Cancellation confirmed by FACe. |
error | Local transmission error — error_code and error_message carry the detail. |
Prefer push over polling? Subscribe to the
webhook events facturae.face_submitted,
facturae.face_status_changed and facturae.face_cancellation_requested.
Requesting cancellation
While the invoice has not been paid or rejected you can
request its cancellation
(anulación 4200). The reason is mandatory and travels to FACe:
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/face-submissions/0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d/cancel \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Content-Type: application/json" \
-d '{ "reason": "Factura emitida por error al organismo equivocado." }'Cancellation is only allowed in a cancellable state (submitted,
registered_rcf, accounted); otherwise the call answers
face_submission_not_cancellable. The submission moves to
cancellation_requested until FACe confirms the final cancelled state.
Test mode
With a test key (fact_test_) the whole flow is simulated: no SOAP call
ever reaches FACe and the submission gets a synthetic registry number
prefixed FACE-SANDBOX-*. Signature and DIR3 validations still apply, so
the sandbox exercises the same error paths as production. See
test mode.
Errors
| HTTP | code / subcode | When |
|---|---|---|
| 404 | resource_not_found | The invoice or submission doesn't exist or belongs to another company. |
| 422 | business_rule_violation / invoice_not_emittable_for_facturae | The invoice is a draft or scheduled, so it has no fiscal number — issue it first. |
| 422 | business_rule_violation / client_missing_dir3_codes | The client lacks one or more DIR3 codes. |
| 422 | business_rule_violation / signing_certificate_required | No active signing certificate — upload one via POST /v1/companies/{company}/verifactu/certificates. |
| 422 | business_rule_violation / face_submission_not_cancellable | The submission is not in a cancellable state. |
| 409 | resource_already_exists / face_submission_already_exists | An active submission already exists for the invoice. |
| 422 | face_submission_rejected | FACe analysed the submission and rejected it with its own code (wrong DIR3 code, invalid signature, duplicate); it did not register the invoice. details.attempt_id identifies the attempt (its error_code is face_rejected): correct the data and submit again with a new identity. |
| 409 | face_transmission_uncertain | A previous attempt has an unknown outcome. Nothing is transmitted; details.attempt_id is the attempt to inspect. |
| 409 | face_submission_attempt_conflict | Another request holds or already completed that attempt. Read it again. |
| 500 | face_submission_completion_failed | The result was not recorded; the attempt is kept (details.attempt_id) and stays blocked. |
| 500 | facturae_artifact_unavailable | The retained XML of the attempt is missing or corrupt; it is not regenerated. |
| 403 | facturae_operation_forbidden | The issuer's current authority does not allow the operation (submit needs send authority; cancel needs edit access to the module). |
| 403 | insufficient_scope | The key lacks the facturae:write scope. |
| 502 | face_transmission_failed | The FACe web service failed or timed out. The attempt is kept and details.attempt_id identifies it: check its status before doing anything else. |