Developer Docs
English
English
Diagram, step-by-step calls, and examples in multiple languages.
A single multipart/form-data call. You get the payment URL and the PDF back.
Endpoint:
| Method | URL |
|---|---|
POST | https://api-sandbox.zertiban.com/flow/v1/flows/pagafactu |
Headers:
| Header | Value |
|---|---|
Authorization | Bearer {access_token} |
x-tenant-id | {businessUuid} |
Replace the values between braces {} with your own:
curl -X POST https://api-sandbox.zertiban.com/flow/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'
amountis in cents:15075= 150.75 EUR.externalIdis your internal identifier: it appears in webhooks and queries, letting you reconcile without storing Zertiban UUIDs.
Response (201 Created):
{
"uuid": "28ffd216-5ee8-4e99-b1a9-511961e9c655",
"createdAt": "2026-04-01T09:15:00Z",
"operations": [
{
"uuid": "b7aa09fe-5678-1234-90ab-cdef98765432",
"url": "https://api-sandbox.zertiban.com/link-validator/v1/links/eyJhbGci...",
"document": "JVBERi0xLjQK..."
}
]
}Persist in your database:
| Field | Purpose |
|---|---|
operations[0].uuid | Status queries and webhook reconciliation. |
operations[0].url | Payment link to send to the customer. |
operations[0].document | Base64 PDF (only present if generateDocument: true). Decode: echo "JVBERi0..." | base64 -d > invoice.pdf. |
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).
If you don't need a document, set "generateDocument": false:
"invoice": {
"externalId": "INV-2026-001",
"generateDocument": false
}The response won't include the document field. The name field in invoice is not required.
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:
-F '[email protected];type=application/pdf'Zertiban appends a PagaFactu page to the end of your PDF (it does not replace it) with:
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.
If you want the payment to execute on a future date, change psd2Payment.type to "FUTURE_PAYMENT" and add the date:
"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:
url by email, WhatsApp or SMS.The customer can access the collection in two ways:
In either case, the customer picks their bank and authorizes the transfer with their online banking (PSD2). They don't need to install anything.
If you'd rather not use webhooks for now, you can poll the status directly:
# Status of an operation
curl https://api-sandbox.zertiban.com/flow/v1/operations/{operationUuid} \
-H "Authorization: Bearer {access_token}" \
-H "x-tenant-id: {businessUuid}"Response (key fields):
{
"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
}
}POST/flow/v1/flows/pagafactu, Content-Type: multipart/form-data
The body is a part named payload with type=application/json.
Root:
| Field | Type | Required | Description |
|---|---|---|---|
externalId | String | No | Your ID for this collection. Appears in webhooks and queries. If reused → 409 Conflict. |
countryCode | String | No | ISO 3166 alpha-2 code (e.g. "ES"). |
operations | Array | Yes | Exactly 1 element for PagaFactu. |
operations[0]:
| Field | Type | Required | Description |
|---|---|---|---|
externalId | String (max 50) | No | Your ID for this operation. Appears in resource.externalId in webhooks. |
configuration | UUID | No | Your configurationUuid. If omitted, the organisation'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). |
payment | Object | Yes | Payment data. |
invoice | Object | Yes | Invoice/document data. |
operations[0].payment:
| Field | Type | Required | Description |
|---|---|---|---|
amount | Integer | Yes | Amount in cents. 15075 = 150.75 EUR. Range: 1–9,999,999. |
currency | String | Yes | ISO 4217. Always "EUR". |
types | Array | Yes | Always ["PSD2_PAYMENT"]. |
concept | String (max 140) | Yes | Description shown in the payer's banking app when authorizing. |
psd2Payment | Object | Yes | PSD2 payment configuration. |
debtor | Object | No | Payer data. |
operations[0].payment.psd2Payment:
| Field | Type | Required | Description |
|---|---|---|---|
type | String | No | "SINGLE_PAYMENT" (immediate) or "FUTURE_PAYMENT" (scheduled). |
product | String | No | "SEPA_CREDIT_TRANSFER" (1–2 days) or "INSTANT_SEPA_CREDIT_TRANSFER". |
requestedExecutionDate | String YYYY-MM-DD | Only with FUTURE_PAYMENT | Must be at least 1 business day in the future. |
creditorAccount.uuid | UUID | No | Your creditorAccountUuid. If you omit the creditorAccount block, the organisation'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:
| Field | Type | Required | Description |
|---|---|---|---|
name | String (max 100) | Yes, if debtor included | Payer's first name. |
lastName | String (max 100) | Yes, if debtor included | Payer's last name. |
email | String (max 255) | No | Valid email format. |
phone | String (max 20) | No | E.164 format (e.g. "+34600123456"). |
operations[0].invoice:
| Field | Type | Required | Description |
|---|---|---|---|
externalId | String | Yes | The invoice ID in your system. |
generateDocument | Boolean | Yes | true: generates a PDF with QR (or appends a PagaFactu page to your PDF). false: no document generated. |
name | String | Yes with generateDocument: true | Document name. |
id | String | Only if attaching a PDF | Must exactly match the multipart part name of the file. |
dueDate | String YYYY-MM-DD | No | Invoice due date. |
collectedAmount | Object | No | Amount already collected previously. |
operations[0].invoice.collectedAmount:
| Field | Type | Required | Description |
|---|---|---|---|
amount | Integer | Yes, if collectedAmount present | In cents. |
currency | String | Yes, if collectedAmount present | ISO 4217 (e.g. "EUR"). |
| Field | Type | Description |
|---|---|---|
uuid | UUID | Flow identifier in Zertiban. |
createdAt | ISO 8601 | Creation timestamp. |
operations[0].uuid | UUID | Operation identifier. |
operations[0].url | URI | Payment link for the customer. |
operations[0].document | Base64 String | Generated PDF. Only present if generateDocument: true. |
The satellite endpoints (get operation, list, cancel, history, statistics, etc.) are shared with ZertiPay and are documented in Business endpoints.
| Code | Cause | Solution |
|---|---|---|
400 | Invalid 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. |
401 | Expired or invalid token | Request a new token. |
403 | Wrong x-tenant-id or missing permissions | Verify the businessUuid is correct. |
404 | Configuration or account UUID not found | Verify them in the Dashboard. |
409 | Duplicate externalId | Use a unique value per request. |
409 FLOW-SERVICE-CREDITOR-ACCOUNT-NOT-ACTIVE | The resolved creditor account (explicit or default) is not ACTIVE | Activate the account in the Dashboard or use another ACTIVE account. |
409 FLOW-SERVICE-DEFAULT-CREDITOR-ACCOUNT-NOT-FOUND | You did not provide creditorAccount and the organisation has no active default creditor account | Mark an account as default in the Dashboard or provide creditorAccount.uuid explicitly. |
409 (disabled configuration) | The provided configuration exists but is disabled | Enable it in the Dashboard or use another configurationUuid. |
409 (no default configuration) | You omitted configuration and the organisation has no active default configuration | Mark a configuration as default or provide configuration explicitly. |
Setup (one-off in the Dashboard)
businessUuidconfigurationUuidclientId + clientSecretcreditorAccountUuidwebhookSecretclientSecret and webhookSecret stored in a secrets manager (not in code)Authentication
clientId:clientSecret in the header, not in the body)Collection creation
uuid, operations[0].uuid and urlgenerateDocument:true → got the PDF in operations[0].documentexternalId 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)FUTURE_PAYMENT) tested if you plan to use itPayer distribution
Webhooks and reconciliation
eventUuid (event-level idempotency)OPERATION_COMPLETED using my externalIdCOMPLETED back to in-progress)Negative scenarios tested
expiresAt honouredObservability
externalId and operationUuid, NOT the access_token or webhookSecretPromotion to production
A correct PagaFactu integration:
externalId as the business key for reconciliation.