Collect Payment

Given a user’s online banking credentials, the Collect Payment Service allows to let users initiate pre-defined payments.

Do not use this service to initiate user-defined payments.
sequenceDiagram actor User participant YAXI participant Your Backend participant Bank User->>+Your Backend: Ticket request Your Backend-->>User: Ticket with payment details (ID, customer's bank, amount, recipient) rect rgb(243, 243, 243) note right of User: Encrypted and verified channel User->>+YAXI: Ticket YAXI->>+YAXI: Verify authenticity YAXI-->>-User: Bank login form User->>+YAXI: Send login credentials end YAXI->>+Bank: Initiate payment opt Strong Customer Authentication Bank-->>-User: Request authorization User->>+Bank: Authorize end Bank-->>-YAXI: OK YAXI-->>-User: OK User->>+Your Backend: OK Your Backend->>+Your Backend: Verify authenticity Your Backend-->>-User: OK

Issue a service ticket backend

Use CollectPayment as a service identifier when issuing tickets. The service expects the following ticket data:

Restrictions on remittance and creditor name:

  • Some banks require a minimum remittance of a few characters.

  • Long texts may be truncated, typically with a limit of 140 characters for remittance and 70 for the creditor name.

  • Providers accept different character sets for the creditor name and remittance. The safe baseline is:

    abcdefghijklmnopqrstuvwxyz
    ABCDEFGHIJKLMNOPQRSTUVWXYZ
    0123456789
    /-?:().,' +
    Space

    Other characters may be accepted, rejected, stripped or replaced.

The service officially only supports payments in EUR to creditor accounts within the eurozone. Payments with other currencies and receivers (within the Single Euro Payments Area) should generally work, though. Use them at your own risk, but feel free to report any issues. Also do not hesitate to contact us to discuss specific requirements.

Issue a service ticket:

  • Python

  • Java

  • Node.js

  • Rust

  • PHP

ticket_id = str(uuid.uuid4())
ticket = issue_ticket(
    ticket_id,
    "CollectPayment",
    {
        "amount": {
            "amount": "100",
            "currency": "EUR",
        },
        "creditorAccount": {
            "iban": COMPANY_IBAN,
        },
        "creditorName": COMPANY_NAME,
        "remittance": f"Sign-up fee {PRODUCT_NAME} {customer_id}",
    },
)
String ticketId = UUID.randomUUID().toString();
String ticket = issueTicket(
    ticketId,
    "CollectPayment",
    Map.of(
        "amount", Map.of(
            "amount", "100",
            "currency", "EUR"
        ),
        "creditorAccount", Map.of(
            "iban", COMPANY_IBAN
        ),
        "creditorName", COMPANY_NAME,
        "remittance", "Sign-up fee " + PRODUCT_NAME + " " + customer_id
    )
);
let ticketId = uuidv4()
let ticket = issueTicket(
  ticketId,
  'CollectPayment',
  {
    amount: { amount: '100', currency: 'EUR' },
    creditorAccount: { iban: COMPANY_IBAN },
    creditorName: COMPANY_NAME,
    remittance: `Sign-up fee ${PRODUCT_NAME} ${customerId}`
  }
)
let ticket_id = Uuid::new_v4().to_string();
let ticket = issue_ticket(
    &ticket_id,
    "CollectPayment",
    json!({
        "amount": {
            "amount": "100",
            "currency": "EUR",
        },
        "creditorAccount": {
            "iban": COMPANY_IBAN,
        },
        "creditorName": COMPANY_NAME,
        "remittance":
            format!("Sign-up fee {PRODUCT_NAME} {customer_id}")
    }),
)?;
$ticketId = uniqid();
$ticket = issueTicket(
    $ticketId,
    "CollectPayment",
    json_encode([
        'amount' => [
            'amount': '100',
            'currency': 'EUR',
        ],
        'creditorAccount' => [
            'iban' => $COMPANY_IBAN,
        ],
        'creditorName' => $COMPANY_NAME,
        'remittance' => 'Sign-up fee '.$PRODUCT_NAME.' '.$customerId,
    ])
);

Instant by default

By default, instant payments are requested if possible, with a fallback to non-instant payments.

In case you need more fine-grained control, e.g. to inform the user or ask for confirmation, you can specify the instant property in the ticket data. With instant set to true, the service will try to request an instant payment and return an UnsupportedProduct error if that fails. With instant set to false, it will request a non-instant payment if possible.

Note that full control is not always possible, as some providers do not offer separate interfaces and/or apply implicit fallbacks.

Debtor identification

If you need the IBAN or owner name of the account the payment was initiated from, you can request this information through the fields property in the ticket data. The result will contain the respective properties.

Requesting debtor data adds an extra fee, equivalent to a call to the accounts service, as outlined in your terms.

Call the service frontend

The client accepts the following arguments:

  • An optional debtor account.

Providing a debtor account in the request may reduce the number of confirmation steps the user must complete. For this reason, you should include a debtor account whenever it is available to you, whether retrieved from the accounts service, stored from a previous transaction, or collected from the user in an earlier step.

With debtor account

You may pass a debtor account as follows. Also refer to your client library for details and documentation.

  • JavaScript

  • Kotlin

  • Rust

  • Swift

  • Lua

await client.collectPayment({
  credentials,
  ticket,
  account: {
    iban: "NL58YAXI1234567890",
    currency: "EUR"
  }
})
client.collectPayment(
    credentials,
    ticket = ticket,
    account = AccountReference(
        AccountIdentifier.Iban("NL58YAXI1234567890"),
        currency = "EUR",
    ),
)
client.collect_payment(
    credentials,
    None,
    ticket,
    Some(AccountReference {
        id: AccountIdentifier::Iban("NL58YAXI1234567890".to_string()),
        currency: Some("EUR".to_string()),
    }),
).await?;
try await client.collectPayment(
    credentials: credentials,
    session: nil,
    ticket: ticket,
    account: AccountReference(
        id: .iban("NL58YAXI1234567890"),
	currency: "EUR"
    )
)
client:collectPayment({
  credentials = credentials,
  ticket = ticket,
  account = {
    iban = "NL58YAXI1234567890",
    currency = "EUR"
  }
})

Without debtor account

If you don’t provide a debtor account, the bank may return a selection dialog. This dialog indicates the Accounts context and provides options for each possible IBAN. If the list contains only one element, its primary purpose is to inform the client of the IBAN (for example, for future use), and you can handle the response automatically. For building a richer user selection dialog, we recommend using the accounts service beforehand (please contact us if this service is not included in your plan).

Without giving a debtor account, you can call the service without any additional arguments:

  • JavaScript

  • Kotlin

  • Rust

  • Swift

  • Lua

await client.collectPayment({
  credentials,
  ticket,
})
client.collectPayment(
    credentials,
    ticket = ticket,
)
client.collect_payment(
    credentials,
    None,
    ticket,
).await?;
try await client.collectPayment(
    credentials: credentials,
    session: nil,
    ticket: ticket,
)
client:collectPayment({
  credentials = credentials,
  ticket = ticket,
})

Result backend

A successful response from the service confirms that the user’s bank has initiated the payment using the properties specified in the ticket you previously issued in your backend. This means that the user has authorized the payment and has neither canceled nor rejected it. Finally, you forward the result to your backend system to reconcile its data with the ticket after verifying its authenticity.

The resulting JWT contains the following data, if requested in the ticket:

Example result:

  • Result JWT

  • Decoded JWT header

  • Decoded JWT payload

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImFwaS1rZXktMjE0YWQ
2MDEtZmFlNS00NWY4LTllNDItYTFiNTdkNTA0MTRjIn0.eyJkYXRhIjp7ImRhdG
EiOnt9LCJ0aWNrZXRJZCI6IjE5YzNjYmYxLWFjYWUtNGE3ZS1hMGU3LTc5OWJiY
mVmMzllYiIsInRpbWVzdGFtcCI6IjIwMjUtMDYtMDFUMTE6MTE6MzAuNDIwODA4
OTE2WiJ9LCJleHAiOjI1NDA4MDgwMDB9.yBf2tn-aFptMbvp-ZV-8faLtC8i785
uVpcTFMfpsHzQ
{
  "typ": "JWT",
  "alg": "HS256",
  "kid": "api-key-214ad601-fae5-45f8-9e42-a1b57d50414c"
}
{
  "data": {
    "data": {}, (1)
    "ticketId": "19c3cbf1-acae-4a7e-a0e7-799bbbef39eb",
    "timestamp": "2025-06-01T11:11:30.420808916Z"
  },
  "exp": 2540808000
}
1 Note that no data is returned here. The fact that a Result was returned means that the payment was successful.

Recovery

If your frontend is unable to retrieve (or forward) the final response, you can use the ticket ID to request the last status from the service. The service retains the status for about 15 minutes and exposes it via an HTTP endpoint:

Method: POST
Path: /collect-payment/status/{ticketId}

curl example
curl -X POST \
  https://api.yaxi.tech/collect-payment/status/0708c88b-382a-4aef-9677-8201c6b1817c