Skip to content
Developer Docs

PagaFactu Implementation

Diagram, step-by-step calls, and examples in multiple languages.

Sequence diagram

Create a collection

A single multipart/form-data call. You get the payment URL and the PDF back.

Endpoint:

MethodURL
POSThttps://nc-api-sandbox.zertiban.com/pagafactu/v1/flows/pagafactu

Headers:

HeaderValue
AuthorizationBearer {access_token}
x-tenant-id{businessUuid}

Replace the values between braces {} with your own:

shell
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": "INVOICE-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": "Invoice 2026-001",
        "debtor": {
          "name": "Ana",
          "lastName": "Martinez",
          "email": "[email protected]"
        }
      },
      "invoice": {
        "externalId": "INV-2026-001",
        "name": "Invoice 2026-001",
        "generateDocument": true
      }
    }]
  };type=application/json'

amount is in cents: 15075 = 150.75 EUR. externalId is your internal identifier: it appears in webhooks and queries, letting you reconcile without storing Zertiban UUIDs.

Response (201 Created):

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

Persist in your database:

FieldPurpose
operations[0].uuidStatus queries and webhook reconciliation.
operations[0].urlPayment link to send to the customer.
operations[0].documentBase64 PDF (only present if generateDocument: true). Decode: echo "JVBERi0..." | base64 -d > invoice.pdf.

Creation variants

1. Zertiban-generated PDF with QR (default)

This is the scenario in the example above: "generateDocument": true with a name. Zertiban generates a new PDF with the payment QR and returns it in operations[0].document (Base64).

2. Without document

If you don't need a document, set "generateDocument": false:

json
"invoice": {
  "externalId": "INV-2026-001",
  "generateDocument": false
}

The response won't include the document field. The name field in invoice is not required.

3. Your own PDF + Zertiban-appended page

If you want to attach your own PDF, keep "generateDocument": true, add "id": "my-invoice" in invoice and upload the file as a second multipart part. The part name must exactly match the value of invoice.id:

shell
-F '[email protected];type=application/pdf'

Zertiban appends a PagaFactu page to the end of your PDF (it does not replace it) with:

  • Payment QR
  • Direct link to the operation

Maximum PDF file size: 8 MB.

If you combine generateDocument: false with an attached file, the file is silently ignored and no document is returned.

4. Scheduled payment

If you want the payment to execute on a future date, change psd2Payment.type to "FUTURE_PAYMENT" and add the date:

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

The date must be at least 1 business day in the future. Only available with SEPA_CREDIT_TRANSFER.

Zertiban does not send communications to the payer. Delivering the link and/or document is your ERP's responsibility.

With the url and document from the previous response you have everything you need:

  • Send the url by email, WhatsApp or SMS.
  • Attach the decoded PDF if you want the customer to have the document.

The customer can access the collection in two ways:

  • Opening the direct link.
  • Scanning the QR on the page appended to the PDF (variant 1 or 3).

In either case, the customer picks their bank and authorizes the transfer with their online banking (PSD2). They don't need to install anything.

Alternative: polling

If you'd rather not use webhooks for now, you can poll the status directly:

shell
# Status of an operation
curl https://nc-api-sandbox.zertiban.com/flow/v1/operations/{operationUuid} \
  -H "Authorization: Bearer {access_token}" \
  -H "x-tenant-id: {businessUuid}"

Response (key fields):

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

Field reference

POST/pagafactu/v1/flows/pagafactu, Content-Type: multipart/form-data

The body is a part named payload with type=application/json.

Root:

FieldTypeRequiredDescription
externalIdStringNoYour ID for this collection. Appears in webhooks and queries. If reused → 409 Conflict.
countryCodeStringNoISO 3166 alpha-2 code (e.g. "ES").
operationsArrayYesExactly 1 element for PagaFactu.

operations[0]:

FieldTypeRequiredDescription
externalIdString (max 50)NoYour ID for this operation. Appears in resource.externalId in webhooks.
configurationUUIDNoYour configurationUuid. If omitted, the business's active default configuration is used. If provided with a non-UUID format → 400. If the provided configuration is disabled → 409 (the default is not applied as fallback).
paymentObjectYesPayment data.
invoiceObjectYesInvoice/document data.

operations[0].payment:

FieldTypeRequiredDescription
amountIntegerYesAmount in cents. 15075 = 150.75 EUR. Range: 1–9,999,999.
currencyStringYesISO 4217. Always "EUR".
typesArrayYesAlways ["PSD2_PAYMENT"].
conceptString (max 140)YesDescription shown in the payer's banking app when authorizing.
psd2PaymentObjectYesPSD2 payment configuration.
debtorObjectNoPayer data.

operations[0].payment.psd2Payment:

FieldTypeRequiredDescription
typeStringNo"SINGLE_PAYMENT" (immediate) or "FUTURE_PAYMENT" (scheduled).
productStringNo"SEPA_CREDIT_TRANSFER" (1–2 days) or "INSTANT_SEPA_CREDIT_TRANSFER".
requestedExecutionDateString YYYY-MM-DDOnly with FUTURE_PAYMENTMust be at least 1 business day in the future.
creditorAccount.uuidUUIDNoYour creditorAccountUuid. If you omit the creditorAccount block, the business's active default creditor account is used. If provided with an invalid format (empty object or non-UUID uuid) → 400. The resolved account (explicit or automatic) must be ACTIVE or it is rejected with 409. In the 201 response, Zertiban returns the finally resolved account in operations[].payment.psd2Payment.creditorAccount.uuid.

operations[0].payment.debtor:

FieldTypeRequiredDescription
nameString (max 100)Yes, if debtor includedPayer's first name.
lastNameString (max 100)Yes, if debtor includedPayer's last name.
emailString (max 255)NoValid email format.
phoneString (max 20)NoE.164 format (e.g. "+34600123456").

operations[0].invoice:

FieldTypeRequiredDescription
externalIdStringYesThe invoice ID in your system.
generateDocumentBooleanYestrue: generates a PDF with QR (or appends a PagaFactu page to your PDF). false: no document generated.
nameStringYes with generateDocument: trueDocument name.
idStringOnly if attaching a PDFMust exactly match the multipart part name of the file.
dueDateString YYYY-MM-DDNoInvoice due date.
collectedAmountObjectNoAmount already collected previously.

operations[0].invoice.collectedAmount:

FieldTypeRequiredDescription
amountIntegerYes, if collectedAmount presentIn cents.
currencyStringYes, if collectedAmount presentISO 4217 (e.g. "EUR").

Creation response (201 Created)

FieldTypeDescription
uuidUUIDFlow identifier in Zertiban.
createdAtISO 8601Creation timestamp.
operations[0].uuidUUIDOperation identifier.
operations[0].urlURIPayment link for the customer.
operations[0].documentBase64 StringGenerated PDF. Only present if generateDocument: true.

Common endpoints

The satellite endpoints (get operation, list, cancel, history, statistics, etc.) are shared with ZertiPay and are documented in Business endpoints.

Common errors

CodeCauseSolution
400Invalid payload (missing field, wrong format, more than 1 operation, configuration or creditorAccount.uuid with non-UUID format)Review the required fields in the reference section.
401Expired or invalid tokenRequest a new token.
403Wrong x-tenant-id or missing permissionsVerify the businessUuid is correct.
404Configuration or account UUID not foundVerify them in the Dashboard.
409Duplicate externalIdUse a unique value per request.
409 FLOW-SERVICE-CREDITOR-ACCOUNT-NOT-ACTIVEThe resolved creditor account (explicit or default) is not ACTIVEActivate the account in the Dashboard or use another ACTIVE account.
409 FLOW-SERVICE-DEFAULT-CREDITOR-ACCOUNT-NOT-FOUNDYou did not provide creditorAccount and the business has no active default creditor accountMark an account as default in the Dashboard or provide creditorAccount.uuid explicitly.
409 (disabled configuration)The provided configuration exists but is disabledEnable it in the Dashboard or use another configurationUuid.
409 (no default configuration)You omitted configuration and the business has no active default configurationMark a configuration as default or provide configuration explicitly.

PagaFactu integration checklist

Setup (one-off in the Dashboard)

  • Business created → businessUuid
  • Flow configuration created → configurationUuid
  • API credentials created → clientId + clientSecret
  • IBAN registered by Zertiban → creditorAccountUuid
  • Webhook registered by Zertiban → webhookSecret
  • clientSecret and webhookSecret stored in a secrets manager (not in code)

Authentication

  • Token obtained with Basic Auth (clientId:clientSecret in the header, not in the body)
  • Token cached and refreshed before expiry (not one per request)
  • 401 triggers a refresh + single retry

Collection creation

  • First 201 obtained in Sandbox → got uuid, operations[0].uuid and url
  • If generateDocument:true → got the PDF in operations[0].document
  • externalId unique per request (using my own business ID, not a Zertiban UUID)
  • amount sent in cents verified with an edge case (e.g. 1, 9,999,999)
  • Scheduled-payment variant (FUTURE_PAYMENT) tested if you plan to use it
  • 4xx errors (400/403/404/409) logged with the exact code and not retried

Payer distribution

  • Link opened in private browsing and on mobile
  • Payment completed in Sandbox against a real bank (minimum amount)

Webhooks and reconciliation

  • Endpoint publicly reachable from the internet (not localhost) and always HTTPS
  • Endpoint responds 2xx in under 5 s; heavy work goes to a queue
  • HMAC signature verified before processing the body
  • Deduplication by eventUuid (event-level idempotency)
  • Invoice updated in the ERP after OPERATION_COMPLETED using my externalId
  • Out-of-order events tolerated (don't downgrade COMPLETED back to in-progress)

Negative scenarios tested

  • Operation cancelled by the payer → status and webhook correct
  • Operation expired → expiresAt honoured
  • Webhook delivered twice → collection isn't duplicated
  • Zertiban 5xx → retry with exponential backoff; 4xx → no retry
  • Manual webhook redelivery produces the same result

Observability

  • Logs include externalId and operationUuid, NOT the access_token or webhookSecret
  • Alert if no webhooks are received for X minutes during business hours
  • Metric of % collections completed vs created

Promotion to production

  • Everything above green in Sandbox
  • Credentials and URLs switched to production in the secrets manager
  • Contact Zertiban for Production activation

End state

A correct PagaFactu integration:

  • Doesn't depend on Zertiban's internal UUIDs: uses your externalId as the business key for reconciliation.
  • Uses webhooks as the primary source of truth for collection state.
  • Avoids polling in production: keep it for bootstrap/testing or as a safety net.