Developer Docs
English
English
Diagram, step-by-step calls, and examples in multiple languages.
The WebApp and bank steps happen without your intervention. Your ERP only talks to the Client API (the arrows involving ZBN).
The endpoint uses multipart/form-data to allow attaching PDF documents in the same request. The structure is:
POST /flow/v1/flows
Content-Type: multipart/form-data; boundary=----boundary
------boundary
Content-Disposition: form-data; name="payload"
Content-Type: application/json ← required
{ "externalId": "...", "operations": [...] }
------boundary
Content-Disposition: form-data; name="doc-invoice"; filename="invoice.pdf"
Content-Type: application/pdf
<PDF binary>
------boundary--The payload part must carry Content-Type: application/json. If it doesn't, the server returns 400.
curl -X POST https://api-sandbox.zertiban.com/flow/v1/flows \
-H "Authorization: Bearer {access_token}" \
-H "x-tenant-id: {businessUuid}" \
-F 'payload={
"externalId": "COLLECTION-2026-001",
"countryCode": "ES",
"operations": [{
"id": "op-1",
"type": "PAYMENT",
"externalId": "OP-2026-001",
"configuration": "{configurationUuid}",
"payment": {
"amount": 15075,
"currency": "EUR",
"concept": "Collection 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": "COLLECTION-2026-001", "countryCode": "ES",
"operations": [{ "id": "op-1", "type": "PAYMENT", "externalId": "OP-2026-001",
"configuration": CONFIGURATION_UUID,
"payment": { "amount": 15075, "currency": "EUR", "concept": "Collection 2026-001",
"types": ["PSD2_PAYMENT"],
"psd2Payment": { "type": "SINGLE_PAYMENT", "product": "SEPA_CREDIT_TRANSFER",
"creditorAccount": {"uuid": CREDITOR_ACCOUNT_UUID} } } }]
}
response = requests.post(
"https://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://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();Response (201 Created):
{
"uuid": "28ffd216-5ee8-4e99-b1a9-511961e9c655",
"externalId": "COLLECTION-2026-001",
"operations": [
{
"uuid": "d1faff9e-...",
"externalId": "OP-2026-001",
"id": "op-1",
"url": "https://zertiban.com/{businessUuid}/{operationUuid}"
}
]
}
amountin cents:15075= 150.75 EUR. |externalIdappears in every webhook so you can reconcile without storing internal UUIDs. Current restriction: each flow contains a single payment operation. OpenAPI note: the service'sopenapi.yamlshowsSINGLE_PAYMENTSandFUTURE_PAYMENTS(plural) in some schemas. That's a typo. The correct values, defined in thePaymentTypeenum in code, areSINGLE_PAYMENTandFUTURE_PAYMENT(singular).
SEPA Instant:
"psd2Payment": { "type": "SINGLE_PAYMENT", "product": "INSTANT_SEPA_CREDIT_TRANSFER",
"creditorAccount": { "uuid": "{creditorAccountUuid}" } }Scheduled payment (1–89 days in the future):
"psd2Payment": { "type": "FUTURE_PAYMENT", "product": "SEPA_CREDIT_TRANSFER",
"requestedExecutionDate": "2026-05-15", "creditorAccount": { "uuid": "{creditorAccountUuid}" } }With attached PDF:
-F 'payload={ "documents":[{"id":"invoice","name":"invoice.pdf"}],
"operations":[{ "payment":{ "documents":[{"documentId":"invoice"}] } }]
};type=application/json' \
-F 'invoice=@./invoice.pdf;type=application/pdf'The file's part name must exactly match documents[].id.
Distribute the url through any channel. The payer opens the link, picks their bank and authorizes the transfer. If you configured redirection.callback.url, their browser will be redirected automatically when the operation reaches a final state.
If you'd rather not use webhooks for now, you can poll the status directly:
curl https://api-sandbox.zertiban.com/flow/v1/operations/{operationUuid}/status \
-H "Authorization: Bearer {access_token}" -H "x-tenant-id: {businessUuid}"POST /flow/v1/flows Content-Type: multipart/form-data.
Payload root:
| Field | Type | Req. | Description |
|---|---|---|---|
externalId | String | No | Your flow ID |
countryCode | String | Yes | ISO 3166 (e.g. "ES") |
additionalLanguage | String | No | ISO 639-1 |
operations | Array | Yes | 1 operation in ZertiPay |
documents | Array | No | Attached PDFs (up to 10) |
labels | Array | No | Key-value labels |
operations[i]:
| Field | Type | Req. | Description |
|---|---|---|---|
id | String | Yes | Local request ID (@NotBlank) |
type | String | Yes | "PAYMENT" |
externalId | String | No | Your operation ID |
configuration | UUID | Yes | Your configurationUuid |
payment | Object | Yes | Payment data |
payment:
| Field | Type | Req. | Description |
|---|---|---|---|
amount | Long | Yes | Positive, in cents |
currency | String | Yes | ISO 4217 (e.g. "EUR") |
concept | String | Yes | Shown in the payer's banking app |
types | Array<String> | Yes | Always ["PSD2_PAYMENT"] |
psd2Payment | Object | Yes | PSD2 configuration |
documents | Array | No | References to documents[].id |
debtor | Object | No | Payer data (name, last name, email, phone) |
psd2Payment:
| Field | Type | Req. | Description |
|---|---|---|---|
type | String | Yes | "SINGLE_PAYMENT" or "FUTURE_PAYMENT" |
product | String | Yes | "SEPA_CREDIT_TRANSFER" or "INSTANT_SEPA_CREDIT_TRANSFER" |
requestedExecutionDate | YYYY-MM-DD | Only FUTURE_PAYMENT | 1–89 days in the future |
creditorAccount.uuid | UUID | Yes | Your creditorAccountUuid |
| Field | Description |
|---|---|
uuid | Flow UUID |
externalId | Your externalId |
operations[i].uuid | Operation UUID |
operations[i].externalId | Your operation externalId |
operations[i].id | The local id you sent |
operations[i].url | Payment link for the payer |
The satellite endpoints (quick status, detail, listing, cancellation, expiration extension, document download, history, statistics and PSD2 payment detail) are shared with PagaFactu and are documented in Business endpoints.
| Code | Typical cause | Solution |
|---|---|---|
400 | Invalid payload: missing field, wrong types (use PSD2_PAYMENT, not SINGLE_PAYMENTS), requestedExecutionDate out of 1–89 day range, non-positive amount, duration without D designator. | Review the validation in the reference section. |
401 | Expired token or invalid credentials. | New token with Basic Auth (not in the body). |
403 | Wrong x-tenant-id or insufficient credential permissions. | Verify businessUuid and credential permissions. |
404 | Configuration, account, operation, flow or payment UUID not found. | Verify with the GET endpoints. |
409 | Cancel already-finished operation, or operation with payment in progress. | Check the status first. |
Setup (one-off in the Dashboard)
businessUuidconfigurationUuidredirection.return.url and redirection.callback.url set if applicableclientId + 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 urlexternalId 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
callback.url receives the redirection on final state (if you configured it)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 ZertiPay integration:
externalId as the business key for reconciliation.