Factuarea APIDevelopers

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:

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"
  }'
FieldDIR3 role
dir3_accounting_officeOficina contable (01)
dir3_managing_bodyÓrgano gestor (02)
dir3_processing_unitUnidad tramitadora (03)
dir3_proponentProponente (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_id creates 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: null is rejected, and an id of another invoice or company is a 404.
  • The Idempotency-Key does not replace submission_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 failureWhat it meansWhat you do
preparedFrozen and signed, never transmitted.Retry with the same submission_id.
transmission_uncertainThe 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 laterFACe has it.Track it like any submission.
errorThe 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"
statusMeaning
preparedSigned XML retained, not yet transmitted. Continue it with its submission_id.
transmission_uncertainTransmission started and the result is unknown. Blocks new submissions until it is reconciled.
submittedPresented to FACe; registry number assigned.
registered_rcfRegistered in the RCF (the administration's accounting registry of invoices).
accountedRecognized as an accounting obligation by the administration.
paidThe administration reports the invoice as paid.
rejectedRejected by the administration — check the reason in FACe and issue a corrected invoice.
cancellation_requestedYou requested cancellation; awaiting FACe confirmation.
cancelledCancellation confirmed by FACe.
errorLocal 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

HTTPcode / subcodeWhen
404resource_not_foundThe invoice or submission doesn't exist or belongs to another company.
422business_rule_violation / invoice_not_emittable_for_facturaeThe invoice is a draft or scheduled, so it has no fiscal number — issue it first.
422business_rule_violation / client_missing_dir3_codesThe client lacks one or more DIR3 codes.
422business_rule_violation / signing_certificate_requiredNo active signing certificate — upload one via POST /v1/companies/{company}/verifactu/certificates.
422business_rule_violation / face_submission_not_cancellableThe submission is not in a cancellable state.
409resource_already_exists / face_submission_already_existsAn active submission already exists for the invoice.
422face_submission_rejectedFACe 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.
409face_transmission_uncertainA previous attempt has an unknown outcome. Nothing is transmitted; details.attempt_id is the attempt to inspect.
409face_submission_attempt_conflictAnother request holds or already completed that attempt. Read it again.
500face_submission_completion_failedThe result was not recorded; the attempt is kept (details.attempt_id) and stays blocked.
500facturae_artifact_unavailableThe retained XML of the attempt is missing or corrupt; it is not regenerated.
403facturae_operation_forbiddenThe issuer's current authority does not allow the operation (submit needs send authority; cancel needs edit access to the module).
403insufficient_scopeThe key lacks the facturae:write scope.
502face_transmission_failedThe FACe web service failed or timed out. The attempt is kept and details.attempt_id identifies it: check its status before doing anything else.

On this page

Need a hand?Contact support