Tokenize cards with API-only

πŸ“˜

This is the most advanced way to send cards to Payrails Vault. We recommend using the Secure Fields or Client-Side Encryption guides if you're looking for a lower integration effort.

If you want full control over collecting card details from your customers and then sending them to us via API, this tokenization type is the right choice. However, it's still important that the card data that travels from your system to ours is encrypted using the highest standards possible.

To achieve this, Payrails uses JWE with an encryption algorithm RSA-OAEP-256 and content encryption A256CBC-HS512.

How it works

Step 1. Collect the card data from your customer

Collect the relevant card data into a JSON object with the following fields. Keep in mind that holderName and securityCode are optional but strongly recommended for increasing your authorization rates when sending a payment request to your Payment Provider.

{
    "cardNumber": "4111111111111111",
    "expiryMonth": "03",
    "expiryYear": "30",
    "securityCode": "737",
    "holderName": "John Doe",
    "holderReference": "customer123"
}

Step 2. Encrypt the card data

Encrypt the full JSON using the Public Key provided by Payrails. If you don't have it yet, please contact our solutions team for help.

  • The Public Key is a PKCS8 RSA public key in PEM format without header and line breaks.
  • The encrypted data should be encrypted using JWE with the encryption algorithm RSA-OAEP-256 and content encryption A256CBC-HS512.

Here are some examples of how to do this in a few programming languages:

package main

import (
	"encoding/json"
	"fmt"

	"github.com/golang-module/dongle/openssl"
	"gopkg.in/square/go-jose.v2"
)

func main() {
	instrumentDetails := InstrumentDetails{
		HolderReference: "customer123",
		HolderName:      "John Doe",
		CardNumber:      "4111111111111111",
		ExpiryMonth:     "03",
		ExpiryYear:      "30",
		SecurityCode:    "737",
	}

	instrumentDetailsJSON, err := json.Marshal(instrumentDetails)
	if err != nil {
		// handle error
		panic(err)
	}
	fmt.Println(string(instrumentDetailsJSON))
	// {"cardNumber":"4111111111111111","expiryMonth":"03","expiryYear":"30","securityCode":"737","holderName":"John Doe","holderReference":"customer123"}

	publicKey := "MIIBCgKCAQEAuJeo7zyuzdBx8biauFhVpy9XXk4lgvOe1/xD2G..."
	encryptedCardData, err := jweEncrypt(publicKey, instrumentDetailsJSON)
	if err != nil {
		// handle error
		panic(err)
	}
	fmt.Println(encryptedCardData)
	// eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0...
}

// InstrumentDetails represents the instrument details to be encrypted
type InstrumentDetails struct {
	CardNumber   string    `json:"cardNumber"`
	ExpiryMonth  string    `json:"expiryMonth"`
	ExpiryYear   string    `json:"expiryYear"`
	SecurityCode string    `json:"securityCode,omitempty"`
	HolderName   string    `json:"holderName"`
	HolderReference string `json:"holderReference"`
}

func jweEncrypt(publicKeyStr string, jsonData []byte) (string, error) {
	publicKey, err := openssl.RSA.ParsePublicKey(openssl.RSA.FormatPublicKey(openssl.PKCS8, []byte(publicKeyStr)))
	if err != nil {
		return "", err
	}
	recipient := jose.Recipient{
		Algorithm: jose.RSA_OAEP_256,
		Key:       publicKey,
		KeyID:     "",
	}

	e, err := jose.NewEncrypter(jose.A256CBC_HS512, recipient, nil)
	if err != nil {
		return "", err
	}

	encrypted, err := e.Encrypt(jsonData)
	if err != nil {
		return "", err
	}
	return encrypted.CompactSerialize()
}
# Due to an issue in python-jose (https://github.com/mpdavis/python-jose/issues/281)
# Please use the fork which allows to use RSA-OAEP-256: https://github.com/jkamp-aws/python-jose
# Ex:
# pip3 install cryptograph
# pip3 install git+https://github.com/jkamp-aws/python-jose

from jose import jwe
import json

def format_public_key_to_pem(public_key):
    pem_header = "-----BEGIN PUBLIC KEY-----"
    pem_footer = "-----END PUBLIC KEY-----"

    chunks = [public_key[i:i+64] for i in range(0, len(public_key), 64)]
    pem_content = "\n".join(chunks)
    pem_key = f"{pem_header}\n{pem_content}\n{pem_footer}"

    return pem_key

data = {
    "cardNumber": "4111111111111111",
    "expiryMonth": "03",
    "expiryYear": "30",
    "securityCode": "737",
    "holderName": "John Doe",
    "holderReference": "customer123"
}

# '...' should be the public key 
publicKey = format_public_key_to_pem('...')

jsonData = json.dumps(data).encode('utf-8')
# {"cardNumber":"4111111111111111","expiryMonth":"03","expiryYear":"30","securityCode":"737","holderName":"John Doe","holderReference":"customer123"}

encryptedCardData = jwe.encrypt(plaintext=jsonData, algorithm='RSA-OAEP-256', encryption='A256CBC-HS512', key=publicKey)
# eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0...
package com.payrails.android.cse

import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWEHeader
import com.nimbusds.jose.JWEObject
import com.nimbusds.jose.Payload
import com.nimbusds.jose.crypto.RSAEncrypter
import kotlinx.serialization.Serializable
import java.security.KeyFactory
import java.security.interfaces.RSAPublicKey
import java.security.spec.RSAPublicKeySpec
import java.util.Base64

@Serializable
data class InstrumentDetails(
    val holderName: String? = null,
    val cardNumber: String,
    val expiryMonth: String,
    val expiryYear: String,
    val securityCode: String? = null,
    val holderReference: String,
)

fun main() {
    val instrumentDetails = InstrumentDetails(
        holderReference = "customer123",
        holderName = "John Doe",
        cardNumber = "4111111111111111",
        expiryMonth = "12",
        expiryYear = "2025",
        securityCode = "123",
    )

    val instrumentDetailsJson = PayrailsCSE.json.encodeToString(InstrumentDetails.serializer(), instrumentDetails)
    println(instrumentDetailsJson)
    // {"cardNumber":"4111111111111111","expiryMonth":"03","expiryYear":"30","securityCode":"737","holderName":"John Doe","holderReference":"customer123"}

    val header = JWEHeader(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256CBC_HS512)

    val publicKeyStr = "MIIBCgKCAQEAuJeo7zyuzdBx8biauFhVpy9XXk4lgvOe1/xD2G..."
    val publicKey = getPublicKey(publicKeyStr)

    val jwe = JWEObject(header, Payload(instrumentDetailsJson))
    jwe.encrypt(RSAEncrypter(publicKey))

    val encryptedInstrumentDetails = jwe.serialize()
    println(encryptedInstrumentDetails)
    // eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0...
}

@Throws(NoSuchAlgorithmException::class, InvalidKeySpecException::class, IOException::class)
private fun getPublicKey(pemPublicKey: String): RSAPublicKey {
    val publicKeyBytes = Base64.getDecoder().decode(pemPublicKey)
    val keySpec = X509EncodedKeySpec(publicKeyBytes)
    val keyFactory = KeyFactory.getInstance("RSA")
    val publicKey = keyFactory.generatePublic(keySpec)
    return publicKey as RSAPublicKey
}

Step 3. Store the card in Payrails Vault

Depending on your use case and the flow you choose, you may want to store the card and authorize its first payment in two different steps or into a single one.

Only Tokenize

In case you want to tokenize first, you can use the encrypted data in the previous step as the encryptedInstrumentDetails field in the Tokenize Instrument API.

Remember to obtain consent from your customer to store the instrument for permanent usage, and choose the right value for the storeInstrument flag according to their choice.

We recommend checking our Authorization Flags guide for optimizing the future authorization rates of that instrument.

The response will contain the id of the newly created Payment Instrument, which can be used later for payments or other use cases.

Here's an example payload of an Authorize action using that stored instrument:

{
  "paymentComposition": [
    {
      "paymentInstrumentId": "384279fe-fee4-441d-9836-d2ef663551ad", //your stored instrument id
      "paymentMethodCode": "card",
      "integrationType": "api",
      "amount": {
        "value": "12.50",
        "currency": "EUR"
      }
    }
  ],
  ...
}

Tokenize and Authorize

If you want to immediately use the tokenized card in a payment, you can use the encrypted data in the previous step as a parameter in the Authorize action as in the following example:

{
  "paymentComposition": [
    {
      "paymentMethodCode": "card",
      "integrationType": "api",
      "amount": {
        "value": "12.50",
        "currency": "EUR"
      },
      "paymentInstrumentData": {
        "encryptedData": ".......encryptedCard.......",
        "futureUsage": "CardOnFile"
      }
    }
  ],
  ...
}

πŸ“˜

In case you want to re-send the security code of the card after the initial tokenization of a card, you can use the encryptedData under payment composition object in the authorize API.