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 Cybersource to process a payment without it ever passing through your own server.
Cybersource uses hashing algorithms to make sure the data has not been tampered with before arriving at their API endpoints. Computing these hashes can be quite a laborious task, thankfully PCI Vault has the Rule Engine which can be leveraged to compute the hashes for you. This is one of the more complicated examples of how to use the rule engine, but it demonstrates its flexibility and power.
In this proxy recipe, we'll show you how to:
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",
"input_field": "*",
"output_field": "body",
"arguments": {
"template": "{\"clientReferenceInformation\": {\"code\": \"{{reference_code}}\"}, \"paymentInformation\": {\"card\": {\"number\": \"{{number}}\", \"expirationMonth\": \"{{expiry_month}}\", \"expirationYear\": \"{{expiry_year}}\"}}, \"orderInformation\": {\"amountDetails\": {\"totalAmount\": \"{{total_amount}}\", \"currency\": \"{{currency}}\"}, \"billTo\": {\"firstName\": \"{{first_name}}\", \"lastName\": \"{{last_name}}\", \"address1\": \"{{address1}}\", \"locality\": \"{{locality}}\", \"administrativeArea\": \"{{administrative_area}}\", \"postalCode\": \"{{postal_code}}\", \"country\": \"{{country}}\", \"email\": \"{{email}}\", \"phoneNumber\": \"{{phone_number}}\"}}}"
}
},
{
"order_position": 2,
"name": "hash",
"input_field": "body",
"output_field": "body_digest",
"arguments": {
"algorithm": "sha-256"
}
},
{
"order_position": 3,
"name": "encode",
"input_field": "body_digest",
"output_field": "body_digest_base64",
"arguments": {
"scheme": "base64",
"hex_input": true
}
},
{
"order_position": 4,
"name": "template",
"input_field": "*",
"output_field": "signature",
"arguments": {
"template": "date: {{{date}}}\ndigest: SHA-256={{{body_digest_base64}}}\nhost: {{{host}}}\nv-c-merchant-id: {{{merchant_id}}}\nrequest-target: {{{request_target}}}"
}
},
{
"order_position": 5,
"name": "hash",
"input_field": "signature",
"output_field": "signature_hash",
"arguments": {
"algorithm": "hmac-sha-256",
"key": "secret-key" // remember to set your secret key here
//"base64_key": true // set to true if the secret key is base64 encoded
}
},
{
"order_position": 6,
"name": "encode",
"input_field": "signature_hash",
"output_field": "signature_hash_base64",
"arguments": {
"scheme": "base64",
"hex_input": true
}
},
{
"order_position": 7,
"name": "template",
"input_field": "*",
"output_field": "signature_header",
"arguments": {
"template": "keyid:\"secret-key-id\",algorithm=\"HmacSHA256\",headers=\"date digest host v-c-merchant-id request-target\",signature=\"{{{signature_hash_base64}}}\"" //remember to set your secret key id here
}
}
]
This rule consists of 7 operations, let's break down what each operation does:
{
"order_position": 1,
"name": "template",
"input_field": "*",
"output_field": "body",
"arguments": {
"template": "{\"clientReferenceInformation\": {\"code\": \"{{reference_code}}\"}, \"paymentInformation\": {\"card\": {\"number\": \"{{number}}\", \"expirationMonth\": \"{{expiry_month}}\", \"expirationYear\": \"{{expiry_year}}\"}}, \"orderInformation\": {\"amountDetails\": {\"totalAmount\": \"{{total_amount}}\", \"currency\": \"{{currency}}\"}, \"billTo\": {\"firstName\": \"{{first_name}}\", \"lastName\": \"{{last_name}}\", \"address1\": \"{{address1}}\", \"locality\": \"{{locality}}\", \"administrativeArea\": \"{{administrative_area}}\", \"postalCode\": \"{{postal_code}}\", \"country\": \"{{country}}\", \"email\": \"{{email}}\", \"phoneNumber\": \"{{phone_number}}\"}}}"
}
}
The first operation will render the whole request body into a string field called body.
The JSON object looks like this when it's parsed:
{
"clientReferenceInformation": {
"code": "{{reference_code}}"
},
"paymentInformation": {
"card": {
"number": "{{number}}",
"expirationMonth": "{{expiry_month}}",
"expirationYear": "{{expiry_year}}"
}
},
"orderInformation": {
"amountDetails": {
"totalAmount": "{{total_amount}}",
"currency": "{{currency}}"
},
"billTo": {
"firstName": "{{first_name}}",
"lastName": "{{last_name}}",
"address1": "{{address1}}",
"locality": "{{locality}}",
"administrativeArea": "{{administrative_area}}",
"postalCode": "{{postal_code}}",
"country": "{{country}}",
"email": "{{email}}",
"phoneNumber": "{{phone_number}}"
}
}
}
You will notice that there are a lot of fields in the template which are not stored in the vault. These fields will be set using the extra_data object when making the proxy request in the next step. The output of this operation will be used to calculate the signature needed by Cybersource.
{
"order_position": 2,
"name": "hash",
"input_field": "body",
"output_field": "body_digest",
"arguments": {
"algorithm": "sha-256"
}
}
The second operation computes a SHA256 digest of the entire body, then outputs that digest into a field called body_digest. Since the output of this operation is binary data, it is encoded as a hexadecimal string to be used by the next operation.
{
"order_position": 3,
"name": "encode",
"input_field": "body_digest",
"output_field": "body_digest_base64",
"arguments": {
"scheme": "base64",
"hex_input": true
}
}
The third operation takes the body_digest hexadecimal string and encodes it as Base64 into another field called body_digest_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.
{
"order_position": 4,
"name": "template",
"input_field": "*",
"output_field": "signature",
"arguments": {
"template": "date: {{{date}}}\ndigest: SHA-256={{{body_digest_base64}}}\nhost: {{{host}}}\nv-c-merchant-id: {{{merchant_id}}}\nrequest-target: {{{request_target}}}"
}
}
The fourth operation builds a signature string composed of fields that will be set at request initiation (extra_data) plus the body_digest_base64 which was computed in the previous operation. The order of the fields in the signature is important and should be noted.
{
"order_position": 5,
"name": "hash",
"input_field": "signature",
"output_field": "signature_hash",
"arguments": {
"algorithm": "hmac-sha-256",
"key": "secret-key" // remember to set your secret key here
//"base64_key": true // set to true if the secret key is base64 encoded
}
},
{
"order_position": 6,
"name": "encode",
"input_field": "signature_hash",
"output_field": "signature_hash_base64",
"arguments": {
"scheme": "base64",
"hex_input": true
}
}
Operations 5 and 6 are similar to the previous operations which calculated the SHA256 digest of the body, but this time it is done for the signature. This time instead of just computing a digest, we use the secret key from the Cybersource dashboard to compute an HMAC (Hash-based Message Authentication Code). The output is then Base64 encoded and saved to a field signature_hash_base64.
{
"order_position": 7,
"name": "template",
"input_field": "*",
"output_field": "signature_header",
"arguments": {
"template": "keyid:\"secret-key-id\",algorithm=\"HmacSHA256\",headers=\"date digest host v-c-merchant-id request-target\",signature=\"{{{signature_hash_base64}}}\"" //remember to set your secret key id here
}
}
The last operation builds a header value to be included in the request. The headers key lists the fields in the same order as they were used to construct the signature string in operation 4. This operation requires the secret key id from the Cybersource dashboard and includes the signature_hash_base64 which was computed in the previous operation.
Note: this one rule is very specific to processing a payment. If multiple Cybersource 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 compute the signature. 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. cybersource_process_payment_body,cybersource_compute_signature. (comma separated list of rule ids)
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 Cybersourceuser: the key used to encrypt the datapassphrase: the passphrase for the key used to encrypt the datarules: comma separated list of rule ids, e.g. cybersource_process_paymentThe request URL will look like https://api.pcivault.io/v1/proxy/post?token=uniquetoken&user=ABCD&passphrase=secretpassphrase&rules=cybersource_process_payment
The request body will look something like this:
{
"request": {
"method": "POST",
"url": "https://api.cybersource.com/pts/v2/payments",
"headers": [
{
"Content-Type": "application/json"
},
{
"Host": "{{host}}"
},
{
"v-c-merchant-id": "{{merchant_id}}"
},
{
"v-c-date": "{{date}}"
},
{
"digest": "SHA-256={{{body_digest_base64}}}"
},
{
"request-target": "{{{request_target}}}"
},
{
"signature": "{{{signature_header}}}"
}
],
"body": "{{{body}}}",
"extra_data": {
"host": "api.cybersource.com",
"merchant_id": "your cybersource merchant id",
"date": "Thu, 18 Jul 2023, 22:18:03",
"request_target": "post /pts/v2/payments/",
"reference_code": "your reference code",
"total_amount": 1000,
"currency": "USD",
"address1": "Address Example",
"locality": "Austin",
"administrative_area": "TX",
"postal_code": "73301",
"country": "USA",
"email": "test@example.com",
"phone_number": "+1 512 555 5555",
"first_name": "John",
"last_name": "Doe"
}
},
"synchronous": true
}
We define the proxy request as being a "POST" method, having all the required Cybersource request headers. For the body we'll set a mustache template that simply renders the body field rendered by our rule. 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 Cybersource response to be returned and will send the result back to us when the request completes.
The response from Cybersource 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": "{{{body}}}",
"extra_data": {
"host": "api.cybersource.com",
"merchant_id": "your cybersource merchant id",
"date": "Thu, 18 Jul 2023, 22:18:03",
"request_target": "post /pts/v2/payments/",
"reference_code": "your reference code",
"total_amount": 1000,
"currency": "USD",
"address1": "Address Example",
"locality": "Austin",
"administrative_area": "TX",
"postal_code": "73301",
"country": "USA",
"email": "test@example.com",
"phone_number": "+1 512 555 5555",
"first_name": "John",
"last_name": "Doe"
}
},
"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 Cybersource
"status_code": 200
}
}
The response from Cybersource is in the result field. The body is returned as a JSON string, which can be parsed and handled appropriately.