Sandbox
Help

Prepare and validate a transfer: full workflow

When making transfers to different countries, currencies, or recipient types (bank accounts, Revolut accounts, or cards), the fields you need to provide – both for creating counterparties and for making the payment itself – vary depending on the destination.

Checking first, before you make the transfer, avoids errors and lets you preview costs and delivery time before committing. This guide walks you through the full flow, linking to dedicated guides for endpoint details.

Example use cases

  • Your business uses an internal dashboard to manage salary payments across regions, and needs to create counterparties with the right details before paying invoices
  • You build a vendor onboarding flow that validates recipient details and previews transfer costs before approval

The full flow

To prepare and validate a transfer, follow these steps:

  1. Check supported countries and currencies: Verify that the destination is supported.
  2. Check counterparty field requirements: Discover which fields are available and required to create a counterparty.
  3. Prepare a counterparty: Create or retrieve the counterparty.
  4. Check transfer field requirements: Discover which fields are available and required for the transfer.
  5. Get an indicative quote: See the expected fees, exchange rate, and delivery time.
  6. Make the payment: Execute the transfer.
Examples in this guide

We'll follow five examples throughout this guide:

  • 🇬🇧 GB/GBP: A domestic transfer to a UK business bank account
  • 🇩🇪 DE/EUR: A cross-currency SWIFT transfer to a German business bank account
  • 🇺🇸 US/USD: A cross-currency transfer to a US business bank account
  • Revolut: A transfer to a Revolut business account (instant, no fees)
  • 💳 Card: A transfer to a saved card

In all examples, we're adding a business counterparty.

1. Check supported countries and currencies

This step applies to external bank account counterparties. Revolut counterparties are added via Revtag and don't require a country or currency. Card counterparties are created via payout link and don't require a country or currency either.

To check which bank countries and currencies are supported, make a GET request to the /counterparties/countries endpoint.

See the API reference: Check supported countries and currencies

As a response, you get all bank countries and their available currencies supported for counterparty creation for your business:

Response
{
  "countries": [
    { "country": "GB", "currencies": ["GBP"] },
    { "country": "US", "currencies": ["USD"] },
    { "country": "DE", "currencies": ["EUR"] }
  ]
}

Check if your desired country and currency combination is supported before proceeding.

2. Check counterparty field requirements

Make a GET request to the /counterparties/fields endpoint with the country, currency, and recipient type (personal or business) as query parameters to discover which fields are needed to create a counterparty for that destination.

See the API reference: Check counterparty field requirements

As a response, you get a fields array where each item describes a conditionally available field – whether it's required or optional – along with its validation rules and allowed values. Always required fields (bank_country, currency, and profile_type) are intentionally excluded from the response.

Field object properties

Each field object contains:

PropertyDescription
nameThe name of the given field, matching the Create a counterparty endpoint request body.
requiredWhether the field must be provided when creating the counterparty.
regexValidation rules with a pattern and, in some cases, a human-readable description.
optionsAllowed values for the field, if applicable. Each option has a value and an optional default flag.

For a UK business counterparty (country=GB, currency=GBP, recipient_type=business), the response looks like this:

Response
{
  "fields": [
    {
      "name": "company_name",
      "required": true,
      "regex": {
        "pattern": "^.{3,255}$",
        "description": "Beneficiary company name cannot be shorter than 3 characters and longer than 255 characters"
      }
    },
    {
      "name": "sort_code",
      "required": true,
      "regex": {
        "pattern": "^\\d{6}$",
        "description": "Must be a 6-digit number"
      }
    },
    {
      "name": "account_no",
      "required": true,
      "regex": {
        "pattern": "^\\d{8}$",
        "description": "Must be an 8-digit number"
      }
    },
    {
      "name": "address",
      "required": true
    }
  ]
}

This means that to create a UK business counterparty with GBP as the currency, you must provide a company_name (3–255 characters), a sort_code (a 6-digit number), an account_no (an 8-digit number), and an address.

For more details and additional examples, see Create a counterparty.

3. Prepare a counterparty

Before creating or using a counterparty, consider validating the recipient's account name (CoP/VoP) to confirm the name matches the bank's records.

Add a new counterparty

Combine the conditionally available fields from the field requirements response with the always required fields (bank_country, currency, and profile_type).

Request
{
  "company_name": "Example Vendor Ltd",
  "profile_type": "business",    // ← always required
  "bank_country": "GB",          // ← always required
  "currency": "GBP",             // ← always required
  "account_no": "12345678",
  "sort_code": "123456",
  "address": {
    "street_line1": "1 Canada Square",
    "city": "London",
    "country": "GB",
    "postcode": "E14 5AB"
  }
}

See the API reference: Create a counterparty

When you create or retrieve a counterparty, the response includes the counterparty id, an accounts array and/or a cards array, if applicable. Each entry in these arrays is a payment method with its own id.

Response
{
  "id": "b53fdd78-8d67-4f63-a103-eeeeef53cac8",    // ← counterparty ID
  "name": "Example Vendor Ltd",
  "profile_type": "business",
  "state": "created",
  "created_at": "2026-07-02T09:34:41.790401Z",
  "updated_at": "2026-07-02T09:34:41.790401Z",
  "accounts": [
    {
      "account_no": "12345678",
      "sort_code": "123456",
      "id": "5c9e171c-7e23-4d6a-b768-aaaaaba535f3",    // ← payment method ID
      "name": "Example Vendor Ltd",
      "bank_country": "GB",
      "currency": "GBP",
      "type": "external"
    }
  ]
}

Save the counterparty id – you'll need it for the transfer.

Retrieve an existing counterparty

Counterparty ID

You can retrieve a list of your counterparties filtering by name and/or account number, and save the counterparty id from the response.

Payment method ID

If the counterparty has multiple payment methods available (for example, 2 accounts, or 1 account and 1 card), you will also need the ID of the account or card to which you want to transfer the money in subsequent steps.

  • For bank account and Revolut counterparties, look in the accounts array for the account id.
  • For card counterparties, look in the cards array for the card id.

Update a counterparty's address

Some payment rails may require a complete address for the recipient, including the region field. If a counterparty was created without an address, or the address is incomplete, you can update the address for the specific payment method using the PATCH /counterparties/{counterparty_id}/payment-methods/{payment_method_id} endpoint.

To get the payment_method_id, retrieve the counterparty's details and find the ID of the relevant account or card in the response.

Request
{
  "address": {
    "street_line1": "1 Canada Square",
    "city": "London",
    "region": "Greater London",  // ← added: not always required at creation, but needed for some payment rails
    "country": "GB",
    "postcode": "E14 5AB"
  }
}

The address object accepts the following fields: street_line1, street_line2, region, city, country, and postcode. The country and postcode fields are required.

A successful response returns 204 No Content, confirming the address was updated.

See the API reference: Update a counterparty payment method

4. Check transfer field requirements

Once you have your counterparty ID, check which fields are needed for the transfer to that destination.

Build the request payload

To check transfer field requirements, you need your account ID and the counterparty ID (and the counterparty's account ID or card ID if they have multiple payment methods).

Request
{
  "account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "receiver": {
    "counterparty_id": "b53fdd78-8d67-4f63-a103-eeeeef53cac8",
    "account_id": "5c9e171c-7e23-4d6a-b768-aaaaaba535f3"        // ← required when counterparty has multiple payment methods
  }
}

Check the field requirements

Make a POST request to the /pay/fields endpoint with the payload above.

See the API reference: Check transfer field requirements

As a response, you get a fields array where each item describes a conditionally available field for this transfer, indicating whether it's required or optional, along with its validation rules and allowed values.

Field object properties

Each field object contains:

PropertyDescription
nameThe field name, matching the /pay endpoint request body.
requiredWhether the field must be provided for this transfer.
validationValidation rules such as min_length, max_length, and regex constraints.
optionsAllowed values for the field, if applicable. Each option has a value and an optional default flag.
Response
{
  "fields": [
    {
      "name": "charge_bearer",
      "required": false,
      "options": [
        { "value": "shared", "default": true },
        { "value": "debtor" }
      ]
    },
    {
      "name": "reference",
      "required": true,
      "validation": {
        "max_length": 140
      }
    }
  ]
}

For a domestic GBP transfer to a UK counterparty, you must provide a reference of up to 140 characters. You can optionally provide a charge_bearer – the default is shared, meaning fees are split between sender and receiver, but you can also set it to debtor to charge all fees to the sender.

For more details and additional examples, see Bank transfers or Card transfers.

5. Get an indicative quote

Make a POST request to the /pay/indicative-quote endpoint to preview the expected fees, exchange rate, and delivery time without committing to the transfer. Provide the same transfer details you would provide to the /pay endpoint (account ID, recipient, amount, currency), along with any required fields from step 4. You may also add any optional fields you want that you discovered in step 4.

See the API reference: Get an indicative quote

For a domestic GBP transfer of 1,000.00 from your GBP account:

Request
{
  "account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "receiver": {
    "counterparty_id": "b53fdd78-8d67-4f63-a103-eeeeef53cac8",
    "account_id": "5c9e171c-7e23-4d6a-b768-aaaaaba535f3"        // ← required when counterparty has multiple payment methods
  },
  "amount": 1000.00,
  "currency": "GBP",
  "charge_bearer": "shared"                                      // ← optional (discovered in step 4)
}
Response
{
  "amount": {
    "amount": 1000.00,
    "currency": "GBP"
  },
  "fee": {
    "amount": 0,
    "currency": "GBP"
  },
  "estimated_total": {
    "amount": 1000.00,
    "currency": "GBP"
  },
  "estimated_arrival": {
    "date": "2026-07-02",
    "speed": "in_seconds"
  }
}

For more details, see Bank transfers or Card transfers.

With all the information gathered – sender account details, counterparty details, and the indicative quote – you can present a human-readable transfer summary for review before executing the payment. The sample scripts in the Automate the flow section show how to build and display this summary.

6. Make the payment

To execute the transfer, make a POST request to the /pay endpoint with the same details you used for the indicative quote, plus a unique request_id (UUID is recommended) for idempotency.

  • The always required fields are: request_id, account_id, receiver (with counterparty_id), amount, and currency.
  • You must also provide a reference, which is required for all example cases, as discovered in step 4.
  • Optionally, add any other fields you discovered in step 4.

Build the payment payload

Request
{
  "request_id": "a1b2c3d4-e5f6-4789-abcd-ef1234567890",  // ← always required (UUID for idempotency)
  "account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "receiver": {
    "counterparty_id": "b53fdd78-8d67-4f63-a103-eeeeef53cac8",
    "account_id": "5c9e171c-7e23-4d6a-b768-aaaaaba535f3"
  },
  "amount": 1000.00,
  "currency": "GBP",
  "charge_bearer": "shared",  // ← optional (discovered in step 4)
  "reference": "Invoice #12345"  // ← required (discovered in step 4)
}

Make the payment

For detailed instructions and example requests, see Bank transfers or Card transfers.

See the API reference: Create a transfer to another account or card

Automate the flow

The method and end result will differ depending on your needs and implementation. To give you an example of how to connect these steps, we provide sample Python scripts below.

The scripts use the standard-library uuid module and require the requests library to be installed.

1. Create a new counterparty and make a transfer

Sample Python script 1: new counterparty

Adjust the configuration variables at the top to match your setup.

create_counterparty_and_transfer.py
import requests
import uuid

# =============================================
# Configuration: adjust these values as needed
# =============================================
API_URL = "https://b2b.revolut.com/api/1.0"
API_TOKEN = "YOUR_API_TOKEN"   # Replace with your own token

HEADERS = {
    "Authorization": f"Bearer {API_TOKEN}",
    "Content-Type": "application/json"
}

# Your account (the pocket you're sending from)
ACCOUNT_ID = "YOUR_ACCOUNT_ID"   # Replace with your own account ID

# Destination details
COUNTRY = "GB"                   # Counterparty's bank country
CURRENCY = "GBP"                 # Counterparty's account currency
RECIPIENT_TYPE = "business"      # "business" or "personal"

# Counterparty data — provide all fields that apply for your destination.
# The script will keep only the fields required by the API (discovered in step 3).
COUNTERPARTY_DATA = {
    "company_name": "Example Vendor Ltd",
    "account_no": "12345678",
    "sort_code": "123456",
    "address": {
        "street_line1": "1 Canada Square",
        "city": "London",
        "region": "Greater London",
        "country": "GB",
        "postcode": "E14 5AB"
    }
}

# Transfer details
AMOUNT = 1.23
REFERENCE = "Invoice #12345"


# =============================================
# Error handling
# =============================================

class ApiError(Exception):
    pass

def check_response(response):
    """Print error details and raise ApiError if the response failed."""
    if response.ok:
        return
    try:
        error_data = response.json()
        print(f"\nAPI Error (HTTP {response.status_code})")
        print(f"  Code:     {error_data.get('code', 'N/A')}")
        print(f"  Message:  {error_data.get('message', 'N/A')}")
        print(f"  Error ID: {error_data.get('error_id', 'N/A')}")
    except Exception:
        print(f"\nAPI Error (HTTP {response.status_code}): {response.text}")
    raise ApiError(f"Request failed with status {response.status_code}")

# =============================================
# Print destination details
# =============================================

def print_destination_details(country, currency, recipient_type, counterparty_type):
    """Print a summary of the destination details."""
    print("=" * 50)
    print(f"DESTINATION: {country} / {currency}")
    print(f"RECIPIENT TYPE: {recipient_type}")
    print(f"COUNTERPARTY TYPE: {counterparty_type}")
    print("=" * 50)

# =============================================
# Step 1: Check supported countries and currencies
# =============================================

def check_supported_countries():
    """GET /counterparties/countries — returns supported country/currency pairs."""
    print("Checking supported countries and currencies...")
    response = requests.get(f"{API_URL}/counterparties/countries", headers=HEADERS)
    check_response(response)
    data = response.json()
    countries = data.get("countries", [])
    print(f"  Found {len(countries)} supported countries")
    return countries

# =============================================
# Step 2: Verify the destination is supported
# =============================================

def verify_destination_supported(countries, country, currency):
    """Check if the desired country/currency combination is supported.

    Returns True if supported, False otherwise.
    """
    for entry in countries:
        if entry.get("country") == country and currency in entry.get("currencies", []):
            print(f"  Destination {country}/{currency} is supported.")
            return True
    print(f"  Destination {country}/{currency} is not supported.")
    return False

# =============================================
# Step 3: Check counterparty field requirements
# =============================================

def check_counterparty_field_requirements(country, currency, recipient_type):
    """GET /counterparties/fields — returns required and applicable fields."""
    print("Checking counterparty field requirements...")
    params = {"country": country, "currency": currency, "recipient_type": recipient_type}
    response = requests.get(f"{API_URL}/counterparties/fields", params=params, headers=HEADERS)
    check_response(response)
    data = response.json()
    required = [f["name"] for f in data.get("fields", []) if f.get("required")]
    optional = [f["name"] for f in data.get("fields", []) if not f.get("required")]
    print(f"  Required fields: {required}")
    print(f"  Optional fields: {optional}")
    return data

# =============================================
# Step 4: Build the counterparty payload
# =============================================

def build_counterparty_payload(field_requirements, country, currency, recipient_type, counterparty_data):
    """Combine always required fields with applicable fields from the requirements response.

    Only fields present in the requirements response (or always required) are included.
    Fields with no data (None) are dropped.
    """
    payload = {
        "profile_type": recipient_type,
        "bank_country": country,
        "currency": currency,
    }

    for field in field_requirements.get("fields", []):
        name = field["name"]
        if name in counterparty_data and counterparty_data[name] is not None:
            payload[name] = counterparty_data[name]

    # Group first_name/last_name into individual_name for personal counterparties
    if recipient_type == "personal":
        individual_name = {}
        if "first_name" in payload:
            individual_name["first_name"] = payload.pop("first_name")
        if "last_name" in payload:
            individual_name["last_name"] = payload.pop("last_name")
        if individual_name:
            payload["individual_name"] = individual_name

    print(f"  Counterparty payload fields: {list(payload.keys())}")
    return payload

# =============================================
# Step 5: Validate the account name (CoP/VoP)
# =============================================

def build_cop_request(counterparty_data, country, currency, recipient_type):
    """Build the CoP request body based on the destination region.

    Returns the request dict, or None if CoP is not available for this destination.
    """
    request = {}

    if recipient_type == "business":
        request["company_name"] = counterparty_data.get("company_name")
    else:
        individual = counterparty_data.get("individual_name", {})
        request["individual_name"] = {
            "first_name": individual.get("first_name") or counterparty_data.get("first_name"),
            "last_name": individual.get("last_name") or counterparty_data.get("last_name"),
        }

    if country == "GB" and currency == "GBP":
        request["account_no"] = counterparty_data.get("account_no")
        request["sort_code"] = counterparty_data.get("sort_code")
    elif country == "AU" and currency == "AUD":
        request["account_no"] = counterparty_data.get("account_no")
        request["bsb"] = counterparty_data.get("bsb")
    elif currency == "EUR" or (country == "RO" and currency == "RON"):
        request["iban"] = counterparty_data.get("iban")
        request["recipient_country"] = country
        request["recipient_currency"] = currency
        if counterparty_data.get("bic"):
            request["bic"] = counterparty_data.get("bic")
    else:
        return None

    return request

def validate_account_name(counterparty_data, country, currency, recipient_type):
    """POST /account-name-validation — validate the account name (CoP/VoP).

    Returns True if the flow should proceed, False if it should stop.
    """
    cop_request = build_cop_request(counterparty_data, country, currency, recipient_type)

    if cop_request is None:
        print("CoP/VoP is not available for this destination.")
        return ask_to_proceed()

    print("Validating account name (CoP/VoP)...")
    try:
        response = requests.post(f"{API_URL}/account-name-validation", json=cop_request, headers=HEADERS)
        check_response(response)
    except ApiError:
        print("  CoP/VoP check failed.")
        return ask_to_proceed()

    result = response.json()
    result_code = result.get("result_code")
    print(f"  Result: {result_code}")

    if result_code in ("matched", "close_match"):
        if result_code == "close_match":
            if "company_name" in result:
                print(f"  Actual name: {result['company_name']}")
            elif "individual_name" in result:
                name = result["individual_name"]
                print(f"  Actual name: {name.get('first_name', '')} {name.get('last_name', '')}")
        return True

    if result_code == "not_matched":
        print("  The account name does not match.")
        return False

    # cannot_be_checked or temporarily_unavailable
    print(f"  The check could not be completed (result: {result_code}).")
    return ask_to_proceed()

def ask_to_proceed():
    """Ask the user whether to proceed after a non-blocking issue.

    Returns True if the user wants to proceed, False otherwise.
    """
    confirm = input("Proceed anyway? ([y]es/[n]o): ").strip().lower()
    if confirm in ("y", "yes"):
        return True
    print("Transfer cancelled.")
    return False

# =============================================
# Step 6: Create the counterparty
# =============================================

def create_counterparty(payload):
    """POST /counterparty — create the counterparty."""
    print("Creating counterparty...")
    response = requests.post(f"{API_URL}/counterparty", json=payload, headers=HEADERS)
    check_response(response)
    data = response.json()
    print(f"  Counterparty ID: {data.get('id')}")
    print(f"  State:           {data.get('state')}")
    return data

# =============================================
# Step 7: Extract payment method details
# =============================================

def extract_payment_method(counterparty_data):
    """Extract the counterparty ID, payment method ID, and human-readable details.

    Returns (counterparty_id, payment_method_id, receiver_key, pm_details).
    """
    counterparty_id = counterparty_data["id"]
    accounts = counterparty_data.get("accounts", [])
    cards = counterparty_data.get("cards", [])

    if accounts:
        account = accounts[0]
        pm_id = account["id"]
        pm_details = {
            "account_ref": account.get("iban") or account.get("account_no") or "Revolut account",
            "address": account.get("address", {}),
        }
        return counterparty_id, pm_id, "account_id", pm_details

    if cards:
        card = cards[0]
        pm_id = card["id"]
        pm_details = {
            "account_ref": f"Card *{card.get('last_digits', '????')} ({card.get('scheme', 'unknown')})",
            "address": {},
        }
        return counterparty_id, pm_id, "card_id", pm_details

    return counterparty_id, None, None, {
        "account_ref": "Revolut account (revtag)",
        "address": {},
    }

def build_receiver(counterparty_id, payment_method_id, receiver_key):
    """Build the receiver object for /pay endpoints."""
    receiver = {"counterparty_id": counterparty_id}
    if payment_method_id:
        receiver[receiver_key] = payment_method_id
    return receiver

# =============================================
# Step 8: Check transfer field requirements
# =============================================

def check_transfer_field_requirements(account_id, receiver):
    """POST /pay/fields — returns required and optional fields for the transfer."""
    print("Checking transfer field requirements...")
    payload = {"account_id": account_id, "receiver": receiver}
    response = requests.post(f"{API_URL}/pay/fields", json=payload, headers=HEADERS)
    check_response(response)
    data = response.json()
    required = [f["name"] for f in data.get("fields", []) if f.get("required")]
    optional = [f["name"] for f in data.get("fields", []) if not f.get("required")]
    print(f"  Required fields: {required}")
    print(f"  Optional fields: {optional}")
    return data

def build_payment_payload(transfer_fields, account_id, receiver, amount, currency, reference):
    """Build the payment payload from always required fields + conditionally available fields.

    Returns (payload, missing_required) where missing_required is a list of required
    field names that have no value.
    """
    payload = {
        "request_id": str(uuid.uuid4()),
        "account_id": account_id,
        "receiver": receiver,
        "amount": amount,
        "currency": currency,
    }

    provided = {"reference": reference}
    missing_required = []

    for field in transfer_fields.get("fields", []):
        name = field["name"]
        is_required = field.get("required", False)

        if name in provided and provided[name] is not None:
            payload[name] = provided[name]
        elif is_required:
            missing_required.append(name)

    return payload, missing_required

# =============================================
# Step 9: Get an indicative quote
# =============================================

def get_indicative_quote(payment_payload, currency):
    """POST /pay/indicative-quote — returns fees, exchange rate, and delivery time."""
    print("Fetching indicative quote...")
    payload = {k: v for k, v in payment_payload.items() if k != "request_id"}
    response = requests.post(f"{API_URL}/pay/indicative-quote", json=payload, headers=HEADERS)
    check_response(response)
    data = response.json()
    fee = data.get("fee", {})
    total = data.get("estimated_total", {})
    print(f"  Fee:   {fee.get('amount', 0)} {fee.get('currency', currency)}")
    print(f"  Total: {total.get('amount', 0)} {total.get('currency', currency)}")
    return data

# =============================================
# Step 10: Get own account details and print transfer summary
# =============================================

def get_account_details(account_id):
    """GET /accounts/{id}/bank-details — returns the sender's bank details."""
    print("Fetching account bank details...")
    response = requests.get(f"{API_URL}/accounts/{account_id}/bank-details", headers=HEADERS)
    check_response(response)
    data = response.json()
    entry = data[0] if isinstance(data, list) else data
    print(f"  Sender: {entry.get('beneficiary', 'Unknown')}")
    return entry

def print_transfer_summary(sender, counterparty_name, pm_details, pm_address, quote):
    """Display a human-readable transfer summary."""
    fee = quote.get("fee", {})
    total = quote.get("estimated_total", {})
    rate = quote.get("estimated_exchange_rate")
    arrival = quote.get("estimated_arrival", {})

    sender_name = sender.get("beneficiary", "Unknown")
    sender_addr = sender.get("beneficiary_address", {})
    sender_ref = sender.get("iban") or sender.get("account_no") or "N/A"

    sender_addr_str = ", ".join(filter(None, [
        sender_addr.get("street_line1"),
        sender_addr.get("city"),
        sender_addr.get("country"),
        sender_addr.get("postcode"),
    ])) if sender_addr else ""

    pm_addr_str = ", ".join(filter(None, [
        pm_address.get("street_line1"),
        pm_address.get("city"),
        pm_address.get("country"),
        pm_address.get("postcode"),
    ])) if pm_address else ""

    print("\n" + "=" * 50)
    print("TRANSFER SUMMARY")
    print("=" * 50)
    print(f"FROM: {sender_name}")
    print(f"      {sender_ref}")
    if sender_addr_str:
        print(f"      {sender_addr_str}")
    print(f"TO:   {counterparty_name}")
    print(f"      {pm_details['account_ref']}")
    if pm_addr_str:
        print(f"      {pm_addr_str}")
    print(f"\nREFERENCE:   {REFERENCE}")
    print("-" * 50)
    print(f"Amount:        {AMOUNT} {CURRENCY}")
    if fee.get("amount") is not None:
        print(f"Fee:           {fee.get('amount')} {fee.get('currency')}")
        if fee.get("breakdown"):
            for key, val in fee["breakdown"].items():
                print(f"  - {key}:     {val.get('amount')} {val.get('currency')}")
    if rate:
        print(f"Exchange rate: {rate}")
    if total.get("amount") is not None:
        print(f"Total cost:    {total.get('amount')} {total.get('currency')}")
    if arrival:
        print(f"Delivery:      {arrival.get('speed', '')} ({arrival.get('date', '')})")
    print("=" * 50)
    print("\n")

# =============================================
# Step 11: Check account balance
# =============================================

def check_balance(account_id, total_cost, total_currency):
    """GET /accounts/{id} — check if the balance covers the total cost.

    Returns True if sufficient, False otherwise.
    """
    print("Checking account balance...")
    response = requests.get(f"{API_URL}/accounts/{account_id}", headers=HEADERS)
    check_response(response)
    account = response.json()
    balance = account.get("balance", 0)
    balance_currency = account.get("currency")

    print(f"  Balance:      {balance} {balance_currency}")
    print(f"  Total cost:   {total_cost} {total_currency}")

    if balance_currency == total_currency and balance < total_cost:
        print("Insufficient funds.")
        return False

    return True

# =============================================
# Step 12: Ask for confirmation
# =============================================

def confirm_transfer():
    """Ask the user to confirm the transfer.

    Returns True if confirmed, False otherwise.
    """
    confirm = input("\nProceed with this transfer? ([y]es/[n]o): ").strip().lower()
    if confirm not in ("y", "yes"):
        print("Transfer cancelled.")
        return False
    return True

# =============================================
# Step 13: Execute the transfer
# =============================================

def make_transfer(payload):
    """POST /pay — execute the transfer."""
    print("Initiating transfer...")
    response = requests.post(f"{API_URL}/pay", json=payload, headers=HEADERS)
    check_response(response)
    result = response.json()
    print(f"  Transaction ID: {result.get('id')}")
    print(f"  State:          {result.get('state')}")
    return result

# =============================================
# Step 14: Check transaction status
# =============================================

def check_transaction_status(transaction_id):
    """GET /transaction/{id} — check the transaction status."""
    print("\nChecking transaction status...")
    response = requests.get(f"{API_URL}/transaction/{transaction_id}", headers=HEADERS)
    check_response(response)
    transaction = response.json()
    status = transaction.get("state")

    print(f"  Transaction ID:         {transaction_id}")
    print(f"  Transaction status:     {status}")

    if status == "pending":
        print("Please check again later.")

    return status

# =============================================
# Main flow
# =============================================

def run():
    # Print destination details
    counterparty_type = "Revolut (revtag)" if "revtag" in COUNTERPARTY_DATA else "bank account"
    print_destination_details(COUNTRY, CURRENCY, RECIPIENT_TYPE, counterparty_type)

    # 1. Check supported countries and currencies
    countries = check_supported_countries()

    # 2. Verify the destination is supported
    if not verify_destination_supported(countries, COUNTRY, CURRENCY):
        return

    # 3. Check counterparty field requirements
    field_requirements = check_counterparty_field_requirements(COUNTRY, CURRENCY, RECIPIENT_TYPE)

    # 4. Build the counterparty payload
    counterparty_payload = build_counterparty_payload(
        field_requirements, COUNTRY, CURRENCY, RECIPIENT_TYPE, COUNTERPARTY_DATA
    )

    # 5. Validate the account name (CoP/VoP)
    if not validate_account_name(COUNTERPARTY_DATA, COUNTRY, CURRENCY, RECIPIENT_TYPE):
        return

    # 6. Create the counterparty
    counterparty = create_counterparty(counterparty_payload)

    # 7. Extract payment method details
    counterparty_id, pm_id, receiver_key, pm_details = extract_payment_method(counterparty)
    receiver = build_receiver(counterparty_id, pm_id, receiver_key)
    print(f"Payment method ID: {pm_id or 'none (revtag-only)'}")

    # 8. Check transfer field requirements
    transfer_fields = check_transfer_field_requirements(ACCOUNT_ID, receiver)

    # Build the payment payload and check for missing required fields
    payment_payload, missing_required = build_payment_payload(
        transfer_fields, ACCOUNT_ID, receiver, AMOUNT, CURRENCY, REFERENCE
    )
    if missing_required:
        print(f"\nMissing required transfer fields: {missing_required}")
        return

    # 9. Get an indicative quote
    quote = get_indicative_quote(payment_payload, CURRENCY)

    # 10. Print transfer summary
    sender = get_account_details(ACCOUNT_ID)
    pm_address = pm_details.get("address", {})
    print_transfer_summary(sender, counterparty.get("name", "Unknown"), pm_details, pm_address, quote)

    # 11. Check that the account balance is sufficient
    total = quote.get("estimated_total", {})
    total_cost = total.get("amount", AMOUNT)
    total_currency = total.get("currency", CURRENCY)
    if not check_balance(ACCOUNT_ID, total_cost, total_currency):
        return

    # 12. Ask for confirmation
    if not confirm_transfer():
        return

    # 13. Execute the transfer
    transaction = make_transfer(payment_payload)
    print("Transfer initiated successfully!")

    # 14. Check transaction status
    check_transaction_status(transaction.get("id"))

if __name__ == "__main__":
    try:
        run()
    except ApiError:
        print("\nTransfer flow stopped due to API error.")
    except KeyboardInterrupt:
        print("\nTransfer flow cancelled by user.")
    except Exception as e:
        print(f"\nUnexpected error: {e}")
Sample output (UK / GBP, business counterparty)
==================================================
DESTINATION: GB / GBP
RECIPIENT TYPE: business
COUNTERPARTY TYPE: bank account
==================================================
Checking supported countries and currencies...
  Found 213 supported countries
  Destination GB/GBP is supported.
Checking counterparty field requirements...
  Required fields: ['company_name', 'account_no', 'sort_code', 'address']
  Optional fields: []
  Counterparty payload fields: ['profile_type', 'bank_country', 'currency', 'company_name', 'account_no', 'sort_code', 'address']
Validating account name (CoP/VoP)...
  Result: matched
Creating counterparty...
  Counterparty ID: b53fdd78-8d67-4f63-a103-eeeeef53cac8
  State:           created
Payment method ID: 5c9e171c-7e23-4d6a-b768-aaaaaba535f3
Checking transfer field requirements...
  Required fields: ['reference']
  Optional fields: ['charge_bearer']
Fetching indicative quote...
  Fee:   0 GBP
  Total: 1000 GBP
Fetching account bank details...
  Sender: Example Corp Ltd

==================================================
TRANSFER SUMMARY
==================================================
FROM: Example Corp Ltd
      31926819
      1 Westminster Way, London, GB, E14 5AB
TO:   Example Vendor Ltd
      12345678
      1 Canada Square, London, GB, E14 5AB

REFERENCE:   Invoice #12345
--------------------------------------------------
Amount:        1000 GBP
Fee:           0 GBP
Total cost:    1000 GBP
Delivery:      in_seconds (2026-07-07)
==================================================


Checking account balance...
  Balance:      50000 GBP
  Total cost:   1000 GBP
Initiating transfer...
  Transaction ID: 3a2b1c0d-9e8f-4a7b-6c5d-4e3f2a1b0c9d
  State:          pending
Transfer initiated successfully!

Checking transaction status...
  Transaction ID:         3a2b1c0d-9e8f-4a7b-6c5d-4e3f2a1b0c9d
  Transaction status:     pending
Please check again later.

2. Make a transfer to an existing counterparty

Sample Python script 2: saved counterparty

Adjust the configuration variables at the top to match your setup.

prepare_and_validate_transfer.py
import requests
import uuid

# =============================================
# Configuration: adjust these values as needed
# =============================================
API_URL = "https://b2b.revolut.com/api/1.0"
API_TOKEN = "YOUR_API_TOKEN"   # Replace with your own token

HEADERS = {
    "Authorization": f"Bearer {API_TOKEN}",
    "Content-Type": "application/json"
}

# Your account (the pocket you're sending from)
ACCOUNT_ID = "YOUR_ACCOUNT_ID"   # Replace with your own account ID

# The counterparty to send to
COUNTERPARTY_ID = "YOUR_COUNTERPARTY_ID"    # Replace with the ID of your counterparty

# Optional: if your counterparty has more than one payment method specified,
# uncomment the second RECEIVER block below and provide the ID of the appropriate payment method (account or card)
RECEIVER = None
#RECEIVER = {
#  "account_id": "YOUR_COUNTERPARTY_ACCOUNT_ID",   # Replace with the account ID of the selected payment method, set card_id to None
#  "card_id": None                                 # Replace with the card ID of the selected payment method, set account_id to None
#}

# Transfer details
AMOUNT = 1000
CURRENCY = "EUR"
REFERENCE = "Invoice #12345"

# Optional: address to set if the counterparty's payment method is missing one
UPDATED_ADDRESS = {
    "street_line1": "Hauptstraße 45",
    "city": "München",
    "region": "Bayern",
    "country": "DE",
    "postcode": "80331"
}

# =============================================
# Error handling
# =============================================

class ApiError(Exception):
    pass

def check_response(response):
    """Print error details and raise ApiError if the response failed."""
    if response.ok:
        return
    try:
        error_data = response.json()
        print(f"\nAPI Error (HTTP {response.status_code})")
        print(f"  Code:     {error_data.get('code', 'N/A')}")
        print(f"  Message:  {error_data.get('message', 'N/A')}")
        print(f"  Error ID: {error_data.get('error_id', 'N/A')}")
    except Exception:
        print(f"\nAPI Error (HTTP {response.status_code}): {response.text}")
    raise ApiError(f"Request failed with status {response.status_code}")

# =============================================
# Print destination details
# =============================================

def print_destination_details(country, currency, recipient_type, counterparty_type):
    """Print a summary of the destination details."""
    print("=" * 50)
    print(f"DESTINATION: {country} / {currency}")
    print(f"RECIPIENT TYPE: {recipient_type}")
    print(f"COUNTERPARTY TYPE: {counterparty_type}")
    print("=" * 50)

# =============================================
# Step 1: Retrieve the counterparty
# =============================================

def retrieve_counterparty(counterparty_id):
    """GET /counterparty/{id} — returns the counterparty data."""
    print(f"Retrieving counterparty {counterparty_id}...")
    response = requests.get(f"{API_URL}/counterparty/{counterparty_id}", headers=HEADERS)
    check_response(response)
    data = response.json()
    print(f"  Name: {data.get('name', 'Unknown')}")
    return data

# =============================================
# Step 2: Resolve the payment method
# =============================================

def resolve_payment_method(counterparty_data):
    """Determine which payment method to use.

    If RECEIVER["account_id"] or RECEIVER["card_id"] is set, use it directly.
    If RECEIVER not set, but counterparty has only one payment method, use it.
    Otherwise, ask for payment method ID.

    Returns (payment_method_id, receiver_key) where receiver_key is
    "account_id", "card_id", or None.
    """
    accounts = counterparty_data.get("accounts", [])
    cards = counterparty_data.get("cards", [])

    if RECEIVER:
        if RECEIVER["account_id"]:
            return RECEIVER["account_id"], "account_id"
        if RECEIVER["card_id"]:
            return RECEIVER["card_id"], "card_id"
        else:
            print("Invalid recipient. Please provide a valid payment method ID (account_id or card_id).")
            return None, None

    if not accounts and not cards:
        return None, None

    if accounts and len(accounts) == 1:
        return accounts[0]["id"], "account_id"

    if cards and len(cards) == 1:
        return cards[0]["id"], "card_id"

    print("Please provide a payment method (account_id or card_id)")

    return None, None

def build_receiver(counterparty_id, payment_method_id, receiver_key):
    """Build the receiver object for /pay endpoints."""
    receiver = {"counterparty_id": counterparty_id}
    if payment_method_id:
        receiver[receiver_key] = payment_method_id
    return receiver

# =============================================
# Step 3: Check transfer field requirements
# =============================================

def check_transfer_fields(account_id, receiver):
    """POST /pay/fields — returns required and optional fields for the transfer."""
    print("Checking transfer field requirements...")
    payload = {"account_id": account_id, "receiver": receiver}
    response = requests.post(f"{API_URL}/pay/fields", json=payload, headers=HEADERS)
    check_response(response)
    data = response.json()
    required = [f["name"] for f in data.get("fields", []) if f.get("required")]
    optional = [f["name"] for f in data.get("fields", []) if not f.get("required")]
    print(f"  Required fields: {required}")
    print(f"  Optional fields: {optional}")
    return data

def build_payment_payload(transfer_fields, account_id, receiver, amount, currency, reference):
    """Build the payment payload from always required fields + conditionally available fields.

    Returns (payload, missing_required) where missing_required is a list of required
    field names that have no value.
    """
    payload = {
        "request_id": str(uuid.uuid4()),
        "account_id": account_id,
        "receiver": receiver,
        "amount": amount,
        "currency": currency,
    }

    provided = {"reference": reference}
    missing_required = []

    for field in transfer_fields.get("fields", []):
        name = field["name"]
        is_required = field.get("required", False)

        if name in provided and provided[name] is not None:
            payload[name] = provided[name]
        elif is_required:
            missing_required.append(name)

    return payload, missing_required

# =============================================
# Step 4: Check and update the counterparty's address
# =============================================

def is_address_complete(address):
    """Check if an address has the minimum required fields."""
    if not address:
        return False
    required = ["street_line1", "city", "country"]
    return all(address.get(field) for field in required)

def check_and_update_address(counterparty_data, payment_method_id, receiver_key):
    """If the payment method's address is missing or incomplete, update it.

    Returns the (possibly updated) address.
    Applicable only to accounts.
    """
    # Check if the payment method is an account; if not, exit
    if receiver_key != "account_id":
        return {}

    accounts = counterparty_data.get("accounts", [])

    # Find the current address on the payment method
    current_address = {}
    for account in accounts:
        if account.get("id") == payment_method_id:
            current_address = account.get("address", {})
            break

    if is_address_complete(current_address):
        print("Address is complete.")
        return current_address

    print("Address is missing or incomplete.")
    if payment_method_id:
        print("Updating address...")
        url = f"{API_URL}/counterparties/{counterparty_data['id']}/payment-methods/{payment_method_id}"
        response = requests.patch(url, json={"address": UPDATED_ADDRESS}, headers=HEADERS)
        check_response(response)
        print("  Address updated.")
        return UPDATED_ADDRESS

    return {}

# =============================================
# Step 5: Get payment method details
# =============================================

def get_payment_method_details(counterparty_data, payment_method_id):
    """Extract human-readable details for the payment method.

    Returns a dict with "account_ref" and "address".
    """
    accounts = counterparty_data.get("accounts", [])
    cards = counterparty_data.get("cards", [])

    for account in accounts:
        if account.get("id") == payment_method_id:
            return {
                "account_ref": account.get("iban") or account.get("account_no") or "Revolut account",
                "address": account.get("address", {}),
            }

    for card in cards:
        if card.get("id") == payment_method_id:
            return {
                "account_ref": f"Card *{card.get('last_digits', '????')} ({card.get('scheme', 'unknown')})",
                "address": {},
            }

    return {
        "account_ref": "Revolut account (revtag)",
        "address": {},
    }

# =============================================
# Step 6: Get own account details
# =============================================

def get_account_details(account_id):
    """GET /accounts/{id}/bank-details — returns the sender's bank details."""
    print("Fetching account bank details...")
    response = requests.get(f"{API_URL}/accounts/{account_id}/bank-details", headers=HEADERS)
    check_response(response)
    data = response.json()
    # bank-details returns an array — use the first entry
    entry = data[0] if isinstance(data, list) else data
    print(f"  Sender: {entry.get('beneficiary', 'Unknown')}")
    return entry

# =============================================
# Step 7: Get an indicative quote
# =============================================

def get_indicative_quote(payment_payload, currency):
    """POST /pay/indicative-quote — returns fees, exchange rate, and delivery time."""
    print("Fetching indicative quote...")
    payload = {k: v for k, v in payment_payload.items() if k != "request_id"}
    response = requests.post(f"{API_URL}/pay/indicative-quote", json=payload, headers=HEADERS)
    check_response(response)
    data = response.json()
    fee = data.get("fee", {})
    total = data.get("estimated_total", {})
    print(f"  Fee:   {fee.get('amount', 0)} {fee.get('currency', currency)}")
    print(f"  Total: {total.get('amount', 0)} {total.get('currency', currency)}")
    return data

# =============================================
# Step 8: Print transfer summary
# =============================================

def print_transfer_summary(sender, counterparty_data, pm_details, pm_address, quote):
    """Display a human-readable transfer summary."""
    fee = quote.get("fee", {})
    total = quote.get("estimated_total", {})
    rate = quote.get("estimated_exchange_rate")
    arrival = quote.get("estimated_arrival", {})

    sender_name = sender.get("beneficiary", "Unknown")
    sender_addr = sender.get("beneficiary_address", {})
    sender_ref = sender.get("iban") or sender.get("account_no") or "N/A"

    sender_addr_str = ", ".join(filter(None, [
        sender_addr.get("street_line1"),
        sender_addr.get("city"),
        sender_addr.get("country"),
        sender_addr.get("postcode"),
    ])) if sender_addr else ""

    pm_addr_str = ", ".join(filter(None, [
        pm_address.get("street_line1"),
        pm_address.get("city"),
        pm_address.get("country"),
        pm_address.get("postcode"),
    ])) if pm_address else ""

    print("\n" + "=" * 50)
    print("TRANSFER SUMMARY")
    print("=" * 50)
    print(f"FROM: {sender_name}")
    print(f"      {sender_ref}")
    if sender_addr_str:
        print(f"      {sender_addr_str}")
    print(f"TO:   {counterparty_data.get('name', 'Unknown')}")
    print(f"      {pm_details['account_ref']}")
    if pm_addr_str:
        print(f"      {pm_addr_str}")
    print(f"\nREFERENCE:   {REFERENCE}")
    print("-" * 50)
    print(f"Amount:        {AMOUNT} {CURRENCY}")
    if fee.get("amount") is not None:
        print(f"Fee:           {fee.get('amount')} {fee.get('currency')}")
        if fee.get("breakdown"):
            for key, val in fee["breakdown"].items():
                print(f"  - {key}:     {val.get('amount')} {val.get('currency')}")
    if rate:
        print(f"Exchange rate: {rate}")
    if total.get("amount") is not None:
        print(f"Total cost:    {total.get('amount')} {total.get('currency')}")
    if arrival:
        print(f"Delivery:      {arrival.get('speed', '')} ({arrival.get('date', '')})")
    print("=" * 50)
    print("\n")

# =============================================
# Step 9: Check account balance
# =============================================

def check_balance(account_id, total_cost, total_currency):
    """GET /accounts/{id} — check if the balance covers the total cost.

    Returns True if sufficient, False otherwise.
    """
    print("Checking account balance...")
    response = requests.get(f"{API_URL}/accounts/{account_id}", headers=HEADERS)
    check_response(response)
    account = response.json()
    balance = account.get("balance", 0)
    balance_currency = account.get("currency")

    print(f"  Balance:      {balance} {balance_currency}")
    print(f"  Total cost:   {total_cost} {total_currency}")

    if balance_currency == total_currency and balance < total_cost:
        print("Insufficient funds.")
        return False

    return True

# =============================================
# Step 10: Ask for confirmation
# =============================================

def confirm_transfer():
    """Ask the user to confirm the transfer.

    Returns True if confirmed, False otherwise.
    """
    confirm = input("\nProceed with this transfer? ([y]es/[n]o): ").strip().lower()
    if confirm not in ("y", "yes"):
        print("Transfer cancelled.")
        return False
    return True

# =============================================
# Step 11: Execute the transfer
# =============================================

def make_transfer(payload):
    """POST /pay — execute the transfer."""
    print("Initiating transfer...")
    response = requests.post(f"{API_URL}/pay", json=payload, headers=HEADERS)
    check_response(response)
    result = response.json()
    print(f"  Transaction ID: {result.get('id')}")
    print(f"  State:          {result.get('state')}")
    return result

# =============================================
# Step 12: Check transaction status
# =============================================

def check_transaction_status(transaction_id):
    """GET /transaction/{id} — check the transaction status."""
    print("\nChecking transaction status...")
    response = requests.get(f"{API_URL}/transaction/{transaction_id}", headers=HEADERS)
    check_response(response)
    transaction = response.json()
    status = transaction.get("state")

    print(f"  Transaction ID:         {transaction_id}")
    print(f"  Transaction status:     {status}")

    if status == "pending":
        print("Please check again later.")

    return status

# =============================================
# Main flow
# =============================================

def run():
    # 1. Retrieve the counterparty
    counterparty = retrieve_counterparty(COUNTERPARTY_ID)

    # 2. Resolve the payment method and build the receiver
    payment_method_id, receiver_key = resolve_payment_method(counterparty)
    receiver = build_receiver(counterparty["id"], payment_method_id, receiver_key)
    print(f"Payment method ID: {payment_method_id or 'none (revtag-only)'}")

    # Print destination details
    recipient_type = counterparty.get("profile_type", "unknown")
    accounts = counterparty.get("accounts", [])
    cards = counterparty.get("cards", [])
    is_revolut = "revtag" in counterparty
    if receiver_key == "account_id":
        for account in accounts:
            if account.get("id") == payment_method_id:
                bank_country = account.get("bank_country", "unknown")
                account_currency = account.get("currency", "unknown")
                break
        else:
            bank_country = "unknown"
            account_currency = "unknown"
        counterparty_type = "Revolut account" if is_revolut else "bank account"
    elif receiver_key == "card_id":
        bank_country = "N/A"
        account_currency = "N/A"
        counterparty_type = "card"
    else:
        bank_country = counterparty.get("country", "unknown")
        account_currency = "N/A"
        counterparty_type = "Revolut (revtag)"
    print_destination_details(bank_country, account_currency, recipient_type, counterparty_type)

    # 3. Check transfer field requirements
    transfer_fields = check_transfer_fields(ACCOUNT_ID, receiver)

    # Build the payment payload and check for missing required fields
    payment_payload, missing_required = build_payment_payload(
        transfer_fields, ACCOUNT_ID, receiver, AMOUNT, CURRENCY, REFERENCE
    )
    if missing_required:
        print(f"\nMissing required transfer fields: {missing_required}")
        return

    # 4. Check and update the counterparty's address
    pm_address = check_and_update_address(counterparty, payment_method_id, receiver_key)

    # 5. Get payment method details
    pm_details = get_payment_method_details(counterparty, payment_method_id)

    # 6. Get own account details
    sender = get_account_details(ACCOUNT_ID)

    # 7. Get an indicative quote
    quote = get_indicative_quote(payment_payload, CURRENCY)

    # 8. Print transfer summary
    print_transfer_summary(sender, counterparty, pm_details, pm_address, quote)

    # 9. Check that the account balance is sufficient
    total = quote.get("estimated_total", {})
    total_cost = total.get("amount", AMOUNT)
    total_currency = total.get("currency", CURRENCY)
    if not check_balance(ACCOUNT_ID, total_cost, total_currency):
        return

    # 10. Ask for confirmation
    if not confirm_transfer():
        return

    # 11. Execute the transfer
    transaction = make_transfer(payment_payload)
    print("Transfer initiated successfully!")

    # 12. Check transaction status
    check_transaction_status(transaction.get("id"))

if __name__ == "__main__":
    try:
        run()
    except ApiError:
        print("\nTransfer flow stopped due to API error.")
    except KeyboardInterrupt:
        print("\nTransfer flow cancelled by user.")
    except Exception as e:
        print(f"\nUnexpected error: {e}")
Sample output (DE / EUR, business counterparty, GBP source account)
Retrieving counterparty 239d6f1f-2222-4ae6-1111-5cc603341b4e...
  Name: Beispiel GmbH
Payment method ID: b209281b-aakk-9911-8d83-ac72469cef8f
==================================================
DESTINATION: DE / EUR
RECIPIENT TYPE: business
COUNTERPARTY TYPE: bank account
==================================================
Checking transfer field requirements...
  Required fields: ['reference']
  Optional fields: ['charge_bearer']
Address is missing or incomplete.
Updating address...
  Address updated.
Fetching account bank details...
  Sender: Example Corp Ltd
Fetching indicative quote...
  Fee:   3 GBP
  Total: 1167.58 GBP

==================================================
TRANSFER SUMMARY
==================================================
FROM: Example Corp Ltd
      31926819
      1 Westminster Way, London, GB, E14 5AB
TO:   Beispiel GmbH
      DE89370400440532013000
      Hauptstraße 45, München, DE, 80331

REFERENCE:   Invoice #12345
--------------------------------------------------
Amount:        1000 EUR
Fee:           3 GBP
Exchange rate: 1.16458
Total cost:    1167.58 GBP
Delivery:      by_date (2026-07-09)
==================================================


Checking account balance...
  Balance:      50000 GBP
  Total cost:   1167.58 GBP

Proceed with this transfer? ([y]es/[n]o): y
Initiating transfer...
  Transaction ID: 7c8d9e0f-1a2b-3c4d-5e6f-7a8b9c0d1e2f
  State:          pending
Transfer initiated successfully!

Checking transaction status...
  Transaction ID:         7c8d9e0f-1a2b-3c4d-5e6f-7a8b9c0d1e2f
  Transaction status:     pending
Please check again later.

3. Interactive: add or fetch a counterparty, then prepare, validate, and make a transfer

Sample Python script 3: interactive, extended flow

Interactive script that combines creating a new counterparty or using an existing one, with full validation and transfer flow. Asks the user at each step — no hardcoded counterparty data, account ID, or transfer details required. For existing counterparties, offers a two-stage selection: enter the ID directly or browse the paginated list. For the source account, offers the same two-stage selection: enter the ID or choose from the list of active accounts. Handles currency mismatches (switch source account with FX-converted amount suggestion, or switch transfer currency with FX conversion), multiple payment methods, and CoP/VoP validation for both new and existing counterparties.

Each step is a separate function for clarity.

Adjust the API URL and token at the top to match your setup.

The script can be run interactively, or you can populate it with data before you run it. For the second option, adjust the configuration variables at the top to match your setup.

interactive_transfer.py
import requests
import uuid

# =============================================
# Configuration: adjust these values as needed
# =============================================
API_URL = "https://b2b.revolut.com/api/1.0"
API_TOKEN = "YOUR_API_TOKEN"   # Replace with your own token

HEADERS = {
    "Authorization": f"Bearer {API_TOKEN}",
    "Content-Type": "application/json"
}

# Optional: pre-fill new counterparty details.
# Leave a field empty ("") to be prompted for it interactively.
# Set COUNTERPARTY_DATA to a full dict to override individual fields entirely.
CP_COUNTRY = ""
CP_CURRENCY = ""
CP_RECIPIENT_TYPE = ""       # "business" or "personal"
CP_COMPANY_NAME = ""
CP_FIRST_NAME = ""
CP_LAST_NAME = ""
CP_ACCOUNT_NO = ""
CP_SORT_CODE = ""
CP_IBAN = ""
CP_BIC = ""
CP_ROUTE = ""
CP_ROUTING_NUMBER = ""
CP_REVTAG = ""
CP_ADDRESS = {
    "street_line1": "",
    "city": "",
    "region": "",
    "country": "",
    "postcode": ""
}
COUNTERPARTY_DATA = None     # Set to a full dict to override the fields above

# =============================================
# Error handling
# =============================================

class ApiError(Exception):
    pass

def check_response(response):
    """Print error details and raise ApiError if the response failed."""
    if response.ok:
        return
    try:
        error_data = response.json()
        print(f"\nAPI Error (HTTP {response.status_code})")
        print(f"  Code:     {error_data.get('code', 'N/A')}")
        print(f"  Message:  {error_data.get('message', 'N/A')}")
        print(f"  Error ID: {error_data.get('error_id', 'N/A')}")
    except Exception:
        print(f"\nAPI Error (HTTP {response.status_code}): {response.text}")
    raise ApiError(f"Request failed with status {response.status_code}")

# =============================================
# Print destination details
# =============================================

def print_destination_details(country, currency, recipient_type, counterparty_type):
    """Print a summary of the destination details."""
    print("=" * 50)
    print(f"DESTINATION: {country} / {currency}")
    print(f"RECIPIENT TYPE: {recipient_type}")
    print(f"COUNTERPARTY TYPE: {counterparty_type}")
    print("=" * 50)

# =============================================
# Helper: ask user for input with default
# =============================================

def ask(prompt, default=""):
    """Prompt the user for input, with an optional default value."""
    if default:
        prompt = f"{prompt} [{default}]: "
    else:
        prompt = f"{prompt}: "
    value = input(prompt).strip()
    return value if value else default

# =============================================
# Step 1: Choose new or existing counterparty
# =============================================

def choose_counterparty_mode():
    """Ask the user whether to create a new counterparty or use an existing one.

    Returns "new" or "existing".
    """
    print("\n1. Create a new counterparty")
    print("2. Use an existing counterparty")
    choice = ask("Select an option (1/2)")
    while choice not in ("1", "2"):
        choice = ask("Invalid choice. Select 1 or 2")
    return "new" if choice == "1" else "existing"

# =============================================
# Step 2: Check supported countries and currencies
# =============================================

def check_supported_countries():
    """GET /counterparties/countries — returns supported country/currency pairs."""
    print("Checking supported countries and currencies...")
    response = requests.get(f"{API_URL}/counterparties/countries", headers=HEADERS)
    check_response(response)
    data = response.json()
    countries = data.get("countries", [])
    print(f"  Found {len(countries)} supported countries")
    return countries

# =============================================
# Step 3: Verify the destination is supported
# =============================================

def verify_destination_supported(countries, country, currency):
    """Check if the desired country/currency combination is supported.

    Returns True if supported, False otherwise.
    """
    for entry in countries:
        if entry.get("country") == country and currency in entry.get("currencies", []):
            print(f"  Destination {country}/{currency} is supported.")
            return True
    print(f"  Destination {country}/{currency} is not supported.")
    return False

# =============================================
# Step 4: Check counterparty field requirements
# =============================================

def check_counterparty_field_requirements(country, currency, recipient_type):
    """GET /counterparties/fields — returns required and applicable fields."""
    print("Checking counterparty field requirements...")
    params = {"country": country, "currency": currency, "recipient_type": recipient_type}
    response = requests.get(f"{API_URL}/counterparties/fields", params=params, headers=HEADERS)
    check_response(response)
    data = response.json()
    required = [f["name"] for f in data.get("fields", []) if f.get("required")]
    optional = [f["name"] for f in data.get("fields", []) if not f.get("required")]
    print(f"  Required fields: {required}")
    print(f"  Optional fields: {optional}")
    return data

# =============================================
# Step 5: Build the counterparty payload
# =============================================

def build_counterparty_payload(field_requirements, country, currency, recipient_type):
    """Build the counterparty payload from config vars or interactive input.

    Uses COUNTERPARTY_DATA if set, otherwise individual CP_* vars.
    Prompts interactively for any field that is empty.
    """
    if COUNTERPARTY_DATA:
        print("  Using provided COUNTERPARTY_DATA.")
        payload = dict(COUNTERPARTY_DATA)
        payload.setdefault("profile_type", recipient_type)
        payload.setdefault("bank_country", country)
        payload.setdefault("currency", currency)
        return payload

    payload = {
        "profile_type": recipient_type,
        "bank_country": country,
        "currency": currency,
    }

    config_map = {
        "company_name": CP_COMPANY_NAME,
        "first_name": CP_FIRST_NAME,
        "last_name": CP_LAST_NAME,
        "account_no": CP_ACCOUNT_NO,
        "sort_code": CP_SORT_CODE,
        "iban": CP_IBAN,
        "bic": CP_BIC,
        "route": CP_ROUTE,
        "routing_number": CP_ROUTING_NUMBER,
        "revtag": CP_REVTAG,
    }

    for field in field_requirements.get("fields", []):
        name = field["name"]
        default = config_map.get(name, "")

        if name == "address":
            address = {}
            for addr_field in ["street_line1", "city", "region", "country", "postcode"]:
                val = CP_ADDRESS.get(addr_field, "")
                val = ask(f"  Enter {addr_field}", val)
                if val:
                    address[addr_field] = val
            if address:
                payload["address"] = address
        else:
            val = ask(f"  Enter {name}", default)
            if val:
                payload[name] = val

    # Group first_name/last_name into individual_name for personal counterparties
    if recipient_type == "personal":
        individual_name = {}
        if "first_name" in payload:
            individual_name["first_name"] = payload.pop("first_name")
        if "last_name" in payload:
            individual_name["last_name"] = payload.pop("last_name")
        if individual_name:
            payload["individual_name"] = individual_name

    print(f"  Counterparty payload fields: {list(payload.keys())}")
    return payload

# =============================================
# Step 6: Validate the account name (CoP/VoP)
# =============================================

def build_cop_request(payload, country, currency, recipient_type):
    """Build the CoP request body based on the destination region.

    Returns the request dict, or None if CoP is not available for this destination.
    """
    request = {}

    if recipient_type == "business":
        request["company_name"] = payload.get("company_name")
    else:
        individual = payload.get("individual_name", {})
        request["individual_name"] = {
            "first_name": individual.get("first_name") or payload.get("first_name"),
            "last_name": individual.get("last_name") or payload.get("last_name"),
        }

    if country == "GB" and currency == "GBP":
        request["account_no"] = payload.get("account_no")
        request["sort_code"] = payload.get("sort_code")
    elif country == "AU" and currency == "AUD":
        request["account_no"] = payload.get("account_no")
        request["bsb"] = payload.get("bsb")
    elif currency == "EUR" or (country == "RO" and currency == "RON"):
        request["iban"] = payload.get("iban")
        request["recipient_country"] = country
        request["recipient_currency"] = currency
        if payload.get("bic"):
            request["bic"] = payload.get("bic")
    else:
        return None

    return request

def ask_to_proceed():
    """Ask the user whether to proceed after a non-blocking issue.

    Returns True if the user wants to proceed, False otherwise.
    """
    confirm = input("Proceed anyway? ([y]es/[n]o): ").strip().lower()
    if confirm in ("y", "yes"):
        return True
    print("Transfer cancelled.")
    return False

def validate_account_name(payload, country, currency, recipient_type):
    """POST /account-name-validation — validate the account name (CoP/VoP).

    Returns True if the flow should proceed, False if it should stop.
    """
    cop_request = build_cop_request(payload, country, currency, recipient_type)

    if cop_request is None:
        print("CoP/VoP is not available for this destination.")
        return ask_to_proceed()

    print("Validating account name (CoP/VoP)...")
    try:
        response = requests.post(f"{API_URL}/account-name-validation", json=cop_request, headers=HEADERS)
        check_response(response)
    except ApiError:
        print("  CoP/VoP check failed.")
        return ask_to_proceed()

    result = response.json()
    result_code = result.get("result_code")
    print(f"  Result: {result_code}")

    if result_code in ("matched", "close_match"):
        if result_code == "close_match":
            if "company_name" in result:
                print(f"  Actual name: {result['company_name']}")
            elif "individual_name" in result:
                name = result["individual_name"]
                print(f"  Actual name: {name.get('first_name', '')} {name.get('last_name', '')}")
        return True

    if result_code == "not_matched":
        print("  The account name does not match.")
        return False

    print(f"  The check could not be completed (result: {result_code}).")
    return ask_to_proceed()

# =============================================
# Step 7: Create the counterparty
# =============================================

def create_counterparty(payload):
    """POST /counterparty — create the counterparty."""
    print("Creating counterparty...")
    response = requests.post(f"{API_URL}/counterparty", json=payload, headers=HEADERS)
    check_response(response)
    data = response.json()
    print(f"  Counterparty ID: {data.get('id')}")
    print(f"  State:           {data.get('state')}")
    return data

# =============================================
# Step 8: Retrieve an existing counterparty
# =============================================

def retrieve_existing_counterparty():
    """Ask the user how to identify an existing counterparty, then retrieve it.

    Two options:
      1. Enter the counterparty ID directly.
      2. Browse the list of counterparties (paginated) and select one.

    Returns the selected counterparty's data (full GET /counterparty/{id} response).
    """
    print("\n1. Enter the counterparty ID")
    print("2. Choose from the list of counterparties")
    choice = ask("Select an option (1/2)")
    while choice not in ("1", "2"):
        choice = ask("Invalid choice. Select 1 or 2")

    if choice == "1":
        cp_id = ask("Enter the counterparty ID")
        return retrieve_counterparty(cp_id)

    # Browse the list (paginated)
    print("Retrieving your counterparties...")
    all_counterparties = []
    created_before = None

    while True:
        params = {"limit": 100}
        if created_before:
            params["created_before"] = created_before
        response = requests.get(f"{API_URL}/counterparties", params=params, headers=HEADERS)
        check_response(response)
        page = response.json()

        if not page:
            break

        all_counterparties.extend(page)

        # Check if there are more pages
        if len(page) < 100:
            break

        # Use the last item's created_at as the cursor for the next page
        created_before = page[-1].get("created_at")
        if not created_before:
            break

        more = ask(f"\n  Loaded {len(all_counterparties)} counterparties so far. Load more? (y/n)", "n")
        if more.lower() != "y":
            break

    if not all_counterparties:
        print("  No counterparties found.")
        return None

    print(f"\n  Found {len(all_counterparties)} counterpart{'y' if len(all_counterparties) == 1 else 'ies'}:\n")
    for i, cp in enumerate(all_counterparties):
        name = cp.get("name", "Unknown")
        profile = cp.get("profile_type", "?")
        cp_type = "Revolut (revtag)" if "revtag" in cp else "bank account" if cp.get("accounts") else "card" if cp.get("cards") else "unknown"

        # Summarise payment methods
        methods = []
        for acc in cp.get("accounts", []):
            cur = acc.get("currency", "?")
            country = acc.get("bank_country", "?")
            methods.append(f"{cur} ({country})")
        for card in cp.get("cards", []):
            methods.append(f"card ({card.get('currency', '?')})")
        if "revtag" in cp:
            methods.append(f"@{cp['revtag']}")
        methods_str = ", ".join(methods) if methods else "no methods"

        print(f"  {i + 1}. {name} [{profile}, {cp_type}] — {methods_str}")

    choice = ask(f"\n  Select a counterparty (1-{len(all_counterparties)})")
    while not choice.isdigit() or int(choice) < 1 or int(choice) > len(all_counterparties):
        choice = ask(f"  Invalid choice. Enter a number 1-{len(all_counterparties)}")

    selected = all_counterparties[int(choice) - 1]
    cp_id = selected["id"]
    print(f"\n  Selected: {selected.get('name', 'Unknown')} ({cp_id})")
    return retrieve_counterparty(cp_id)


def retrieve_counterparty(counterparty_id):
    """GET /counterparty/{id} — returns the counterparty data."""
    print(f"Retrieving counterparty {counterparty_id}...")
    response = requests.get(f"{API_URL}/counterparty/{counterparty_id}", headers=HEADERS)
    check_response(response)
    data = response.json()
    print(f"  Name: {data.get('name', 'Unknown')}")
    return data

# =============================================
# Step 9: Resolve the payment method
# =============================================

def resolve_payment_method(counterparty_data):
    """Determine which payment method to use.

    If the counterparty has a single payment method, use it directly.
    If multiple, list them and ask the user to select.
    If none (revtag-only), return (None, None).

    Returns (payment_method_id, receiver_key).
    """
    accounts = counterparty_data.get("accounts", [])
    cards = counterparty_data.get("cards", [])

    if not accounts and not cards:
        return None, None

    all_methods = []
    for account in accounts:
        all_methods.append(("account_id", account))
    for card in cards:
        all_methods.append(("card_id", card))

    if len(all_methods) == 1:
        receiver_key, method = all_methods[0]
        print(f"  Single payment method found: {method.get('id')}")
        return method["id"], receiver_key

    print("\nMultiple payment methods available:")
    for i, (key, method) in enumerate(all_methods, 1):
        if key == "account_id":
            method_type = "bank account"
            ref = method.get("iban") or method.get("account_no") or "Revolut account"
            currency = method.get("currency", "N/A")
            print(f"  {i}. [{method_type}] {method.get('name', 'N/A')}{ref} ({currency})")
            print(f"     ID: {method['id']}")
        else:
            method_type = "card"
            ref = f"*{method.get('last_digits', '????')} ({method.get('scheme', 'unknown')})"
            print(f"  {i}. [{method_type}] {method.get('name', 'N/A')}{ref}")
            print(f"     ID: {method['id']}")
    print(f"  {len(all_methods) + 1}. Cancel")

    choice = ask(f"Select a payment method (1-{len(all_methods) + 1})")
    while not choice.isdigit() or int(choice) < 1 or int(choice) > len(all_methods) + 1:
        choice = ask(f"Invalid choice. Select 1-{len(all_methods) + 1}")

    idx = int(choice) - 1
    if idx == len(all_methods):
        print("Transfer cancelled.")
        return None, None

    receiver_key, method = all_methods[idx]
    return method["id"], receiver_key

def build_receiver(counterparty_id, payment_method_id, receiver_key):
    """Build the receiver object for /pay endpoints."""
    receiver = {"counterparty_id": counterparty_id}
    if payment_method_id:
        receiver[receiver_key] = payment_method_id
    return receiver

# =============================================
# Step 10: Get destination details
# =============================================

def get_destination_details(counterparty_data, payment_method_id, receiver_key):
    """Extract destination details from the resolved payment method.

    Returns (country, currency, recipient_type, counterparty_type).
    """
    recipient_type = counterparty_data.get("profile_type", "unknown")
    accounts = counterparty_data.get("accounts", [])
    is_revolut = "revtag" in counterparty_data

    if receiver_key == "account_id":
        for account in accounts:
            if account.get("id") == payment_method_id:
                bank_country = account.get("bank_country", "unknown")
                account_currency = account.get("currency", "unknown")
                break
        else:
            bank_country = "unknown"
            account_currency = "unknown"
        counterparty_type = "Revolut account" if is_revolut else "bank account"
    elif receiver_key == "card_id":
        bank_country = "N/A"
        account_currency = "N/A"
        counterparty_type = "card"
    else:
        bank_country = counterparty_data.get("country", "unknown")
        account_currency = "N/A"
        counterparty_type = "Revolut (revtag)"

    return bank_country, account_currency, recipient_type, counterparty_type

# =============================================
# Step 11: Ask for source account and transfer details
# =============================================

def ask_source_account():
    """Ask the user how to identify their source account, then retrieve it.

    Two options:
      1. Enter the account ID directly.
      2. Choose from the list of active accounts.

    Returns the account ID.
    """
    print("\n1. Enter your account ID (the pocket to send from)")
    print("2. Choose from the list of your accounts")
    choice = ask("Select an option (1/2)")
    while choice not in ("1", "2"):
        choice = ask("Invalid choice. Select 1 or 2")

    if choice == "1":
        return ask("Enter your account ID")

    # List accounts and let the user pick
    print("Retrieving your accounts...")
    response = requests.get(f"{API_URL}/accounts", headers=HEADERS)
    check_response(response)
    accounts = response.json()

    active = [a for a in accounts if a.get("state") == "active"]

    if not active:
        print("  No active accounts found.")
        return ask("Enter your account ID manually")

    print(f"\n  Found {len(active)} active account{'s' if len(active) != 1 else ''}:\n")
    for i, acc in enumerate(active):
        name = acc.get("name", "Unknown")
        cur = acc.get("currency", "?")
        balance = acc.get("balance", "?")
        print(f"  {i + 1}. {name} [{cur}] — balance: {balance}")

    choice = ask(f"\n  Select an account (1-{len(active)})")
    while not choice.isdigit() or int(choice) < 1 or int(choice) > len(active):
        choice = ask(f"  Invalid choice. Enter a number 1-{len(active)}")

    selected = active[int(choice) - 1]
    acc_id = selected["id"]
    print(f"\n  Selected: {selected.get('name', 'Unknown')} ({acc_id})")
    return acc_id

def ask_transfer_details():
    """Ask the user for the transfer amount and currency.

    Returns (amount, currency).
    """
    amount_str = ask("Enter the amount to transfer")
    while not amount_str or not amount_str.replace(".", "").replace(",", "").isdigit():
        amount_str = ask("Invalid amount. Enter a numeric value")
    amount = float(amount_str.replace(",", ""))
    currency = ask("Enter the transfer currency (e.g. GBP)").upper()
    return amount, currency

# =============================================
# Step 12: Check currency mismatch
# =============================================

def get_account_currency(account_id):
    """GET /accounts/{id} — returns the account's currency."""
    response = requests.get(f"{API_URL}/accounts/{account_id}", headers=HEADERS)
    check_response(response)
    account = response.json()
    return account.get("currency", "unknown")

def get_matching_accounts(currency):
    """GET /accounts — find active accounts matching the given currency.

    Returns a list of matching account dicts.
    """
    response = requests.get(f"{API_URL}/accounts", headers=HEADERS)
    check_response(response)
    accounts = response.json()
    return [a for a in accounts if a.get("currency") == currency and a.get("state") == "active"]

def get_exchange_rate(from_currency, to_currency, amount):
    """GET /rate — returns the exchange rate and converted amount."""
    params = {"from": from_currency, "to": to_currency, "amount": amount}
    response = requests.get(f"{API_URL}/rate", params=params, headers=HEADERS)
    check_response(response)
    return response.json()

def check_currency_mismatch(account_id, transfer_currency, destination_currency, amount):
    """Check if the transfer currency matches the destination currency.

    If mismatch, offer the user three options:
    1. Switch to a source account whose currency matches the destination
    2. Switch the transfer currency to the destination currency (with FX preview)
    3. Cancel

    Returns (account_id, transfer_currency, amount_or_flag):
      - amount_or_flag is None when currencies match or option 2 is chosen
        (option 2 returns the string "fx" so the caller can do the conversion)
      - amount_or_flag is a float when option 1 is chosen (new amount in dest currency)
      - Returns None entirely if cancelled.
    """
    if transfer_currency == destination_currency:
        return account_id, transfer_currency, None

    print(f"\nCurrency mismatch: transfer is {transfer_currency}, destination is {destination_currency}.")
    print("Options:")
    print("  1. Use a different source account (matching destination currency)")
    print("  2. Switch transfer currency to destination currency (FX conversion)")
    print("  3. Cancel")

    choice = ask("Select an option (1/2/3)")
    while choice not in ("1", "2", "3"):
        choice = ask("Invalid choice. Select 1, 2, or 3")

    if choice == "3":
        print("Transfer cancelled.")
        return None

    if choice == "1":
        matching = get_matching_accounts(destination_currency)
        if not matching:
            print(f"  No active accounts found with currency {destination_currency}.")
            print("  Consider option 2 (switch transfer currency) instead.")
            return check_currency_mismatch(account_id, transfer_currency, destination_currency, amount)

        print(f"\nAccounts matching {destination_currency}:")
        for i, acc in enumerate(matching, 1):
            print(f"  {i}. {acc.get('name', 'N/A')} — balance: {acc.get('balance', 0)} {acc.get('currency')}")
            print(f"     ID: {acc['id']}")
        print(f"  {len(matching) + 1}. Cancel")

        sel = ask(f"Select an account (1-{len(matching) + 1})")
        while not sel.isdigit() or int(sel) < 1 or int(sel) > len(matching) + 1:
            sel = ask(f"Invalid choice. Select 1-{len(matching) + 1}")

        idx = int(sel) - 1
        if idx == len(matching):
            print("Transfer cancelled.")
            return None

        new_account_id = matching[idx]["id"]
        print(f"  Switched to account {new_account_id} ({matching[idx].get('currency')})")

        # Convert the original amount to the destination currency via FX rate
        print(f"\n  Converting {amount} {transfer_currency}{destination_currency}...")
        fx_data = get_exchange_rate(transfer_currency, destination_currency, amount)
        rate = fx_data.get("rate", 0)
        converted_amount = round(amount * rate, 2)
        print(f"  FX rate: {transfer_currency}{destination_currency}: {rate}")
        print(f"  Suggested amount: {converted_amount} {destination_currency}")

        entered = ask(f"  Use this amount? (y/n)", "y")
        if entered.lower() in ("y", "yes"):
            return new_account_id, destination_currency, converted_amount

        amount_str = ask(f"  Enter your own amount ({destination_currency})")
        while not amount_str or not amount_str.replace(".", "").replace(",", "").isdigit():
            amount_str = ask("  Invalid amount. Enter a numeric value")
        return new_account_id, destination_currency, float(amount_str.replace(",", ""))

    if choice == "2":
        print(f"\nChecking FX rate: {transfer_currency}{destination_currency}...")
        fx_data = get_exchange_rate(transfer_currency, destination_currency, amount)
        rate = fx_data.get("rate", 0)
        converted_amount = round(amount * rate, 2)
        print(f"  FX rate: {transfer_currency}{destination_currency}: {rate}")
        print(f"  Converted amount: {converted_amount} {destination_currency}")
        confirm = ask(f"Confirm transfer of {converted_amount} {destination_currency}? (y/n)")
        if confirm not in ("y", "yes"):
            print("Transfer cancelled.")
            return None
        return account_id, destination_currency, converted_amount

# =============================================
# Step 13: Check transfer field requirements
# =============================================

def check_transfer_field_requirements(account_id, receiver):
    """POST /pay/fields — returns required and optional fields for the transfer."""
    print("Checking transfer field requirements...")
    payload = {"account_id": account_id, "receiver": receiver}
    response = requests.post(f"{API_URL}/pay/fields", json=payload, headers=HEADERS)
    check_response(response)
    data = response.json()
    required = [f["name"] for f in data.get("fields", []) if f.get("required")]
    optional = [f["name"] for f in data.get("fields", []) if not f.get("required")]
    print(f"  Required fields: {required}")
    print(f"  Optional fields: {optional}")
    return data

def build_payment_payload(transfer_fields, account_id, receiver, amount, currency, reference):
    """Build the payment payload from always required fields + conditionally available fields.

    Returns (payload, missing_required) where missing_required is a list of required
    field names that have no value.
    """
    payload = {
        "request_id": str(uuid.uuid4()),
        "account_id": account_id,
        "receiver": receiver,
        "amount": amount,
        "currency": currency,
    }

    provided = {"reference": reference}
    missing_required = []

    for field in transfer_fields.get("fields", []):
        name = field["name"]
        is_required = field.get("required", False)

        if name in provided and provided[name] is not None:
            payload[name] = provided[name]
        elif is_required:
            missing_required.append(name)

    return payload, missing_required

# =============================================
# Step 14: Get an indicative quote
# =============================================

def get_indicative_quote(payment_payload, currency):
    """POST /pay/indicative-quote — returns fees, exchange rate, and delivery time."""
    print("Fetching indicative quote...")
    payload = {k: v for k, v in payment_payload.items() if k != "request_id"}
    response = requests.post(f"{API_URL}/pay/indicative-quote", json=payload, headers=HEADERS)
    check_response(response)
    data = response.json()
    fee = data.get("fee", {})
    total = data.get("estimated_total", {})
    print(f"  Fee:   {fee.get('amount', 0)} {fee.get('currency', currency)}")
    print(f"  Total: {total.get('amount', 0)} {total.get('currency', currency)}")
    return data

# =============================================
# Step 15: Get own account details and print transfer summary
# =============================================

def get_account_details(account_id):
    """GET /accounts/{id}/bank-details — returns the sender's bank details.

    Falls back to GET /accounts/{id} for the account name when bank details
    are not available (e.g. currency pockets without local bank details).
    """
    print("Fetching account bank details...")
    response = requests.get(f"{API_URL}/accounts/{account_id}/bank-details", headers=HEADERS)
    check_response(response)
    data = response.json()
    if isinstance(data, list) and data:
        entry = data[0]
    elif isinstance(data, dict):
        entry = data
    else:
        # No bank details — fall back to account info for at least the name
        print("  No bank details available, fetching account info...")
        acct_resp = requests.get(f"{API_URL}/accounts/{account_id}", headers=HEADERS)
        check_response(acct_resp)
        acct = acct_resp.json()
        entry = {"beneficiary": acct.get("name", "Unknown")}
    print(f"  Sender: {entry.get('beneficiary', 'Unknown')}")
    return entry

def print_transfer_summary(sender, counterparty_name, pm_details, pm_address, amount, currency, reference, quote):
    """Display a human-readable transfer summary."""
    fee = quote.get("fee", {})
    total = quote.get("estimated_total", {})
    rate = quote.get("estimated_exchange_rate")
    arrival = quote.get("estimated_arrival", {})

    sender_name = sender.get("beneficiary", "Unknown")
    sender_addr = sender.get("beneficiary_address", {})
    sender_ref = sender.get("iban") or sender.get("account_no") or "N/A"

    sender_addr_str = ", ".join(filter(None, [
        sender_addr.get("street_line1"),
        sender_addr.get("city"),
        sender_addr.get("country"),
        sender_addr.get("postcode"),
    ])) if sender_addr else ""

    pm_addr_str = ", ".join(filter(None, [
        pm_address.get("street_line1"),
        pm_address.get("city"),
        pm_address.get("country"),
        pm_address.get("postcode"),
    ])) if pm_address else ""

    print("\n" + "=" * 50)
    print("TRANSFER SUMMARY")
    print("=" * 50)
    print(f"FROM: {sender_name}")
    print(f"      {sender_ref}")
    if sender_addr_str:
        print(f"      {sender_addr_str}")
    print(f"TO:   {counterparty_name}")
    print(f"      {pm_details['account_ref']}")
    if pm_addr_str:
        print(f"      {pm_addr_str}")
    print(f"\nREFERENCE:   {reference}")
    print("-" * 50)
    print(f"Amount:        {amount} {currency}")
    if fee.get("amount") is not None:
        print(f"Fee:           {fee.get('amount')} {fee.get('currency')}")
        if fee.get("breakdown"):
            for key, val in fee["breakdown"].items():
                print(f"  - {key}:     {val.get('amount')} {val.get('currency')}")
    if rate:
        print(f"Exchange rate: {rate}")
    if total.get("amount") is not None:
        print(f"Total cost:    {total.get('amount')} {total.get('currency')}")
    if arrival:
        print(f"Delivery:      {arrival.get('speed', '')} ({arrival.get('date', '')})")
    print("=" * 50)
    print("\n")

# =============================================
# Step 16: Check account balance
# =============================================

def check_balance(account_id, total_cost, total_currency):
    """GET /accounts/{id} — check if the balance covers the total cost.

    Returns True if sufficient, False otherwise.
    """
    print("Checking account balance...")
    response = requests.get(f"{API_URL}/accounts/{account_id}", headers=HEADERS)
    check_response(response)
    account = response.json()
    balance = account.get("balance", 0)
    balance_currency = account.get("currency")

    print(f"  Balance:      {balance} {balance_currency}")
    print(f"  Total cost:   {total_cost} {total_currency}")

    if balance_currency == total_currency and balance < total_cost:
        print("Insufficient funds.")
        return False

    return True

# =============================================
# Step 17: Ask for confirmation
# =============================================

def confirm_transfer():
    """Ask the user to confirm the transfer.

    Returns True if confirmed, False otherwise.
    """
    confirm = input("\nProceed with this transfer? ([y]es/[n]o): ").strip().lower()
    if confirm not in ("y", "yes"):
        print("Transfer cancelled.")
        return False
    return True

# =============================================
# Step 18: Execute the transfer
# =============================================

def make_transfer(payload):
    """POST /pay — execute the transfer."""
    print("Initiating transfer...")
    response = requests.post(f"{API_URL}/pay", json=payload, headers=HEADERS)
    check_response(response)
    result = response.json()
    print(f"  Transaction ID: {result.get('id')}")
    print(f"  State:          {result.get('state')}")
    return result

# =============================================
# Step 19: Check transaction status (optional)
# =============================================

def check_transaction_status(transaction_id):
    """GET /transaction/{id} — check the transaction status."""
    print("\nChecking transaction status...")
    response = requests.get(f"{API_URL}/transaction/{transaction_id}", headers=HEADERS)
    check_response(response)
    transaction = response.json()
    status = transaction.get("state")

    print(f"  Transaction ID:         {transaction_id}")
    print(f"  Transaction status:     {status}")

    if status == "pending":
        print("Please check again later.")

    return status

# =============================================
# Main flow
# =============================================

def run():
    # 1. Choose new or existing counterparty
    mode = choose_counterparty_mode()

    if mode == "new":
        # 2. Get destination details (from config or interactive)
        country = ask("Enter counterparty bank country (e.g. GB)", CP_COUNTRY)
        currency = ask("Enter counterparty account currency (e.g. GBP)", CP_CURRENCY)
        recipient_type = ask("Enter recipient type (business/personal)", CP_RECIPIENT_TYPE)

        # Print destination details (pre-creation)
        counterparty_type = "Revolut (revtag)" if CP_REVTAG else "bank account"
        print_destination_details(country, currency, recipient_type, counterparty_type)

        # 3. Check supported countries and currencies
        countries = check_supported_countries()

        # 4. Verify the destination is supported
        if not verify_destination_supported(countries, country, currency):
            return

        # 5. Check counterparty field requirements
        field_requirements = check_counterparty_field_requirements(country, currency, recipient_type)

        # 6. Build the counterparty payload
        counterparty_payload = build_counterparty_payload(field_requirements, country, currency, recipient_type)

        # 7. Validate the account name (CoP/VoP)
        if not validate_account_name(counterparty_payload, country, currency, recipient_type):
            return

        # 8. Create the counterparty
        counterparty = create_counterparty(counterparty_payload)

    else:
        # 9. Retrieve an existing counterparty (enter ID or choose from list)
        counterparty = retrieve_existing_counterparty()

        # 10. Validate the account name (CoP/VoP) — offer for existing counterparties too
        accounts = counterparty.get("accounts", [])
        country = counterparty.get("country", "")
        if accounts:
            if not country:
                country = accounts[0].get("bank_country", "")
            currency = accounts[0].get("currency", "")
            recipient_type = counterparty.get("profile_type", "business")
        else:
            currency = ""
            recipient_type = counterparty.get("profile_type", "business")

        if country and currency:
            cop_payload = {}
            cp_name = counterparty.get("name", "")
            if recipient_type == "business":
                cop_payload["company_name"] = cp_name
            else:
                # Split the full name into first_name/last_name for CoP
                parts = cp_name.split()
                if len(parts) == 1:
                    cop_payload["individual_name"] = {"first_name": parts[0], "last_name": ""}
                elif len(parts) == 2:
                    cop_payload["individual_name"] = {"first_name": parts[0], "last_name": parts[1]}
                else:
                    # Multiple parts — list split options for the user to choose
                    print(f"\n  The counterparty name is: {cp_name}")
                    print("  Select the correct first name / last name split:")
                    for i, split_idx in enumerate(range(1, len(parts)), 1):
                        first = " ".join(parts[:split_idx])
                        last = " ".join(parts[split_idx:])
                        print(f"    {i}. First: {first} | Last: {last}")
                    sel = ask(f"  Select an option (1-{len(parts) - 1})")
                    while not sel.isdigit() or int(sel) < 1 or int(sel) > len(parts) - 1:
                        sel = ask(f"  Invalid choice. Enter 1-{len(parts) - 1}")
                    split_idx = int(sel)
                    cop_payload["individual_name"] = {
                        "first_name": " ".join(parts[:split_idx]),
                        "last_name": " ".join(parts[split_idx:]),
                    }
            if accounts:
                cop_payload["account_no"] = accounts[0].get("account_no", "")
                cop_payload["sort_code"] = accounts[0].get("sort_code", "")
                cop_payload["iban"] = accounts[0].get("iban", "")
                cop_payload["bic"] = accounts[0].get("bic", "")

            print("\nWould you like to validate the account name (CoP/VoP)?")
            if ask("Run CoP/VoP check? (y/n)") in ("y", "yes"):
                if not validate_account_name(cop_payload, country, currency, recipient_type):
                    return

    # 11. Resolve the payment method
    payment_method_id, receiver_key = resolve_payment_method(counterparty)
    if not payment_method_id and not receiver_key and not ("revtag" in counterparty):
        print("No payment method available.")
        return

    receiver = build_receiver(counterparty["id"], payment_method_id, receiver_key)
    print(f"Payment method ID: {payment_method_id or 'none (revtag-only)'}")

    # 12. Get destination details
    bank_country, dest_currency, recipient_type, cp_type = get_destination_details(
        counterparty, payment_method_id, receiver_key
    )
    print_destination_details(bank_country, dest_currency, recipient_type, cp_type)

    # 13. Ask for source account and transfer details
    account_id = ask_source_account()
    amount, transfer_currency = ask_transfer_details()
    reference = ask("Enter the payment reference")

    # 14. Check currency mismatch
    if dest_currency and dest_currency != "N/A":
        result = check_currency_mismatch(account_id, transfer_currency, dest_currency, amount)
        if result is None:
            return
        account_id, transfer_currency, mismatch_result = result

        if mismatch_result is not None:
            amount = mismatch_result

    # 15. Check transfer field requirements
    transfer_fields = check_transfer_field_requirements(account_id, receiver)

    # Build the payment payload and check for missing required fields
    payment_payload, missing_required = build_payment_payload(
        transfer_fields, account_id, receiver, amount, transfer_currency, reference
    )
    if missing_required:
        print(f"\nMissing required transfer fields: {missing_required}")
        return

    # 16. Get an indicative quote
    quote = get_indicative_quote(payment_payload, transfer_currency)

    # 17. Print transfer summary
    sender = get_account_details(account_id)
    pm_details = {
        "account_ref": "",
        "address": {},
    }
    accounts = counterparty.get("accounts", [])
    cards = counterparty.get("cards", [])
    for account in accounts:
        if account.get("id") == payment_method_id:
            pm_details = {
                "account_ref": account.get("iban") or account.get("account_no") or "Revolut account",
                "address": account.get("address", {}),
            }
            break
    for card in cards:
        if card.get("id") == payment_method_id:
            pm_details = {
                "account_ref": f"Card *{card.get('last_digits', '????')} ({card.get('scheme', 'unknown')})",
                "address": {},
            }
            break
    if not pm_details["account_ref"]:
        pm_details = {"account_ref": "Revolut account (revtag)", "address": {}}

    print_transfer_summary(sender, counterparty.get("name", "Unknown"), pm_details, pm_details["address"],
                           amount, transfer_currency, reference, quote)

    # 18. Check that the account balance is sufficient
    total = quote.get("estimated_total", {})
    total_cost = total.get("amount", amount)
    total_currency = total.get("currency", transfer_currency)
    if not check_balance(account_id, total_cost, total_currency):
        return

    # 19. Ask for confirmation
    if not confirm_transfer():
        return

    # 20. Execute the transfer
    transaction = make_transfer(payment_payload)
    print("Transfer initiated successfully!")

    # 21. Ask if user wants to check transaction status
    check_status = ask("\nCheck transaction status now? (y/n)")
    if check_status in ("y", "yes"):
        check_transaction_status(transaction.get("id"))

if __name__ == "__main__":
    try:
        run()
    except ApiError:
        print("\nTransfer flow stopped due to API error.")
    except KeyboardInterrupt:
        print("\nTransfer flow cancelled by user.")
    except Exception as e:
        print(f"\nUnexpected error: {e}")
Sample output (new UK / GBP business counterparty, GBP source account)
1. Create a new counterparty
2. Use an existing counterparty
Select an option (1/2): 1
Enter counterparty bank country (e.g. GB): GB
Enter counterparty account currency (e.g. GBP): GBP
Enter recipient type (business/personal): business
==================================================
DESTINATION: GB / GBP
RECIPIENT TYPE: business
COUNTERPARTY TYPE: bank account
==================================================
Checking supported countries and currencies...
  Found 213 supported countries
  Destination GB/GBP is supported.
Checking counterparty field requirements...
  Required fields: ['company_name', 'account_no', 'sort_code', 'address']
  Optional fields: []
  Enter company_name: Example Vendor Ltd
  Enter account_no: 70294360
  Enter sort_code: 30-96-17
  Enter street_line1: 1 Canada Square
  Enter city: London
  Enter region:
  Enter country: GB
  Enter postcode: E14 5AB
  Counterparty payload fields: ['profile_type', 'bank_country', 'currency', 'company_name', 'account_no', 'sort_code', 'address']
Validating account name (CoP/VoP)...
  Result: matched
Creating counterparty...
  Counterparty ID: b53fdd78-8d67-4f63-a103-eeeeef53cac8
  State:           created
  Single payment method found: 5c9e171c-7e23-4d6a-b768-aaaaaba535f3
Payment method ID: 5c9e171c-7e23-4d6a-b768-aaaaaba535f3
==================================================
DESTINATION: GB / GBP
RECIPIENT TYPE: business
COUNTERPARTY TYPE: bank account
==================================================

1. Enter your account ID (the pocket to send from)
2. Choose from the list of your accounts
Select an option (1/2): 1
Enter your account ID: a3b4c5d6-e7f8-9012-abcd-ef12345678901
Enter the amount to transfer: 1000
Enter the transfer currency (e.g. GBP): GBP
Enter the payment reference: Invoice #12345
Checking transfer field requirements...
  Required fields: ['reference']
  Optional fields: ['charge_bearer']
Fetching indicative quote...
  Fee:   0 GBP
  Total: 1000 GBP
Fetching account bank details...
  Sender: Example Corp Ltd

==================================================
TRANSFER SUMMARY
==================================================
FROM: Example Corp Ltd
      92109701
      11 Washington Walk APT 327, Washington, GB, PE3 9UP
TO:   Example Vendor Ltd
      70294360
      1 Canada Square, London, GB, E14 5AB

REFERENCE:   Invoice #12345
--------------------------------------------------
Amount:        1000 GBP
Fee:           0 GBP
Total cost:    1000 GBP
Delivery:      in_seconds (2026-07-07)
==================================================

Checking account balance...
  Balance:      50000 GBP
  Total cost:   1000 GBP

Proceed with this transfer? ([y]es/[n]o): y
Initiating transfer...
  Transaction ID: 3a2b1c0d-9e8f-4a7b-6c5d-4e3f2a1b0c9d
  State:          pending
Transfer initiated successfully!

Check transaction status now? (y/n): y

Checking transaction status...
  Transaction ID:         3a2b1c0d-9e8f-4a7b-6c5d-4e3f2a1b0c9d
  Transaction status:     completed
Sample output (existing UK / GBP business counterparty, GBP source account)
1. Create a new counterparty
2. Use an existing counterparty
Select an option (1/2): 2

1. Enter the counterparty ID
2. Choose from the list of counterparties
Select an option (1/2): 2
Retrieving your counterparties...

  Loaded 100 counterparties so far. Load more? (y/n) [n]: n

  Found 100 counterparties:

  1. Example Vendor Ltd [business, bank account] — GBP (GB)
  2. Acme Corp [business, bank account] — EUR (DE)
  3. John Smith [personal, Revolut (revtag)] — @johnsmith
  ... (97 more)

  Select a counterparty (1-100): 1

  Selected: Example Vendor Ltd (b53fdd78-8d67-4f63-a103-eeeeef53cac8)
Retrieving counterparty b53fdd78-8d67-4f63-a103-eeeeef53cac8...
  Name: Example Vendor Ltd

Would you like to validate the account name (CoP/VoP)?
Run CoP/VoP check? (y/n): y
Validating account name (CoP/VoP)...
  Result: matched
  Single payment method found: 5c9e171c-7e23-4d6a-b768-aaaaaba535f3
Payment method ID: 5c9e171c-7e23-4d6a-b768-aaaaaba535f3
==================================================
DESTINATION: GB / GBP
RECIPIENT TYPE: business
COUNTERPARTY TYPE: bank account
==================================================

1. Enter your account ID (the pocket to send from)
2. Choose from the list of your accounts
Select an option (1/2): 2
Retrieving your accounts...

  Found 5 active accounts:

  1. GBP pocket [GBP] — balance: 50000
  2. EUR pocket [EUR] — balance: 10000
  3. USD pocket [USD] — balance: 5000
  4. Payroll [GBP] — balance: 12000
  5. Misc [GBP] — balance: 0

  Select an account (1-5): 1

  Selected: GBP pocket (a3b4c5d6-e7f8-9012-abcd-ef12345678901)
Enter the amount to transfer: 1000
Enter the transfer currency (e.g. GBP): GBP
Enter the payment reference: Invoice #12345
Checking transfer field requirements...
  Required fields: ['reference']
  Optional fields: ['charge_bearer']
Fetching indicative quote...
  Fee:   0 GBP
  Total: 1000 GBP
Fetching account bank details...
  Sender: Example Corp Ltd

==================================================
TRANSFER SUMMARY
==================================================
FROM: Example Corp Ltd
      92109701
      11 Washington Walk APT 327, Washington, GB, PE3 9UP
TO:   Example Vendor Ltd
      70294360
      1 Canada Square, London, GB, E14 5AB

REFERENCE:   Invoice #12345
--------------------------------------------------
Amount:        1000 GBP
Fee:           0 GBP
Total cost:    1000 GBP
Delivery:      in_seconds (2026-07-07)
==================================================

Checking account balance...
  Balance:      50000 GBP
  Total cost:   1000 GBP

Proceed with this transfer? ([y]es/[n]o): y
Initiating transfer...
  Transaction ID: 3a2b1c0d-9e8f-4a7b-6c5d-4e3f2a1b0c9d
  State:          pending
Transfer initiated successfully!

Check transaction status now? (y/n): y

Checking transaction status...
  Transaction ID:         3a2b1c0d-9e8f-4a7b-6c5d-4e3f2a1b0c9d
  Transaction status:     completed

What's next

Now that you've prepared, validated, and made your transaction, learn how to:

See also how to automate payouts at scale.

Rate this page