Saltar al contenido
Developer Docs

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.

shell
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'
python
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"]
javascript
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;
java
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):

json
{
  "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}"
    }
  ]
}

amount en céntimos: 15075 = 150,75 EUR. | externalId aparece 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 fichero openapi.yaml del servicio muestra SINGLE_PAYMENTS y FUTURE_PAYMENTS (en plural) en algunos esquemas. Es una errata. Los valores correctos, definidos en el enum PaymentType del código, son SINGLE_PAYMENT y FUTURE_PAYMENT (singular).

Variantes de creación

SEPA Instant:

json
"psd2Payment": { "type": "SINGLE_PAYMENT", "product": "INSTANT_SEPA_CREDIT_TRANSFER",
  "creditorAccount": { "uuid": "{creditorAccountUuid}" } }

Pago programado (1–89 días en el futuro):

json
"psd2Payment": { "type": "FUTURE_PAYMENT", "product": "SEPA_CREDIT_TRANSFER",
  "requestedExecutionDate": "2026-05-15", "creditorAccount": { "uuid": "{creditorAccountUuid}" } }

Con PDF adjunto:

shell
-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:

shell
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:

CampoTipoOblig.Descripción
externalIdStringNoTu ID del flujo
countryCodeStringISO 3166 (ej. "ES")
additionalLanguageStringNoISO 639-1
operationsArray1 operación en Zertipay
documentsArrayNoPDFs adjuntos (hasta 10)
labelsArrayNoEtiquetas clave-valor

operations[i]:

CampoTipoOblig.Descripción
idStringID local del request (@NotBlank)
typeString"PAYMENT"
externalIdStringNoTu ID de la operación
configurationUUIDTu configurationUuid
paymentObjectDatos del pago

payment:

CampoTipoOblig.Descripción
amountLongPositivo, en céntimos
currencyStringISO 4217 (ej. "EUR")
conceptStringVisible en la app bancaria del pagador
typesArray<String>Siempre ["PSD2_PAYMENT"]
psd2PaymentObjectConfiguración PSD2
documentsArrayNoReferencias a documents[].id
debtorObjectNoDatos del pagador (nombre, apellido, email, teléfono)

psd2Payment:

CampoTipoOblig.Descripción
typeString"SINGLE_PAYMENT" o "FUTURE_PAYMENT"
productString"SEPA_CREDIT_TRANSFER" o "INSTANT_SEPA_CREDIT_TRANSFER"
requestedExecutionDateYYYY-MM-DDSolo FUTURE_PAYMENT1–89 días en el futuro
creditorAccount.uuidUUIDTu creditorAccountUuid

Respuesta de creación (201)

CampoDescripción
uuidUUID del flujo
externalIdTu externalId
operations[i].uuidUUID de la operación
operations[i].externalIdTu externalId de la operación
operations[i].idEl id local que enviaste
operations[i].urlEnlace 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ódigoCausa típicaSolución
400Payload 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 DRevisa la validación en la sección de referencia
401Token expirado o credenciales inválidasNuevo token con Basic Auth (no en el body)
403x-tenant-id incorrecto o permisos de credencial insuficientesVerifica businessUuid y los permisos de las credenciales
404UUID de configuración, cuenta, operación, flujo o pago no encontradoVerifica con los endpoints GET
409Cancelar operación ya finalizada, o con pago en cursoConsulta 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.url y redirection.callback.url configuradas si aplica
  • Credenciales API creadas → clientId + clientSecret
  • IBAN registrado por Zertiban → creditorAccountUuid
  • Webhook registrado por Zertiban → webhookSecret
  • clientSecret y webhookSecret guardados en un gestor de secretos (no en el código)

Autenticación

  • Token obtenido con Basic Auth (clientId:clientSecret en 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].uuid y url
  • externalId único por petición (uso mi ID de negocio, no un UUID de Zertiban)
  • amount enviado 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.url recibe 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_COMPLETED usando mi externalId
  • Eventos fuera de orden tolerados (no degradar de COMPLETED a procesando)

Escenarios negativos probados

  • Operación cancelada por el pagador → estado y webhook correctos
  • Operación caducada → expiresAt respetado
  • 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 externalId y operationUuid, NO el access_token ni el webhookSecret
  • 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 externalId como 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.