Make direct foreign currency transfers
This recipe guides you through integrating the Foreign Exchange (FX) direct transfer workflow. It allows your application to handle multi-currency cross-border direct account settlements where currency conversion, live exchange rates, and real-time fee acceptance are required.
Prerequisites
- The customer has onboarded the Outbound Payments app and a payment account is available.
- The payment account must include
INITIATE_FX_PAYMENTunder itsavailableOperations. - You have Banqup permissions.
Step 1: Validate the transfer and detect the foreign currency
The first step validates your direct transfer request details (such as account IDs, formatting, and route validity) and determines if the transfer requires a foreign currency exchange transaction.
Endpoint: /payments/v2/spaces/{spaceId}/payments/context:validate
Path parameter:
spaceId
Requires a JSON request body (see
curlexample below).
curl -L -X POST 'https://{{serverURL}}/payments/v2/spaces/{spaceId}/payments/context:validate' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-d '{
"debtorAccountId":"92cc8808-0d19-41fb-a6f9-b70bd5441c88",
"payments":[
{
"counterPartyAmount":{
"currency":"RON",
"value":"20"
},
"creditor":{
"account":{
"identifier":"RO71RZBR4836955184545888",
"scheme":"IBAN",
"country":"RO",
"currency":"RON",
"bank":{
"identifier":"RZBRROBU",
"scheme":"BIC"
}
},
"address":{
"streetName":"Albinelor",
"buildingNumber":"21B",
"postalCode":"310225",
"city":"Sibiu",
"country":"Romania",
"countryCode":"RO"
},
"name":"Romanian Timber",
"verificationId":""
},
"endToEndIdentification":"BTX695263c346694d4eb7dcf81b367b9999",
"reference":{
"value":"pre-validation",
"type":"UNSTRUCTURED"
}
}
]
}'
Successful response:
When
paymentExecutionMethodis"FOREIGN", proceed to Step 2 to retrieve live fees. Re-call with theacceptedFeeIdonce the user accepts the transaction fee quote.
{
"validationResult": "VALID",
"paymentExecutionMethod": "FOREIGN"
}
Step 2: Retrieve FX fees & exchange rate
Retrieves live fee estimates, currency conversion exchange rates, and a transaction fee quote. This quote must be accepted explicitly by the user.
Endpoint: /payments/v2/spaces/{spaceId}/payments/context/fees
Path parameter:
spaceId
Requires a JSON request body (see
curlexample below).
curl -L -X POST 'https://{{serverURL}}/payments/v2/spaces/{spaceId}/payments/context/fees' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-d '{
"debtorAccountId":"92cc8808-0d19-41fb-a6f9-b70bd5441c88",
"payments":[
{
"counterPartyAmount":{
"currency":"RON",
"value":"20"
},
"creditor":{
"account":{
"id":"b9eb36e8-ca32-4661-9bba-94b89a8888ff",
"identifier":"RO71RZBR4836955184545888",
"scheme":"IBAN",
"country":"RO",
"currency":"RON",
"bank":{
"identifier":"RZBRROBU",
"scheme":"BIC"
}
},
"address":{
"streetName":"Albinelor",
"buildingNumber":"21B",
"postalCode":"310225",
"city":"Sibiu",
"country":"Romania"
},
"name":"Romanian Timber",
"verificationId":"VOP-nh6Zj0Y54u2zVkkk"
},
"endToEndIdentification":"BTX608ab688f4ac4b53a8286a2665c5ddf8",
"reference":{
"value":"Transfer",
"type":"UNSTRUCTURED"
}
}
]
}'
Successful response:
{
"id": "80f3ebd7-942e-424f-8a49-0af688b2bbb8",
"originatingPartyAmount": {
"value": "8.97",
"currency": "EUR"
},
"counterPartyAmount": {
"value": "20",
"currency": "RON"
},
"feeAmount": {
"value": "5.00",
"currency": "EUR"
},
"expirationTime": "2026-06-10T06:26:38.037709509Z",
"currencyConversion": {
"conversionRate": 0.1985,
"sellAmount": {
"value": "3.97",
"currency": "EUR"
},
"buyAmount": {
"value": "20",
"currency": "RON"
}
},
"agreementPolicy": "CONSENT_REQUIRED"
}
User integration rules
- Quote Display: Surface the foreign amount (
counterPartyAmount), total cost (originatingPartyAmount), FX fee (feeAmount), and conversion rate directly to the user. - Consent: If
agreementPolicyis"CONSENT_REQUIRED", require an explicit accept action. - Countdown Timer: Start a countdown timer from
expirationTime(FX quotes generally expire in ~2 minutes). - Quote Expiration / Re-fetch: If the timer expires or a
FEE_MISMATCH_OR_EXPIREDerror occurs, re-fetch fees and re-present to the user. - ID Persistence: Save the response
idasacceptedFeeId— this must be passed during Step 4.
Step 3: Verification of payee (VOP)
Required for all FX payment accounts across all countries and payment schemes. Verification of Payee (VOP) cross-references the provided beneficiary name with the bank account holder record.
Note: For non-SEPA routing (e.g., US ABA), a TECH_ERROR response is expected behavior.
Endpoint: /payments/v1/spaces/{spaceId}/paymentAccounts:verify
Path parameter:
spaceId
Requires a JSON request body (see
curlexample below).
curl -L -X POST 'https://{{serverURL}}/payments/v1/spaces/{spaceId}/paymentAccounts:verify' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-d '{
"accountHolderName": "Romanian Timber",
"accountIdentifier":{
"bank":{
"identifier":"RZBRROBU",
"scheme":"BIC"
},
"country":"RO",
"currency":"RON",
"identifier":"RO71RZBR4836955184545888",
"scheme":"IBAN"
}
}'
Successful response:
Verification of Payee (VOP) response codes
| Code | ID Format | Pass as creditorVerificationId in execute? | Notes |
|---|---|---|---|
MATCH | "VOP-{alphanumeric}" e.g. VOP-2HvJi2TMRUPMLpBV | YES | Exact match |
CLOSE_MATCH | "VOP-{alphanumeric}" e.g. VOP-2HvJi2TMRUPMLpBV | YES | Name is similar but not exact |
NO_MATCH | "VOP-{alphanumeric}" e.g. VOP-2HvJi2TMRUPMLpBV | YES | Warn user of name mismatch |
NOT_POSSIBLE | "VOP-{alphanumeric}" | YES | Warn user that verification was not possible |
TECH_ERROR | "btxErrId{hex}" | YES | Warn user that verification was not possible |
{
"result": "MATCH",
"id": "VOP-gUJ35Vo3LMZppp8p"
}
Step 4: Execute direct transfer
Executes the direct transfer payment by linking the accepted FX fee ID and verification details. On completion, a redirect URL for Strong Customer Authentication (SCA) / Multi-Factor Authentication (MFA) is returned.
Endpoint: /payments/v2/spaces/{spaceId}/payments
Path parameter:
spaceId
Requires a JSON request body (see
curlexample below).
curl -L -X POST 'https://{{serverURL}}/payments/v2/spaces/{spaceId}/payments' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-d '{
"acceptedFeeId":"80f3ebd7-942e-424f-8a49-0af688b2bbb8",
"debtorAccountId":"92cc8808-0d19-41fb-a6f9-b70bd5441c88",
"payments":[
{
"counterPartyAmount":{
"currency":"RON",
"value":"20"
},
"creditor":{
"account":{
"id":"b9eb36e8-ca32-4661-9bba-94b89a8888ff",
"identifier":"RO71RZBR4836955184545888",
"scheme":"IBAN",
"country":"RO",
"currency":"RON",
"bank":{
"identifier":"RZBRROBU",
"scheme":"BIC"
}
},
"address":{
"streetName":"Albinelor",
"buildingNumber":"21B",
"postalCode":"310225",
"city":"Sibiu",
"country":"Romania",
"countryCode":"RO"
},
"name":"Romanian Timber",
"verificationId":"VOP-gUJ35Vo3LMZppp8p"
},
"endToEndIdentification":"BTX390b2d8497d2471cb0f0d5c53b56e888",
"reference":{
"value":"Transfer",
"type":"UNSTRUCTURED"
}
}
]
}'
Successful response:
{
"paymentId": "844e5993-31a5-4500-bd2e-fefb26615fff",
"redirectUrl": "https://accounts.staging.ibis.unifiedpost-payments.com/web/payments/844e5993-31a5-4500-bd2e-fefb26615fff"
}
Strong Customer Authentication (SCA): After execution, redirect the user immediately to
redirectUrlto authorize the transfer.