DEV Community

Cover image for How to Handle Rapyd Payouts with FX
Kyle Pollock for Rapyd

Posted on • Updated on • Originally published at community.rapyd.net

How to Handle Rapyd Payouts with FX

By Allan MacGregor
Photo by Al Amin Shamim

In today's remote work environment, working with international employees, vendors, and contractors is becoming more and more common. Handling payouts in foreign currencies can be quite challenging and add to business complexity as it might require you to account for conversion rates or handle multiple payment processors.

Fortunately, with Rapyd Payouts with FX, you can easily handle payouts for international employees and vendors without incurring high fees or foreign exchange charges.

Adopting a programmatic solution for handling foreign currency payouts can help in the following scenarios:

  • Automatically paying international employees across different currencies.
  • Determining the foreign exchange (FX) rate before making a payout.
  • Optimizing conversion rates for different currencies.
  • Handling mass payouts in a single operation.

This article demonstrates how to build a simple application that uses Rapyd Payouts with foreign currencies and illustrates the data flow for creating and processing a payment in a foreign currency.

What Are Rapyd Payouts with FX?

Rapyd Disburse is a global payout platform with support for over a hundred countries and a wide range of payout methods, including:

  • Bank transfers
  • eWallets
  • Cards
  • Cash

Available as both an API and a web interface, Rapyd Disburse allows merchants, developers, and other service providers to create and manage payouts in a simple and intuitive way.

Various scenarios might require you to issue payouts in a foreign currency, including the following examples:

  • Working with international employees across multiple countries, who want to be paid in their local currency.
  • Paying directly to international vendors.
  • Purchasing digital goods from international marketplaces.

The following is a high-level illustration of the workflow that you’ll implement using this tutorial.

Payout Workflow

Prerequisites for This Tutorial

For this tutorial, you’ll need to make sure you have a well-functioning Elixir environment; the easiest way to do so is to follow the Elixir installation instructions, which will give you a couple of options for:

  • Local installation on Linux, Windows, and macOS.
  • Dockerized versions of Elixir.
  • Package manager versions setups.

For this tutorial, the local install is the recommended option as it’s the easiest way to get started. Additionally, you’ll need to have npm installed locally and a running version of PostgreSQL.

NPM

Install Node.js. Note that your system might have it preinstalled.

PostgreSQL

PostgreSQL can be a little tricky to install depending on the operating system you’re using. For this tutorial, you can leverage Docker and get a local version running by doing the following:

  1. Create a folder to persist the DB data:
> mkdir ${HOME}/phoenix-postgres-data/
Enter fullscreen mode Exit fullscreen mode
  1. Run a Docker container with the PostgreSQL image:
$ docker run -d \
    --name phoenix-psql \
    -e POSTGRES_PASSWORD=Phoenix1234 \
    -v ${HOME}/phoenix-postgres-data/:/var/lib/postgresql/data \
    -p 5432:5432 \
    postgres
Enter fullscreen mode Exit fullscreen mode
  1. Validate the container is running:
> docker ps

CONTAINER ID   IMAGE      COMMAND                  CREATED         STATUS        PORTS                                  NAMES
11cbe1d2bc2f   postgres   "docker-entrypoint.s…"   6 seconds ago   Up 5 seconds  5432/tcp, 0.0.0.0:5432->5432/tcp       phoenix-psql
I can
that was
Enter fullscreen mode Exit fullscreen mode
  1. Validate PostgreSQL is up and running:
> docker exec -it phoenix-psql bash

root@11cbe1d2bc2f:/# psql -h localhost -U postgres

psql (13.2 (Debian 13.2-1.pgdg100+1))
Type "help" for help.

postgres=# \l
                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges   
-----------+----------+----------+------------+------------+-----------------------
 postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 | ...

Enter fullscreen mode Exit fullscreen mode

Setting Up Phoenix

Now that you have the necessary dependencies, you can go ahead and install the Phoenix application generator and create your first Phoenix app. The Phoenix generator is distributed as a mix archive and can be installed by running the following command:

> mix archive. install hex phx_new 1.5.8
Enter fullscreen mode Exit fullscreen mode

Implementing Rapyd Payouts with FX

Along with the previously mentioned prerequisites, you’ll also need to have a Rapyd account and a local copy of the Rapyd FX project repository.

Rapyd FX Example Landing

Getting an API Key and Setting Up Request Authorization

Before you can start building the checkout page, you’ll need to get an API key from Rapyd. This API key will be used to build your auth headers added to every API request.

From the sandbox account you created in the Rapyd Client Portal, retrieve the Secret key and the Access key. This section can be accessed by clicking Developers from the left-side navigation.

Rapyd API Key

Every request to the Rapyd API requires the following headers:

  • access_key: The access key generated when you created your Rapyd account.
  • content_type: The content type of the request. This is always application/json.
  • salt: A random string is used to create the signature.
  • signature: A generated value per request
  • timestamp: The current time in milliseconds.

The signature header needs to be generated for every request made to Rapyd's API; without this header, there’s no way to authenticate the request, and it will definitely be rejected. As part of your implementation, you’ll have to calculate the signature and add it to the request headers.

The signature is calculated with the following formula:

signature = BASE64 ( HASH ( http_method + url_path + salt + timestamp + access_key + secret_key + body_string ) )

The sample application provides a sample endpoint to test the API key and signature. Let's see what happens if you don't have a key or signature setup.

Make sure the application is running and visit localhost:4000/testing; when you click the button, you should see the following response.

Failed Signature

Let's take a look at the controller code in /lib/rapyd_fx_example_web/controllers/testing_controller.ex:

  def index(conn, _params) do
    response = HTTPoison.get! "https://sandboxapi.rapyd.net/v1/data/countries"
    response = response.body |> Jason.decode!
    render(conn, "index.html", response: response)
  end
Enter fullscreen mode Exit fullscreen mode

The request failed since you are missing the authentication headers completely. You can refactor the application code to handle the request signature. First, update config/dev.exs and add the following code block:

config :rapyd_fx_example,
  rapyd_access_key: "YOUR_ACCESS_KEY",
  rapyd_secret_key: "YOUR_SECRET_KEY"
Enter fullscreen mode Exit fullscreen mode

Then replace the values with your API key and Secret key from the Rapyd Client Portal.

Next, refactor the index controller to account for the headers:

  def index(conn, _params) do
    # Define the base url and target path
    base_url = "https://sandboxapi.rapyd.net"
    url_path = "/v1/data/countries"

    # Generate the values needed for the headers
    access_key = Application.fetch_env!(:rapyd_fx_example, :rapyd_access_key)
    salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
    timestamp = System.os_time(:second)
    signature = sign_request("get", url_path, salt, timestamp, access_key, "")

    # Build the headers
    headers = [
      {"access_key", access_key},
      {"salt", salt},
      {"timestamp", timestamp},
      {"url_path", url_path},
      {"signature", signature}
    ]

    response = HTTPoison.get!(base_url <> url_path, headers)
    response = response.body |> Jason.decode!()
    render(conn, "index.html", response: response)
  end
Enter fullscreen mode Exit fullscreen mode

As you can tell, all you’ve done is add the headers to the request. More importantly, the following code generates the signature:

  def sign_request(http_method, url_path, salt, timestamp, access_key, body) do
    secret_key = Application.fetch_env!(:rapyd_fx_example, :rapyd_secret_key)

    cond do
      body == "" ->
        body = ""
      true ->
        {:ok, body} = body |> Jason.encode()
    end

    signature_string =
      [http_method, url_path, salt, timestamp, access_key, secret_key, body] |> Enum.join("")

    :crypto.mac(:hmac, :sha256, secret_key, signature_string)
    |> Base.encode16(case: :lower)
    |> Base.encode64()
  end
Enter fullscreen mode Exit fullscreen mode

This functional takes the following parameters:

  • http_method: The HTTP method used in the request.
  • url_path: The path of the request.
  • salt: A random string used to generate the signature.
  • timestamp: The current time in milliseconds.
  • access_key: The access key generated when you created your Rapyd account.
  • secret_key: The secret key generated when you created your Rapyd account.
  • body: The body of the request.

For the signature generation, you should ensure that you encode the body using Base64, as otherwise it might cause mismatches between the signature generated and the validation on the Rapyd API end.

You should now be able to make a request to the API and see the response. In this case, a list of all the countries supported by Rapyd.

Success Signature

Creating an eWallet and a Beneficiary

Before you start implementing the payout flow, you’ll need to create the following:

  • An eWallet, which will be the account that will be used to make the payout.
  • A beneficiary, which will be the account that will receive the funds.

Rapyd Wallet

Rapyd Wallet is a white-label digital wallet used to receive, store, and send money. Rapyd Wallet can hold one or more accounts, each with its own currency and balance.

You can create a testing wallet from the sandbox dashboard. Start by logging into the Rapyd dashboard and making sure the account is in sandbox mode.

Next, navigate to the Wallet management section by going to Wallets > Accounts.

Rapyd create wallet

Next, click on Create Wallet and select Personal Wallet in the modal.

Wallet Type

In the Create Personal Wallet dialog, make sure to fill in all the information and feel free to use fake data as required.

Create Personal Wallet

Once done, you should receive a notification confirming the wallet has been created successfully and displaying the wallet ID.

Wallet ID

Now that your wallet is created, you need to add some funds. Click on the view details link on your newly created wallet, scroll to the Virtual Account section, and click Create Virtual Account.

Create Virtual Account

Next, go ahead and click on the new virtual account Simulate Bank Transfer.

Simulate bank transfer

Assuming everything went well, you should be able to see the newly created wallet in the list of wallets. Keep track of the wallet ID as you’ll be using it as part of your payout request.

Beneficiary

In the same fashion, you’ll need to create a beneficiary account. The beneficiary is a recipient that will be receiving your payout.

Start by generating a new controller and the corresponding views with the following command:

mix phx.gen.html Rapyd Beneficiary beneficiaries first_name:string last_name:string currency:string uuid:string
Enter fullscreen mode Exit fullscreen mode

Then, run the following command:

mix ecto.migrate
Enter fullscreen mode Exit fullscreen mode

Next, you’ll need to make the route available by adding the following code to the lib/rapyd_fx_example_web/router.ex file:


  scope "/", RapydFxExampleWeb do
    pipe_through :browser
    resources "/beneficiaries", BeneficiaryController

end
Enter fullscreen mode Exit fullscreen mode

Copy the following methods to generate the signature and the body of the request to lib/rapyd_fx_example_web/controllers/beneficiary_controller.ex:

  def sign_request(http_method, url_path, salt, timestamp, access_key, body) do
    secret_key = Application.fetch_env!(:rapyd_fx_example, :rapyd_secret_key)

    cond do
      body == "" ->
        body = ""
      true ->
        {:ok, body} = body |> Jason.encode()
    end

    signature_string =
      [http_method, url_path, salt, timestamp, access_key, secret_key, body] |> Enum.join("")

    :crypto.mac(:hmac, :sha256, secret_key, signature_string)
    |> Base.encode16(case: :lower)
    |> Base.encode64()
  end

  defp create_beneficiary_request(params) do
    {:ok, body} =
      %{
        first_name: params["first_name"],
        last_name: params["last_name"],
        country: "CA",
        city: "Toronto",
        address: "123 Fake Street",
        state: "Ontario",
        postcode: "M6C2R8",
        currency: params["currency"],
        category: "bank",
        entity_type: "individual",
        payment_type: "priority",
        account_number: "111111111111",
        bic_swift: "11111111"
      }
      |> Jason.encode()

      body
  end
Enter fullscreen mode Exit fullscreen mode

Next, proceed to update the create function on the same controller:

  def create(conn, %{"beneficiary" => beneficiary_params}) do
    # Define the base url and target path
    base_url = "https://sandboxapi.rapyd.net"
    url_path = "/v1/payouts/beneficiary"

    # Generate the values needed for the headers
    access_key = Application.fetch_env!(:rapyd_fx_example, :rapyd_access_key)
    salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
    timestamp = System.os_time(:second)

    # Generate request body
    body = create_beneficiary_request(beneficiary_params)

    signature = sign_request("post", url_path, salt, timestamp, access_key, to_string(body))

    # Build the headers
    headers = [
      {"access_key", access_key},
      {"salt", salt},
      {"timestamp", timestamp},
      {"url_path", url_path},
      {"signature", signature}
    ]

    response = HTTPoison.post!(base_url <> url_path, body, headers)
    response = response.body |> Jason.decode!()

    # Replace the Wallet params for id and url
    beneficiary_params = beneficiary_params
      |> Map.put("uuid", response["data"]["id"])

    case Rapyd.create_beneficiary(beneficiary_params) do
      {:ok, beneficiary} ->
        conn
        |> put_flash(:info, "Beneficiary created successfully.")
        |> redirect(to: Routes.beneficiary_path(conn, :show, beneficiary))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end
Enter fullscreen mode Exit fullscreen mode

And update the lib/rapyd_fx_example_web/templates/beneficiary/form.html.heex form to the following:

<.form let={f} for={@changeset} action={@action}>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <%= label f, :first_name %>
  <%= text_input f, :first_name %>
  <%= error_tag f, :first_name %>

  <%= label f, :last_name %>
  <%= text_input f, :last_name %>
  <%= error_tag f, :last_name %>

  <%= label f, :currency %>
  <%= text_input f, :currency %>
  <%= error_tag f, :currency %>

  <div>
    <%= submit "Save" %>
  </div>
</.form>
Enter fullscreen mode Exit fullscreen mode

Open localhost:4000/beneficiaries/new, and create a beneficiary by entering the beneficiary's first name, last name, and the currency that they will be paid in and click Save.

You should now be able to see the newly created beneficiary in the list of beneficiaries on the Rapyd dashboard. Keep track of the beneficiary ID, as you’ll be using it as part of your payout request.

New Beneficiary

Creating a Payout Request

Now that you have a wallet and a beneficiary, you can create a payout request. You can create payout requests with FX in a few different ways. For this example, you’ll generate a payout request that requires manual confirmation of the exchange rate.

The following workflow illustrates the steps required.

Sequence Diagram

Start by generating a new controller and the corresponding views with the following command:

mix phx.gen.html Rapyd Payout payouts beneficiary:string beneficiary_entity_type:string payout_amount:float payout_currency:string ewallet_id:string payout_transaction:string
Enter fullscreen mode Exit fullscreen mode

Then, run the following command:

mix ecto.migrate
Enter fullscreen mode Exit fullscreen mode

Next, you need to make the route available by adding the following code to the lib/rapyd_fx_example_web/router.ex file:


  scope "/", RapydFxExampleWeb do
    pipe_through :browser

    resources "/payouts", PayoutController

Enter fullscreen mode Exit fullscreen mode

Copy the following methods to generate the signature and the body of the request to lib/rapyd_fx_example_web/controllers/payout_controller.ex:

  def sign_request(http_method, url_path, salt, timestamp, access_key, body) do
    secret_key = Application.fetch_env!(:rapyd_fx_example, :rapyd_secret_key)

    cond do
      body == "" ->
        body = ""
      true ->
        {:ok, body} = body |> Jason.encode()
    end

    signature_string =
      [http_method, url_path, salt, timestamp, access_key, secret_key, body] |> Enum.join("")

    :crypto.mac(:hmac, :sha256, secret_key, signature_string)
    |> Base.encode16(case: :lower)
    |> Base.encode64()
  end

  defp create_payout_request(params) do
    {:ok, body} =
      %{
        beneficiary: params["beneficiary"],
        ewallet: params["ewallet_id"],
        beneficiary_entity_type: params["beneficiary_entity_type"],
        confirm_automatically: false,
        description: "FX with confirmation",
        payout_amount: params["payout_amount"],
        payout_method_type: "ca_general_bank",
        payout_currency: params["payout_currency"],
        sender: %{
          country: "US",
          city: "Austin",
          address: "123 Rodeo Drive",
          state: "Texas",
          postcode: "73220",
          name: "Bob Smith",
          currency: "USD",
          entity_type: "company",
          identification_value: "123456789",
          identification_type: "incorporation_number"
        },
        sender_country: "US",
        sender_currency: "USD",
        sender_entity_type: "company",
      }
      |> Jason.encode()

      body
  end
Enter fullscreen mode Exit fullscreen mode

Next, as you’ve done before, update the create method to the following:

  def create(conn, %{"payout" => payout_params}) do

    # Define the base url and target path
    base_url = "https://sandboxapi.rapyd.net"
    url_path = "/v1/payouts"

    # Generate the values needed for the headers
    access_key = Application.fetch_env!(:rapyd_fx_example, :rapyd_access_key)
    salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
    timestamp = System.os_time(:second)

    # Generate request body
    body = create_payout_request(payout_params)

    signature = sign_request("post", url_path, salt, timestamp, access_key, to_string(body))

    # Build the headers
    headers = [
      {"access_key", access_key},
      {"salt", salt},
      {"timestamp", timestamp},
      {"url_path", url_path},
      {"signature", signature}
    ]

    response = HTTPoison.post!(base_url <> url_path, body, headers)
    response = response.body |> Jason.decode!()

    # Replace the Wallet params for id and url
    payout_params = payout_params
      |> Map.put("payout_transaction", response["data"]["id"])

    case Rapyd.create_payout(payout_params) do
      {:ok, payout} ->
        conn
        |> put_flash(:info, "Payout created successfully.")
        |> redirect(to: Routes.payout_path(conn, :show, payout))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end
Enter fullscreen mode Exit fullscreen mode

You can now move forward and initiate your payout transactions. Open http://localhost:4000/payouts/new in your browser and enter the following information:

  • Beneficiary: The ID of the beneficiary you created in the previous step.
  • Beneficiary entity type: In this case, individual.
  • Payout amount: The amount of money that you’re looking to transfer; in this case, use 20 dollars.
  • Payout currency: This must match the beneficiary currency, so use “CAD”.
  • Ewallet: The ID of the wallet you created at the beginning of this article.
  • Payout Transaction: This value will be replaced as we create the payout request, enter “0” in the form.

If everything worked correctly, you should see the following confirmation.

Payout confirmation

However, if you look at the Rapyd dashboard, the payout is still not there because the payout still needs to be confirmed in a separate request.

Making a Request to Confirm a Payout

Because you’re generating a payout with a foreign currency, you need to confirm the transfer. You can do this with a simple API call.

Start by updating the lib/rapyd_fx_example_web/controllers/payout_controller.ex and adding a new confirmation action:

  def confirm(conn, %{"id" => id}) do
    # Define the base url and target path
    base_url = "https://sandboxapi.rapyd.net"
    url_path = "/v1/payouts/confirm/" <> id

    # Generate the values needed for the headers
    access_key = Application.fetch_env!(:rapyd_fx_example, :rapyd_access_key)
    salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
    timestamp = System.os_time(:second)

    # Generate request body
    body = ""

    signature = sign_request("post", url_path, salt, timestamp, access_key, to_string(body))

    # Build the headers
    headers = [
      {"access_key", access_key},
      {"salt", salt},
      {"timestamp", timestamp},
      {"url_path", url_path},
      {"signature", signature}
    ]

    response = HTTPoison.post!(base_url <> url_path, body, headers)
    response = response.body |> Jason.decode!()

    conn
    |> put_flash(:info, "Payout confirmed successfully.")
    |> redirect(to: Routes.payout_path(conn, :index, []))
  end
Enter fullscreen mode Exit fullscreen mode

Next, update the lib\rapyd_fx_example_web\templates\payout\index.html.heex template to add a new payout confirmation action:

...
<%= for payout <- @payouts do %>
    <tr>
      <td><%= payout.beneficiary %></td>
      <td><%= payout.beneficiary_entity_type %></td>
      <td><%= payout.payout_amount %></td>
      <td><%= payout.payout_currency %></td>
      <td><%= payout.ewallet_id %></td>
      <td><%= payout.payout_transaction %></td>

      <td>
        <span><%= link "Show", to: Routes.payout_path(@conn, :show, payout) %></span>
        <span><%= link "Confirm", to: Routes.payout_path(@conn, :confirm, payout) %></span>
        <span><%= link "Delete", to: Routes.payout_path(@conn, :delete, payout), method: :delete, data: [confirm: "Are you sure?"] %></span>
      </td>
    </tr>
<% end %>

...
Enter fullscreen mode Exit fullscreen mode

By adding the confirmation action, you can easily confirm the transaction after it’s been created.

From the list view, you can click the confirm link that you just added to confirm the transaction and let the funds change hands. The payout should now be visible under the sender's wallet transaction list.

Note: Transactions must be confirmed within five minutes of their creation, or they’ll be automatically closed.

Payout Transaction

You’re not quite done yet, as the transaction needs to be closed before the money is transferred.

Closing the Payout Transaction

First, create a new controller action to handle the request to complete the payment by opening lib/rapyd_fx_example_web/controllers/payout_controller.ex and adding the following code:

  def complete(conn, %{"id" => id}) do
    payout = Rapyd.get_payout!(id)
    IO.inspect(payout)
    # Define the base url and target path
    base_url = "https://sandboxapi.rapyd.net"
    url_path = "/v1/payouts/complete/" <> payout.payout_transaction <> "/" <> to_string(payout.payout_amount * 100)

    # Generate the values needed for the headers
    access_key = Application.fetch_env!(:rapyd_fx_example, :rapyd_access_key)
    salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
    timestamp = System.os_time(:second)

    # Generate request body
    body = ""

    signature = sign_request("post", url_path, salt, timestamp, access_key, to_string(body))

    # Build the headers
    headers = [
      {"access_key", access_key},
      {"salt", salt},
      {"timestamp", timestamp},
      {"url_path", url_path},
      {"signature", signature}
    ]

    response = HTTPoison.post!(base_url <> url_path, body, headers)
    response = response.body |> Jason.decode!()

    conn
    |> put_flash(:info, "Payout completed successfully.")
    |> redirect(to: Routes.payout_path(conn, :index, []))
  end
Enter fullscreen mode Exit fullscreen mode

Next, as you did before, you’ll update your lib\rapyd_fx_example_web\templates\payout\index.html.heex template to include the confirmation action:

...
<%= for payout <- @payouts do %>
    <tr>
      <td><%= payout.beneficiary %></td>
      <td><%= payout.beneficiary_entity_type %></td>
      <td><%= payout.payout_amount %></td>
      <td><%= payout.payout_currency %></td>
      <td><%= payout.ewallet_id %></td>
      <td><%= payout.payout_transaction %></td>

      <td>
        <span><%= link "Show", to: Routes.payout_path(@conn, :show, payout) %></span>
        <span><%= link "Confirm", to: Routes.payout_path(@conn, :confirm, payout) %></span>
        <span><%= link "Complete", to: Routes.payout_path(@conn, :complete, payout) %></span>
        <span><%= link "Delete", to: Routes.payout_path(@conn, :delete, payout), method: :delete, data: [confirm: "Are you sure?"] %></span>
      </td>
    </tr>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Finally, you can, go to the Disburse tab to confirm the transaction was completed successfully.

Completed Transaction

Conclusion

In this article, you learned about the powerful set of APIs that Rapyd provides in order to handle payments and payouts across borders, and how you can use these APIs to programmatically generate payouts in foreign currencies.

The article also illustrated the flow required to create and issue a payout in a foreign currency and the different objects that need to be created to generate a transaction.

With Rapyd Payout, paying your employees in the right currency with ease and confidence is simple and extremely flexible.

Top comments (0)