PCI Vault Logo
Alliance Pay

Sending card data to Alliance Pay

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 Alliance Pay to process a payment without it ever passing through your own server.

Overview

Alliance Pay 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. The full request body gets encrypted and passed in a JSON object field. 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 Alliance Pay
  2. Initiate a proxy request
  3. Handle the response from Alliance Pay

Step 1: Create a rule to encrypt the data sent to Alliance Pay

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 template field names would need to be adjusted for, depending on your specific data format):

[
  {
    "order_position": 1,
    "name": "template",
    "intput_field": "*",
    "output_field": "body",
    "arguments": {
      "template": "{\"reference\":\"{{reference}}\",\"paymentoption\":\"C\",\"country\":\"NG\",\"card\":{\"cvv\":\"{{cvv}}\",\"cardnumber\":\"{{card_number}}\",\"expirymonth\":\"{{expiry_month}}\",\"expiryyear\":\"{{expiry_year_short}}\",\"pin\":\"{{pin}}\"}}"
    }
  },
  {
    "order_position": 2,
    "name": "encrypt",
    "intput_field": "body",
    "output_field": "body_encrypted",
    "arguments": {
      "algorithm": "rsa",
      "key": "MIIEnzANBgkqhkiG9w0BAQEFAAOCBIwAMIIEhwKCBH4A1DcoOCo9oNveYlWf96rd...",
      "base64_key": true
    }
  },
  {
    "order_position": 3,
    "name": "encode",
    "intput_field": "body_encrypted",
    "output_field": "body_encrypted_base64",
    "arguments": {
      "scheme": "base64",
      "hex_input": true
    }
  }
]

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

Operation 1

{
  "order_position": 1,
  "name": "template",
  "intput_field": "*",
  "output_field": "body",
  "arguments": {
    "template": "{\"reference\":\"{{reference}}\",\"paymentoption\":\"C\",\"country\":\"NG\",\"card\":{\"cvv\":\"{{cvv}}\",\"cardnumber\":\"{{card_number}}\",\"expirymonth\":\"{{expiry_month}}\",\"expiryyear\":\"{{expiry_year_short}}\",\"pin\":\"{{pin}}\"}}"
  }
}

The first operation outputs the full request body into a body field. The values stored in the vault will be populated to make a JSON string. When parsed, the template looks as follows:

{
  "reference": "{{reference}}",
  "paymentoption": "C",
  "country": "NG",
  "card": {
    "cvv": "{{cvv}}",
    "cardnumber": "{{card_number}}",
    "expirymonth": "{{expiry_month}}",
    "expiryyear": "{{expiry_year_short}}",
    "pin": "{{pin}}"
  }
}

Operation 2

{
  "order_position": 2,
  "name": "encrypt",
  "intput_field": "body",
  "output_field": "body_encrypted",
  "arguments": {
    "algorithm": "rsa",
    "key": "MIIEnzANBgkqhkiG9w0BAQEFAAOCBIwAMIIEhwKCBH4A1DcoOCo9oNveYlWf96rd...",
    "base64_key": true
  }
}

The second operation encrypts the JSON string using RSA encryption with the padding scheme from PKCS #1 v1.5. We also set our encryption key obtained from Alliance Pay, but the key format you get from Alliance Pay is a custom format and we first need to do some pre-processing to convert it to an RSA key format that the operation can use. Use the python script below to convert the key:

pip install pycryptodome

Create a file convert.py containing the following code.

import sys
import base64

from Crypto.PublicKey import RSA

import xml.etree.ElementTree as ET


def get_xml_component(xmlstring, _field):
    try:
        modulus_value = ET.fromstring(xmlstring).findtext(_field) or ""
        return modulus_value
    except Exception as e:
        print(f"Error: {e}")
        return ""

def to_rsa_key(alliance_pay_key: str):
  decoded_string = base64.b64decode(alliance_pay_key).decode('utf-8').split('!')[1]
  
  modulus = get_xml_component(decoded_string, "Modulus")
  exponent = get_xml_component(decoded_string, "Exponent")

  modulus_int = int.from_bytes(base64.b64decode(modulus), 'big')
  exponent_int = int.from_bytes(base64.b64decode(exponent), 'big')

  return RSA.construct((modulus_int, exponent_int))

if __name__ == '__main__':
  key = to_rsa_key(sys.argv[1])
  print(key.export_key())

Then convert your key with:

python convert.py base64encoded-alliancepay-key

This will print the RSA key you need to use:

-----BEGIN PUBLIC KEY-----\nMIIEnzANBgkqhkiG9w0BAQEFAAOCBIwAMIIEhwKCBH4A1DcoOCo9oNveYlWf96rd\nxjUqZM5z1cGNJ+1uvDXNoA5/sfPzmtrooct5ZfwA5/I03u
...
...
+GT9w4wye\n6H1p7PVfHAfOIFUIyPq1WTi07Dg5y0ae3u0odZKJAgMBAAE=\n-----END PUBLIC KEY-----

Copy everything between -----BEGIN PUBLIC KEY-----\n and \n-----END PUBLIC KEY-----, use that as the key argument when creating your rule.

Operation 3

{
  "order_position": 3,
  "name": "encode",
  "intput_field": "body_encrypted",
  "output_field": "body_encrypted_base64",
  "arguments": {
    "scheme": "base64",
    "hex_input": true
  }
}

The third operation takes the body_encrypted hexadecimal string and encodes it as Base64 into another field called body_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.

Note: this one rule is very specific to processing a payment. If multiple Alliance Pay endpoints are to be used, it would be a good idea to create a separate rule to output the body of each request from the rule to encrypt it. When making the proxy request one can apply multiple rules in succession, with the computed fields from the first being available to the next e.g. alliancepay_process_payment_body,alliancepay_encrypt_body. (comma separated list of rule ids)

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 Alliance Pay
  • 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. alliancepay_process_payment

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

The request body will look something like this:

{
  "request": {
    "method": "POST",
    "url": "https://checkout-api.alliancepay.cloud/checkout/order/pay",
    "headers": [
      {
        "Content-Type": "application/json"
      },
      {
        "api-key": "{{api_key}}"
      }
    ],
    "body": "{\"data\": \"{{body_encrypted_base64}}\"}",
    "extra_data": {
      "api_key": "PGW-PUBLICKEY-TEST-.....",
      "reference": "ORDER1",
      "pin": "12345"
    }
  },
  "synchronous": true
}

We define the proxy request as being a "POST" method, having the required Alliance Pay request headers like api-key. For the body we'll set a mustache template that simply renders the body_encrypted_base64 field as the value for data. 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 Alliance Pay response to be returned and will send the result back to us when the request completes.

Step 3: Handle the response

The response from Alliance Pay 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": "{\"data\": \"{{body_encrypted_base64}}\"}",
    "extra_data": {
      "api_key": "PGW-PUBLICKEY-TEST-.....",
      "reference": "ORDER1",
      "pin": "12345"
    }
  },
  "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 Alliance Pay
    "status_code": 200
  }
}

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