API Reference

REST API for automated extraction of Spanish invoices. PDF → signed Facturae 3.2.2 with XAdES-BES in a single call.

Base URL https://api.facturax.app
Version v1.0.0
Authentication Bearer JWT · X-API-Key
Format JSON · multipart/form-data
Main endpoint

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.

MODE 1 — PDF → XML
Send the invoice PDF. The AI extracts the data and generates the signed XML. Ideal if you receive PDF invoices from your vendors.
MODE 2 — JSON → XML
Your software already has the data. Send a JSON and get the signed XML back. No OCR, no extraction — pure format conversion.
POST /convert multipart/form-data
FieldTypeDescription
file required File PDF, JPG or PNG of the invoice (max. 20MB)
Example — cURL
curl -X POST https://api.facturax.app/convert \
  -H "X-API-Key: fct_your_api_key" \
  -F "file=@invoice.pdf"
Example — Python
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"])
POST /convert application/json
Field reference — Issuer
Field Type Required Description
vendor_namestringrequiredIssuer's name or company name
vendor_vatstringrequiredIssuer's CIF, NIF or NIE. E.g.: B12345678
vendor_addressstringoptionalIssuer's full address
vendor_citystringoptionalIssuer's city
vendor_postal_codestringoptionalPostal code (5 digits). E.g.: 28001
vendor_provincestringoptionalIssuer's province
vendor_countrystringoptionalISO 3166-1 alpha-3 code. Default: ESP
vendor_emailstringoptionalIssuer's contact email
Recipient
Field Type Required Description
buyer_namestringrequiredRecipient's name or company name
buyer_vatstringrequiredRecipient's CIF, NIF or NIE
buyer_addressstringoptionalRecipient's address
buyer_citystringoptionalRecipient's city
buyer_postal_codestringoptionalRecipient's postal code
Invoice
Field Type Required Description
invoice_numberstringrequiredInvoice number. E.g.: F2024-001
invoice_datestringrequiredIssue date in YYYY-MM-DD format. E.g.: 2024-06-15
totalnumberrequiredTotal amount in euros (tax included)
subtotalnumberoptionalTax base (excluding VAT)
taxnumberoptionalVAT amount in euros
declared_tax_ratenumberoptionalMain VAT rate as %. Values: 21 · 10 · 4 · 0
irpf_ratenumberoptionalPersonal income tax withholding percentage. E.g.: 15
irpf_amountnumberoptionalWithholding tax amount (negative). E.g.: -15.00
due_datestringoptionalDue date in YYYY-MM-DD format
currencystringoptionalISO 4217 code. Default: EUR
ibanstringoptionalIssuer's IBAN. E.g.: ES21 0049 0001 5021 0001 3303
payment_methodstringoptionalValues: transferencia (bank transfer) · domiciliacion (direct debit) · cheque (check) · efectivo (cash) · tarjeta (card)
Line items — line_items[]
Optional but recommended array. If omitted, the XML is generated with a generic line based on the header amounts. Including it improves FACe validation and accounting traceability.
Field Type Required Description
descriptionstringrequiredDescription of the item
quantitynumberrequiredQuantity. Accepts decimals. E.g.: 1, 2.5
unit_pricenumberrequiredUnit price excluding VAT
totalnumberrequiredLine total excluding VAT (unit_price × quantity)
tax_ratenumberoptionalVAT % for this line. E.g.: 21 · 10 · 4 · 0
Full example
{
  // 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
    }
  ]
}
Minimal example — required fields only
{
  "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
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}'
Response (both modes)
{
  "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"
  }
}
SIGN WITH YOUR OWN CERTIFICATE

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.

The fastest way: import the collection
All calls preconfigured with real examples. Just paste your API key and run.
Download Postman collection
Postman → Import → Upload File
1
Create your account
Sign up for free → Dashboard → API Keys → New key
2
Generate your first signed invoice
Call /convert with your invoice JSON — you'll get back the XML signed with XAdES-BES
3
To production
The signed XML is ready to upload directly to FACe
Step 1: Sign up at facturax.app/auth and get your API key from Dashboard → API Keys.

Step 2: Make your first call. The free plan includes 2 invoices with no card required.
CURL — basic extraction
# 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"
PYTHON
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"])
NODE.JS
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

PYTHON — PDF → Facturae XAdES-BES in 2 steps
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.

JSON — Error format
{
  "error_code": "QUOTA_EXCEEDED",   // machine-readable code — stable across versions
  "detail":     "Quota exceeded. 150/150 used this month."  // human message — may change
}
PYTHON — Retry logic
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_codeHTTPRetryable?Cause & action
Authentication
AUTH_REQUIRED401NoMissing API key or JWT. Add X-API-Key or Authorization: Bearer.
AUTH_INVALID_KEY401NoIncorrect, revoked or non-existent API key. Check it in the dashboard.
AUTH_INVALID_TOKEN401NoExpired or tampered JWT. Re-authenticate with POST /auth/login.
ACCESS_DENIED403NoUnverified email, suspended account, or resource belonging to another user.
Quota & rate limit
QUOTA_EXCEEDED429NoMonthly plan exhausted or API credits at zero. Add credits or wait for the monthly reset.
RATE_LIMIT_EXCEEDED429YesToo many requests per minute. Wait the Retry-After value and retry.
PLAN_REQUIRED403NoThis feature requires a higher plan (e.g.: webhooks on Pro, subaccounts on Business).
BATCH_NOT_AVAILABLE403NoBatch processing requires the Starter plan or higher.
BATCH_LIMIT_EXCEEDED400NoThe batch exceeds the maximum number of files allowed by the plan.
File
FILE_TYPE_INVALID400NoUnsupported format. Accepts PDF, PNG, JPG and WebP.
FILE_CONTENT_MISMATCH400NoThe file extension doesn't match its actual content (magic bytes). Check that the file isn't corrupted or renamed.
FILE_TOO_LARGE413NoFile larger than 20 MB. Compress or split before uploading.
Extraction & XML generation
EXTRACTION_FAILED500YesTransient error in OCR or GPT. Retry with exponential backoff (max 3 times).
XML_BUILD_FAILED500YesError building the Facturae XML. Transient — retry. If it persists, contact support.
XML_SIGN_FAILED500NoError signing with XAdES-BES. Check that the .p12 certificate is valid and the password is correct.
SERVICE_ERROR500YesGeneric internal error. Always transient — retry. If it persists for more than 5 min, check parse.facturax.app/status.
Certificate
CERT_INVALID_FORMAT400NoThe file is not a valid .p12/.pfx.
CERT_INVALID_PASSWORD400NoIncorrect .p12 password.
CERT_TOO_LARGE400NoCertificate larger than 5 MB.
CERT_NOT_FOUND404NoThe certificate_id doesn't exist or belongs to another user.
Resources
INVOICE_NOT_FOUND404NoThe log_id doesn't exist or was created with a different API key.
JOB_NOT_FOUND404NoThe batch job_id doesn't exist or has already expired (TTL 24h).
WEBHOOK_NOT_FOUND404NoThe webhook doesn't exist or belongs to another user.
KEY_NOT_FOUND404NoThe API key doesn't exist or has already been revoked.
Invalid input
INVALID_INPUT400NoRequired parameter missing or incorrectly formatted. The detail field specifies which one.
INVALID_EMAIL400NoInvalid email format during registration.
DISPOSABLE_EMAIL400NoThe email's domain is a temporary or disposable one.
INVALID_PLAN400NoUnknown plan in checkout. Valid values: starter, pro.
WEBHOOK_INVALID_EVENT400NoUnknown webhook event. Valid values: invoice.extracted, invoice.signed, quota.warning, *.
WEBHOOK_INVALID_URL400NoThe webhook URL isn't HTTPS or doesn't respond to an OPTIONS request.
Payments
STRIPE_NOT_CONFIGURED503YesThe payment system is temporarily unavailable. Retry in a few minutes.
STRIPE_ERROR500YesError in the payment gateway. Transient — retry or contact support.
Quota headers: Every /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.

Plan Limit Window Notes
Free / Starter / Pro / Business60 req/minPer API keyIndependently for each API key
Unauthenticated (IPs)10 req/minPer IPOnly for /status and public endpoints
⚡ ERP INTEGRATIONS — IMPORTANT RECOMMENDATION

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.

Relevant response headers
Header Description
Retry-AfterSeconds until the request can be retried. Only present in 429 responses.
X-Quota-UsedInvoices consumed in the current period.
X-Quota-LimitTotal invoice limit for the plan.
X-Quota-RemainingInvoices remaining before the quota is exhausted.
Proper rate limit handling in Python
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
💡 ALTERNATIVE FOR HIGH VOLUME

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.

PYTHON — accounting firm pipeline
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+)

PYTHON — batch of 50 invoices
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

PYTHON — webhook receiver (Flask)
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.

API Key (recommended for integrations): 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.
✓ CORS — Open for any origin

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.

EXAMPLE
# 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"
POST /auth/login Get access JWT

Body (JSON)

FieldTypeRequiredDescription
emailstringrequiredAccount email
passwordstringrequiredPassword (minimum 8 characters)

Response

JSON
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type":   "bearer",
  "user": {
    "id":        1,
    "email":     "usuario@empresa.com",
    "full_name": "Juan García",
    "plan":      "pro"
  }
}

Response codes

200Successful login. Returns the JWT.
401Incorrect email or password.
403Unverified email.

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.

confidence_score: A value between 0.0 and 1.0 indicating extraction reliability. Calculated based on the required fields detected correctly.
≥ 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.
POST /extract Extract data from an invoice AUTH

Request — multipart/form-data

FieldTypeRequiredDescription
filefilerequiredInvoice in PDF, PNG, JPG or WebP. Maximum 20MB. Supports multi-page and scanned documents.

Examples

CURL
curl -X POST https://api.facturax.app/extract \
  -H "X-API-Key: fct_your_api_key" \
  -F "file=@invoice.pdf"
PYTHON
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"])
JAVASCRIPT
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

JSON
{
  "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

200Extraction completed. X-Quota-* headers included.
400Unsupported file type or corrupted file.
401Invalid API key or JWT.
413File too large (max. 20MB).
429Quota exhausted. Retry-After header included.
GET /history Extraction history with filters AUTH

Query params

FieldTypeRequiredDescription
limitintegeroptionalRecords per page, 1-200 (default 50)
offsetintegeroptionalRecords to skip for pagination (default 0)
qstringoptionalFree-text search across filename, issuer, invoice number and external_ref
statusstringoptionalok or error to filter by result
date_fromstringoptionalStart date in YYYY-MM-DD format
date_tostringoptionalEnd date in YYYY-MM-DD format
subaccount_idintegeroptionalFilter by subaccount ID (Business plan only)
The response includes the filters field with the active filters and has_more for pagination. Each item includes external_ref and subaccount_id if they were sent in the original request.

Response codes

200Paginated list with total, has_more and a filters object with the applied filters.
GET /metrics API usage metrics AUTH
JSON
{
  "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 }
  ]
}
GET /quota Current quota status AUTH

A quick way to check the quota without calling /metrics. Useful for dashboards or deciding whether to launch a batch.

JSON
{
  "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.

Limits per plan: Starter → 20 invoices/batch · Pro → 100 · Business → 500.
Recommended polling interval is every 3-5 seconds. Don't poll faster to avoid wasting your rate limit quota.
POST /batch/job Create a background processing job AUTH · Starter+

Request — multipart/form-data

FieldTypeDescription
filesfile[]Array of PDF/PNG/JPG/WebP invoices. Max depends on plan.
JSON — immediate response
{
  "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."
}
GET /batch/job/{job_id} Job status (polling) AUTH
JSON
{
  "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": "..." } }
  ]
}
GET /batch/jobs User's last 20 jobs AUTH

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.

🇪🇸 SPANISH E-INVOICING DOCS

For full documentation on Facturae XML generation, XAdES-BES signing, the interactive signer demo, and managed certificates, see the FacturaX.app API docs (Spanish).

Open facturax.app/api-docs →

Anti-fraud

Manage your vendors' trusted IBANs and confirm or dismiss duplicate alerts. Anti-fraud checks run automatically on every extraction.

GET /vendors List vendors with registered IBAN AUTH

Response

JSON
{
  "vendors": [
    {
      "vendor_vat":   "A95758389",
      "trusted_iban": "ES7621000813610123456789",
      "last_seen":    "2024-03-15 10:32:00"
    }
  ]
}
PUT /vendors/{vendor_vat}/iban Update a vendor's trusted IBAN AUTH

Path params

FieldTypeDescription
vendor_vatstringVendor's CIF/NIF (e.g.: A95758389)

Body (JSON)

FieldTypeDescription
ibanstringNew 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.

POST /categorize Categorize an invoice according to the PGC AUTH

Body (JSON)

FieldTypeRequiredDescription
vendor_namestringoptionalIssuer's name
vendor_vatstringoptionalIssuer's CIF/NIF
line_itemsarrayoptionalInvoice line items with description
totalnumberoptionalTotal amount (affects expense vs investment)

Response

JSON
{
  "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.

POST /keys Create new API key JWT

Body (JSON)

FieldTypeRequiredDescription
namestringrequiredDescriptive name (e.g.: "My ERP")
planstringoptional"free" or "pro". Defaults to "free".
Important: The API key is only shown once when created. Store it somewhere safe.

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.

WITHOUT WEBHOOK (polling)
Your server → GET /status → wait
Your server → GET /status → wait
Your server → GET /status → done
WITH WEBHOOK (push)
FacturaX processes the invoice
FacturaX → POST yourserver.com/hook
Your server receives the result ✓
Signature verification: Every webhook includes the X-Factura-Signature: sha256=HASH header. Verify that the hash matches HMAC-SHA256(payload, your_secret) to confirm the request is authentic.
VERIFICATION IN NODE.JS
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)
  );
}
POST /webhooks Create webhook AUTH

Body (JSON)

FieldTypeRequiredDescription
urlstringrequiredHTTPS URL where you'll receive events
eventsarrayoptional["*"] for all, or one or more of: "invoice.extracted", "invoice.signed", "quota.warning". Defaults to ["*"].
secretstringoptionalSecret for HMAC-SHA256 signing. If not provided, it is generated automatically.

Webhook payload

JSON — invoice.extracted
{
  "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 } }
  }
}
JSON — invoice.signed
{
  "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"
  }
}
JSON — quota.warning
{
  "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)."
  }
}
invoice.signed fires when the XAdES-BES XML is in storage. Use download_endpoint to download it without polling.
quota.warning fires at 15% remaining invoices so the ERP can alert before running out of balance.
DELETE /webhooks/{id} Delete webhook AUTH

Response codes

200Webhook successfully deleted.
404Webhook not found.

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.

🇪🇸 SPANISH E-INVOICING DOCS

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.

Open facturax.app/api-docs →

Subaccounts

Multi-tenant for ERPs and agencies. Create independent subaccounts per client company — isolated quota, history and API keys. Available on the Business plan.

Typical flow: create a subaccount → create an API key with its own quota → the ERP uses that key with X-External-Ref → query history and metrics per subaccount from the dashboard or the API.
GET /subaccounts/summary Executive summary with quota alerts AUTH

Aggregated usage, 30-day invoices and automatic alerts at 90% of quota.

JSON
{
  "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 }]
}
POST /subaccounts Create subaccount + assign API key AUTH
PYTHON — FULL FLOW
# 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()
GET /subaccounts/{id}/history History filterable by text, status and dates AUTH
FieldTypeDescription
qstringFree text: filename, issuer, invoice number, external_ref
statusstringok or error
date_from / date_tostringYYYY-MM-DD — date range
limit / offsetintegerPagination (max 200/page)
GET /subaccounts/{id}/metrics Metrics with top vendors and daily usage AUTH
JSON
{ "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 }] }