Guía de integración ERP

De cero a XML firmado
en 10 minutos

Conecta tu ERP a la API de FacturaX.app y genera facturas electrónicas Facturae 3.2.2 con firma XAdES-BES listas para FACe. Sin instalar librerías de firma, sin gestionar certificados manualmente.

Python PHP Node.js cURL
Flujo completo de integración:
Tu ERP
factura PDF o JSON
POST /extract
OCR + campos
POST /convert
Facturae XML
Webhook signed
XAdES-BES listo
FACe
Administración
1
Autenticación
Obtén tu API key y verifica que la conexión funciona
⏱ ~1 min

Ve a tu dashboard → API Keys → Crear key. Copia la clave fct_... — solo se muestra una vez. Luego verifica la conexión:

cURL
Python
PHP
Node.js
BASH
# Verificar estado de la API (sin auth)
curl https://api.facturax.app/v1/status

# Verificar tu cuota
curl https://api.facturax.app/quota \
  -H "X-API-Key: fct_TU_API_KEY"
PYTHON
import requests

API_KEY  = "fct_TU_API_KEY"
BASE_URL = "https://api.facturax.app"

HEADERS = {
    "X-API-Key": API_KEY,
}

# Verificar estado
r = requests.get(f"{BASE_URL}/v1/status")
print(r.json())  # {"status":"ok","version":"1.1.0",...}

# Verificar cuota disponible
r = requests.get(f"{BASE_URL}/quota", headers=HEADERS)
data = r.json()
# plan_remaining = plan mensual (se resetea) · api_credits = créditos API (sin caducidad)
print(f"Plan: {data['plan_remaining']}/{data['plan_quota']} | Créditos API: {data['api_credits']}")
PHP
<?php
$apiKey  = 'fct_TU_API_KEY';
$baseUrl = 'https://api.facturax.app';

// Verificar cuota
$ch = curl_init("$baseUrl/quota");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ["X-API-Key: $apiKey"],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

// Plan mensual y créditos API separados
echo "Plan: {$response['plan_remaining']}/{$response['plan_quota']}\n";
echo "Créditos API: {$response['api_credits']} (sin caducidad)\n";
NODE.JS
const API_KEY  = 'fct_TU_API_KEY';
const BASE_URL = 'https://api.facturax.app';
const HEADERS  = { 'X-API-Key': API_KEY };

// Verificar cuota
const r = await fetch(`${BASE_URL}/quota`, { headers: HEADERS });
const data = await r.json();
// Plan mensual y créditos API son campos separados
console.log(`Plan: ${data.plan_remaining}/${data.plan_quota} | Créditos API: ${data.api_credits}`);
Sandbox: Añade el header X-Sandbox: true a cualquier petición para probar sin consumir cuota. El resultado incluye datos ficticios con la marca [SANDBOX].
2
Extraer datos de una factura
Sube un PDF o imagen y recibe todos los campos estructurados
⏱ ~2 min

Envía el archivo como multipart/form-data. Incluye X-External-Ref con tu propio ID de factura para mapear sin tabla auxiliar.

Python
PHP
Node.js
cURL
PYTHON
import requests, uuid

def extract_invoice(pdf_path: str, erp_invoice_id: str) -> dict:
    with open(pdf_path, "rb") as f:
        r = requests.post(
            f"{BASE_URL}/extract",
            headers={
                "X-API-Key":        API_KEY,
                "X-Idempotency-Key": str(uuid.uuid4()),  # reintento seguro
                "X-External-Ref":    erp_invoice_id,     # tu ID del ERP
            },
            files={"file": (pdf_path, f, "application/pdf")},
        )

    r.raise_for_status()
    data = r.json()

    # Cabeceras útiles de la respuesta
    remaining = r.headers.get("X-Quota-Remaining")
    replayed  = r.headers.get("X-Idempotency-Replayed")  # "true" si era reintento

    return data

# Uso
invoice = extract_invoice("factura.pdf", "ERP-INV-2025-0312")
print(f"Emisor:  {invoice['vendor_name']} ({invoice['vendor_vat']})")
print(f"Total:   {invoice['total']} {invoice['currency']}")
print(f"Nº fac:  {invoice['invoice_number']}")
print(f"Cuenta:  {invoice['accounting']['account_code']} — {invoice['accounting']['account_label']}")
PHP
function extractInvoice($pdfPath, $erpInvoiceId): array {
    $ch = curl_init("https://api.facturax.app/extract");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => [
            "X-API-Key: {$GLOBALS['apiKey']}",
            "X-Idempotency-Key: " . bin2hex(random_bytes(16)),
            "X-External-Ref: $erpInvoiceId",
        ],
        CURLOPT_POSTFIELDS => [
            'file' => new CURLFile($pdfPath, 'application/pdf'),
        ],
    ]);
    $response = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return $response;
}

$invoice = extractInvoice('factura.pdf', 'ERP-INV-2025-0312');
echo "Emisor: {$invoice['vendor_name']}\n";
echo "Total: {$invoice['total']} EUR\n";
NODE.JS
import { readFileSync } from 'fs';
import { randomUUID }  from 'crypto';

async function extractInvoice(pdfPath, erpInvoiceId) {
    const form = new FormData();
    form.append('file', new Blob([readFileSync(pdfPath)], { type: 'application/pdf' }), pdfPath);

    const r = await fetch(`${BASE_URL}/extract`, {
        method: 'POST',
        headers: {
            'X-API-Key':        API_KEY,
            'X-Idempotency-Key': randomUUID(),
            'X-External-Ref':    erpInvoiceId,
        },
        body: form,
    });

    if (!r.ok) {
        const err = await r.json();
        throw new Error(`${err.error_code}: ${err.detail}`);
    }
    return r.json();
}

const invoice = await extractInvoice('factura.pdf', 'ERP-INV-2025-0312');
console.log(`Emisor: ${invoice.vendor_name} | Total: ${invoice.total} EUR`);
BASH
curl -X POST https://api.facturax.app/extract \
  -H "X-API-Key: fct_TU_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "X-External-Ref: ERP-INV-2025-0312" \
  -F "file=@factura.pdf"
Respuesta relevante para el ERP
JSON — RESPUESTA
{
  "_log_id":        42,           // ID interno — guárdalo para /invoices/{id}/facturae
  "external_ref":   "ERP-INV-2025-0312",  // tu ID devuelto
  "vendor_name":    "Iberdrola S.A.U.",
  "vendor_vat":     "A95758389",
  "invoice_number": "FAC-2024-0312",
  "invoice_date":   "2024-03-12",
  "total":          187.43,
  "subtotal":       154.90,
  "tax":            32.53,
  "accounting": {
    "account_code":  "628",
    "account_label": "Suministros",
    "confidence":    "alta"
  },
  "confidence_score": 0.97,
  "warnings": []
}
3
Generar el XML Facturae firmado
Si tienes los datos estructurados, salta el OCR y ve directo al XML
⏱ ~2 min

Dos opciones: desde el log_id de una extracción previa, o directo desde JSON si tu ERP ya tiene los datos.

Python — desde log_id
Python — desde JSON
PHP
Node.js
PYTHON — DESDE EXTRACCIÓN
# 1. Extraer la factura y guardar el log_id
invoice  = extract_invoice("factura.pdf", "ERP-INV-2025-0312")
log_id   = invoice["_log_id"]

# 2. Generar y descargar el XML firmado
r = requests.post(
    f"{BASE_URL}/invoices/{log_id}/facturae",
    headers=HEADERS,
)
r.raise_for_status()

# 3. Guardar el .xml en disco
with open(f"factura_{log_id}.xml", "wb") as f:
    f.write(r.content)

print(f"XML firmado guardado: factura_{log_id}.xml")
PYTHON — JSON DIRECTO (SIN OCR)
# Si tu ERP ya tiene los datos — salta el OCR completamente
payload = {
    "invoice_data": {
        "vendor_name":        "Mi Empresa S.L.",
        "vendor_vat":         "B12345678",
        "vendor_address":     "Calle Mayor 1",
        "vendor_city":        "Madrid",
        "vendor_postal_code": "28001",
        "vendor_province":    "Madrid",
        "vendor_country":     "ESP",
        "buyer_name":         "Ayuntamiento de Madrid",
        "buyer_vat":          "P2807900B",
        "invoice_number":     "2025-001",
        "invoice_date":       "2025-05-01",
        "due_date":           "2025-06-01",
        "currency":           "EUR",
        "subtotal":           1000.00,
        "tax":                210.00,
        "total":              1210.00,
        "line_items": [
            {
                "description": "Servicios de desarrollo web",
                "quantity":    10,
                "unit_price":  100.00,
                "total":       1000.00,
                "tax_rate":    21,
            }
        ],
    },
    "external_ref": "ERP-INV-2025-001",
    "sign":           True,
    # Opción A — certificado personal del usuario (guardado en el dashboard)
    # Opción B — certificate_id para ERPs con múltiples clientes:
    # "certificate_id": "cert_a1b2c3d4..."  # de POST /certificates
}

r = requests.post(
    f"{BASE_URL}/convert-to-facturae/download",
    headers={**HEADERS, "X-Idempotency-Key": str(uuid.uuid4())},
    json=payload,
)
r.raise_for_status()

with open("factura_2025-001.xml", "wb") as f:
    f.write(r.content)

print("XML Facturae 3.2.2 con XAdES-BES guardado")
PHP — JSON DIRECTO
$payload = [
    'invoice_data' => [
        'vendor_name'    => 'Mi Empresa S.L.',
        'vendor_vat'     => 'B12345678',
        'invoice_number' => '2025-001',
        'invoice_date'   => '2025-05-01',
        'total'          => 1210.00,
        'subtotal'       => 1000.00,
        'tax'            => 210.00,
        'line_items'     => [[
            'description' => 'Servicios',
            'quantity'    => 1,
            'unit_price'  => 1000.00,
            'total'       => 1000.00,
            'tax_rate'    => 21,
        ]],
    ],
    'external_ref' => 'ERP-INV-2025-001',
    'sign'         => true,
];

$ch = curl_init('https://api.facturax.app/convert-to-facturae/download');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: {$apiKey}",
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
]);
$xml = curl_exec($ch);
file_put_contents('factura_2025-001.xml', $xml);
curl_close($ch);
NODE.JS — JSON DIRECTO
import { writeFileSync } from 'fs';

const r = await fetch(`${BASE_URL}/convert-to-facturae/download`, {
    method: 'POST',
    headers: { ...HEADERS, 'Content-Type': 'application/json' },
    body: JSON.stringify({
        invoice_data: {
            vendor_name:    'Mi Empresa S.L.',
            vendor_vat:     'B12345678',
            invoice_number: '2025-001',
            invoice_date:   '2025-05-01',
            total: 1210, subtotal: 1000, tax: 210,
            line_items: [{ description: 'Servicios', quantity: 1,
                           unit_price: 1000, total: 1000, tax_rate: 21 }],
        },
        external_ref: 'ERP-INV-2025-001',
        sign: true,
    }),
});

writeFileSync('factura.xml', Buffer.from(await r.arrayBuffer()));
⚡ Integraciones ERP — rate limiting
La API permite 60 peticiones/minuto por API key. Si procesas facturas en batch (fin de mes, procesamiento nocturno), añade un delay de 1 segundo entre llamadas para no superar el límite:
for factura in facturas:
    resultado = convert(factura)
    time.sleep(1)  # máx 60/min, sin 429
Si recibes un 429, espera el valor del header Retry-After antes de reintentar. Para volúmenes superiores a 60 facturas/min de forma sostenida, contacta con nosotros — los planes Business tienen límites personalizados.
4
Recibir notificaciones por webhook
El ERP recibe el aviso cuando el XML está listo — sin polling
⏱ ~3 min

Registra un endpoint en tu ERP y suscríbete a los eventos que necesitas. Verifica siempre la firma HMAC para descartar peticiones falsas.

Registrar webhook
Python — receptor
PHP — receptor
Node.js — receptor
PYTHON — REGISTRAR
r = requests.post(f"{BASE_URL}/webhooks", headers=HEADERS, json={
    "url":    "https://mi-erp.com/facturax/webhook",
    "events": ["invoice.signed", "quota.warning"],
    "secret": "mi_secret_para_verificar_hmac",
})
print(r.json())  # {"id": 1, "url": "...", "events": [...]}
PYTHON — FASTAPI / FLASK RECEPTOR
import hmac, hashlib
from fastapi import Request, HTTPException

WEBHOOK_SECRET = "mi_secret_para_verificar_hmac"

async def facturax_webhook(request: Request):
    body      = await request.body()
    signature = request.headers.get("X-Factura-Signature", "")

    # ── Verificar firma HMAC-SHA256 ──────────────────────────
    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(), body, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise HTTPException(401, "Firma inválida")

    # ── Procesar evento ──────────────────────────────────────
    event = (await request.json())
    e_type = event["event"]
    data   = event["data"]

    if e_type == "invoice.signed":
        # El XML firmado ya está en custodia — descargar si lo necesitas
        log_id       = data["log_id"]
        external_ref = data["external_ref"]   # tu ID del ERP
        expires_at   = data["expires_at"]
        mark_invoice_signed(external_ref, log_id, expires_at)

    elif e_type == "quota.warning":
        # Avisar al equipo antes de quedarse sin créditos
        send_alert(f"FacturaX: {data['remaining']} facturas restantes ({data['pct_remaining']}%)")  # 'remaining' en el payload del webhook (distinto de GET /quota)

    return {"ok": True}
PHP — RECEPTOR
$secret    = 'mi_secret_para_verificar_hmac';
$body      = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_FACTURA_SIGNATURE'] ?? '';
$expected  = 'sha256=' . hash_hmac('sha256', $body, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit('Firma inválida');
}

$event  = json_decode($body, true);
$type   = $event['event'];
$data   = $event['data'];

if ($type === 'invoice.signed') {
    markInvoiceSigned($data['external_ref'], $data['log_id']);
} elseif ($type === 'quota.warning') {
    sendAlert("FacturaX cuota: {$data['pct_used']}% usada");
}

http_response_code(200);
echo 'ok';
NODE.JS (EXPRESS) — RECEPTOR
import { createHmac, timingSafeEqual } from 'crypto';

const WEBHOOK_SECRET = 'mi_secret_para_verificar_hmac';

app.post('/facturax/webhook', express.raw({ type: '*/*' }), (req, res) => {
    const sig      = req.headers['x-factura-signature'] ?? '';
    const expected = 'sha256=' + createHmac('sha256', WEBHOOK_SECRET)
                        .update(req.body).digest('hex');

    if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected)))
        return res.status(401).send('Firma inválida');

    const { event, data } = JSON.parse(req.body);

    if (event === 'invoice.signed')
        markInvoiceSigned(data.external_ref, data.log_id);
    else if (event === 'quota.warning')
        sendAlert(`FacturaX cuota: ${data.pct_used}% usada`);

    res.json({ ok: true });
});
5
Manejo de errores y reintentos
Qué errores reintentar y con qué estrategia
⏱ ~1 min

Todos los errores devuelven error_code + detail. Toma decisiones sobre error_code, nunca sobre el texto de detail.

error_codeHTTPReintentoEstrategia
QUOTA_EXCEEDED429NoEsperar reset mensual o comprar créditos
EXTRACTION_FAILED500Sí × 3Backoff: 2s, 4s, 8s. Misma idempotency key
SERVICE_ERROR500Sí × 3Backoff: 5s, 15s, 30s. Consultar /status
RATE_LIMIT_EXCEEDED429EsperarCabecera Retry-After indica los segundos
FILE_TYPE_INVALID400NoCorregir formato antes de reintentar
CERT_INVALID_PASSWORD400NoVerificar certificado y contraseña
AUTH_INVALID_KEY401NoRevisar API key en el dashboard
PYTHON — HELPER CON REINTENTO
import time, requests

RETRYABLE = {"EXTRACTION_FAILED", "SERVICE_ERROR", "XML_BUILD_FAILED"}

def call_with_retry(method, url, max_retries=3, **kwargs):
    idempotency_key = kwargs.get("headers", {}).get("X-Idempotency-Key")

    for attempt in range(max_retries):
        r = requests.request(method, url, **kwargs)

        if r.status_code == 429:
            wait = int(r.headers.get("Retry-After", 60))
            time.sleep(wait)
            continue

        if r.status_code >= 500:
            error_code = r.json().get("error_code", "")
            if error_code in RETRYABLE and attempt < max_retries - 1:
                time.sleep(2 ** (attempt + 1))  # 2s, 4s, 8s
                continue

        return r

    return r
6
Monitorizar cuota y alertas
Evita que tu ERP falle silenciosamente a mitad de mes
⏱ ~1 min
PYTHON — GUARDIA DE CUOTA
def check_quota_before_batch(n_invoices: int) -> bool:
    """Comprueba si hay cuota suficiente antes de un lote."""
    r    = requests.get(f"{BASE_URL}/quota", headers=HEADERS)
    data = r.json()

    # Sumar plan mensual + créditos API (sin caducidad)
    total = data.get("plan_remaining", 0) + data.get("api_credits", 0)
    if total < n_invoices:
        raise RuntimeError(
            f"Cuota insuficiente: {total} disponibles, "
            f"necesitas {n_invoices}. Reset plan: {data.get('quota_reset_at', '—')}"
        )
    return True

# Antes de procesar un lote de facturas
check_quota_before_batch(len(facturas_pendientes))
✓ Integración completa. Tu ERP ya puede: extraer facturas con OCR, generar XML Facturae 3.2.2 firmado con XAdES-BES, recibir notificaciones cuando el XML está listo, manejar errores con reintentos correctos y monitorizar la cuota sin sorpresas.
← Documentación completa
Solo firmar XML — para ERPs con generación propia
Si tu ERP ya genera Facturae, solo necesitas la firma XAdES-BES
⏱ ~30 min de integración

Si tu ERP ya genera el XML Facturae correctamente pero no tiene firma XAdES-BES integrada, no necesitas cambiar nada de cómo generas facturas. Solo añade una llamada a /sign que recibe el XML y devuelve el .xml firmado listo para FACe.

Flujo para ERPs multi-cliente:
POST /certificates (1 vez/cliente) → certificate_id
Tu ERP genera XML → POST /sign + X-Certificate-ID → .xml → FACe
1 crédito por factura. El usuario final no sabe que existe FacturaX.
Python
PHP
Node.js
cURL
PYTHON
import requests

# Paso 1 (una vez por cliente): subir el .p12 y guardar el certificate_id
with open("cliente.p12", "rb") as cert:
    r = requests.post(
        "https://api.facturax.app/certificates",
        headers={"X-API-Key": "fct_tu_api_key"},
        files={"file": cert},
        data={"password": "contraseña_p12", "name": "Cliente ABC S.L."},
    )
certificate_id = r.json()["certificate_id"]  # guardar en tu BD

# Paso 2 (en cada factura): el XML lo genera tu ERP como siempre
with open("factura_sin_firmar.xml", "rb") as f:
    r = requests.post(
        "https://api.facturax.app/sign",
        headers={
            "X-API-Key":        "fct_tu_api_key",
            "X-Certificate-ID": certificate_id,  # firma con el cert del cliente
            "X-External-Ref":   "F2024-001",
        },
        files={"file": ("factura.xml", f, "application/xml")},
    )
    r.raise_for_status()
    data = r.json()

# Guardar el .xml firmado
with open(data["filename"], "w", encoding="utf-8") as out:
    out.write(data["xml"])

print(f"✓ {data['filename']} — listo para subir a FACe")
PHP
// El XML lo genera tu ERP como siempre
$xml_path = 'factura_sin_firmar.xml';
$ch = curl_init('https://api.facturax.app/sign');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => [
        'file' => new CURLFile($xml_path, 'application/xml'),
    ],
    CURLOPT_HTTPHEADER     => [
        'X-API-Key: fct_tu_api_key',
        'X-External-Ref: F2024-001',
    ],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
file_put_contents($data['filename'], $data['xml']);
echo "✓ {$data['filename']} — listo para FACe";
NODE.JS
const fs       = require('fs');
const FormData = require('form-data');

const form = new FormData();
form.append('file', fs.createReadStream('factura_sin_firmar.xml'));

const res  = await fetch('https://api.facturax.app/sign', {
  method:  'POST',
  headers: {
    'X-API-Key':      'fct_tu_api_key',
    'X-External-Ref': 'F2024-001',
    ...form.getHeaders(),
  },
  body: form,
});
const data = await res.json();
fs.writeFileSync(data.filename, data.xml, 'utf8');
console.log(`✓ ${data.filename} — listo para FACe`);
cURL
curl -X POST https://api.facturax.app/sign \
  -H "X-API-Key: fct_tu_api_key" \
  -H "X-External-Ref: F2024-001" \
  -F "file=@factura_sin_firmar.xml"
✓ TAMBIÉN COMPATIBLE CON LOTES

Para batch de fin de mes añade time.sleep(1) entre llamadas para respetar el límite de 60 req/min. El webhook invoice.signed incluye el external_ref para correlacionar con tu ERP.