ERP Integration Guide

From zero to structured JSON
in 10 minutes

Connect your ERP to the Parse extraction API and turn any invoice — PDF, scan or photo — into structured, validated JSON. Optional add-on for Spain: generate Facturae 3.2.2 XML with XAdES-BES signature, ready for FACe, with no signing libraries or certificates to manage manually.

Python PHP Node.js cURL
Core flow — every Parse account:
Your ERP
PDF, scan or photo
POST /extract
OCR + fields
Structured JSON
Ready to use in your ERP
Optional add-on 🇪🇸 ES only — if you need to submit to the Spanish public sector:
Structured JSON
From the core flow above
POST /convert
Facturae XML
Webhook signed
XAdES-BES ready
FACe
Public administration
1
Authentication
Get your API key and verify the connection works
⏱ ~1 min

Go to your dashboard → API Keys → Create key. Copy the fct_... key — it's only shown once. Then verify the connection:

cURL
Python
PHP
Node.js
BASH
# Check API status (no auth)
curl https://api.facturax.app/v1/status

# Check your quota
curl https://api.facturax.app/quota \
  -H "X-API-Key: fct_YOUR_API_KEY"
PYTHON
import requests

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

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

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

# Check available quota
r = requests.get(f"{BASE_URL}/quota", headers=HEADERS)
data = r.json()
# plan_remaining = monthly plan (resets) · api_credits = API credits (no expiry)
print(f"Plan: {data['plan_remaining']}/{data['plan_quota']} | API credits: {data['api_credits']}")
PHP
<?php
$apiKey  = 'fct_YOUR_API_KEY';
$baseUrl = 'https://api.facturax.app';

// Check quota
$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);

// Monthly plan and API credits are separate
echo "Plan: {$response['plan_remaining']}/{$response['plan_quota']}\n";
echo "API credits: {$response['api_credits']} (no expiry)\n";
NODE.JS
const API_KEY  = 'fct_YOUR_API_KEY';
const BASE_URL = 'https://api.facturax.app';
const HEADERS  = { 'X-API-Key': API_KEY };

// Check quota
const r = await fetch(`${BASE_URL}/quota`, { headers: HEADERS });
const data = await r.json();
// Monthly plan and API credits are separate fields
console.log(`Plan: ${data.plan_remaining}/${data.plan_quota} | API credits: ${data.api_credits}`);
Sandbox: Add the X-Sandbox: true header to any request to test without consuming quota. The result includes mock data marked with [SANDBOX].
⚡ Skip the boilerplate: import the Postman collection to get every endpoint pre-configured, or point your codegen at the OpenAPI schema to generate a typed client.
2
Extract data from an invoice
Upload a PDF or image and get all the structured fields back
⏱ ~2 min

Send the file as multipart/form-data. Include X-External-Ref with your own invoice ID to map results without an extra table.

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()),  # safe retries
                "X-External-Ref":    erp_invoice_id,     # your ERP ID
            },
            files={"file": (pdf_path, f, "application/pdf")},
        )

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

    # Useful response headers
    remaining = r.headers.get("X-Quota-Remaining")
    replayed  = r.headers.get("X-Idempotency-Replayed")  # "true" if this was a retry

    return data

# Usage
invoice = extract_invoice("invoice.pdf", "ERP-INV-2025-0312")
print(f"Vendor:  {invoice['vendor_name']} ({invoice['vendor_vat']})")
print(f"Total:   {invoice['total']} {invoice['currency']}")
print(f"Inv. no: {invoice['invoice_number']}")
print(f"Account: {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('invoice.pdf', 'ERP-INV-2025-0312');
echo "Vendor: {$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('invoice.pdf', 'ERP-INV-2025-0312');
console.log(`Vendor: ${invoice.vendor_name} | Total: ${invoice.total} EUR`);
BASH
curl -X POST https://api.facturax.app/extract \
  -H "X-API-Key: fct_YOUR_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "X-External-Ref: ERP-INV-2025-0312" \
  -F "file=@invoice.pdf"
Response fields relevant to the ERP
JSON — RESPONSE
{
  "_log_id":        42,           // internal ID — save it for /invoices/{id}/facturae
  "external_ref":   "ERP-INV-2025-0312",  // your ID, returned back
  "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" // (Utilities),
    "confidence":    "alta" // (high)
  },
  "confidence_score": 0.97,
  "warnings": []
}
3
Generate the signed Facturae XML 🇪🇸 ES only · optional
Only needed if you submit invoices to the Spanish public sector. If you already have structured data, skip OCR and go straight to the XML
⏱ ~2 min

Two options: from the log_id of a previous extraction, or directly from JSON if your ERP already has the data.

Python — from log_id
Python — from JSON
PHP
Node.js
PYTHON — FROM AN EXTRACTION
# 1. Extract the invoice and save the log_id
invoice  = extract_invoice("invoice.pdf", "ERP-INV-2025-0312")
log_id   = invoice["_log_id"]

# 2. Generate and download the signed XML
r = requests.post(
    f"{BASE_URL}/invoices/{log_id}/facturae",
    headers=HEADERS,
)
r.raise_for_status()

# 3. Save the .xml to disk
with open(f"invoice_{log_id}.xml", "wb") as f:
    f.write(r.content)

print(f"Signed XML saved: invoice_{log_id}.xml")
PYTHON — DIRECT JSON (NO OCR)
# If your ERP already has the data — skip OCR entirely
payload = {
    "invoice_data": {
        "vendor_name":        "My Company Ltd.",
        "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" # (Web development services),
                "quantity":    10,
                "unit_price":  100.00,
                "total":       1000.00,
                "tax_rate":    21,
            }
        ],
    },
    "external_ref": "ERP-INV-2025-001",
    "sign":           True,
    # Option A — the user's own certificate (saved in the dashboard)
    # Option B — certificate_id for ERPs with multiple clients:
    # "certificate_id": "cert_a1b2c3d4..."  # from 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("invoice_2025-001.xml", "wb") as f:
    f.write(r.content)

print("Facturae 3.2.2 XML with XAdES-BES saved")
PHP — DIRECT JSON
$payload = [
    'invoice_data' => [
        'vendor_name'    => 'My Company Ltd.',
        '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' // (Services),
            '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('invoice_2025-001.xml', $xml);
curl_close($ch);
NODE.JS — DIRECT JSON
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:    'My Company Ltd.',
            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('invoice.xml', Buffer.from(await r.arrayBuffer()));
⚡ ERP integrations — rate limiting
The API allows 60 requests/minute per API key. If you process invoices in batches (month-end, overnight processing), add a 1-second delay between calls to stay within the limit:
for invoice in invoices:
    result = convert(invoice)
    time.sleep(1)  # max 60/min, no 429
If you get a 429, wait for the value of the Retry-After header before retrying. For sustained volumes above 60 invoices/min, contact us — Business plans have custom limits.
4
Receive webhook notifications
Your ERP gets notified when the XML is ready — no polling needed
⏱ ~3 min

Register an endpoint on your ERP and subscribe to the events you need. Always verify the HMAC signature to reject forged requests.

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

WEBHOOK_SECRET = "my_hmac_verification_secret"

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

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

    if not hmac.compare_digest(signature, expected):
        raise HTTPException(401, "Invalid signature")

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

    if e_type == "invoice.signed":
        # The signed XML is already in custody — download if you need it
        log_id       = data["log_id"]
        external_ref = data["external_ref"]   # your ERP ID
        expires_at   = data["expires_at"]
        mark_invoice_signed(external_ref, log_id, expires_at)

    elif e_type == "quota.warning":
        # Alert the team before running out of credits
        send_alert(f"FacturaX: {data['remaining']} invoices remaining ({data['pct_remaining']}%)")  # 'remaining' is in the webhook payload (different from GET /quota)

    return {"ok": True}
PHP — RECEIVER
$secret    = 'my_hmac_verification_secret';
$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('Invalid signature');
}

$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 quota: {$data['pct_used']}% used");
}

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

const WEBHOOK_SECRET = 'my_hmac_verification_secret';

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('Invalid signature');

    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 quota: ${data.pct_used}% used`);

    res.json({ ok: true });
});
5
Error handling and retries
Which errors to retry and what strategy to use
⏱ ~1 min

All errors return error_code + detail. Make decisions based on error_code, never on the detail text.

error_codeHTTPRetryStrategy
QUOTA_EXCEEDED429NoWait for the monthly reset or buy credits
EXTRACTION_FAILED500Yes × 3Backoff: 2s, 4s, 8s. Same idempotency key
SERVICE_ERROR500Yes × 3Backoff: 5s, 15s, 30s. Check /status
RATE_LIMIT_EXCEEDED429WaitThe Retry-After header tells you how many seconds
FILE_TYPE_INVALID400NoFix the format before retrying
CERT_INVALID_PASSWORD400NoCheck the certificate and password
AUTH_INVALID_KEY401NoCheck the API key in the dashboard
PYTHON — RETRY HELPER
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
Monitor quota and alerts
Avoid your ERP silently failing mid-month
⏱ ~1 min
PYTHON — QUOTA GUARD
def check_quota_before_batch(n_invoices: int) -> bool:
    """Check there's enough quota before running a batch."""
    r    = requests.get(f"{BASE_URL}/quota", headers=HEADERS)
    data = r.json()

    # Sum monthly plan + API credits (no expiry)
    total = data.get("plan_remaining", 0) + data.get("api_credits", 0)
    if total < n_invoices:
        raise RuntimeError(
            f"Not enough quota: {total} available, "
            f"need {n_invoices}. Plan reset: {data.get('quota_reset_at', '—')}"
        )
    return True

# Before processing a batch of invoices
check_quota_before_batch(len(pending_invoices))
✓ Integration complete. Your ERP can now: extract invoices with OCR into structured JSON, receive webhook notifications, handle errors with proper retries, and monitor quota without surprises. Optionally, for Spain: generate signed Facturae 3.2.2 XML with XAdES-BES, ready for FACe.
← Full documentation
Sign XML only — for ERPs with their own generation 🇪🇸 ES only · optional
If your ERP already generates Facturae, you just need the XAdES-BES signature
⏱ ~30 min integration

If your ERP already generates the Facturae XML correctly but doesn't have XAdES-BES signing built in, you don't need to change anything about how you generate invoices. Just add a call to /sign, which takes the XML and returns the signed .xml ready for FACe.

Flow for multi-client ERPs:
POST /certificates (once per client) → certificate_id
Your ERP generates XML → POST /sign + X-Certificate-ID → .xml → FACe
1 credit per invoice. The end user never needs to know FacturaX exists.
Python
PHP
Node.js
cURL
PYTHON
import requests

# Step 1 (once per client): upload the .p12 and save the certificate_id
with open("client.p12", "rb") as cert:
    r = requests.post(
        "https://api.facturax.app/certificates",
        headers={"X-API-Key": "fct_your_api_key"},
        files={"file": cert},
        data={"password": "p12_password", "name": "Cliente ABC S.L."},
    )
certificate_id = r.json()["certificate_id"]  # save it in your DB

# Step 2 (per invoice): the XML is generated by your ERP as usual
with open("unsigned_invoice.xml", "rb") as f:
    r = requests.post(
        "https://api.facturax.app/sign",
        headers={
            "X-API-Key":        "fct_your_api_key",
            "X-Certificate-ID": certificate_id,  # sign with the client's certificate
            "X-External-Ref":   "F2024-001",
        },
        files={"file": ("invoice.xml", f, "application/xml")},
    )
    r.raise_for_status()
    data = r.json()

# Save the signed .xml
with open(data["filename"], "w", encoding="utf-8") as out:
    out.write(data["xml"])

print(f"✓ {data['filename']} — ready to upload to FACe")
PHP
// The XML is generated by your ERP as usual
$xml_path = 'unsigned_invoice.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_your_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']} — ready for FACe";
NODE.JS
const fs       = require('fs');
const FormData = require('form-data');

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

const res  = await fetch('https://api.facturax.app/sign', {
  method:  'POST',
  headers: {
    'X-API-Key':      'fct_your_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} — ready for FACe`);
cURL
curl -X POST https://api.facturax.app/sign \
  -H "X-API-Key: fct_your_api_key" \
  -H "X-External-Ref: F2024-001" \
  -F "file=@unsigned_invoice.xml"
✓ ALSO WORKS WITH BATCHES

For month-end batches, add time.sleep(1) between calls to respect the 60 req/min limit. The invoice.signed webhook includes the external_ref to correlate with your ERP.