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
| Holded | Factuarea | Notes |
|---|---|---|
contacts | contacts | Una identitat fiscal amb rols acumulables customer, supplier i lead. Conserva tots dos rols si un contacte compra i ven. |
products | products | Nomenclatura idèntica. |
documents/invoice | invoices | Endpoint dedicat. |
documents/estimate | quotes | Canvi de nom: Holded fa servir "estimate", Factuarea "quote". |
documents/proform | proformas | Reanomenat a "proforma" sense abreujar. |
documents/waybill | delivery_notes | Nomenclatura canònica espanyola/legal. |
documents/purchase | purchase_invoices | |
documents/recurring | recurring_invoices | |
taxes | taxes | Mateix concepte. |
numerations | series | Holded "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}. |
tags | tags | Etiquetes de classificació lliure en un document (slugs en minúscula, ≤ 40 caràcters, ≤ 30 per document). |
| custom fields | custom_fields | Metadades d'integració tipades [{field, value}] en un document (≤ 50 entrades). |
webhooks | webhook_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_...oX-API-Key: fact_test_.... OpenAPI estàndard.
2. Identificadors
- Holded: IDs opacs de tipus string-numèric.
- Factuarea: cada recurs té una key
idel 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) peliddel recurs. Consulta Paginació.
4. Errors
- Holded: status code + array
errorso stringerror. - 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-Keyamb TTL de 24h. Consulta Idempotència.
Endpoints equivalents (operacions més comunes)
| Operació | Holded | Factuarea |
|---|---|---|
| Llistar factures | GET /invoicing/v1/documents/invoice | GET /v1/invoices |
| Crear factura | POST /invoicing/v1/documents/invoice | POST /v1/invoices |
| Marcar factura com a pagada | POST /invoicing/v1/documents/invoice/{id}/pay | POST /v1/invoices/{id}/mark-paid |
| Enviar factura per email | POST /invoicing/v1/documents/invoice/{id}/send | POST /v1/invoices/{id}/send |
| Descarregar PDF | GET /invoicing/v1/documents/invoice/{id}/pdf | GET /v1/invoices/{id}/pdf |
| Llistar clients | GET /invoicing/v1/contacts?type=client | GET /v1/contacts?roles=customer |
| Crear client | POST /invoicing/v1/contacts (amb type=client) | POST /v1/contacts |
| Convertir pressupost en factura | POST /invoicing/v1/documents/estimate/{id}/convert | POST /v1/quotes/{id}/convert |
| Crear webhook | POST /invoicing/v1/webhooks | POST /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:
contactId→client_id(FK explícita; el valor és un UUID v7).date(timestamp) →issued_on(YYYY-MM-DD), ambdue_onobligatori.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_idobligatori — 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ó
- Inventari: nombre de contactes, productes, factures històriques, webhooks actius.
- Desa l’ID de Holded a
external_iddel contacte canònic. Resol-lo ambPOST /v1/contacts/find-by-external-id, limitat a l’empresa autenticada. Reutilitza l’iddel contacte aclient_idosupplier_iddels documents segons el seu rol actiu. - 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_idcompatible; 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.
- Catàlegs: impostos, sèries, productes i després
presentacions/variants/ofertes de proveïdor i tarifes → primer. Conserva
l'ID origen a cada
- Doble escriptura temporal: durant 1–2 setmanes, escriu a totes dues plataformes. Reconcilia les diferències diàriament.
- Webhooks: configura els nous endpoints, desplega el handler amb verificació HMAC i executa'l en paral·lel.
- Cut-over: deixa d'escriure a Holded, deshabilita els webhooks allà.
- Suport: contacta amb
support@factuarea.comindicant elrequest_iddavant 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
sentexceptemark-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.