Integration guides

Verified payments

Add account-ownership verification and an approval gate to an Open Banking payment


Beta

Verified payments are currently in beta. Request/response shapes might be subject to change. Please reach out to your Volt contact before integrating.

Overview

A verified payment is a standard Open Banking payment with an Account Ownership Verification step placed in front of settlement. Before the payment is sent to the bank, the payer authenticates at their bank — confirming they have access to the account they are paying from — and Volt matches their name against the account holder name on the bank record. Volt sends you the account holder data and the name-match result, and the payment pauses for your approval decision — only once you approve does Volt initiate the transfer.

It runs as a single payment on the Global API and works with any integration channel, , or the . Adding the X-Volt-Verified-Payment: true header to your existing POST /payments call is the only change required — the request body and the rest of your integration stay exactly the same. The verification and approval gate are surfaced through the usual paymentInitiationFlow, and you do not manage a separate account-access request; Volt handles the verification internally.

Availability

Verified payments are available for Open Banking (EU, UK) payments at institutions that support account verification. Talk to your Volt account manager to have verified payments enabled for your application.


How it works

A verified payment moves through two distinct ADDITIONAL_AUTHORIZATION_REQUIRED checkpoints:

  1. Account authorisation — the payer authorises at their bank, and Volt uses that authorisation to verify account ownership and read the account holder name.
  2. Approval gate — once verification and the name match complete, Volt sends you the account holder data and the name-match result, and the payment waits for your approve/refuse decision.

Steps

Authenticate with Volt

In order to use Volt's API you need to be authenticated. Use this to handle authentication with Volt's API.

Create the verified payment

Call POST /payments with your usual X-Volt-Initiation-Channel (here api) and add the X-Volt-Verified-Payment: true header. Everything else is the standard Open Banking payment body.

POST /payments
curl --request POST \
  --url https://gateway.volt.io/payments \
  --header 'authorization: Bearer <your access token>' \
  --header 'content-type: application/json' \
  --header 'idempotency-key: <your idempotency key>' \
  --header 'x-volt-api-version: 1' \
  --header 'x-volt-initiation-channel: api' \
  --header 'x-volt-verified-payment: true' \
  --data '{
    "currency": "EUR",
    "amount": 1000,
    "paymentReference": "ABCDE12345F6GH78X9",
    "internalReference": "your-internal-reference",
    "payer": {
      "email": "johndoe@example.com",
      "firstName": "John",
      "lastName": "Doe",
      "reference": "JDOE-101"
    },
    "device": {
      "ip": "192.168.0.1",
      "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0",
      "fingerprint": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"
    },
    "paymentSystem": "OPEN_BANKING_EU",
    "openBankingEU": {
      "type": "SERVICES",
      "institutionId": "<institution id>",
      "accountIdentifiers": {
        "iban": "DE75512108001245126199"
      }
    },
    "communication": {
      "return": {
        "urls": {
          "unified": { "url": "https://mywebsite.com/payment" }
        }
      },
      "notifications": {
        "url": "https://mywebsite.com/webhooks"
      }
    },
    "termsAndConditionsAccepted": true
  }'

The payer name powers the match

Volt matches payer.firstName and payer.lastName against the account holder name returned by the bank, so supply them as accurately as you can. Volt sends you the match result so you can decide whether to approve the payment (see step 5).

The payment is created and immediately enters account-ownership verification, returning ADDITIONAL_AUTHORIZATION_REQUIRED. The paymentInitiationFlow guides the payer through bank authorisation — for most banks, a redirect:

POST /payments response
{
  "id": "fbd0a297-9c96-48c9-a85e-ef5d3d986e96",
  "status": "ADDITIONAL_AUTHORIZATION_REQUIRED", 
  "paymentInitiationFlow": {
    "status": "PROCESSING",
    "details": {
      "reason": "AWAITING_USER_REDIRECT",
      "redirect": {
        "url": "https://vo.lt/short",
        "directUrl": "https://myBank.com/authorisation?code=xyz"
      }
    }
  }
  // ...
}

Payer authorises at their bank

The payer completes authentication and authorisation within the selected bank interface. This first authorisation is what lets Volt verify the account. The payment stays at ADDITIONAL_AUTHORIZATION_REQUIRED until it completes.

Select the account, if prompted

If the verified account holder has more than one account, the payment pauses at status ACCOUNT_SELECTION and the paymentInitiationFlow returns an ACCOUNT_ID input. Its allowedValues list the candidate accounts, and its propertyPath tells you where to put your choice. Submit the chosen account with PATCH /payments/{id}, the same way you resolve any required input. With a single account, this step is skipped automatically.

Volt matches the name, calculates the score and notifies you

Once the payer has authorised, Volt confirms the payer has access to the account and matches the payer name you supplied against the account holder name. Volt then sends the match score and the account holder data to your notification URL so you can decide whether to proceed. The notification contains a paymentInitiationFlow object, which now asks for your approval decision:

You can configure payment notifications in the Fuzebox or pass it directly in the POST /payments request.

Name-match score notification
{
  "id": "fbd0a297-9c96-48c9-a85e-ef5d3d986e96",
  "status": "ADDITIONAL_AUTHORIZATION_REQUIRED",
  "score": 0.89,
  "accountData": {
    "accounts": [
      {
        "owner": "JANE DOE",
        "balances": [
          {
            "type": "AVAILABLE",
            "amount": {
              "value": 20,
              "currency": "EUR"
            }
          }
        ],
        "identifiers": {
          "iban": "DE69500105176334166465"
        }
      }
    ]
  },
  "paymentInitiationFlow": {
    "status": "WAITING_FOR_INPUT",
    "requiredInput": [
      {
        "name": "CUSTOMER_APPROVAL",
        "propertyPath": "customer.approval.status",
        "allowedValues": [
          {
            "name": "APPROVE",
            "value": "APPROVED",
            "description": null
          },
          {
            "name": "REFUSE",
            "value": "REFUSED",
            "description": null
          }
        ]
      }
    ]
  }
}

Account holder data is delivered once

The account holder data (accountData) is included only in this notification — it is not returned by GET /payments/{id}. Persist it from the webhook if you need it for your approval logic or your records.

The notification carries:

  • score — name-match score as a floating-point value (for example, 0.89); a higher value indicates a closer match between the payer name you supplied and the account holder name.
  • accountData.accounts[].owner — account holder name returned by the bank.
  • accountData.accounts[].balances — account balances, when available.
  • accountData.accounts[].identifiers — account identifiers, such as iban.
  • paymentInitiationFlow — asks for your approval decision through the CUSTOMER_APPROVAL required input.

When account holder data is unavailable

If Volt can't retrieve the account holder data, the accountData fields are omitted and score is 0.0. The approval gate still opens, so it remains your decision whether to approve or refuse the payment.

While you're weighing the approval decision, the payer is still on your checkout — if you're polling GET /payments/{id} on their behalf (for example, in a hosted checkout screen), the paymentInitiationFlow reports a different, purely informational shape:

GET /payments/{id} response — payer view
{
  "id": "fbd0a297-9c96-48c9-a85e-ef5d3d986e96",
  "status": "ADDITIONAL_AUTHORIZATION_REQUIRED",
  "paymentInitiationFlow": {
    "status": "PROCESSING",
    "details": {
      "reason": "AWAITING_CUSTOMER_APPROVAL"
    }
  }
  // ...
}

Two shapes, two audiences

The WAITING_FOR_INPUT/CUSTOMER_APPROVAL shape from the notification above is addressed to you — it's what you get back if you call GET/PATCH /payments/{id} directly. PROCESSING/AWAITING_CUSTOMER_APPROVAL is addressed to the payer — it's what a payer-facing checkout polling the same endpoint sees while your decision is still pending.

Submit your approval decision

Resolve the CUSTOMER_APPROVAL input by sending the decision to PATCH /payments/{id}. The value must be either APPROVED or REFUSED.

PATCH /payments/{id}
curl --request PATCH \
  --url https://gateway.volt.io/payments/fbd0a297-9c96-48c9-a85e-ef5d3d986e96 \
  --header 'authorization: Bearer <your access token>' \
  --header 'content-type: application/json' \
  --data '{
    "customer": {
      "approval": {
        "status": "APPROVED"
      }
    }
  }'
  • APPROVED → the payment advances to APPROVED_BY_RISK and Volt initiates the transfer.
  • REFUSED → the payment ends at REFUSED_BY_RISK and is not sent to the bank.

The approval gate stays open for 30 seconds. If no decision is submitted within that window, the payment is automatically refused (REFUSED_BY_RISK), so resolve the input promptly.

Payer authorises the payment

After approval, Volt initiates the payment, and the payer completes the process within the selected bank interface to authorise the transfer — using whichever authorisation flow the bank supports.

Receive the result

Poll the GET /payments/{id} endpoint until the payment reaches a final state, or rely on the payment notification (webhook) Volt sends when the status changes. On success, the payment reaches COMPLETED.

You can configure payment notifications in the Fuzebox or pass it directly in the POST /payments request.

Payment statuses

A verified payment uses the standard Volt payment statuses. The sequence specific to this flow is below; see the for the full reference.

StatusMeaning in the verified flowNotification
NEW_PAYMENTPayment created; account-ownership verification is starting.No
ADDITIONAL_AUTHORIZATION_REQUIREDEither the payer must authorise at their bank (verification), or — after verification — the payment is awaiting your approval decision.No
ACCOUNT_SELECTIONThe account holder has more than one account; one is being selected.No
APPROVED_BY_RISKYou approved the payment; Volt is initiating the transfer.No
REFUSED_BY_RISKYou refused the payment, or the approval gate timed out. The payment is not sent to the bank.Yes
BANK_REDIRECTThe payer is being redirected to the bank to authorise the transfer.Yes
COMPLETEDThe payment was instructed and accepted by the payer's bank.Yes
FAILEDThe verification authorisation could not be completed.Yes
CANCELLED_BY_USERThe payer declined the authorisation at their bank.Yes
ABANDONED_BY_USERThe payer abandoned the payment.Yes

Testing in sandbox

In , Volt's synthetic banks let you drive a verified payment through any authorisation flow without a real bank connection.

The payer authorises twice — once to verify the account, then again to authorise the payment — and each is steered separately, so a single input is not enough to pin down a scenario. Three things have to line up:

LeverWhat it controls
payer.referenceThe authorisation flow the synthetic bank uses to verify the account.
amountHow the bank behaves when the payment is authorised.
InstitutionWhether the flow is a redirect or an embedded one. Usually picked by the payer on the checkout.

You don't have to specify the institution when you create the payment. In the usual flow, you send the payer to the checkout URL returned by POST /payments, where they can select their bank from the list — choose Synthetic Redirect or Synthetic Embedded. Both are registered under Germany (DE), Austria (AT) and the United Kingdom (GB), so select one of these countries on the checkout to find them.

To specify the bank upfront, look it up by name using GET /institutions and pass the returned ID as openBankingEU.institutionId. The same endpoint also tells you which account identifiers are required by that bank.

Scenarios

amount is the value you send on POST /payments in fractional units, so 200 is €2.00 or £2.00 depending on currency.

Flowamountpayer.referenceInstitution
200redirect-2-stepSynthetic Redirect
with 300preauth-with-decoupledSynthetic Redirect
with second redirect350preauth-with-second-redirectSynthetic Redirect
with 400embedded-decoupledSynthetic Embedded
, no SCA step410embedded-no-scaSynthetic Embedded
with an OTP challenge450embedded-otpSynthetic Embedded

The reference picks the flow, not the amount

payer.reference selects the verification flow; the amount only steers how the payment is authorised. Both still have to match the table for the payment to behave end-to-end as described.

Unrecognised references fall back silently

If payer.reference isn't one of the values above, no flow is requested and the synthetic bank uses its default behaviour — the payment still succeeds, it just won't follow the scenario you expected.

Returning multiple accounts

Any flow name can be prefixed with _multi__multi_embedded-otp, _multi_redirect-2-step, and so on. The synthetic bank then reports more than one account, which is what puts the payment into ACCOUNT_SELECTION status. Without the prefix a single account is returned and that step is skipped.

Volt reads only the part of payer.reference before the first |, so you can keep your own identifier in the same field — redirect-2-step|JDOE-101 selects the redirect flow and preserves your reference.

Example

Scenario 1 — redirect flow, amount 200. The institution is left out, so the payer selects Synthetic Redirect on the checkout.

POST /payments (Sandbox)
curl --request POST \
  --url https://api.sandbox.volt.io/payments \
  --header 'authorization: Bearer <your access token>' \
  --header 'content-type: application/json' \
  --header 'idempotency-key: <your idempotency key>' \
  --header 'x-volt-api-version: 1' \
  --header 'x-volt-initiation-channel: api' \
  --header 'x-volt-verified-payment: true' \
  --data '{
    "currency": "EUR",
    "amount": 200,
    "paymentReference": "ABCDE12345F6GH78X9",
    "payer": {
      "email": "johndoe@example.com",
      "firstName": "John",
      "lastName": "Doe",
      "reference": "redirect-2-step"
    },
    "paymentSystem": "OPEN_BANKING_EU",
    "openBankingEU": {
      "type": "SERVICES"
    },
    "communication": {
      "return": {
        "urls": {
          "unified": { "url": "https://mywebsite.com/payment" }
        }
      },
      "notifications": {
        "url": "https://mywebsite.com/webhooks"
      }
    },
    "termsAndConditionsAccepted": true
  }'

Send the payer to the checkout URL from the response, pick the institution from the table, and the payment follows the same eight steps as in production — verification, the approval gate, then the payment authorisation.


Next steps

How is this guide?

Last updated on