Pay foreign currency invoices
This recipe guides you through integrating the Foreign Exchange (FX) document payment workflow. It allows your application to handle multi-currency cross-border invoice 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: Retrieve payment details
Endpoint: /datastore/documents/transaction/v2/spaces/{spaceId}/documents:paydetails
Path parameter:
spaceId
Requires a JSON request body (see
curlexample below).
curl --location 'https://{{serverURL}}/datastore/documents/transaction/v2/spaces/{spaceId}/documents:paydetails' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_TOKEN' \
--data '{
"documents": ["df8d2deb-fcb0-41c0-8b21-29f750d78d4a"]
}'
Successful response:
{
"documents": [
{
"id": "df8d2deb-fcb0-41c0-8b21-29f750d78d4a",
"metadata": {
"dueDate": "2026-06-25",
"supplier": {
"address": {
"country": "BE",
"postCode": "1024",
"townName": "Los Angeles",
"streetName": "1234 Havelaan"
},
"legalName": "The SUA Supplier",
"tradeName": "The SUA Supplier"
},
"issueDate": "2026-05-26",
"documentTotals": {
"netAmount": "15",
"vatAmount": "15",
"grossAmount": "15",
"payableAmount": "15"
},
"totalAmount": "15",
"currencyCode": "RON",
"paymentMethod": "CARD",
"documentNumber": "INVOICE-207",
"paymentMeans": [
{
"paymentReference": {
"type": "UNSTRUCTURED",
"value": "us-26051",
"scheme": "OGM_VCS"
}
}
],
"supplierReference": "Supplier reference",
"$schema": "https://btx.unifiedpost.com/btx/datastore/document/document_metadata/v1/document-metadata-schema.json"
},
"status": {
"pay": "UNPAID",
"document": "FULLY_APPROVED"
},
"tasks": [
{
"extensionName": "com.unifiedpost.btx.aggregation:markAsPaid",
"context": {}
},
{
"extensionName": "com.unifiedpost.btx.data:markAsRefused",
"context": {}
},
{
"extensionName": "com.unifiedpost.btx.data:markAsResolved",
"context": {}
},
{
"extensionName": "com.unifiedpost.btx.data:pay",
"context": {}
}
],
"supplier": {
"type": "business",
"id": "9e6e8533-8e85-404f-a0e6-c5495539bf1e",
"name": "The RO Supplier",
"paymentAccounts": [
{
"id": "b9eb36e8-ca32-4661-9bba-94b89a8301ff",
"identifier": "RO71RZBR4836955184545156",
"scheme": "IBAN",
"country": "RO",
"currency": "RON",
"bank": {
"identifier": "RZBRROBU",
"scheme": "BIC"
},
"spaceId": "e57cafcb-16bd-447a-b81c-67ba3152c4d8",
"ownedBySpace": false,
"active": true,
"ownerName": "The BE Customer"
}
]
},
"category": "INVOICE"
}
]
}
Step 2: Retrieve FX fees & exchange rate
Endpoint: /solution/business/v2/spaces/{spaceId}/payments/documents:validate
Path parameters:
spaceId
Requires a JSON request body (see
curlexample below).
The paymentAccountId must be the ID that belongs to the account used to initiate the payment.
If you have multiple creditor payment accounts available, mention the one you want to use in the body.
If there is only one, we will automatically select that one.
curl --location 'https://{{serverURL}}/solution/business/v1/spaces/{spaceId}/payments/documents:validate' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_TOKEN' \
--data '{
"documents": [
{
"creditorPaymentAccountId": "98f49e1d-2e92-4467-8117-8386126a7653",
"id": "df8d2deb-fcb0-41c0-8b21-29f750d78d4a"
}
],
"paymentAccountId": "92cc8808-0d19-41fb-a6f9-b70bd5441c6b"
}'
Successful response:
Key response codes in paymentsMessages
| Code | Type | Action |
|---|---|---|
FX_PAYMENT | INFO | Trigger the foreign exchange (FX) payment flow. |
NEW_FEE_DETECTED | ERROR | Pass the newly detected fee ID as acceptedFeeId in the next validation request. |
FAILED_CONTEXT_VALIDATION | ERROR | Render missing fields for incomplete beneficiary address details. |
RESTRICTED_ZONE | ERROR (sub-code) | Block payment immediately; beneficiary country is restricted. |
RESTRICTED_CURRENCY | ERROR (sub-code) | Block payment immediately; beneficiary currency is restricted. |
SERVICE_UNAVAILABLE | ERROR | Display a retry prompt; the underlying liquidity provider (Currency Cloud) is unreachable. |
PAYMENT_EXECUTION_METHOD_PROVIDER_OUT_OF_WORKING_HOURS | ERROR | Display an out-of-hours message, e.g. FX trading is closed (Mon 00:00 – Fri 21:30 GMT). |
Key response codes in documentsMessages
| Code | Type | Action |
|---|---|---|
MISSING_CREDITOR_ACCOUNT | ERROR | Prompt the user to select or confirm the beneficiary bank account. |
NEW_PAYMENT_MEANS_ACCOUNT_DETECTED | INFO | Display the newly detected account details to the user for confirmation. |
{
"status": "INVALID",
"validationResult": {
"paymentMessages": [
{
"code": "NEW_FEE_DETECTED",
"type": "ERROR",
"data": {
"agreementPolicy": "CONSENT_REQUIRED",
"counterPartyAmount": {
"value": "15",
"currency": "RON"
},
"currencyConversion": {
"buyAmount": {
"value": "15",
"currency": "RON"
},
"conversionRate": 0.1987,
"sellAmount": {
"value": "2.98",
"currency": "EUR"
}
},
"expirationTime": "2026-05-26T12:01:55.043635175Z",
"feeAmount": {
"value": "5.00",
"currency": "EUR"
},
"id": "9708f909-5452-4e76-9a12-3c2d5385e15f",
"originatingPartyAmount": {
"value": "7.98",
"currency": "EUR"
}
}
},
{
"code": "FX_PAYMENT",
"type": "INFO"
}
],
"documentsMessages": []
}
}
Step 3: Retrieve creditor account (optional)
Call this endpoint only if you got the error MISSING_CREDITOR_ACCOUNT to identify the creditor account.
Endpoint: /solution/business/v1/spaces/{spaceId}/payments/documents:findCreditorAccount
Path parameters:
spaceId
Requires a JSON request body (see
curlexample below).
curl --location 'https://{{serverURL}}/solution/business/v1/spaces/{spaceId}/payments/documents:findCreditorAccount' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_TOKEN' \
--data '{
"documentIds": ["d6b0fbcc-1900-407a-abcf-984b69ba9045"]
}'
Successful response:
{
"documents": [
{
"documentId": "d6b0fbcc-1900-407a-abcf-984b69ba9045"
}
]
}
Step 4: Final validation
Validates the transaction after adding the creditor account and accepting the fee.
Must be re-invoked on any subsequent address modification or fee refresh.
Endpoint: /solution/business/v2/spaces/{spaceId}/payments/documents:validate
Path parameters:
spaceId
Requires a JSON request body (see
curlexample below).
curl --location 'https://{{serverURL}}/solution/business/v1/spaces/{spaceId}/payments/documents:validate' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_TOKEN' \
--data '{
"paymentAccountId": "92cc8808-0d19-41fb-a6f9-b70bd5441c6b",
"acceptedFeeId": "071f9d1b-84dd-4dd9-890b-a86c54931101",
"documents": [
{
"id": "d6b0fbcc-1900-407a-abcf-984b69ba9045",
"creditorPaymentAccountId": "98f49e1d-2e92-4467-8117-8386126a7653"
}
]
}'
Successful response:
Proceed to execution only if status = "VALID". If the response returns "INVALID", catch the remaining errors, surface them in the UI, and re-trigger validation after user correction.
{
"status": "VALID",
"validationResult": {
"paymentMessages": [
{
"code": "FX_PAYMENT",
"type": "INFO"
}
],
"documentsMessages": []
}
}
Step 5: 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 parameters:
spaceId
Requires a JSON request body (see
curlexample below).
curl --location 'https://{{serverURL}}/payments/v1/spaces/{spaceId}/paymentAccounts:verify' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_TOKEN' \
--data '{
"accountHolderName": "The RO Supplier",
"accountIdentifier": {
"id": "b9eb36e8-ca32-4661-9bba-94b89a8301ff",
"identifier": "RO71RZBR4836955184545156",
"scheme": "IBAN",
"country": "RO",
"currency": "RON",
"bank": {
"identifier": "RZBRROBU",
"scheme": "BIC"
},
"spaceId": "e57cafcb-16bd-447a-b81c-67ba3152c4d8",
"ownedBySpace": false,
"active": true,
"ownerName": "The BE Customer"
}
}'
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}" e.g. VOP-2HvJi2TMRUPMLpBV | YES | Warn user that verification was not possible |
TECH_ERROR | "btxErrId{hex}" | YES | Warn user that verification was not possible |
{
"result": "MATCH",
"id": "VOP-gUJ35Vo3LMZppf8k"
}
Step 6: Pay invoice
After execution, redirect the client to redirectUrl for SCA/MFA. Await the return webhook or browser redirect to your callback page - do not poll the API.
Endpoint: /solution/business/v2/spaces/{spaceId}/payments/documents:execute
Path parameters:
spaceId
Requires a JSON request body (see
curlexample below).
curl --location 'https://{{serverURL}}/solution/business/v1/spaces/{spaceId}/payments/documents:execute' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_TOKEN' \
--data '{
"documents": [
{
"id": "df8d2deb-fcb0-41c0-8b21-29f750d78d4a",
"creditorPaymentAccountId": "98f49e1d-2e92-4467-8117-8386126a7653",
"creditorVerificationId": "04810220-eab3-40f7-a379-827721f16029",
"partialPaymentAmount": "500.00" // optional
}
],
"paymentAccountId": "92cc8808-0d19-41fb-a6f9-b70bd5441c6b",
"redirectUrl": "https://your-app.com/payments?showPaymentStatus=true",
"locale": "en",
"acceptedFeeId": "071f9d1b-84dd-4dd9-890b-a86c54931101"
}'
Successful response:
{
"paymentId": "bd4cfccf-8701-4812-9f19-cf43faceb9ab",
"redirectUrl": "https://accounts.staging.ibis.unifiedpost-payments.com/web/payments/<paymentId>"
}