Factuarea APIDevelopers

Migració des de Holded

Mapeig de recursos Holded → Factuarea, nomenclatura, endpoints equivalents i script en Python.

Aquesta guia documenta la migració des de l'API de Holded (un dels principals competidors en el sector del SaaS de facturació espanyol) a la Public API v1 de Factuarea. Cobreix el mapeig de recursos, les diferències de nomenclatura, els endpoints equivalents i un script d'exemple en Python que importa contactes normalitzats.

Mapeig de recursos

HoldedFactuareaNotes
contactscontactsUna identitat fiscal amb rols acumulables customer, supplier i lead. Conserva tots dos rols si un contacte compra i ven.
productsproductsNomenclatura idèntica.
documents/invoiceinvoicesEndpoint dedicat.
documents/estimatequotesCanvi de nom: Holded fa servir "estimate", Factuarea "quote".
documents/proformproformasReanomenat a "proforma" sense abreujar.
documents/waybilldelivery_notesNomenclatura canònica espanyola/legal.
documents/purchasepurchase_invoices
documents/recurringrecurring_invoices
taxestaxesMateix concepte.
numerationsseriesHolded "numeration", Factuarea "series". El format de Holded es mapeja a number_format, una màscara de numeració configurable (padding + token d'any + separador), p. ex. {code}-{YYYY}-{000}.
tagstagsEtiquetes de classificació lliure en un document (slugs en minúscula, ≤ 40 caràcters, ≤ 30 per document).
custom fieldscustom_fieldsMetadades d'integració tipades [{field, value}] en un document (≤ 50 entrades).
webhookswebhook_endpoints (+ anidat deliveries)Factuarea separa la configuració de l'endpoint de la traçabilitat de lliuraments (GET /v1/webhook_endpoints/{id}/deliveries).

Diferències clau

1. Autenticació

  • Holded: header key: <api_key>.
  • Factuarea: Authorization: Bearer fact_test_... o X-API-Key: fact_test_.... OpenAPI estàndard.

2. Identificadors

  • Holded: IDs opacs de tipus string-numèric.
  • Factuarea: cada recurs té una key id el valor de la qual és un UUID v7 (01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b) — codifica un timestamp i és ordenable lexicogràficament. Les foreign keys fan servir *_id (p. ex. client_id).

Desa l’ID de Holded a external_id del contacte canònic. Resol-lo amb POST /v1/contacts/find-by-external-id, limitat a l’empresa autenticada. Reutilitza l’id del contacte a client_id o supplier_id dels documents segons el seu rol actiu.

3. Paginació

  • Holded: ?starttmp=...&endtmp=... (timestamps a la URL).
  • Factuarea: paginació per cursor (starting_after, ending_before) pel id del recurs. Consulta Paginació.

4. Errors

  • Holded: status code + array errors o string error.
  • Factuarea: embolcall { error: { type, code, message, request_id, doc_url } }. Consulta Errors.

5. Webhooks

  • Holded: payload sense signar (validació basada en IP).
  • Factuarea: signatura HMAC SHA256 obligatòria, tolerància de ±5min, reintents exponencials fins a 8 intents. Consulta Webhooks.

6. Idempotència

  • Holded: no suportada.
  • Factuarea: header Idempotency-Key amb TTL de 24h. Consulta Idempotència.

Endpoints equivalents (operacions més comunes)

OperacióHoldedFactuarea
Llistar facturesGET /invoicing/v1/documents/invoiceGET /v1/invoices
Crear facturaPOST /invoicing/v1/documents/invoicePOST /v1/invoices
Marcar factura com a pagadaPOST /invoicing/v1/documents/invoice/{id}/payPOST /v1/invoices/{id}/mark-paid
Enviar factura per emailPOST /invoicing/v1/documents/invoice/{id}/sendPOST /v1/invoices/{id}/send
Descarregar PDFGET /invoicing/v1/documents/invoice/{id}/pdfGET /v1/invoices/{id}/pdf
Llistar clientsGET /invoicing/v1/contacts?type=clientGET /v1/contacts?roles=customer
Crear clientPOST /invoicing/v1/contacts (amb type=client)POST /v1/contacts
Convertir pressupost en facturaPOST /invoicing/v1/documents/estimate/{id}/convertPOST /v1/quotes/{id}/convert
Crear webhookPOST /invoicing/v1/webhooksPOST /v1/webhook_endpoints

Diferències de payload

Crear factura

Holded:

POST /invoicing/v1/documents/invoice
{
  "contactId": "5e1c2a3b4f5d6e7f8a9b0c1d",
  "date": 1747314060,
  "items": [
    { "name": "Service", "units": 1, "subtotal": 99.00, "tax": 21 }
  ]
}

Factuarea:

POST /v1/invoices
Idempotency-Key: 01928f10-7c0e-7c4a-9b7d-2f8a6e3c1d4b

{
  "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01",
  "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02",
  "issued_on": "2026-05-15",
  "due_on": "2026-06-15",
  "lines": [
    {
      "description": "Service",
      "quantity": 1,
      "unit_price": 99.00,
      "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03"
    }
  ]
}

Canvis:

  • contactIdclient_id (FK explícita; el valor és un UUID v7).
  • date (timestamp) → issued_on (YYYY-MM-DD), amb due_on obligatori.
  • items[].subtotal (import) → lines[].unit_price (preu unitari; l'API calcula els totals).
  • items[].tax (percentatge en línia) → lines[].tax_rate_id (FK al catàleg d'impostos).
  • series_id obligatori — Factuarea exigeix configurar la sèrie abans d'emetre (coherència amb l'AEAT).

Webhooks: signatura

Holded no signa. Factuarea sí (HMAC SHA256). Després de migrar has de validar la signatura al teu handler. Consulta Webhooks.

Migrar contactes normalitzats (Python)

Prepara un fitxer JSON a partir de l’export de Holded amb kind explícit, roles acumulables i identitat fiscal vàlida. L’script envia payloads canònics, conserva external_id, manté una idempotency key entre reintents i mostra els conflictes per revisar-los manualment. No endevina si és persona o empresa ni omet proveïdors en silenci. Comença amb una clau fact_test_.

import json
import os
import requests
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential

key = os.environ['FACTUAREA_API_KEY']
if not key.startswith('fact_test_'):
    raise ValueError('Run the migration in test mode first')

@retry(retry=retry_if_exception_type((requests.ConnectionError, requests.Timeout)),
       stop=stop_after_attempt(5), wait=wait_exponential(max=10), reraise=True)
def create_contact(contact):
    response = requests.post(
        'https://api.factuarea.com/v1/contacts',
        headers={
            'Authorization': f'Bearer {key}',
            'Idempotency-Key': f"holded-contact-{contact['external_id']}",
        },
        json=contact,
        timeout=30,
    )
    response.raise_for_status()
    return response.json()['data']

with open('holded-contacts.json', encoding='utf-8') as source:
    contacts = json.load(source)
for contact in contacts:
    result = create_contact(contact)
    print(contact['external_id'], result['id'])
[
  {
    "external_id": "holded-42",
    "name": "Distribuciones Ejemplo SL",
    "kind": "company",
    "roles": ["customer", "supplier"],
    "tax_id": "B12345674",
    "address": {"line_1": "Calle Mayor", "country_code": "ES"}
  }
]

Importació massiva de contactes des de l’export de Holded

Fes servir POST /v1/contacts/import/preview abans de POST /v1/contacts/import. Tots dos requereixen contacts:write, Idempotency-Key i multipart/form-data. L’importador admet fitxers CSV, TXT, XLSX o XLS de fins a 10 MB.

El preset de mapeig

mapping associa camp destí → capçalera de la columna d’origen, per exemple mapping[name]=Name. Omet-lo si les capçaleres ja fan servir els noms canònics. Els destins desconeguts es rebutgen; no es descarten en silenci. Inclou name, una identitat fiscal vàlida i almenys un rol, indicat al fitxer o mitjançant target_roles.

Camps destí

L’importador canònic admet external_id, kind, roles, tags, adreça (address_line_1, country_code), identitat fiscal alternativa, billing_emails, bank_accounts, metadata, codis DIR3 i valors per rol (customer_*, supplier_*). Mapeja només les columnes que vulguis escriure. Conserva tots dos rols si un contacte compra i ven; no divideixis mai la seva identitat fiscal en registres duplicats.

Pas 1 — previsualitzar

curl -X POST https://api.factuarea.com/v1/contacts/import/preview \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "file=@contacts.csv" \
  -F "mapping[name]=Name" \
  -F "mapping[tax_id]=VAT number" \
  -F "mapping[external_id]=Id" \
  -F "target_roles[]=customer" \
  -F "conflict_strategy=reject"

La previsualització classifica cada fila com a create, update, add_role, merge_candidate, conflict o invalid. Revisa errors, warnings i target_uuid; resol les identitats ambigües abans d’importar. La resposta inclou total, recomptes per acció, dry_run i queued, en lloc de l’embolcall de l’importador antic de clients.

Pas 2 — importar

Envia el fitxer revisat i el mateix mapeig a /v1/contacts/import, amb una idempotency key nova. dry_run=true també valida sense escriure. El valor predeterminat conflict_strategy=reject protegeix les dades existents; tria update o merge deliberadament després de revisar la previsualització. Les importacions grans poden retornar 202 amb queued=true: l’acceptació no demostra que totes les files hagin acabat.

Pas 3 — conciliar l’ID de Holded

Desa l’ID de Holded a external_id del contacte canònic. Resol-lo amb POST /v1/contacts/find-by-external-id, limitat a l’empresa autenticada. Reutilitza l’id del contacte a client_id o supplier_id dels documents segons el seu rol actiu.

Checklist de migració

  1. Inventari: nombre de contactes, productes, factures històriques, webhooks actius.
  2. Desa l’ID de Holded a external_id del contacte canònic. Resol-lo amb POST /v1/contacts/find-by-external-id, limitat a l’empresa autenticada. Reutilitza l’id del contacte a client_id o supplier_id dels documents segons el seu rol actiu.
  3. Migració per fases:
    • Catàlegs: impostos, sèries, productes i després presentacions/variants/ofertes de proveïdor i tarifes → primer. Conserva l'ID origen a cada external_id compatible; no inventis camps de Holded que no estiguin documentats.
    • Mestres: contactes canònics amb els seus rols → segon.
    • Documents històrics: factures, pressupostos, etc. → tercer.
  4. Doble escriptura temporal: durant 1–2 setmanes, escriu a totes dues plataformes. Reconcilia les diferències diàriament.
  5. Webhooks: configura els nous endpoints, desplega el handler amb verificació HMAC i executa'l en paral·lel.
  6. Cut-over: deixa d'escriure a Holded, deshabilita els webhooks allà.
  7. Suport: contacta amb support@factuarea.com indicant el request_id davant de qualsevol incidència durant la migració.

Diferències intencionades

Alguns comportaments de Holded no repliquem a propòsit:

  • Anul·lar vs eliminar una factura: Holded permet eliminar factures. Factuarea no — emetre i després eliminar és un anti-patró davant de l'AEAT. Fes servir POST /v1/invoices/{id}/annul (anul·lar) o emet una factura rectificativa.
  • Editar una factura emesa: Holded permet reemetre un PDF diferent. Factuarea bloqueja els canvis després de sent excepte mark-paid, annul, create-corrective. És deliberat.
  • Calculadora d'IVA en línia: Holded accepta el percentatge d'IVA a cada línia. Factuarea requereix una FK al catàleg d'impostos per garantir la coherència i els informes.

Són decisions de producte, no limitacions tècniques. Si trobes un cas d'ús real que no puguem cobrir, contacta amb producte.

En aquesta pàgina

Et donem un cop de mà?Contactar amb suport