API Reference
REST API for automated extraction of Spanish invoices. PDF → signed Facturae 3.2.2 with XAdES-BES in a single call.
POST /convert
The most important endpoint of the API. Accepts a PDF or a JSON with the invoice data and returns the Facturae XML 3.2.2 signed with XAdES-BES, along with the extracted structured data.
curl -X POST https://api.facturax.app/convert \ -H "X-API-Key: fct_your_api_key" \ -F "file=@invoice.pdf"
import requests response = requests.post( "https://api.facturax.app/convert", headers={"X-API-Key": "fct_your_api_key"}, files={"file": open("invoice.pdf", "rb")} ) data = response.json() # Save the signed XML with open(data["filename"], "w") as f: f.write(data["xml"])
| Field | Type | Required | Description |
|---|---|---|---|
| vendor_name | string | required | Issuer's name or company name |
| vendor_vat | string | required | Issuer's CIF, NIF or NIE. E.g.: B12345678 |
| vendor_address | string | optional | Issuer's full address |
| vendor_city | string | optional | Issuer's city |
| vendor_postal_code | string | optional | Postal code (5 digits). E.g.: 28001 |
| vendor_province | string | optional | Issuer's province |
| vendor_country | string | optional | ISO 3166-1 alpha-3 code. Default: ESP |
| vendor_email | string | optional | Issuer's contact email |
| Field | Type | Required | Description |
|---|---|---|---|
| buyer_name | string | required | Recipient's name or company name |
| buyer_vat | string | required | Recipient's CIF, NIF or NIE |
| buyer_address | string | optional | Recipient's address |
| buyer_city | string | optional | Recipient's city |
| buyer_postal_code | string | optional | Recipient's postal code |
| Field | Type | Required | Description |
|---|---|---|---|
| invoice_number | string | required | Invoice number. E.g.: F2024-001 |
| invoice_date | string | required | Issue date in YYYY-MM-DD format. E.g.: 2024-06-15 |
| total | number | required | Total amount in euros (tax included) |
| subtotal | number | optional | Tax base (excluding VAT) |
| tax | number | optional | VAT amount in euros |
| declared_tax_rate | number | optional | Main VAT rate as %. Values: 21 · 10 · 4 · 0 |
| irpf_rate | number | optional | Personal income tax withholding percentage. E.g.: 15 |
| irpf_amount | number | optional | Withholding tax amount (negative). E.g.: -15.00 |
| due_date | string | optional | Due date in YYYY-MM-DD format |
| currency | string | optional | ISO 4217 code. Default: EUR |
| iban | string | optional | Issuer's IBAN. E.g.: ES21 0049 0001 5021 0001 3303 |
| payment_method | string | optional | Values: transferencia (bank transfer) · domiciliacion (direct debit) · cheque (check) · efectivo (cash) · tarjeta (card) |
line_items[]| Field | Type | Required | Description |
|---|---|---|---|
| description | string | required | Description of the item |
| quantity | number | required | Quantity. Accepts decimals. E.g.: 1, 2.5 |
| unit_price | number | required | Unit price excluding VAT |
| total | number | required | Line total excluding VAT (unit_price × quantity) |
| tax_rate | number | optional | VAT % for this line. E.g.: 21 · 10 · 4 · 0 |
{
// Issuer
"vendor_name": "Servicios Tech S.L.",
"vendor_vat": "B12345678",
"vendor_address": "Calle Mayor 1, 2º",
"vendor_city": "Madrid",
"vendor_postal_code": "28001",
"vendor_province": "Madrid",
"vendor_country": "ESP",
// Recipient
"buyer_name": "Ayuntamiento de Alcobendas",
"buyer_vat": "P2800200B",
"buyer_address": "Plaza Mayor 1",
"buyer_city": "Alcobendas",
"buyer_postal_code": "28100",
// Invoice
"invoice_number": "F2024-001",
"invoice_date": "2024-06-15",
"due_date": "2024-07-15",
"currency": "EUR",
"payment_method": "transferencia",
"iban": "ES21 0049 0001 5021 0001 3303",
// Amounts
"subtotal": 1000.00,
"declared_tax_rate": 21,
"tax": 210.00,
"total": 1210.00,
"irpf_rate": 15,
"irpf_amount": -150.00,
// Line items
"line_items": [
{
"description": "Invoicing module development",
"quantity": 10,
"unit_price": 100.00,
"total": 1000.00,
"tax_rate": 21
}
]
}{
"vendor_name": "Tech S.L.",
"vendor_vat": "B12345678",
"buyer_name": "Ayuntamiento de Alcobendas",
"buyer_vat": "P2800200B",
"invoice_number": "F2024-001",
"invoice_date": "2024-06-15",
"total": 121.00
}curl -X POST https://api.facturax.app/convert \ -H "X-API-Key: fct_your_api_key" \ -H "Content-Type: application/json" \ -d '{"vendor_name":"Tech S.L.","vendor_vat":"B12345678","buyer_name":"Ayto Alcobendas","buyer_vat":"P2800200B","invoice_number":"F-001","invoice_date":"2024-06-15","total":121.0}'
{
"xml": "<?xml version=\"1.0\"...>", // Full XML as string
"xml_base64": "PD94bWwg...", // Base64-encoded XML for binary use
"signed": true, // true if signed with XAdES-BES
"source": "pdf", // "pdf" or "json"
"invoice": {
"vendor_name": "Iberdrola S.A.U.",
"vendor_vat": "A95758389",
"invoice_number":"F2024-001",
"invoice_date": "2024-06-15",
"subtotal": 100.00,
"tax": 21.00,
"total": 121.00,
"iban": "ES21 0049...",
"accounting": { "account_code": "628", "account_label": "Suministros" // Utilities (Spanish PGC account) }
},
"warnings": [],
"confidence": 0.97, // Only in PDF mode
"filename": "facturae_F2024-001.xml",
"meta": {
"version": "3.2.2",
"standard": "Facturae",
"signature": "XAdES-BES"
}
}If your software manages your clients' certificates, you can pass them directly in the call. The certificate is not stored on our servers — it is used once and discarded.
import base64, requests with open("client_certificate.p12", "rb") as f: cert_b64 = base64.b64encode(f.read()).decode() response = requests.post( "https://api.facturax.app/convert", headers={ "X-API-Key": "fct_your_api_key", "X-P12-B64": cert_b64, "X-P12-Password": "certificate_password", }, files={"file": open("invoice.pdf", "rb")} )
⚡ Quick Start — 5 minutes
From zero to your first extracted invoice in under 5 minutes. No configuration, no OAuth, no hassle.
/convert with your invoice JSON — you'll get back the XML signed with XAdES-BESStep 2: Make your first call. The free plan includes 2 invoices with no card required.
# Extract all data from a PDF invoice curl -X POST https://api.facturax.app/extract \ -H "X-API-Key: fct_your_api_key" \ -F "file=@invoice.pdf"
import requests API_KEY = "fct_your_api_key" with open("invoice.pdf", "rb") as f: res = requests.post( "https://api.facturax.app/extract", headers={"X-API-Key": API_KEY}, files={"file": ("invoice.pdf", f, "application/pdf")}, ) data = res.json() print(data["vendor_name"], data["total"], data["confidence_score"])
import fs from 'fs'; import FormData from 'form-data'; import fetch from 'node-fetch'; const fd = new FormData(); fd.append('file', fs.createReadStream('invoice.pdf'), 'invoice.pdf'); const res = await fetch('https://api.facturax.app/extract', { method: 'POST', headers: { 'X-API-Key': 'fct_your_api_key', ...fd.getHeaders() }, body: fd, }); const data = await res.json(); console.log(data.vendor_name, data.total);
Full flow: PDF → signed Facturae
import requests API_KEY = "fct_your_api_key" BASE = "https://api.facturax.app" HEADERS = {"X-API-Key": API_KEY} # Step 1 — Extract data from the PDF with open("invoice.pdf", "rb") as f: r1 = requests.post(f"{BASE}/extract", headers=HEADERS, files={"file": ("invoice.pdf", f, "application/pdf")}) r1.raise_for_status() invoice_data = r1.json() # Check confidence before continuing if invoice_data["confidence_score"] < 0.7: print("Low confidence — review manually", invoice_data["warnings"]) # Step 2 — Generate and download signed Facturae XML r2 = requests.post(f"{BASE}/convert-to-facturae/download", headers={**HEADERS, "Content-Type": "application/json"}, json={"invoice_data": invoice_data, "sign": True}) r2.raise_for_status() with open("signed_invoice.xml", "wb") as out: out.write(r2.content) print("✓ Signed Facturae saved to signed_invoice.xml")
Error codes
All errors return JSON with error_code (machine-readable) and detail (human-readable). Use error_code for retry logic — never parse detail.
{
"error_code": "QUOTA_EXCEEDED", // machine-readable code — stable across versions
"detail": "Quota exceeded. 150/150 used this month." // human message — may change
}
RETRYABLE = {"EXTRACTION_FAILED", "SERVICE_ERROR", "XML_BUILD_FAILED"}
def handle_error(response):
err = response.json()
code = err.get("error_code", "UNKNOWN")
if code in RETRYABLE:
retry_with_backoff() # transient error — retry
elif code == "QUOTA_EXCEEDED":
alert_team("Add credits") # do not retry — needs human action
elif code == "RATE_LIMIT_EXCEEDED":
wait = response.headers.get("Retry-After", 60)
time.sleep(int(wait)) # wait Retry-After then retry
elif code in {"AUTH_INVALID_KEY", "AUTH_REQUIRED"}:
raise ConfigError("Check API key") # never retry
Full error_code catalog
| error_code | HTTP | Retryable? | Cause & action |
|---|---|---|---|
| Authentication | |||
| AUTH_REQUIRED | 401 | No | Missing API key or JWT. Add X-API-Key or Authorization: Bearer. |
| AUTH_INVALID_KEY | 401 | No | Incorrect, revoked or non-existent API key. Check it in the dashboard. |
| AUTH_INVALID_TOKEN | 401 | No | Expired or tampered JWT. Re-authenticate with POST /auth/login. |
| ACCESS_DENIED | 403 | No | Unverified email, suspended account, or resource belonging to another user. |
| Quota & rate limit | |||
| QUOTA_EXCEEDED | 429 | No | Monthly plan exhausted or API credits at zero. Add credits or wait for the monthly reset. |
| RATE_LIMIT_EXCEEDED | 429 | Yes | Too many requests per minute. Wait the Retry-After value and retry. |
| PLAN_REQUIRED | 403 | No | This feature requires a higher plan (e.g.: webhooks on Pro, subaccounts on Business). |
| BATCH_NOT_AVAILABLE | 403 | No | Batch processing requires the Starter plan or higher. |
| BATCH_LIMIT_EXCEEDED | 400 | No | The batch exceeds the maximum number of files allowed by the plan. |
| File | |||
| FILE_TYPE_INVALID | 400 | No | Unsupported format. Accepts PDF, PNG, JPG and WebP. |
| FILE_CONTENT_MISMATCH | 400 | No | The file extension doesn't match its actual content (magic bytes). Check that the file isn't corrupted or renamed. |
| FILE_TOO_LARGE | 413 | No | File larger than 20 MB. Compress or split before uploading. |
| Extraction & XML generation | |||
| EXTRACTION_FAILED | 500 | Yes | Transient error in OCR or GPT. Retry with exponential backoff (max 3 times). |
| XML_BUILD_FAILED | 500 | Yes | Error building the Facturae XML. Transient — retry. If it persists, contact support. |
| XML_SIGN_FAILED | 500 | No | Error signing with XAdES-BES. Check that the .p12 certificate is valid and the password is correct. |
| SERVICE_ERROR | 500 | Yes | Generic internal error. Always transient — retry. If it persists for more than 5 min, check parse.facturax.app/status. |
| Certificate | |||
| CERT_INVALID_FORMAT | 400 | No | The file is not a valid .p12/.pfx. |
| CERT_INVALID_PASSWORD | 400 | No | Incorrect .p12 password. |
| CERT_TOO_LARGE | 400 | No | Certificate larger than 5 MB. |
| CERT_NOT_FOUND | 404 | No | The certificate_id doesn't exist or belongs to another user. |
| Resources | |||
| INVOICE_NOT_FOUND | 404 | No | The log_id doesn't exist or was created with a different API key. |
| JOB_NOT_FOUND | 404 | No | The batch job_id doesn't exist or has already expired (TTL 24h). |
| WEBHOOK_NOT_FOUND | 404 | No | The webhook doesn't exist or belongs to another user. |
| KEY_NOT_FOUND | 404 | No | The API key doesn't exist or has already been revoked. |
| Invalid input | |||
| INVALID_INPUT | 400 | No | Required parameter missing or incorrectly formatted. The detail field specifies which one. |
| INVALID_EMAIL | 400 | No | Invalid email format during registration. |
| DISPOSABLE_EMAIL | 400 | No | The email's domain is a temporary or disposable one. |
| INVALID_PLAN | 400 | No | Unknown plan in checkout. Valid values: starter, pro. |
| WEBHOOK_INVALID_EVENT | 400 | No | Unknown webhook event. Valid values: invoice.extracted, invoice.signed, quota.warning, *. |
| WEBHOOK_INVALID_URL | 400 | No | The webhook URL isn't HTTPS or doesn't respond to an OPTIONS request. |
| Payments | |||
| STRIPE_NOT_CONFIGURED | 503 | Yes | The payment system is temporarily unavailable. Retry in a few minutes. |
| STRIPE_ERROR | 500 | Yes | Error in the payment gateway. Transient — retry or contact support. |
/extract response includes X-Quota-Used, X-Quota-Limit and X-Quota-Remaining to monitor usage without extra calls.Rate limit: 60 req/min per API key (independent of IP). The
Retry-After header indicates the seconds to wait. The X-RateLimit-Plan header indicates which plan is being limited.Idempotency: Add
X-Idempotency-Key: <uuid> to retry without duplicating usage. For 24h, the same key returns the cached response without consuming quota. On replay, the response includes X-Idempotency-Replayed: true.500 with no error_code: If you get a 500 with no
error_code field, it's an unanticipated error. Treat these as SERVICE_ERROR (retry up to 3 times with backoff) and report to soporte@facturax.app with the X-Request-ID from the response header.
Rate limits
Requests are limited per API key, not per IP. All authenticated plans share the same base limit. OPTIONS requests don't count.
If you send invoices in batches (dozens or hundreds at once), add a 1-second delay between calls or use the POST /batch endpoint, which processes multiple invoices in a single request and isn't subject to this per-invoice limit. Without a delay, requests exceeding 60/min will receive a 429 with the Retry-After header.
import requests, time def convert_with_retry(invoice_data, api_key, max_retries=3): headers = {"X-API-Key": api_key, "Content-Type": "application/json"} for attempt in range(max_retries): r = requests.post("https://api.facturax.app/convert", headers=headers, json=invoice_data) if r.status_code == 429: wait = int(r.headers.get("Retry-After", 60)) print(f"Rate limit — waiting {wait}s") time.sleep(wait) continue r.raise_for_status() return r.json() raise Exception("Persistent rate limit after 3 retries") # For batches: process with a delay between calls invoices = [...] # list of dicts with invoice data for invoice in invoices: result = convert_with_retry(invoice, "fct_your_api_key") time.sleep(1) # 1s between calls → max 60/min, never hits the limit
If you need to sustainably process more than 60 invoices/minute, contact soporte@facturax.app — Business plans have custom limits.
Use cases
Real integration flows for the most common use cases.
1. Accounting firm — automated email processing
Receive invoices by email, upload them to FacturaX, get the JSON and the signed Facturae, and register them in the accounting software.
import requests def process_invoice(pdf_path: str) -> dict: # 1. Extract data with open(pdf_path, "rb") as f: res = requests.post( "https://api.facturax.app/extract", headers={"X-API-Key": API_KEY}, files={"file": (pdf_path, f, "application/pdf")} ) data = res.json() # 2. Alert if low confidence or fraud detected if data.get("confidence_score", 1) < 0.75: alert_review(pdf_path, data["warnings"]) fraud = data.get("fraud_checks", {}) if fraud.get("iban_check", {}).get("detected"): alert_iban_fraud(data["vendor_name"], data["iban"]) # 3. PGC account assigned automatically account = data["accounting"]["account_code"] # e.g.: "628" # 4. Generate signed Facturae if invoiced to a Public Administration if is_for_public_administration(data["buyer_vat"]): r2 = requests.post( "https://api.facturax.app/convert-to-facturae/download", headers={"X-API-Key": API_KEY}, json={"invoice_data": data, "sign": True} ) with open(pdf_path.replace(".pdf", ".xml"), "wb") as out: out.write(r2.content) return data
2. Bulk processing with batch jobs (Starter+)
import requests, time, glob files = glob.glob("invoices/*.pdf")[:50] handles = [("files", (open(f, "rb"))) for f in files] # Create background job — responds immediately res = requests.post("https://api.facturax.app/batch/job", headers={"X-API-Key": API_KEY}, files=handles) job_id = res.json()["job_id"] # Poll until complete while True: status = requests.get(f"https://api.facturax.app/batch/job/{job_id}", headers={"X-API-Key": API_KEY}).json() print(f"{status['processed']}/{status['total']} processed") if status["status"] == "done": break time.sleep(3)
3. Webhook — real-time notification
from flask import Flask, request import hmac, hashlib app = Flask(__name__) WEBHOOK_SECRET = "your_webhook_secret" @app.route("/webhook", methods=["POST"]) def webhook(): sig = request.headers.get("X-Factura-Signature", "") expected = "sha256=" + hmac.new( WEBHOOK_SECRET.encode(), request.data, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(sig, expected): return "Invalid signature", 401 data = request.json() # Process the event print(data["event"], data["data"]["vendor_name"]) return "", 200
Authentication
Every request must include an API key in the X-API-Key header or a JWT in Authorization: Bearer <token>. API keys are created from the dashboard or via POST /keys.
X-API-Key: fct_xxxxxxxxxxxx. Ideal for server-to-server. Available from the Pro plan.JWT (for the dashboard): Get a token with
POST /auth/login and include it as Authorization: Bearer <token>. Valid for 7 days.
The API accepts requests from any domain — you can call it directly from your frontend, your web ERP, or any application with no extra configuration. Security is provided by the API key, not by the request origin.
If you see a CORS or Failed to fetch error during your first tests, make sure you're including the X-API-Key header correctly — it's an authentication issue, not an origin issue.
# With API Key curl -X POST https://api.facturax.app/extract \ -H "X-API-Key: fct_your_api_key" \ -F "file=@invoice.pdf" # With JWT curl -X POST https://api.facturax.app/extract \ -H "Authorization: Bearer eyJhbGci..." \ -F "file=@invoice.pdf"
Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
| string | required | Account email | |
| password | string | required | Password (minimum 8 characters) |
Response
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"user": {
"id": 1,
"email": "usuario@empresa.com",
"full_name": "Juan García",
"plan": "pro"
}
}
Response codes
Invoice extraction
The main endpoint. Accepts an invoice in PDF, PNG, JPG or WebP and returns all extracted fields as JSON, along with anti-fraud checks (Pro+) and automatic PGC accounting categorization.
— ≥ 0.9: complete extraction, safe to process automatically.
— 0.7 – 0.9: partial extraction, review the
warnings.— < 0.7: manual review recommended. The invoice may be poorly scanned or in an unusual format.
Quota headers: Every response includes
X-Quota-Used, X-Quota-Limit and X-Quota-Remaining.
Request — multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
| file | file | required | Invoice in PDF, PNG, JPG or WebP. Maximum 20MB. Supports multi-page and scanned documents. |
Examples
curl -X POST https://api.facturax.app/extract \ -H "X-API-Key: fct_your_api_key" \ -F "file=@invoice.pdf"
import requests with open("invoice.pdf", "rb") as f: res = requests.post( "https://api.facturax.app/extract", headers={"X-API-Key": "fct_your_api_key"}, files={"file": ("invoice.pdf", f, "application/pdf")}, ) data = res.json() # Check confidence if data["confidence_score"] >= 0.9: process_automatically(data) else: send_for_review(data, data["warnings"])
const fd = new FormData(); fd.append('file', file); // file is a File object from the input const res = await fetch('https://api.facturax.app/extract', { method: 'POST', headers: { 'X-API-Key': 'fct_your_api_key' }, body: fd }); const data = await res.json(); console.log(`Quota: ${res.headers.get('X-Quota-Remaining')} remaining`);
Response
{
"vendor_name": "Iberdrola S.A.U.",
"vendor_vat": "A95758389",
"vendor_address": "C/ Hermosilla 3",
"vendor_city": "Madrid",
"buyer_name": "My Company S.L.",
"buyer_vat": "B12345678",
"invoice_number": "FAC-2024-0312",
"invoice_date": "2024-03-15",
"due_date": "2024-04-15",
"currency": "EUR",
"subtotal": 154.90,
"tax": 32.53,
"total": 187.43,
"iban": "ES7621000813610123456789",
"payment_method": "transferencia",
"irpf_rate": null,
"declared_tax_rate": 21,
"line_items": [
{
"description": "March electricity usage",
"quantity": 1,
"unit_price": 154.90,
"tax_rate": 21,
"total": 154.90
}
],
"confidence_score": 0.97,
"warnings": [],
"accounting": {
"account_code": "628",
"account_label": "Suministros" // Utilities (Spanish PGC account),
"type": "gasto", // "expense"
"type_label": "Gasto del ejercicio", // "Operating expense"
"confidence": "alta", // "high"
"method": "regla", // "rule"
"reasoning": "Iberdrola es proveedor de suministros eléctricos." // "Iberdrola is an electricity utility provider"
},
"fraud_checks": {
"duplicate": { "detected": false },
"vat_verification": { "valid": true, "status": "verified" },
"iban_check": { "detected": false }
},
"_log_id": 42
}
// Response headers:
// X-Quota-Used: 3
// X-Quota-Limit: 150
// X-Quota-Remaining: 147
Response codes
Query params
| Field | Type | Required | Description |
|---|---|---|---|
| limit | integer | optional | Records per page, 1-200 (default 50) |
| offset | integer | optional | Records to skip for pagination (default 0) |
| q | string | optional | Free-text search across filename, issuer, invoice number and external_ref |
| status | string | optional | ok or error to filter by result |
| date_from | string | optional | Start date in YYYY-MM-DD format |
| date_to | string | optional | End date in YYYY-MM-DD format |
| subaccount_id | integer | optional | Filter by subaccount ID (Business plan only) |
Response codes
{
"total": 87,
"avg_confidence": 0.924,
"total_errors": 2,
"total_with_warnings": 11,
"quota": 150,
"used": 87,
"remaining": 63,
"plan": "starter",
"quota_reset_at": "2024-04-15 08:00:00",
"daily_usage": [
{ "day": "2024-03-15", "count": 12 }
]
}
A quick way to check the quota without calling /metrics. Useful for dashboards or deciding whether to launch a batch.
{
"plan": "starter",
"quota": 150,
"used": 87,
"remaining": 63,
"quota_reset_at": "2024-04-15 08:00:00",
"pct_used": 58.0
}
Batch Jobs
Process multiple invoices in the background. The job is created and returns job_id immediately — no need to wait. Available from the Starter plan. You'll receive an email when it finishes.
Recommended polling interval is every 3-5 seconds. Don't poll faster to avoid wasting your rate limit quota.
Request — multipart/form-data
| Field | Type | Description |
|---|---|---|
| files | file[] | Array of PDF/PNG/JPG/WebP invoices. Max depends on plan. |
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"total": 25,
"status": "running",
"message": "Processing 25 invoices in the background. You'll receive an email when done."
}
{
"job_id": "550e8400-...",
"status": "done",
"total": 25,
"processed": 25,
"failed": 1,
"created_at": "2024-03-15 10:00:00",
"finished_at":"2024-03-15 10:01:43",
"items": [
{ "filename": "invoice1.pdf", "status": "ok", "result": { ... } },
{ "filename": "invoice2.pdf", "status": "error", "result": { "error": "..." } }
]
}
Returns the last 20 batch jobs sorted by date descending, without individual items (use GET /batch/job/{job_id} for that).
Facturae XML & XAdES-BES signing 🇪🇸 Spain only
FacturaX also provides a full toolkit for Spanish electronic invoicing: POST /sign (sign an existing Facturae XML with XAdES-BES), POST /convert-to-facturae (generate Facturae 3.2.2 XML from extracted data, ready for FACe and Public Administration), and managed certificates for multi-client ERPs.
For full documentation on Facturae XML generation, XAdES-BES signing, the interactive signer demo, and managed certificates, see the FacturaX.app API docs (Spanish).
Anti-fraud
Manage your vendors' trusted IBANs and confirm or dismiss duplicate alerts. Anti-fraud checks run automatically on every extraction.
Response
{
"vendors": [
{
"vendor_vat": "A95758389",
"trusted_iban": "ES7621000813610123456789",
"last_seen": "2024-03-15 10:32:00"
}
]
}
Path params
| Field | Type | Description |
|---|---|---|
| vendor_vat | string | Vendor's CIF/NIF (e.g.: A95758389) |
Body (JSON)
| Field | Type | Description |
|---|---|---|
| iban | string | New trusted IBAN (no spaces) |
Accounting categorization 🇪🇸 Spain only
Automatically assigns the Spanish General Accounting Plan (PGC) account using deterministic rules + AI with the full PGC embedded.
Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
| vendor_name | string | optional | Issuer's name |
| vendor_vat | string | optional | Issuer's CIF/NIF |
| line_items | array | optional | Invoice line items with description |
| total | number | optional | Total amount (affects expense vs investment) |
Response
{
"account_code": "628",
"account_label": "Suministros" // Utilities (Spanish PGC account),
"type": "gasto", // "expense"
"type_label": "Gasto del ejercicio", // "Operating expense"
"reasoning": "Iberdrola es un proveedor de suministros eléctricos." // "Iberdrola is an electricity utility provider",
"confidence": "alta", // "high"
"method": "regla", // "rule"
"suggestions": []
}
API Keys
Manage API keys for external integrations. Available on Pro and Business plans.
Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Descriptive name (e.g.: "My ERP") |
| plan | string | optional | "free" or "pro". Defaults to "free". |
Webhooks — Real-time notifications
Instead of polling every X seconds asking "is my invoice ready yet?", set up a webhook and FacturaX will notify your server the moment processing finishes. Available on the Pro plan and above.
Your server → GET /status → wait
Your server → GET /status → done
FacturaX → POST yourserver.com/hook
Your server receives the result ✓
X-Factura-Signature: sha256=HASH header. Verify that the hash matches HMAC-SHA256(payload, your_secret) to confirm the request is authentic.
const crypto = require('crypto'); function verifyWebhook(payload, signature, secret) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); }
Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | required | HTTPS URL where you'll receive events |
| events | array | optional | ["*"] for all, or one or more of: "invoice.extracted", "invoice.signed", "quota.warning". Defaults to ["*"]. |
| secret | string | optional | Secret for HMAC-SHA256 signing. If not provided, it is generated automatically. |
Webhook payload
{
"event": "invoice.extracted",
"timestamp": 1710500000,
"data": {
"log_id": 42,
"external_ref": "ERP-INV-2025-0312",
"filename": "invoice.pdf",
"vendor_name": "Iberdrola S.A.U.",
"vendor_vat": "A95758389",
"invoice_number": "FAC-2024-0312",
"total": 187.43,
"confidence": 0.97,
"accounting": { "account_code": "628" },
"fraud_checks": { "duplicate": { "detected": false } }
}
}
{
"event": "invoice.signed",
"timestamp": 1710500003,
"data": {
"log_id": 42,
"external_ref": "ERP-INV-2025-0312",
"b2_file_id": "4_z1a2b3c4...",
"invoice_number": "FAC-2024-0312",
"invoice_date": "2024-03-12",
"expires_at": "2028-03-12",
"download_endpoint": "/invoices/42/facturae"
}
}
{
"event": "quota.warning",
"timestamp": 1710500010,
"data": {
"used": 128,
"quota": 150,
"remaining": 22,
"pct_remaining": 14.7,
"plan": "starter",
"message": "Only 22 invoices left (14.7% of your balance)."
}
}
download_endpoint to download it without polling.quota.warning fires at 15% remaining invoices so the ERP can alert before running out of balance.
Response codes
Managed certificates 🇪🇸 Spain only
For ERPs with multiple clients that need XAdES-BES signing: upload each company's .p12 once via POST /certificates and get a certificate_id to reuse in /convert-to-facturae without resending the file on every request. Available on Pro and Business plans.
Full documentation for POST /certificates, GET /certificates and DELETE /certificates/{id} is available in the FacturaX.app API docs (Spanish), alongside the Facturae signing endpoints.
Subaccounts
Multi-tenant for ERPs and agencies. Create independent subaccounts per client company — isolated quota, history and API keys. Available on the Business plan.
X-External-Ref → query history and metrics per subaccount from the dashboard or the API.Aggregated usage, 30-day invoices and automatic alerts at 90% of quota.
{
"count": 3, "total_used": 87, "total_quota": 450, "pct_used": 19.3,
"alerts": [{ "subaccount": "Acme S.L.", "slug": "acme-sl", "pct": 94.0 }],
"subaccounts": [{ "id": 1, "name": "Acme S.L.", "total_used": 47, "total_quota": 50 }]
}
# 1. Create subaccount sub = requests.post(f"{BASE_URL}/subaccounts", headers=HEADERS, json={"name": "Acme S.L.", "notes": "CIF B12345678"}).json() # 2. Create API key with independent quota key = requests.post(f"{BASE_URL}/subaccounts/{sub['id']}/keys", headers=HEADERS, json={"name": "Acme — Prod", "quota": 150}).json() acme_key = key["raw_key"] # store it — only returned once # 3. Extract with Acme's key + their invoice ID requests.post(f"{BASE_URL}/extract", headers={"X-API-Key": acme_key, "X-External-Ref": "ACME-2025-001"}, files={"file": open("invoice.pdf", "rb")}) # 4. Query Acme's history with filters hist = requests.get(f"{BASE_URL}/subaccounts/{sub['id']}/history", headers=HEADERS, params={"q": "Iberdrola", "date_from": "2025-05-01", "status": "ok"}).json()
| Field | Type | Description |
|---|---|---|
| q | string | Free text: filename, issuer, invoice number, external_ref |
| status | string | ok or error |
| date_from / date_to | string | YYYY-MM-DD — date range |
| limit / offset | integer | Pagination (max 200/page) |
{ "total": 47, "avg_confidence": 0.934, "total_errors": 1,
"keys": [{ "name": "Acme — Prod", "quota": 150, "used": 47, "remaining": 103 }],
"daily_usage": [{ "day": "2025-05-01", "count": 8 }],
"top_vendors": [{ "vendor": "Iberdrola S.A.U.", "count": 12 }] }