Skip to main content

Overview

Offramps with transfers let your users convert stablecoin to international fiat currencies through a quote-based flow. Unlike payment routes, which create persistent pipes for USD offramps, transfers are one-time operations with locked exchange rates and fees. Use transfers for international payouts to currencies like COP, MXN, and BRL. The process involves three steps:
  1. Create a payment account: register the user’s bank account details as a payment account.
  2. Create a quote: get a quote with the exchange rate, fees, and destination amount.
  3. Create a transfer: execute the transfer using the quote before it expires.

Before you begin

Make sure you have:
  • API access to the payment accounts, quotes, and transfers endpoints.
  • User bank account details: the destination bank account information (details vary by country and rail).
  • A stablecoin source: users need stablecoin on a supported chain to transfer.

Key concepts

Payment accounts

A payment account is a stored representation of a user’s bank account or other fiat destination. See Payment Accounts for the general concept and the US-specific fields; this page covers the additional fields required for international destinations. For international transfers, you’ll create an externalFiatAccount type payment account containing the user’s bank details for the destination country.

Quotes

A quote locks in the exact exchange rate, fees, and destination amount for a transfer, unlike an exchange rate preview, which is indicative only and doesn’t lock anything in. Quotes have an expiration time, so you must create the transfer before the quote expires. Key information in a quote includes:
  • Exchange rate: the rate used for currency conversion, excluding fees.
  • Fees: breakdown of Rain fees and any sender fees.
  • Destination amount: the exact amount the user receives in fiat.
  • Expiration: when the quote expires and is no longer valid.

Transfers

A transfer executes the movement of funds from crypto to fiat using a locked quote. When you create a transfer, Rain generates a depositAddress where the user sends their stablecoin. Once Rain receives the funds, it automatically converts and sends them to the destination bank account.

API flow

Action / API callExternal / networkWebhook Rain sends you
UserProvides bank details
1YouPOST /payment-accounts
2RainReturns payment account
3YouPOST /quotes
4RainReturns quote with rate & fees
5YouDisplays quote details
6YouPOST /transfers
7RainReturns transfer with depositAddress
8YouProvides deposit address
9UserSends stablecoin to deposit address
10RainConverts to fiat
11BankReceives fiat transfer
12WebhooktransactionTransfer
The steps above, in text:

Step 1: create a payment account

Before creating a quote, register the user’s bank account as a payment account with Create a payment account. Set externalFiatAccount.currency and rail to the destination’s currency and rail. The other required fields vary by country and rail, so see Payment account details for the fields each rail requires, including the extra beneficiary identification some rails need for third-party payouts. Set thirdParty to false when the bank account belongs to the user initiating the transfer, or to true when it belongs to a different party, such as a vendor. The response returns the payment account’s id, which you reference when you create a quote.
Additional fields may be required depending on the transfer amount or specific compliance requirements. Contact your Rain representative for detailed requirements.

Step 2: create a quote

Once you have a payment account, request a quote to see the exchange rate, fees, and destination amount.

Request

Call Create a quote with the userId, a source (amount, currency and rail), and a destination (currency, rail, and an address of type paymentAccount with its id).
You can optionally specify a source address if you know which wallet will be sending the funds. Some rails may require this information.

Response

Quote fields

Quotes expire after a short period (typically 15 minutes). If the quote expires, you must create a new quote before creating a transfer.

Step 3: create a transfer

If the user is happy with the exchange rate and fees, create a transfer using the quote.

Request

Call Create a transfer with the quoteId from the quote. The parameters and headers are below.

Request parameters

Headers

Reusing an idempotency-key with a different request body returns a 409 conflict. Generate a new key for each distinct transfer.

Response

The response includes the transfer details along with a depositAddress where the user sends their stablecoin:

Response fields

The depositAddress is unique to this transfer. Only send the exact source amount of the specified currency (for example, 100 USDC on Base) to this address. Sending other tokens or incorrect amounts may result in failed transfers or loss of funds.

Deposit window

The transfer’s expiresAt is the deadline for sending funds to the deposit address: 24 hours after the transfer is created. This is separate from the quote’s expiresAt, which only limits when the transfer can be created. The exchange rate and fees from the quote hold for the whole deposit window. If no deposit arrives by expiresAt, the transfer moves to expired. Funds sent after that are not converted at the original rate or a new one. Contact Rain support to have them returned to the sender.

What happens when a user deposits crypto

Rain creates the transfer when you call POST /transfers, and sends a transactionTransfer webhook with action created right away. Its status is awaiting_transfer until funds arrive. When a user then sends stablecoin to the deposit address:
  1. Crypto received: Rain detects the incoming stablecoin transfer.
  2. Validation: Rain validates the amount and currency against the transfer.
  3. Conversion: Rain converts the stablecoin to the destination fiat currency.
  4. Status updates: as the transfer progresses, Rain sends transactionTransfer webhooks with action updated for status changes.
  5. Fiat transfer: Rain sends the funds to the user’s bank account via the specified rail.
  6. Transfer completed: the transfer settles, and Rain sends a transactionTransfer webhook with action completed.

Webhooks

Rain sends webhooks during the transfer lifecycle to keep you informed of status changes: See Money Movement Webhooks for how a transfer moves through these events, and the transfer object reference for the full webhook payload schema.

Transfer statuses

Rain checks a transfer against your program’s partner-managed limits when you create it. A transfer over the limit is rejected with 400 Bad Request.

Query transfers

You can query all transfer transactions via the Transactions API. Filter by type=transfer to see transfer transactions. To get a specific transaction by ID, call Get a transaction by its ID.

Cancel a transfer

To cancel a transfer that hasn’t completed, call DELETE /v1/transfers/{transactionId} with the transaction ID the create call returned. A successful cancel returns 200 with the transfer, its status now cancelled. Once a transfer passes the point where Rain can stop it, the call returns 409 with Transfer can no longer be cancelled.

Supported configurations

Source (crypto)

Destination (fiat)

See Money Movement Limits for per-corridor limits, supported use cases, settlement times and cutoffs, and Payment Account Details for the bank fields each rail requires.
✱ Supported currencies available upon request. Contact your Rain representative to enable them for your organization.

Error handling

Errors return the HTTP status, its reason, and a message:
Errors that happen after Rain creates a transfer don’t come back on the request. Track them through the transfer’s status and refundReason instead. For example, a deposit window that runs out moves the transfer to expired, and a deposit that doesn’t match the transfer is refunded with the reason amount_mismatch. See Refund reasons.

Quote errors

Transfer errors

A 503 can happen after Rain has already started the transfer. Retry with the same idempotency-key so Rain returns the existing transfer instead of creating a duplicate. Never retry a 503 with a new key.

Best practices

  1. Use idempotency keys: always include a unique idempotency-key header with each transfer request to prevent duplicate transfers on retries.
  2. Display quote details: show users the exchange rate, fees, and destination amount before they confirm the transfer.
  3. Handle expiration: implement logic to refresh quotes if the user takes too long to confirm.
  4. Monitor webhooks: set up webhook handlers to track transfer status in real time.
  5. Validate bank details: create payment accounts during user onboarding to catch errors early.
  6. Provide clear instructions: make sure users understand they must send the exact amount to the deposit address.
  7. Handle 503 responses: retry a 503 with the same idempotency-key, never a new one.

What’s next

Set up onramps

Enable fiat deposits into crypto.

Set up offramps

Enable fiat withdrawals from crypto via payment routes.

Payment accounts

See the full request, response, and field reference for registering a bank account.

Partner-managed limits

Set limits on onramp and offramp volume and handle deposits that exceed them.

Set up webhooks

Receive transfer notifications as they happen.