PCI Vault Logo
Paypal

Sending card data to Paypal

PCI Vault has the ability to forward card data to third parties.

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

Paypal does not require any complicated hashed signatures or data encryption to be applied on the data. This removes the need to create a custom rule and makes forwarding the card data easy. You'll only need to authenticate your app first in order to get an access token from Paypal then use it in the proxy request. In the rest of this recipe we'll assume that you have already obtained this token before initiating the proxy request, see Paypal API Authentication.

Overview

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

  1. Initiate a proxy request
  2. Handle the response

Step 1: 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 Paypal
  • user: the key used to encrypt the data
  • passphrase: the passphrase for the key used to encrypt the data

Optional:

  • reference: if the card data was stored with a reference, this will be required to identify it in the vault

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

The request body will look something like this:

{
  "request": {
    "method": "POST",
    "url": "https://api-m.sandbox.paypal.com/v3/vault/setup-tokens",
    "headers": [
      {
        "Content-Type": "application/json"
      },
      {
        "Authorization": "Bearer PAYPAL-ACCESS-TOKEN" // <- replace with your token
      }
    ],
    "body": "{\"customer\": {\"id\": \"{{customer_id}}\",\"merchant_customer_id\": \"{{merchant_customer_id}}\"},\"payment_source\": {\"card\": {\"number\": \"{{card_number}}\",\"expiry\": \"{{expiry_year}}-{{expiry_month}}\",\"name\": \"{{card_holder}}\",\"security_code\": \"{{cvv}}\",\"billing_address\": {\"address_line_1\": \"{{address_line_1}}\",\"address_line_2\": \"{{address_line_2}}\",\"admin_area_1\": \"{{admin_area_1}}\",\"admin_area_2\": \"{{admin_area_2}}\",\"postal_code\": \"{{postal_code}}\",\"country_code\": \"{{country_code}}\"}}}}",
    "extra_data": {
      "customer_id": "BygeLlrpZF",
      "merchant_customer_id": "customer@merchant.com",
      "address_line_1": "2211 N First Street",
      "address_line_2": "17.3.160",
      "admin_area_1": "CA",
      "admin_area_2": "San Jose",
      "postal_code": "95131",
      "country_code": "US"
    }
  },
  "synchronous": true
}

We define the proxy request as being a "POST" method, having content-type of application/json and our Paypal access token as headers. For the body we'll set a mustache template, where PCI Vault will populate the values from the vault before sending the request to Paypal. Note that one can construct the body template string with fields already populated or like in this case, set an extra_data object whose fields will be merged with the stored data before being populated in the mustache template. Using extra_data is often simpler and cleaner than string concatenation.

When parsed, the body parameter looks like:

{
    "customer": {
        "id": "{{customer_id}}",
        "merchant_customer_id": "{{merchant_customer_id}}"
    },
    "payment_source": {
        "card": {
            "number": "{{card_number}}",
            "expiry": "{{expiry_year}}-{{expiry_month}}",
            "name": "{{card_holder}}",
            "security_code": "{{cvv}}",
            "billing_address": {
                "address_line_1": "{{address_line_1}}",
                "address_line_2": "{{address_line_2}}",
                "admin_area_1": "{{admin_area_1}}",
                "admin_area_2": "{{admin_area_2}}",
                "postal_code": "{{postal_code}}",
                "country_code": "{{country_code}}"
            }
        }
    }
}

Note the field names like expiry_year and expiry_month will exist in the vault if the data was captured using the Hosted Capture Form, if your data differs you will need to create a custom rule using the Rule Engine to convert your expiry date to the correct format before using it in this template.

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

Step 2: Handle the response from Paypal

The response from Paypal 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-m.sandbox.paypal.com/v3/vault/setup-tokens",
    "headers": [
      {
        "content-type": "application/xml"
      },
      {
        "authorization": "Bearer PAYPAL-ACCESS-TOKEN"
      }
    ],
    "body": "...",
    "extra_data": {
      "customer_id": "BygeLlrpZF",
      "merchant_customer_id": "customer@merchant.com",
      "address_line_1": "2211 N First Street",
      "address_line_2": "17.3.160",
      "admin_area_1": "CA",
      "admin_area_2": "San Jose",
      "postal_code": "95131",
      "country_code": "US"
    }
  },
  "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]", // <- Paypal response
    "status_code": 200
  }
}

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

After parsing the response, the ID of the Setup Token can be extracted and used to create a permanent Payment Token