Change Log

March 4th 2026

Reminders

The following were marked as deprecated in previous posts and are now passed their expiration deadline. Please ensure these are no longer used by your team as code related to them can be deleted at any time.

  • CRYPTO should no longer be used as a rail option in POST /payments (use the actual blockchain name instead, like "POLYGON")
  • chain field should no longer be used in POST /payments (replaced by using the blockchain name in the "rail" param as described above)
  • memo field should no longer used in POST /payments (use "reference" instead)
  • POST /configuration/corridors should no longer be used (use POST /payments/corridors instead)
  • entityId field in GET /payments/:id response should no longer be used (use "customerId" instead)
  • stateOrRegion field should no longer be used in POST /customers or POST /accounts (use stateRegionOrProvince instead)
  • estimatedSourceAmount, estimatedDestinationAmount, and estimatedTotalFeeInSourceCurrency fields should no longer be used in GET /payments (use source.amount, destination.amount, and fee fields instead)
  • paymentAccountHolder object from the GET /accounts response should no longer be used
  • GET /payments/entity/:entityId and GET /payment/recipient/:recipient should no longer be used (use GET /payments/customer/:customerId instead)

IP Whitelisting

If you've created API keys with IP whitelisting, this whitelisting will start being enforced on March 30th (Breaking Change).

Rate Limit

The rate limit described in the Rate Limits page of the documentation will start being enforced on March 15th (Breaking Change).

Customers

Beginning March 30th (Breaking Change), if onboarding an individual, they must be between 18 and 65 years old.

Payments

Error Codes

The following new error codes for failed/refunded payments will start being used on March 15th:

  1. FUNDING_RECEIVED_AFTER_QUOTE_EXPIRED
  2. INVALID_EXTERNAL_ACCOUNT_NUMBER
  3. INVALID_EXTERNAL_ACCOUNT_HOLDER_NAME
  4. INVALID_EXTERNAL_ACCOUNT_HOLDER_ADDRESS
  5. FUNDS_RETURNED_BY_RECEIVING_BANK
  6. EXTERNAL_ACCOUNT_HOLDER_ADDRESS_IS_PMB_OR_PO_BOX
  7. RFI_NOT_ANSWERED_IN_TIME
  8. EXTERNAL_ACCOUNT_DEPOSIT_LIMIT_EXCEEDED

Payment Reason

Payment reason OTHER will no longer be an acceptable option starting March 30th (Breaking Change).

POST Response, GET Response, and Webhook standardization

Current State and Issue

The POST /payments response, GET /payments response, GET /payments/:id response, and payment webhook events all have different response structures.

Solution

All POST/GET responses and webhook events related to payments will have the same structure. To avoid breaking changes, this update will occur in 2 steps. The first step is purely additive, which means we combined all the fields across all responses and webhooks. On March 30th, the second step will then be to remove the fields marked for removal below (Breaking Change). This will ensure a cleaner and normalized payload for responses and webhooks.

Structure starting today until March 30th:


{
    "id": "wm5tzpjwzigabt199y529t17",
    "lifecycle": {
        "createdAt": "2026-02-18T18:53:03.109Z",
        "sentAt": "2026-02-18T18:53:03.109Z",
        "completedAt": "2026-02-18T18:53:03.109Z",
        "expiredAt": null,
        "refundedAt": null
    },
    "createdAt": "2026-02-18T18:53:03.109Z", // To be removed (replaced by lifecycle.createdAt)
    "updatedAt": "2026-02-19T16:15:04.283Z", // To be removed
    "sentAt": null, // To be removed (replaced by lifecycle.sentAt)
    "completedAt": null, // To be removed (replaced by lifecycle.completedAt)
    "entityId": "cm5e74nqk0003x228bspddfe8", // To be removed (replaced by customerId)
    "customerId": "cm5e74nqk0003x228bspddfe8",
    "status": "PROCESSING",
    "statusReasons": null,
    "error": null,
    "type": "EXTERNAL",
    "paymentType": "EXTERNAL", // To be removed (replaced by type)
    "comment": null,
    "memo": null, // To be removed (replaced by reference)
    "reference": null,
    "documentReference": null, // To be removed
    "returnAddress": null,
    "reason": "PAYMENT_FOR_GOODS_AND_SERVICES", // To be removed (replaced by paymentReason)
    "paymentReason": "PAYMENT_FOR_GOODS_AND_SERVICES", 
    "fee": 1.07,
    "developerFee": {
        "fixed": null,
        "variable": null,
        "total": null
    },
    "exchangeRate": 1, // To be removed (replaced by quote.exchangeRate)
    "quote": {
        "exchangeRate": 1,
        "expiresAt": null
    },
    "traceNumber": null, // To be removed (replaced by tracking.traceNumber)
    "omad": null, // To be removed (replaced by tracking.omad)
    "imad": null, // To be removed (replaced by tracking.imad)
    "uetr": null, // To be removed (replaced by tracking.uetr)
    "tracking": {
        "traceNumber": null,
        "omad": null,
        "imad": null,
        "certificate": null,
        "uetr": null
    },
    "files": [
        "https://walapay-..."
    ],
    "estimatedTotalFeeInSourceCurrency": 1.07, // To be removed
    "configurations": {
        "expiredQuoteBehavior": "REFUND_PAYMENT"
    },
    "source": {
        "amount": 10,
        "estimatedSourceAmount": 10, // To be removed
        "currencyCode": "USDC",
        "accountId": null,
        "rail": "POLYGON",
        "chain": "POLYGON", // To be removed (replaced by rail)
        "fromAddress": "0x82553ce775b52e0cab198bc057bb0c21f10d5f07",
        "transactionHash": "0x82553ce775b52e0cab198bc057bb0c21f10d5f07",
        "fromAccountNumber": null,
        "accountNumber": null // To be removed (replaced by fromAccountNumber)
    },
    "destination": {
        "amount": 8.93,
        "estimatedAmount": 8.93, // To be removed
        "rail": "SWIFT",
        "currencyCode": "USD",
        "chain": null,  // To be removed (replaced by rail)
        "accountId": "cmlrcej3j0001kysadfjor6sdl7x",
        "transactionHash": "0x82553ce775b52e0cab198bc057bb0c21f10d5f07",
        "account": { // Only available for GET response + To be removed
            "id": "cmlrcej3j0001ky7ljor6rl7x",
            "accountHolderName": "ACME LIMITED",
            "type": "EXTERNAL_BANK_ACCOUNT",
            "accountType": "CHECKING",
            "bankName": "Zheshang Chouzhou Commercial Bank Co., Ltd.",
            "accountNumber": "123456789",
            "routingNumber": null,
            "iban": null,
            "bicSwift": "CZCBCN2XXXX",
            "ifscCode": null,
            "sortCode": null,
            "bsbNumber": null,
            "nccNumber": null,
            "transitNumber": null,
            "bankCode": null,
            "clabeNumber": null,
            "routingCode": null,
            "branchCode": null,
            "clearingCode": null,
            "cnapsCode": null,
            "nubanCode": null,
            "pixCode": null,
            "address": null
        }
    },
    "fundingInstructions": {
        "rail": "POLYGON",
        "chain": "POLYGON", // To be removed (replaced by rail)
        "amount": 10,
        "toAddress": "0xExampleAddress1234567890098765432",
        "fromAddress": "0x82553ce775b52e0CaB198bc057bb0c21f10D5F07",
        "currencyCode": "USDC"
    },
    "customer": { // Only available for GET response + To be removed
        "id": "cm5e74nqk0003x228bspddfe8",
        "type": "INDIVIDUAL",
        "name": "John Doe",
        "email": "[email protected]"
    },
    "sendingCustomer": {  // Only available for GET response + To be removed
        "id": "cm5e74nqk0003x228bspddfe8",
        "name": "John Doe",
        "type": "INDIVIDUAL",
        "email": "[email protected]"
    }
}

Structure starting March 30th:

{
    "id": "wm5tzpjwzigabt199y529t17",
    "lifecycle": {
        "createdAt": "2026-02-18T18:53:03.109Z",
        "sentAt": "2026-02-18T18:53:03.109Z",
        "completedAt": "2026-02-18T18:53:03.109Z",
        "expiredAt": null,
        "refundedAt": null
    },
    "customerId": "cm5e74nqk0003x228bspddfe8",
    "status": "PROCESSING",
    "statusReasons": null,
    "error": null,
    "type": "EXTERNAL",
    "comment": null,
    "reference": null,
    "returnAddress": null,
    "paymentReason": "PAYMENT_FOR_GOODS_AND_SERVICES",
    "fee": 1.07,
    "developerFee": {
        "fixed": null,
        "variable": null,
        "total": null
    },
    "quote": {
        "exchangeRate": 1,
        "expiresAt": "2026-02-18T18:53:03.109Z"
    },
    "tracking": {
        "traceNumber": null,
        "omad": null,
        "imad": null,
        "certificate": null,
        "uetr": null
    },
    "files": [
        "https://walapay-...."
    ],
    "configurations": {
        "expiredQuoteBehavior": "REFUND_PAYMENT"
    },
    "source": {
        "amount": 10,
        "currencyCode": "USDC",
        "accountId": null,
        "rail": "POLYGON",
        "fromAddress": "0x82553ce775b52e0cab198bc057bb0c21f10d5f07",
        "transactionHash": "0x82553ce775b52e0cab198bc057bb0c21f10d5f07",
        "fromAccountNumber": null
    },
    "destination": {
        "amount": 8.93,
        "rail": "LOCAL",
        "currencyCode": "GBP",
        "transactionHash": "0x82553ce775b52e0cab198bc057bb0c21f10d5f07",
        "accountId": "cmlrcej3j0001ky7ljor6rl7x"
    },
    "fundingInstructions": {
        "rail": "POLYGON",
        "amount": 10,
        "toAddress": "0xExampleAddress1234567890098765432",
        "fromAddress": "0x82553ce775b52e0CaB198bc057bb0c21f10D5F07",
        "currencyCode": "USDC"
    },
}

Expiration

Current State and Issue

Payments have a guaranteed source/destination quote as long as they are funded before the expiration period (e.g, 15 minutes). For payments that include a source.accountId in the payload, this is not relevant since funds are immediately pulled from the source account. For payments without a source.accountId, however, this can mean that a payment status changes from AWAITING_FUNDS to EXPIRED if not funded in time (and funds sent to an EXPIRED payment are refunded). This method has two issues:

  1. It's not clear at what point exactly the status will change to EXPIRED
  2. There is an edge case where a customer can send funding before the expiry deadline, but because of blockchain delays, Walapay isn't notified of this transaction until after expiration. This can cause the following scenario:
    1. Customer sends funds before expiry deadline but there are blockchain delays in receiving the notification
    2. Quote expires and the user sees the payment change from AWAITING_FUNDS to EXPIRED
    3. User doesn't understand what happened to the funds they sent
    4. The payment then completes after the blockchain traffic clears and the status changes from EXPIRED to SENT (and then COMPLETED). User is now even more confused

Solution

To fix this, we are changing how payments expire:

  • To make it clear when the expiration window closes, we have added the quote.expiresAt field in the payment response (see previous section). This is the exact time at which the quote is no longer valid. Any funds that are sent before this deadline will guarantee the quote (even if there are blockchain or bank delays that mean Walapay receives the notification after the expiry time)
  • The payment response will also include the configurations.expiredQuoteBehavior field, which describes the expected behavior when funds are sent after the quote expiry. Currently, REFUND_PAYMENT is the default and only option (we will soon be expanding this)
  • The EXPIRED status of a payment no longer represents when the quote expires. Instead, quote validity is defined only by a new quote.expiresAt field. A payment will stay in AWAITING_FUNDS for 3 business days if it is not funded. If no funds arrive in that period, the payment is then moved to EXPIRED purely for housekeeping (so very old, unused payments do not stay open forever).

The scenario described in issue 2 would now go like this:

  1. Customer sends funds before expiry deadline, but there are blockchain or banking delays
  2. Payment quote expires (i.e., the expiresAt time is now past), but the payment status itself stays on AWAITING_FUNDS
  3. Funding notification is received after delays cleared and Walapay sees that the funds were sent before the expiry deadline. The payment goes to PENDING and continues as a normal payment

This expiresAt parameter is available today and should be used as the source of truth for quote validity. The change to EXPIRED status occurring after 3 business days, however, will start taking effect on March 30th. (Breaking Change).

September 12th 2025

  • When creating an external digital asset wallet, you no longer need to match the accountHolder.address.countryCode to the country for which you would like to onramp funds. In the past, if you wanted to do a BRL to USDC onramp, you would have to create the external digital asset wallet with the accountHolder.address.countryCode of BR. If you wanted to use the same wallet address to onramp from PHP, you would have to create another external digital asset wallet with accountHolder.address.countryCode of PH. This is no longer the case. One external digital asset wallet can be used for all onramps (and accountHolder.address.countryCode should represent where the user actually lives)
  • External digital asset wallets will start requiring currency codes. This means that if you want to use one address to send USDC and USDT, you will need to create two accounts to represent each currency (with only the currency code differing between the two). Breaking change: Starting October 10th, creating an external digital asset wallet will return an error if no currency code is provided. For all existing wallets that don't have a currency code by that date, these will be automatically updated to have the currency code of their last payment (defaulting to USDC if no payment exists). EDIT: The breaking change date for this has been postponed. We will provide a new breaking change date shortly.
  • For virtual bank accounts with liquidation information, liquidationAccountId can be provided instead of liquidationInformation. To do this, the liquidation account must first be created and its ID can then be used in the liquidationAccountId field for a virtual bank account. Breaking change: Starting October 10th, the liquidationInformation parameter will be deprecated entirely. EDIT: The breaking change date for this has been postponed. We will provide a new breaking change date shortly.
  • When creating a business customer, the estimatedAnnualRevenueUSD field had two incorrect options: FIFTY_MILLION_TO_TWENTY_FIVE_MILLION and OVER_TWENTY_FIVE_MILLION. Two new options have been added to replace these: FIFTY_MILLION_TO_TWO_HUNDRED_AND_FIFTY_MILLION and OVER_TWO_HUNDRED_AND_FIFTY_MILLION. Breaking change: Starting October 10th, the old incorrect fields will be removed
  • You can now include a returnAddress field when creating a stablecoin-to-fiat payment in case you want the funds returned to a specific address in case of payment failure
  • The reference field for payments will replace the memo field. Breaking change: Starting October 10th, the memo field will be deprecated
  • In the GET /accounts/:id and accounts webhook events, the accountHolder object will replace paymentAccountHolder. Currently, both are present. Breaking change: Starting October 10th, only accountHolder will remain

September 8th 2025

Summary: Deposits are being replaced with payments of type PAYIN.

Previously, when funds were sent to a Virtual Account, a new deposit record was created with details regarding the source of the money. However, for Virtual Accounts with automatic conversion to stablecoins or fiat, no data was provided regarding where the money was sent. To fix this issue, we are deprecating the use of deposits entirely and introducing a new paymentType parameter for payments. The types are described below:

  • INTERNAL: Funds are sent from a Virtual Account to another Virtual Account (e.g., from a virtual bank account to a virtual digital asset wallet)
  • PAYIN: Funds are sent from an External Account to a Virtual Account (e.g., from an external bank account to a virtual bank account). This will replace the deposit record
  • PAYOUT: Funds are sent from a Virtual Account to an External Account (e.g., from a virtual bank account to an external digital asset wallet)
  • EXTERNAL: Funds are sent from an External Account to another External Account (e.g., from an external bank account to an external digital asset wallet).

By representing deposits as payments, you will be able to see all money movement in a chronological order and keep track of automatic conversions. Below is a short representation of the change:

Before:

  1. Funds are sent to a virtual account
  2. A deposit record is created and an account.deposit webhook is triggered
  3. No record is created or webhook triggered for the automatic conversion

After:

  1. Funds are sent to a virtual account
  2. A PAYIN payment record is created and a payment.created webhook is triggered
  3. Another payment record is created to represent the automatic conversion (PAYOUT or INTERNAL depending on the destination account type) and a payment.created webhook is triggered

Breaking Change: Deposits will be deprecated fully on September 30th. Before then, your existing deposits will be transferred to your payments as PAYINs.

August 25th 2025

Summary: The main objective of this release is to allow developers to select a specific rail for a payment. Previously, only LOCAL, CRYPTO, and WIRE were allowed. The dashboard has NOT yet been updated to use these new endpoints.

Corridors

The first step was creating a new GET /payments/corridors endpoint that allows you to see which corridors/rails are available. A few notes:

  • "rails" now contains all supported blockchains and all supported fiat rails. "chain" has been deprecated
  • "currencyCodes" and "rails" are now arrays, allowing us to greatly reduce the number of objects returned by the response. Instead of having an object for every permutation of blockchain and stablecoin (e.g., POLYGON/USDC, ETHEREUM/USDC, BASE/USDC), these are all concatenated into one object
  • Instead of having fees for LOCAL, CRYPTO, and WIRE, fees are now specific to the source and destination rail combination.

Breaking Change: GET configurations/corridors will be deprecated September 30th.

Before (GET configurations/corridors):

"data": [
        {
            "source": {
                "currencyCode": "USDC",
                "chain": "POLYGON"
            },
            "destination": {
                "currencyCode": "BRL",
                "chain": null,
                "countryCode": "BR"
            },
            "fees": {
                "LOCAL": {
                    "fixedFeeInUSD": 1,
                    "variableFeeInSourceCurrency": 0.005
                },
                "CRYPTO": {
                    "fixedFeeInUSD": 1,
                    "variableFeeInSourceCurrency": 0.005
                },
                "WIRE": {
                    "fixedFeeInUSD": null,
                    "variableFeeInSourceCurrency": null
                }
            }
        }
]

After (GET payments/corridors):

"data": [
        {
            "source": {
                "currencyCodes": [
                    "USDC",
                    "USDT",
                    "EURC"
                ],
                "rails": [
                    "POLYGON",
                    "ETHEREUM",
                    "BASE",
                    "SOLANA",
                    "TRON"
                ]
            },
            "destination": {
                "currencyCodes": [
                    "BRL"
                ],
                "rails": [
                    "TED"
                ],
                "countryCode": "BR"
            },
            "fees": {
                "fixed": 1,
                "variable": 0.005
            }
        }
]

Customers

Previously, if your team had USD corridors enabled, all onboarded customers would require passing through compliance checks mandated by US banks (which tend to be slower). Since it's possible not all your customers need access to USD corridors, you can now specify which capabilities you would like to enable for the onboarded customer: USD and OTHER (putting both or not including the capabilities parameter in the request will give the customer access to all corridors).

Important note: USD capability also covers EUR onramps.

Example

{
    "type": "INDIVIDUAL",
    "capabilities": ["OTHER"], // Optional
    "firstName": "Jason",
    "lastName": "Smith",
    ....
}

Accounts

GET /requirements

With the ability to choose which rail to send the payment, it is also important to know which parameters are required for each rail when creating an account. We have therefore created a new GET /accounts/requirements endpoint to replace the existing GET /accounts/requirements/external-bank-accounts endpoint. A few notes:

  • Objects are not grouped by INDIVIDUAL and BUSINESS anymore. Instead, each variable will have an accountHolderTypes array to designate to whom it applies
  • The regex provided for IBANs is now specific to the country

Breaking Change: GET /accounts/requirements/external-bank-accounts will be deprecated September 30th.

Before (GET /accounts/requirements/external-bank-accounts):

{
    "INDIVIDUAL": [
        {
            "variableName": "bank.accountNumber",
            "regex": "^[0-9]{2,15}$",
            "example": "001000456789",
            "enum": []
        },
        ....
    ],
    "BUSINESS": [
        {
            "variableName": "bank.accountNumber",
            "regex": "^[0-9]{2,15}$",
            "example": "001000456789",
            "enum": []
        },
        ...
    ]
}

After (GET /accounts/requirements):

[
    {
        "variableName": "bank.accountNumber",
        "rails": [
            "TED"
        ],
        "accountHolderTypes": [
            "INDIVIDUAL",
            "BUSINESS"
        ],
        "regex": "^[0-9]{2,15}$",
        "example": "001000456789",
        "enum": []
    },
    ...
    {
        "variableName": "bank.pixCode",
        "rails": [
            "PIX"
        ],
        "accountHolderTypes": [
            "INDIVIDUAL",
            "BUSINESS"
        ],
        "regex": "^.{1,100}$",
        "example": "01.234.456/5432-10 (cnpj) | 123.456.789-87 (cpf) | +5511912345678",
        "enum": []
    }
]

POST /accounts

In addition to the revamped requirements endpoint, you can now specify the rails you want to unlock for the account you create. Currently, this can be done for two different countries:

  • US: ACH, ACH_SAME_DAY, and DOMESTIC WIRE. Some US banks don't have all three rails for their accounts, so it's important to designate which rails can be used to send funds. If you do not include the "rails" field in the request, the account will get access to all 3 by default
  • BR: TED and PIX. These rails have different field requirements. By selecting which rail you want to enable for the account, you can make sure to only ask your customer for the actual fields required for the rail you want to use. If you do not include the "rails" field in the request, the account will get access to TED by default

Example

{
    "type": "EXTERNAL_BANK_ACCOUNT",
    "currencyCode": "BRL",
    "isThirdParty": true,
    "rails": ["TED", "PIX"],
    "bank": {
        "name": "BANCO DO BRASIL S.A",
    ....
}

Payments

GET /rate

With the introduction of specific rails, the GET /payments/rate/walapay has also been replaced with GET /payments/rate to provide the details of the specific rail chosen. A few notes:

  • Instead of query parameters sourceChain and destinationChain, it is sourceRail and destinationRail
  • You can still use LOCAL as the value for "rail". Behind the scenes, we map this value to the appropriate country-specific rail
  • The current /payments/rate/walapay will append the results of the new endpoint automatically so that you can transition from one to the other smoothly

Breaking Change: GET /payments/rate/walapay will be deprecated September 30th.

Before (GET /payments/rate/walapay):

{
    "midMarketRate": 5.4952529294,
    "invertedMidMarketRate": 0.1819755763,
    "walapayFees": {
        "description": "The Walapay fees for the payment based on the destination rail",
        "LOCAL": {
            "fixed": 1,
            "variable": 0.001
        },
        "WIRE": {
            "fixed": 45,
            "variable": 0.0065
        },
        "CRYPTO": {
            "fixed": null,
            "variable": null
        }
    },
    "walapayRates": {
        "description": "[Deprecated] The mid market rate minus the Walapay variable fee: mid market rate * (1 - Walapay variable fee)",
        "LOCAL": 5.4897576764706,
        "WIRE": 5.459533785358901,
        "CRYPTO": null
    },
    "developerFees": {
        "description": "The developer fees for the payment. Added on top of the walapay fees.",
        "fixed": null,
        "variable": null
    },
    "calculatedAmounts": {
        "LOCAL": {
            "sourceAmount": 679.27,
            "destinationAmount": 3723.52,
            "totalFeeInSourceCurrency": 1.68
        },
        "WIRE": {
            "sourceAmount": 727.03,
            "destinationAmount": 3723.52,
            "totalFeeInSourceCurrency": 49.44
        },
        "CRYPTO": {
            "sourceAmount": null,
            "destinationAmount": null,
            "totalFeeInSourceCurrency": null
        }
    },
    "quoteId": "cmdxts4kx0000dunid010mqd7"
}

After (GET /payments/rate):

{
    "midMarketRate": 5.495449870300001,
    "invertedMidMarketRate": 0.18196857419999998,
    "walapayFee": {
        "fixed": 1,
        "variable": 0.007
    },
    "developerFee": {
        "fixed": 1,
        "variable": 0.01
    },
    "calculatedAmount": {
        "description": "The calculated amounts for the payment, along with total fee (both walapay and developer).destinationAmount = (((sourceAmount - walapayFixedFee) * (1 - walapayVariableFee)) - developerFixedFee) * midMarketRate * (1 - developerVariableFee). sourceAmount = (destinationAmount / (midMarketRate * (1 - developerVariableFee)) + developerFixedFee) / (1 - walapayVariableFee) + walapayFixedFee.",
        "sourceAmount": 691.24,
        "destinationAmount": 3723.52,
        "totalFeeInSourceCurrency": 13.68
    },
    "quoteId": "cmdxtdy6e000010y04ovhu28i"
}

POST /payments

In addition to the new rate endpoint, the POST /payments endpoint has been updated to allow you to choose a country-specific rail if desired. A few notes:

  • To start, the currencies that will have multiple options:
    • USD: ACH, ACH_SAME_DAY, DOMESTIC_WIRE
    • BRL: TED, PIX
    • INR: IMPS, IMPS_WITH_FIRC
  • You can still use LOCAL if you want Walapay to choose the default option on your behalf
  • You can still use CRYPTO in the request if the rail is a blockchain. We will map the value provided for chain to replace CRYPTO under the hood. The mapped value will be saved as the rail
  • For SWIFT payments, you should use the rail type "SWIFT" (WIRE is deprecated)

Before:

{
    "source": {
        "currencyCode": "USDC",
        "amount": 10,
        "rail": "CRYPTO",
        "chain": "POLYGON",
        "fromAddress": "0x9dE08B842B9f9E27c478fF816330118939a7BF90"
    },
    "destination": {
        "currencyCode": "USD",
        "rail": "LOCAL",
        "accountId": "cmca02pfl000cfc5qjs3emffc"
    },
    "comment": "Test Transaction",
    "memo": "Test",
    "paymentReason": "CONSULTING_FEES",
    "documentReference": "Test"
}

After (with "source.rail" as "POLYGON" and "destination.rail" as "ACH_SAME_DAY"):

{
    "source": {
        "currencyCode": "USDC",
        "amount": 10,
        "rail": "POLYGON",
        "fromAddress": "0x9dE08B842B9f9E27c478fF816330118939a7BF90"
    },
    "destination": {
        "currencyCode": "USD",
        "rail": "ACH_SAME_DAY",
        "accountId": "cmca02pfl000cfc5qjs3emffc"
    },
    "comment": "Test Transaction",
    "memo": "Test",
    "paymentReason": "CONSULTING_FEES",
    "documentReference": "Test"
}