PCI Vault Logo
Flutterwave

Sending card data to Flutterwave

PCI Vault has the ability to forward card data to third parties. The data can be transformed to the format required by the third party at the time of the request, without modifying the stored data itself. This is done by using custom rules.

In this proxy recipe, we'll detail how to cater to the use-case, where card data is forwarded to Flutterwave to create a payment method without it ever passing through your own server.

Overview

Flutterwave uses encryption to make request data difficult to read if that data were intercepted. The encryption also ensures that the data being sent is coming from a known source as only that sender will have access to the encryption key. Each field with sensitive data gets encrypted on its own and sent through as separate fields. Computing the encrypted data can be quite a laborious task, thankfully PCI Vault has the Rule Engine which can be leveraged to do the heavy lifting for you.

In this proxy recipe, we'll show you how to:

  1. Create a rule to encrypt the data sent to Flutterwave
  2. Initiate a proxy request
  3. Handle the response from Flutterwave

Step 1: Create a rule to encrypt the data sent to Flutterwave

To create a custom rule, we'll make use of the Rules API, specifically the Create Rule endpoint.

First set a unique id for your rule, this is a string which will be referenced later.

For the list of operations we'll use the following (the field names would need to be adjusted for, depending on your specific data format):

[
  {
    "order_position": 1,
    "name": "nonce",
    "output_field": "nonce"
  },
  {
    "order_position": 2,
    "name": "encrypt",
    "input_field": "card_number",
    "output_field": "card_number_encrypted",
    "arguments": {
      "algorithm": "aes-gcm-256",
      "key": "53c2bfe280a1c2b9c3aae2809ec2a324",
      "hex_key": true,
      "nonce_field": "nonce"
    }
  },
  {
    "order_position": 3,
    "name": "encode",
    "input_field": "card_number_encrypted",
    "output_field": "card_number_encrypted_base64",
    "arguments": {
      "scheme": "base64",
      "hex_input": true
    }
  },
  {
    "order_position": 4,
    "name": "encrypt",
    "input_field": "expiry_month",
    "output_field": "expiry_month_encrypted",
    "arguments": {
      "algorithm": "aes-gcm-256",
      "key":  "53c2bfe280a1c2b9c3aae2809ec2a324",
      "hex_key": true,
      "nonce_field": "nonce"
    }
  },
  {
    "order_position": 5,
    "name": "encode",
    "input_field": "expiry_month_encrypted",
    "output_field": "expiry_month_encrypted_base64",
    "arguments": {
      "scheme": "base64",
      "hex_input": true
    }
  },
  {
    "order_position": 6,
    "name": "encrypt",
    "input_field": "expiry_year",
    "output_field": "expiry_year_encrypted",
    "arguments": {
      "algorithm": "aes-gcm-256",
      "key": "53c2bfe280a1c2b9c3aae2809ec2a324",
      "hex_key": true,
      "nonce_field": "nonce"
    }
  },
  {
    "order_position": 7,
    "name": "encode",
    "input_field": "expiry_year_encrypted",
    "output_field": "expiry_year_encrypted_base64",
    "arguments": {
      "scheme":    "base64",
      "hex_input": true
    }
  },
  {
    "order_position": 8,
    "name": "encrypt",
    "input_field": "cvv",
    "output_field": "cvv_encrypted",
    "arguments": {
      "algorithm": "aes-gcm-256",
      "key": "53c2bfe280a1c2b9c3aae2809ec2a324",
      "hex_key": true,
      "nonce_field": "nonce"
    }
  },
  {
    "order_position": 9,
    "name": "encode",
    "input_field": "cvv_encrypted",
    "output_field": "cvv_encrypted_base64",
    "arguments": {
      "scheme": "base64",
      "hex_input": true
    }
  }
]

This rule consists of 9 operations, let's break down what each operation does:

Operation 1

{
  "order_position": 1,
  "name": "nonce",
  "output_field": "nonce"
}

The first operation will generate a nonce (a unique use-only-once number). Our operation generates a string (default length: 12 bytes) of characters [a-z], [A-Z] and [0-9]. This string is outputted into a field called nonce and will be used as the IV (initialization vector) for the encryption.

Operation 2

{
  "order_position": 2,
  "name": "encrypt",
  "input_field": "card_number",
  "output_field": "card_number_encrypted",
  "arguments": {
    "algorithm": "aes-gcm-256",
    "key": "53c2bfe280a1c2b9c3aae2809ec2a324",
    "hex_key": true,
    "nonce_field": "nonce"
  }
}

The second operation takes the value from card_number and encrypts it using AES256 with GCM. The 256 bit key can either be set as a hexadecimal string when "hex_key": true or as a Base64 encoded string when "base64_key": true. Here we use a hex string. We also specify the name of the field where the nonce from the previous operation is stored. The output of the operation is a hexadecimal string which we output to card_number_encrypted.

Operation 3

{
  "order_position": 3,
  "name": "encode",
  "input_field": "card_number_encrypted",
  "output_field": "card_number_encrypted_base64",
  "arguments": {
    "scheme": "base64",
    "hex_input": true
  }
}

The third operation takes the card_number_encrypted hexadecimal string and encodes it as Base64 into another field called card_number_encrypted_base64. Note we set "hex_input": true to tell the operation that the input field is hex. Otherwise it would assume that the input is a utf-8 string. This Base64 encoded string is what will be sent to Flutterwave.

Operations 4 to 9

{
  "order_position": 4,
  "name": "encrypt",
  "input_field": "expiry_month",
  "output_field": "expiry_month_encrypted",
  "arguments": {
    "algorithm": "aes-gcm-256",
    "key": "53c2bfe280a1c2b9c3aae2809ec2a324",
    "hex_key": true,
    "nonce_field": "nonce"
  }
},
{
  "order_position": 5,
  "name": "encode",
  "input_field": "expiry_month_encrypted",
  "output_field": "expiry_month_encrypted_base64",
  "arguments": {
    "scheme": "base64",
    "hex_input": true
  }
},
{
  "order_position": 6,
  "name": "encrypt",
  "input_field": "expiry_year_short",
  "output_field": "expiry_year_encrypted",
  "arguments": {
    "algorithm": "aes-gcm-256",
    "key": "53c2bfe280a1c2b9c3aae2809ec2a324",
    "hex_key": true,
    "nonce_field": "nonce"
  }
},
{
  "order_position": 7,
  "name": "encode",
  "input_field": "expiry_year_encrypted",
  "output_field": "expiry_year_encrypted_base64",
  "arguments": {
    "scheme": "base64",
    "hex_input": true
  }
},
{
  "order_position": 8,
  "name": "encrypt",
  "input_field": "cvv",
  "output_field": "cvv_encrypted",
  "arguments": {
    "algorithm": "aes-gcm-256",
    "key": "53c2bfe280a1c2b9c3aae2809ec2a324",
    "hex_key": true,
    "nonce_field": "nonce"
  }
},
{
  "order_position": 9,
  "name": "encode",
  "input_field": "cvv_encrypted",
  "output_field": "cvv_encrypted_base64",
  "arguments": {
    "scheme": "base64",
    "hex_input": true
  }
}

The rest of the operations do exactly the same as the first 3, but for the other fields with sensitive data: expiry_month, expiry_year_short and cvv.

Step 2: Initiate a proxy request

We'll use the Proxy Post endpoint to initiate a proxy request. The following query string parameters are required:

  • token: the token identifying the card we'd like to send to Flutterwave
  • user: the key used to encrypt the data
  • passphrase: the passphrase for the key used to encrypt the data
  • rules: comma separated list of rule ids, e.g. flutterwave_encrypt

The request URL will look like https://api.pcivault.io/v1/proxy/post?token=uniquetoken&user=ABCD&passphrase=secretpassphrase&rules=flutterwave_encrypt

The request body will look something like this:

{
  "request": {
    {
      "method": "POST",
      "url": "https://developersandbox-api.flutterwave.com/payment-methods",
      "headers": [
        {
          "Content-Type": "application/json"
        },
        {
          "Authorization": "Bearer {{auth_token}}"
        },
        {
          "X-Trace-Id": "{{trace_id}}"
        },
        {
          "X-Idempotency-Key": "{{idempotency_key}}"
        }
      ],
      "body": "{\"type\":\"card\",\"card\": {\"nonce\": \"{{nonce}}\",\"encrypted_expiry_month\": \"{{expiry_month_encrypted_base64}}\",\"encrypted_expiry_year\": \"{{expiry_year_encrypted_base64}}\",\"encrypted_card_number\": \"{{card_number_encrypted_base64}}\",\"encrypted_cvv\": \"{{cvv_encrypted_base64}}\"}}",
      "extra_data": {
        "auth_token": "your_auth_token",
        "trace_id": "your_trace_id",
        "idempotency_key": "your_idempotency_key"
      }
    }
  },
  "synchronous": true
}

We define the proxy request as being a "POST" method, having all the required Flutterwave request headers. For the body we'll set a mustache template which when parsed is in the format:

{
  "type":"card",
  "card": {
    "nonce": "{{nonce}}",
    "encrypted_expiry_month": "{{expiry_month_encrypted_base64}}",
    "encrypted_expiry_year": "{{expiry_year_encrypted_base64}}",
    "encrypted_card_number": "{{card_number_encrypted_base64}}",
    "encrypted_cvv": "{{cvv_encrypted_base64}}"
  }
}

Here we output the nonce string used as IV, as well as all the Base64 encoded encrypted fields.

We set the extra_data object whose fields will be merged with the stored data before the rule(s) are applied. All the fields outputted by rule operations can be outputted in headers, body and even url fields using mustache templates.

One optional parameter that is set in this example is "synchronous": true. This will tell PCI Vault to wait for the Flutterwave response to be returned and will send the result back to us when the request completes.

Step 3: Handle the response

The response from Flutterwave will be wrapped in a JSON object like this:

{
  "id": "SLHpcudvjdUCokB2UCkR4n",
  "status": "success",
  "type": "post",
  "use_static_ip": false,
  "synchronous": true,
  "request": {
    "method": "POST",
    "url": "https://api.cybersource.com/pts/v2/payments",
    "headers": [
      {
        "content-type": "application/json"
      },
      ...
    ],
    "body": "{\"type\":\"card\",\"card\": {\"nonce\": \"{{nonce}}\",\"encrypted_expiry_month\": \"{{expiry_month_encrypted_base64}}\",\"encrypted_expiry_year\": \"{{expiry_year_encrypted_base64}}\",\"encrypted_card_number\": \"{{card_number_encrypted_base64}}\",\"encrypted_cvv\": \"{{cvv_encrypted_base64}}\"}}",
    "extra_data": {
      "auth_token": "your_auth_token",
      "trace_id": "your_trace_id",
      "idempotency_key": "your_idempotency_key"
    }
  },
  "webhook": {
    "status": "pending"
  },
  "max_attempts": 1,
  "attempts": 0,
  "debug_mode": false,
  "received": "2025-11-12T14:06:06.621177+00:00",
  "result": {
    "headers": [
      {
        "Content-Type": "application/json"
      },
      ...
    ],
    "body": "[third party resonse as JSON string]", // response body from Flutterwave
    "status_code": 200
  }
}

The response from Flutterwave is in the result field. The body is returned as a JSON string, which can be parsed and handled appropriately.