Implementación ZertiPay
Diagrama, llamadas paso a paso y ejemplos en múltiples lenguajes.
Diagrama E2E
Los pasos de la WebApp y del banco ocurren sin tu intervención. Tu ERP solo interactúa con la API Cliente (flechas con ZBN).
Crear un cobro
El endpoint usa multipart/form-data para permitir adjuntar documentos PDF en la misma petición. La estructura es:
POST /flow/v1/flows
Content-Type: multipart/form-data; boundary=----boundary
------boundary
Content-Disposition: form-data; name="payload"
Content-Type: application/json ← obligatorio
{ "externalId": "...", "operations": [...] }
------boundary
Content-Disposition: form-data; name="doc-factura"; filename="factura.pdf"
Content-Type: application/pdf
<binario PDF>
------boundary--**El part **payload debe llevar Content-Type: application/json. Si no, el servidor devuelve 400.
curl -X POST https://nc-api-sandbox.zertiban.com/flow/v1/flows \
-H "Authorization: Bearer {access_token}" \
-H "x-tenant-id: {businessUuid}" \
-F 'payload={
"externalId": "COBRO-2026-001",
"countryCode": "ES",
"operations": [{
"id": "op-1",
"type": "PAYMENT",
"externalId": "OP-2026-001",
"configuration": "{configurationUuid}",
"payment": {
"amount": 15075,
"currency": "EUR",
"concept": "Cobro 2026-001",
"types": ["PSD2_PAYMENT"],
"psd2Payment": {
"type": "SINGLE_PAYMENT",
"product": "SEPA_CREDIT_TRANSFER",
"creditorAccount": { "uuid": "{creditorAccountUuid}" }
},
"debtor": { "name": "Ana", "lastName": "Martinez", "email": "[email protected]" }
}
}]
};type=application/json'import requests, json
payload = {
"externalId": "COBRO-2026-001", "countryCode": "ES",
"operations": [{ "id": "op-1", "type": "PAYMENT", "externalId": "OP-2026-001",
"configuration": CONFIGURATION_UUID,
"payment": { "amount": 15075, "currency": "EUR", "concept": "Cobro 2026-001",
"types": ["PSD2_PAYMENT"],
"psd2Payment": { "type": "SINGLE_PAYMENT", "product": "SEPA_CREDIT_TRANSFER",
"creditorAccount": {"uuid": CREDITOR_ACCOUNT_UUID} } } }]
}
response = requests.post(
"https://nc-api-sandbox.zertiban.com/flow/v1/flows",
headers={"Authorization": f"Bearer {access_token}", "x-tenant-id": BUSINESS_UUID},
files={"payload": (None, json.dumps(payload), "application/json")}
)
data = response.json()
payment_url = data["operations"][0]["url"]const FormData = require('form-data');
const form = new FormData();
form.append('payload', JSON.stringify(payload), { contentType: 'application/json' });
const response = await axios.post('https://nc-api-sandbox.zertiban.com/flow/v1/flows', form, {
headers: { Authorization: `Bearer ${accessToken}`, 'x-tenant-id': BUSINESS_UUID, ...form.getHeaders() }
});
const paymentUrl = response.data.operations[0].url;MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("payload", objectMapper.writeValueAsString(request)).contentType(MediaType.APPLICATION_JSON);
FlowCreatedResponse result = webClient.post().uri("/flow/v1/flows")
.header("Authorization", "Bearer " + accessToken).header("x-tenant-id", businessUuid)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(builder.build()))
.retrieve().bodyToMono(FlowCreatedResponse.class).block();Respuesta (201 Created):
{
"uuid": "28ffd216-5ee8-4e99-b1a9-511961e9c655",
"externalId": "COBRO-2026-001",
"operations": [
{
"uuid": "d1faff9e-...",
"externalId": "OP-2026-001",
"id": "op-1",
"url": "https://zertiban.com/{businessUuid}/{operationUuid}"
}
]
}
amounten céntimos:15075= 150,75 EUR. |externalIdaparece en todos los webhooks para reconciliar sin guardar UUIDs internos. Restricción actual: cada flujo contiene una sola operación de pago. Nota sobre el OpenAPI: el ficheroopenapi.yamldel servicio muestraSINGLE_PAYMENTSyFUTURE_PAYMENTS(en plural) en algunos esquemas. Es una errata. Los valores correctos, definidos en el enumPaymentTypedel código, sonSINGLE_PAYMENTyFUTURE_PAYMENT(singular).
Variantes de creación
SEPA Instant:
"psd2Payment": { "type": "SINGLE_PAYMENT", "product": "INSTANT_SEPA_CREDIT_TRANSFER",
"creditorAccount": { "uuid": "{creditorAccountUuid}" } }Pago programado (1–89 días en el futuro):
"psd2Payment": { "type": "FUTURE_PAYMENT", "product": "SEPA_CREDIT_TRANSFER",
"requestedExecutionDate": "2026-05-15", "creditorAccount": { "uuid": "{creditorAccountUuid}" } }Con PDF adjunto:
-F 'payload={ "documents":[{"id":"factura","name":"factura.pdf"}],
"operations":[{ "payment":{ "documents":[{"documentId":"factura"}] } }]
};type=application/json' \
-F 'factura=@./factura.pdf;type=application/pdf'El nombre del part del fichero debe coincidir exactamente con documents[].id.
Enviar el enlace al pagador
Distribuye la url por cualquier canal. El pagador abre el enlace, selecciona su banco y autoriza la transferencia. Si configuraste redirection.callback.url, su navegador será redirigido automáticamente cuando la operación alcance un estado final.
Alternativa: consulta por polling
Si prefieres no usar webhooks de momento, puedes consultar el estado directamente:
curl https://nc-api-sandbox.zertiban.com/flow/v1/operations/{operationUuid}/status \
-H "Authorization: Bearer {access_token}" -H "x-tenant-id: {businessUuid}"Referencia de campos POST /flow/v1/flows
Content-Type: multipart/form-data.
Raíz del payload:
| Campo | Tipo | Oblig. | Descripción |
|---|---|---|---|
externalId | String | No | Tu ID del flujo |
countryCode | String | Sí | ISO 3166 (ej. "ES") |
additionalLanguage | String | No | ISO 639-1 |
operations | Array | Sí | 1 operación en Zertipay |
documents | Array | No | PDFs adjuntos (hasta 10) |
labels | Array | No | Etiquetas clave-valor |
operations[i]:
| Campo | Tipo | Oblig. | Descripción |
|---|---|---|---|
id | String | Sí | ID local del request (@NotBlank) |
type | String | Sí | "PAYMENT" |
externalId | String | No | Tu ID de la operación |
configuration | UUID | Sí | Tu configurationUuid |
payment | Object | Sí | Datos del pago |
payment:
| Campo | Tipo | Oblig. | Descripción |
|---|---|---|---|
amount | Long | Sí | Positivo, en céntimos |
currency | String | Sí | ISO 4217 (ej. "EUR") |
concept | String | Sí | Visible en la app bancaria del pagador |
types | Array<String> | Sí | Siempre ["PSD2_PAYMENT"] |
psd2Payment | Object | Sí | Configuración PSD2 |
documents | Array | No | Referencias a documents[].id |
debtor | Object | No | Datos del pagador (nombre, apellido, email, teléfono) |
psd2Payment:
| Campo | Tipo | Oblig. | Descripción |
|---|---|---|---|
type | String | Sí | "SINGLE_PAYMENT" o "FUTURE_PAYMENT" |
product | String | Sí | "SEPA_CREDIT_TRANSFER" o "INSTANT_SEPA_CREDIT_TRANSFER" |
requestedExecutionDate | YYYY-MM-DD | Solo FUTURE_PAYMENT | 1–89 días en el futuro |
creditorAccount.uuid | UUID | Sí | Tu creditorAccountUuid |
Respuesta de creación (201)
| Campo | Descripción |
|---|---|
uuid | UUID del flujo |
externalId | Tu externalId |
operations[i].uuid | UUID de la operación |
operations[i].externalId | Tu externalId de la operación |
operations[i].id | El id local que enviaste |
operations[i].url | Enlace de pago para el pagador |
Endpoints comunes
Los endpoints satélite (consulta rápida, detalle, listado, cancelación, ampliación de caducidad, descarga de documentos, historial, estadísticas y detalle del pago PSD2) son comunes con PagaFactu y están documentados en Endpoints de negocio.
Errores frecuentes
| Código | Causa típica | Solución |
|---|---|---|
400 | Payload inválido: campo faltante, types incorrecto (usa PSD2_PAYMENT no SINGLE_PAYMENTS), requestedExecutionDate fuera de rango 1–89 días, amount no positivo, duration sin designador D | Revisa la validación en la sección de referencia |
401 | Token expirado o credenciales inválidas | Nuevo token con Basic Auth (no en el body) |
403 | x-tenant-id incorrecto o permisos de credencial insuficientes | Verifica businessUuid y los permisos de las credenciales |
404 | UUID de configuración, cuenta, operación, flujo o pago no encontrado | Verifica con los endpoints GET |
409 | Cancelar operación ya finalizada, o con pago en curso | Consulta el estado antes |
Checklist de integración ZertiPay
Configuración (una sola vez en el Dashboard)
- Negocio creado →
businessUuid - Configuración de flujo creada →
configurationUuid-
redirection.return.urlyredirection.callback.urlconfiguradas si aplica
-
- 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 -
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)
-
callback.urlrecibe la redirección al estado final (si la configuraste)
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) - Cobro actualizado 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 ZertiPay:
- 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.