Implementación PagaFactu
Diagrama, llamadas paso a paso y ejemplos en múltiples lenguajes.
Diagrama de secuencia
Crear un cobro
Una sola llamada multipart/form-data. Recibes la URL de pago y el PDF.
Endpoint:
| Método | URL |
|---|---|
POST | https://nc-api-sandbox.zertiban.com/pagafactu/v1/flows/pagafactu |
Headers:
| Header | Valor |
|---|---|
Authorization | Bearer {access_token} |
x-tenant-id | {businessUuid} |
Sustituye los valores entre llaves {} por los tuyos:
curl -X POST https://nc-api-sandbox.zertiban.com/pagafactu/v1/flows/pagafactu \
-H "Authorization: Bearer {access_token}" \
-H "x-tenant-id: {businessUuid}" \
-F 'payload={
"externalId": "FACTURA-2026-001",
"countryCode": "ES",
"operations": [{
"externalId": "OP-2026-001",
"configuration": "{configurationUuid}",
"payment": {
"amount": 15075,
"currency": "EUR",
"types": ["PSD2_PAYMENT"],
"psd2Payment": {
"type": "SINGLE_PAYMENT",
"product": "SEPA_CREDIT_TRANSFER",
"creditorAccount": { "uuid": "{creditorAccountUuid}" }
},
"concept": "Factura 2026-001",
"debtor": {
"name": "Ana",
"lastName": "Martinez",
"email": "[email protected]"
}
},
"invoice": {
"externalId": "INV-2026-001",
"name": "Factura 2026-001",
"generateDocument": true
}
}]
};type=application/json'
amountva en céntimos:15075= 150,75 EUR.externalIdes tu identificador interno: aparece en los webhooks y en las consultas, lo que te permite reconciliar sin guardar los UUIDs de Zertiban.
Respuesta (201 Created):
{
"uuid": "28ffd216-5ee8-4e99-b1a9-511961e9c655",
"createdAt": "2026-04-01T09:15:00Z",
"operations": [
{
"uuid": "b7aa09fe-5678-1234-90ab-cdef98765432",
"url": "https://nc-api-sandbox.zertiban.com/link-validator/v1/links/eyJhbGci...",
"document": "JVBERi0xLjQK..."
}
]
}Guarda en tu base de datos:
| Campo | Para qué |
|---|---|
operations[0].uuid | Consultas de estado y reconciliación con webhooks |
operations[0].url | Enlace de pago que envías al cliente |
operations[0].document | PDF en Base64 (solo presente si generateDocument: true). Decodifica: echo "JVBERi0..." | base64 -d > factura.pdf |
Variantes de creación
1. PDF con QR generado por Zertiban (caso por defecto)
Es el escenario del ejemplo de arriba: "generateDocument": true con un name. Zertiban genera un PDF nuevo con el QR de cobro y lo devuelve en operations[0].document (Base64).
2. Sin documento
Si no necesitas documento, usa "generateDocument": false:
"invoice": {
"externalId": "INV-2026-001",
"generateDocument": false
}La respuesta no incluirá el campo document. El campo name de invoice no es necesario.
3. Tu propio PDF + página añadida por Zertiban
Si quieres adjuntar tu propio PDF, mantén "generateDocument": true, añade "id": "mi-factura" en invoice y sube el fichero como segundo part multipart. El nombre del part debe coincidir exactamente con el valor de invoice.id:
-F '[email protected];type=application/pdf'Zertiban añade una página PagaFactu al final de tu PDF (no lo reemplaza) con:
- QR de cobro
- Enlace directo a la operación
Tamaño máximo del fichero PDF: 8 MB.
Si combinas generateDocument: false con un fichero adjunto, el fichero se ignora silenciosamente y no se devuelve ningún documento.
4. Cobro programado
Si quieres que el pago se ejecute en una fecha futura, cambia psd2Payment.type a "FUTURE_PAYMENT" y añade la fecha:
"psd2Payment": {
"type": "FUTURE_PAYMENT",
"product": "SEPA_CREDIT_TRANSFER",
"requestedExecutionDate": "2026-05-01",
"creditorAccount": { "uuid": "{creditorAccountUuid}" }
}La fecha debe ser al menos 1 día hábil en el futuro. Solo disponible con SEPA_CREDIT_TRANSFER.
Enviar el enlace al cliente
Zertiban no envía comunicaciones al pagador. El envío del enlace y/o documento es responsabilidad de tu ERP.
Con la url y el document de la respuesta anterior tienes todo lo que necesitas:
- Envía la
urlpor email, WhatsApp o SMS - Adjunta el PDF decodificado si quieres que el cliente tenga el documento
El cliente puede acceder al cobro de dos formas:
- Abriendo el enlace directo
- Escaneando el QR de la página añadida al PDF (variante 1 o 3)
En cualquiera de los casos, el cliente selecciona su banco y autoriza la transferencia con su banca online (PSD2). No necesita instalar nada.
Alternativa: consulta por polling
Si prefieres no usar webhooks de momento, puedes consultar el estado directamente:
# Estado de una operación
curl https://nc-api-sandbox.zertiban.com/flow/v1/operations/{operationUuid} \
-H "Authorization: Bearer {access_token}" \
-H "x-tenant-id: {businessUuid}"Respuesta (campos principales):
{
"uuid": "b7aa09fe-...",
"externalId": "OP-2026-001",
"status": "COMPLETED",
"type": "PAYMENT",
"flowUuid": "28ffd216-...",
"configurationUuid": "...",
"expirationOffset": "P30D",
"expiresAt": "2026-05-01T09:15:00Z",
"createdAt": "2026-04-01T09:15:00Z",
"payment": {
"paymentUuid": "...",
"amount": 15075,
"currency": "EUR",
"concept": "Factura 2026-001",
"types": ["PSD2_PAYMENT"],
"psd2Payment": {
"type": "SINGLE_PAYMENT",
"product": "SEPA_CREDIT_TRANSFER",
"creditorAccount": { "uuid": "..." }
},
"subject": {
"name": "Ana",
"lastName": "Martinez",
"email": "[email protected]"
}
},
"invoice": {
"externalId": "INV-2026-001",
"dueDate": null,
"collectedAmount": null
}
}Referencia de campos
POST/pagafactu/v1/flows/pagafactu, Content-Type: multipart/form-data
El body es un part llamado payload con type=application/json.
Raíz:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
externalId | String | No | Tu ID para este cobro. Aparece en webhooks y consultas. Si se repite → 409 Conflict. |
countryCode | String | No | Código ISO 3166 alpha-2 (ej. "ES"). |
operations | Array | Sí | Exactamente 1 elemento para PagaFactu. |
operations[0]:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
externalId | String (max 50) | No | Tu ID para esta operación. Aparece en resource.externalId de los webhooks. |
configuration | UUID | No | Tu configurationUuid. Si se omite, se usa la configuración predeterminada activa del business. Si se informa con formato no UUID → 400. Si la configuración informada está deshabilitada → 409 (no se aplica la predeterminada como fallback). |
payment | Object | Sí | Datos del pago. |
invoice | Object | Sí | Datos de la factura/documento. |
operations[0].payment:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
amount | Entero | Sí | Importe en céntimos. 15075 = 150,75 EUR. Rango: 1–9.999.999. |
currency | String | Sí | ISO 4217. Siempre "EUR". |
types | Array | Sí | Siempre ["PSD2_PAYMENT"]. |
concept | String (max 140) | Sí | Concepto visible en la app bancaria del pagador al autorizar. |
psd2Payment | Object | Sí | Configuración del pago PSD2. |
debtor | Object | No | Datos del pagador. |
operations[0].payment.psd2Payment:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
type | String | No | "SINGLE_PAYMENT" (inmediato) o "FUTURE_PAYMENT" (fecha programada). |
product | String | No | "SEPA_CREDIT_TRANSFER" (1–2 días) o "INSTANT_SEPA_CREDIT_TRANSFER" (segundos). |
requestedExecutionDate | String YYYY-MM-DD | Solo con FUTURE_PAYMENT | Debe ser al menos 1 día hábil en el futuro. |
creditorAccount.uuid | UUID | No | Tu creditorAccountUuid. Si omites el bloque creditorAccount, se usa la cuenta beneficiaria predeterminada activa del business. Si lo informas con formato inválido (objeto vacío o uuid no UUID) → 400. La cuenta resuelta (explícita o automática) debe estar ACTIVE o se rechaza con 409. En la respuesta 201 Zertiban devuelve la cuenta finalmente resuelta en operations[].payment.psd2Payment.creditorAccount.uuid. |
operations[0].payment.debtor:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
name | String (max 100) | Sí, si incluyes debtor | Nombre del pagador. |
lastName | String (max 100) | Sí, si incluyes debtor | Apellido del pagador. |
email | String (max 255) | No | Formato email válido. |
phone | String (max 20) | No | Formato E.164 (ej. "+34600123456"). |
operations[0].invoice:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
externalId | String | Sí | El ID de la factura en tu sistema. |
generateDocument | Boolean | Sí | true: genera PDF con QR (o añade página PagaFactu a tu PDF). false: no genera ni devuelve documento. |
name | String | Sí con generateDocument: true | Nombre del documento. |
id | String | Solo si adjuntas PDF | Debe coincidir exactamente con el nombre del part multipart del fichero. |
dueDate | String YYYY-MM-DD | No | Fecha de vencimiento de la factura. |
collectedAmount | Object | No | Importe ya cobrado previamente. |
operations[0].invoice.collectedAmount:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
amount | Entero | Sí, si incluyes collectedAmount | En céntimos. |
currency | String | Sí, si incluyes collectedAmount | ISO 4217 (ej. "EUR"). |
Respuesta de creación (201 Created)
| Campo | Tipo | Descripción |
|---|---|---|
uuid | UUID | Identificador del flujo en Zertiban. |
createdAt | ISO 8601 | Fecha de creación. |
operations[0].uuid | UUID | Identificador de la operación. |
operations[0].url | URI | Enlace de pago para el cliente. |
operations[0].document | String Base64 | PDF generado. Solo presente si generateDocument: true. |
Endpoints comunes
Los endpoints satélite (consultar operación, listar, cancelar, historial, estadísticas, etc.) son comunes con ZertiPay y están documentados en Endpoints de negocio.
Errores frecuentes
| Código | Causa | Solución |
|---|---|---|
400 | Payload inválido (campo faltante, formato incorrecto, más de 1 operación, configuration o creditorAccount.uuid con formato no UUID) | Revisa los campos obligatorios en la sección de referencia |
401 | Token expirado o inválido | Solicita un nuevo token |
403 | x-tenant-id incorrecto o sin permisos | Verifica que el businessUuid es correcto |
404 | UUID de configuración o cuenta no encontrado | Verifícalos en el Dashboard |
409 | externalId duplicado | Usa un valor único por petición |
409 FLOW-SERVICE-CREDITOR-ACCOUNT-NOT-ACTIVE | La cuenta beneficiaria resuelta (explícita o por defecto) no está ACTIVE | Activa la cuenta en el Dashboard o usa otra ACTIVE |
409 FLOW-SERVICE-DEFAULT-CREDITOR-ACCOUNT-NOT-FOUND | No has informado creditorAccount y el business no tiene cuenta beneficiaria predeterminada activa | Marca una cuenta como predeterminada en el Dashboard o informa creditorAccount.uuid explícitamente |
409 (configuración deshabilitada) | El configuration informado existe pero está deshabilitado | Habilítalo en el Dashboard o usa otro configurationUuid |
409 (sin configuración por defecto) | Omitiste configuration y el business no tiene una configuración predeterminada activa | Marca una configuración como predeterminada o informa configuration explícitamente |
Checklist de integración PagaFactu
Configuración (una sola vez en el Dashboard)
- Negocio creado →
businessUuid - Configuración de flujo creada →
configurationUuid - Credenciales API creadas →
clientId+clientSecret - IBAN registrado por Zertiban →
creditorAccountUuid - Webhook registrado por Zertiban →
webhookSecret -
clientSecretywebhookSecretguardados en un gestor de secretos (no en el código)
Autenticación
- Token obtenido con Basic Auth (
clientId:clientSecreten el header, no en el body) - Token cacheado y renovado antes de expirar (no uno por petición)
- 401 dispara renovación + reintento una vez
Creación del cobro
- Primer 201 obtenido en Sandbox → tengo
uuid,operations[0].uuidyurl - Si
generateDocument:true→ tengo el PDF enoperations[0].document -
externalIdúnico por petición (uso mi ID de negocio, no un UUID de Zertiban) -
amountenviado en céntimos verificado con un caso límite (p.ej. 1, 9.999.999) - Variante de pago programada (
FUTURE_PAYMENT) probada si voy a usarla - Errores 4xx (400/403/404/409) se loguean con el código exacto y no se reintentan
Distribución al pagador
- Enlace abierto en modo incógnito y desde móvil
- Pago completado en Sandbox con un banco real (importe mínimo)
Webhooks y reconciliación
- Endpoint público accesible desde internet (no localhost) y siempre HTTPS
- Endpoint responde 2xx en menos de 5 s; el trabajo pesado va a una cola
- Firma HMAC verificada antes de procesar el body
- Deduplicación por
eventUuid(idempotencia a nivel de evento) - Factura actualizada en el ERP tras
OPERATION_COMPLETEDusando miexternalId - Eventos fuera de orden tolerados (no degradar de
COMPLETEDa procesando)
Escenarios negativos probados
- Operación cancelada por el pagador → estado y webhook correctos
- Operación caducada →
expiresAtrespetado - Webhook entregado dos veces → no se duplica el cobro
- 5xx de Zertiban → reintento con backoff exponencial; 4xx → sin reintento
- Reenvío manual de un webhook reproduce el mismo resultado
Observabilidad
- Logs incluyen
externalIdyoperationUuid, NO elaccess_tokenni elwebhookSecret - Alerta si no se reciben webhooks durante X minutos en horario laboral
- Métrica de % de cobros completados vs creados
Paso a producción
- Todo lo anterior verde en Sandbox
- Credenciales y URLs cambiadas a producción en el gestor de secretos
- Contactar con Zertiban para activación en Producción
Estado final
Una integración correcta de PagaFactu:
- No depende de UUIDs internos de Zertiban: usa tu
externalIdcomo clave de negocio para reconciliar. - Usa webhooks como fuente principal de verdad del estado de los cobros.
- Evita el polling en producción: resérvalo para arranque/pruebas o como red de seguridad.