Procare Pay Integration Service

Complete API Documentation for Payment Processing

Add Saved Payment Method

POST /v1/rest/merchants/{merchantId}/payors/{payorId}/savedPaymentMethods

Description: Adds a new payment method (credit card or bank account) to an existing payor profile. Use this endpoint when a customer wants to add a second card, add a backup payment option, or update their payment information after a card expires.

What is this endpoint used for?

This endpoint adds another payment method to a customer's existing payment profile. Common scenarios include:

  • Add Backup Card: Customer wants to save a second credit card as a backup payment option
  • Card Expired: Customer's primary card expired and they need to add the new card with updated expiration date
  • Add Bank Account: Customer wants to add ACH bank account in addition to their credit card
  • Business vs Personal: Customer wants separate cards for business and personal expenses
  • Family Members: Add payment methods for different family members under one account
  • Set New Default: Replace the current default payment method with a new one
Difference from Create Payor:

Use POST /merchants/{merchantId}/payors when creating a NEW customer profile with their FIRST payment method. Use this endpoint (POST /savedPaymentMethods) when adding ADDITIONAL payment methods to an EXISTING customer.

Important Security Note: Payment card numbers and bank account numbers must be encrypted using the public PAN (Primary Account Number) key before sending to this API. Use GET /v1/rest/keys/pan to retrieve the encryption key. Never send unencrypted account numbers.
Important Note: It is strongly recommended that integrators use the Procare Pay Hosted Payments Page to manage payor profiles and payment methods. This API is intended for advanced use cases where direct API integration is necessary. Using the hosted page ensures PCI compliance and reduces the scope of sensitive data handling. Calling this endpoint directly from your integrated application will include your application in PCI scope!

Path Parameters

merchantId (required)
Type: string
Pattern: Must be an 11-digit number
Description: The unique 11-digit merchant identifier
Example: 12345678901
payorId (required)
Type: string
Description: The payor identifier for the existing customer profile
Example: 550e8400-e29b-41d4-a716-446655440000 (GUID format) or 123456789 (64-bit integer)

Request Body

The request body should be a JSON object with the following properties:

Field Type Required Description
paymentMethodType string Optional Type of payment method. Values: "CreditCard" (for credit/debit cards), "Checking" (for checking accounts), "Saving" (for savings accounts)
encryptedAccountNumber string Required The encrypted account number (card number or bank account number). Must be encrypted using the PAN public key
cardExpiry string Required for credit cards Card expiration date in MMYY format (e.g., "1225" for December 2025). Required when paymentMethodType is "CreditCard"
abaRoutingNumber string Required for ACH 9-digit bank routing number. Required when paymentMethodType is "Checking" or "Saving"
accountHolderFirstName string Optional First name of the account holder
accountHolderLastName string Optional Last name of the account holder
accountHolderStreetLine1 string Optional Billing address street line 1
accountHolderStreetLine2 string Optional Billing address street line 2
accountHolderCity string Optional Billing address city
accountHolderRegion string Optional Billing address state/province (e.g., "CA", "TX", "ON")
accountHolderPostalCode string Optional Billing address ZIP/postal code
accountHolderPhoneNumber string Optional Contact phone number
accountHolderEmail string Optional Email address for payment receipts and notifications
setAsDefaultPaymentMethod boolean Optional If true, makes this the default payment method for the payor. If false or omitted, keeps the current default. Default: false
customerAccountType string Optional External account type identifier (user-defined for your own system tracking)
customerAccountId string Optional External customer ID from your system (for linking to your customer database)
sponsorKey string Optional Sponsor identifier (1-8 characters). Used for tracking payment source or sponsor relationships
allowTransactionalEmails boolean Optional Whether to send transaction emails. Automatically set to false if email is not provided. Default: false
customerData object Optional Key-value pairs for additional custom data (e.g., {"referralCode": "SPRING2025", "accountLevel": "Premium"})
idLimitedBitFlag boolean Optional If true, uses 64-bit integer IDs. If false, uses GUID format IDs. Should match the format used when creating the payor. Default: false

Response

Returns HTTP 201 (Created) with a SavedPaymentMethodResponse object containing:

Field Type Description
payorId string The payor identifier (matches the payorId from the URL)
savedPaymentMethodId string The unique identifier for the newly created payment method
maskedAccountNumber string Masked version of the account number (e.g., "************1234" for cards, "*****6789" for bank accounts)
accountType string The card brand or account type (e.g., "VISA", "MC", "AMEX", "DISC", "ECHK", "SAV")
isDefault boolean Whether this is the default payment method for the payor
isDeleted boolean Whether the payment method is deleted (will be false for newly created methods)
sponsorKey string The sponsor key if provided in the request

Error Responses

Status Code Meaning Common Causes
400 Bad Request
  • Invalid or missing required fields
  • Invalid card expiry format (must be MMYY)
  • Invalid routing number format (must be 9 digits)
  • SponsorKey exceeds 8 characters or is empty string
  • PaymentMethodType is invalid or missing
  • EncryptedAccountNumber is missing or invalid
404 Not Found
  • Merchant ID does not exist
  • Payor ID does not exist or was deleted
  • Payor does not belong to the specified merchant
408 Request Timeout The encryption or payment processor took too long to respond. Retry the request
500 Internal Server Error Server-side error. Check error message for details

Code Examples

using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Security.Cryptography;

public class PaymentMethodClient
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl;
    private readonly string _bearerToken;

    public PaymentMethodClient(string baseUrl, string bearerToken)
    {
        _baseUrl = baseUrl;
        _bearerToken = bearerToken;
        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {bearerToken}");
    }

    // Step 1: Get the PAN encryption key
    public async Task GetPanKeyAsync()
    {
        string endpoint = $"{_baseUrl}/v1/rest/keys/pan";
        HttpResponseMessage response = await _httpClient.GetAsync(endpoint);
        response.EnsureSuccessStatusCode();

        string json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize(json);
    }

    // Step 2: Encrypt account number with RSA
    public string EncryptWithRSA(string plainText, string publicKeyPem)
    {
        using (RSA rsa = RSA.Create())
        {
            rsa.ImportFromPem(publicKeyPem);
            byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
            byte[] encryptedBytes = rsa.Encrypt(plainBytes, RSAEncryptionPadding.Pkcs1);
            return Convert.ToBase64String(encryptedBytes);
        }
    }

    // Step 3: Add saved payment method to existing payor
    public async Task AddCreditCardAsync(
        string merchantId,
        string payorId,
        string cardNumber,
        string expiryMMYY,
        string firstName,
        string lastName,
        bool setAsDefault = false)
    {
        // Get encryption key
        PublicKey panKey = await GetPanKeyAsync();

        // Encrypt the card number
        string encryptedCard = EncryptWithRSA(cardNumber, panKey.Key);

        // Clear the plain text card number from memory
        cardNumber = null;

        // Build the request
        var request = new
        {
            paymentMethodType = "CreditCard",
            encryptedAccountNumber = encryptedCard,
            cardExpiry = expiryMMYY,
            accountHolderFirstName = firstName,
            accountHolderLastName = lastName,
            setAsDefaultPaymentMethod = setAsDefault
        };

        string endpoint = $"{_baseUrl}/v1/rest/merchants/{merchantId}/payors/{payorId}/savedPaymentMethods";
        string json = JsonSerializer.Serialize(request);
        HttpContent content = new StringContent(json, Encoding.UTF8, "application/json");

        HttpResponseMessage response = await _httpClient.PostAsync(endpoint, content);
        response.EnsureSuccessStatusCode();

        string responseJson = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize(responseJson);
    }

    // Add ACH bank account
    public async Task AddBankAccountAsync(
        string merchantId,
        string payorId,
        string accountNumber,
        string routingNumber,
        string accountType, // "Checking" or "Saving"
        string firstName,
        string lastName,
        bool setAsDefault = false)
    {
        // Get encryption key
        PublicKey panKey = await GetPanKeyAsync();

        // Encrypt the account number
        string encryptedAccount = EncryptWithRSA(accountNumber, panKey.Key);

        // Clear the plain text account number from memory
        accountNumber = null;

        // Build the request
        var request = new
        {
            paymentMethodType = accountType,
            encryptedAccountNumber = encryptedAccount,
            abaRoutingNumber = routingNumber,
            accountHolderFirstName = firstName,
            accountHolderLastName = lastName,
            setAsDefaultPaymentMethod = setAsDefault
        };

        string endpoint = $"{_baseUrl}/v1/rest/merchants/{merchantId}/payors/{payorId}/savedPaymentMethods";
        string json = JsonSerializer.Serialize(request);
        HttpContent content = new StringContent(json, Encoding.UTF8, "application/json");

        HttpResponseMessage response = await _httpClient.PostAsync(endpoint, content);
        response.EnsureSuccessStatusCode();

        string responseJson = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize(responseJson);
    }
}

// Usage example
public class Program
{
    public static async Task Main()
    {
        string baseUrl = "https://api.example.com";
        string bearerToken = "your-oauth-token-here";
        string merchantId = "12345678901";
        string payorId = "550e8400-e29b-41d4-a716-446655440000";

        PaymentMethodClient client = new PaymentMethodClient(baseUrl, bearerToken);

        // Add a new credit card as backup payment method
        SavedPaymentMethodResponse newCard = await client.AddCreditCardAsync(
            merchantId: merchantId,
            payorId: payorId,
            cardNumber: "4111111111111111",
            expiryMMYY: "1227",
            firstName: "Jane",
            lastName: "Doe",
            setAsDefault: false  // Keep current default, add this as backup
        );

        Console.WriteLine($"New payment method added: {newCard.MaskedAccountNumber}");
        Console.WriteLine($"Payment method ID: {newCard.SavedPaymentMethodId}");
        Console.WriteLine($"Is default: {newCard.IsDefault}");
    }
}

public class PublicKey
{
    public string Key { get; set; }
    public string KeyId { get; set; }
}

public class SavedPaymentMethodResponse
{
    public string PayorId { get; set; }
    public string SavedPaymentMethodId { get; set; }
    public string MaskedAccountNumber { get; set; }
    public string AccountType { get; set; }
    public bool IsDefault { get; set; }
    public bool IsDeleted { get; set; }
    public string SponsorKey { get; set; }
}
    
Imports System.Net.Http
Imports System.Text
Imports System.Text.Json
Imports System.Security.Cryptography

Public Class PaymentMethodClient
    Private ReadOnly _httpClient As HttpClient
    Private ReadOnly _baseUrl As String
    Private ReadOnly _bearerToken As String

    Public Sub New(baseUrl As String, bearerToken As String)
        _baseUrl = baseUrl
        _bearerToken = bearerToken
        _httpClient = New HttpClient()
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {bearerToken}")
    End Sub

    ' Step 1: Get the PAN encryption key
    Public Async Function GetPanKeyAsync() As Task(Of PublicKey)
        Dim endpoint As String = $"{_baseUrl}/v1/rest/keys/pan"
        Dim response As HttpResponseMessage = Await _httpClient.GetAsync(endpoint)
        response.EnsureSuccessStatusCode()

        Dim json As String = Await response.Content.ReadAsStringAsync()
        Return JsonSerializer.Deserialize(Of PublicKey)(json)
    End Function

    ' Step 2: Encrypt account number with RSA
    Public Function EncryptWithRSA(plainText As String, publicKeyPem As String) As String
        Using rsa As RSA = RSA.Create()
            rsa.ImportFromPem(publicKeyPem)
            Dim plainBytes As Byte() = Encoding.UTF8.GetBytes(plainText)
            Dim encryptedBytes As Byte() = rsa.Encrypt(plainBytes, RSAEncryptionPadding.Pkcs1)
            Return Convert.ToBase64String(encryptedBytes)
        End Using
    End Function

    ' Step 3: Add saved payment method to existing payor
    Public Async Function AddCreditCardAsync(
        merchantId As String,
        payorId As String,
        cardNumber As String,
        expiryMMYY As String,
        firstName As String,
        lastName As String,
        Optional setAsDefault As Boolean = False) As Task(Of SavedPaymentMethodResponse)

        ' Get encryption key
        Dim panKey As PublicKey = Await GetPanKeyAsync()

        ' Encrypt the card number
        Dim encryptedCard As String = EncryptWithRSA(cardNumber, panKey.Key)

        ' Clear the plain text card number from memory
        cardNumber = Nothing

        ' Build the request
        Dim request = New With {
            .paymentMethodType = "CreditCard",
            .encryptedAccountNumber = encryptedCard,
            .cardExpiry = expiryMMYY,
            .accountHolderFirstName = firstName,
            .accountHolderLastName = lastName,
            .setAsDefaultPaymentMethod = setAsDefault
        }

        Dim endpoint As String = $"{_baseUrl}/v1/rest/merchants/{merchantId}/payors/{payorId}/savedPaymentMethods"
        Dim json As String = JsonSerializer.Serialize(request)
        Dim content As HttpContent = New StringContent(json, Encoding.UTF8, "application/json")

        Dim response As HttpResponseMessage = Await _httpClient.PostAsync(endpoint, content)
        response.EnsureSuccessStatusCode()

        Dim responseJson As String = Await response.Content.ReadAsStringAsync()
        Return JsonSerializer.Deserialize(Of SavedPaymentMethodResponse)(responseJson)
    End Function

    ' Add ACH bank account
    Public Async Function AddBankAccountAsync(
        merchantId As String,
        payorId As String,
        accountNumber As String,
        routingNumber As String,
        accountType As String,
        firstName As String,
        lastName As String,
        Optional setAsDefault As Boolean = False) As Task(Of SavedPaymentMethodResponse)

        ' Get encryption key
        Dim panKey As PublicKey = Await GetPanKeyAsync()

        ' Encrypt the account number
        Dim encryptedAccount As String = EncryptWithRSA(accountNumber, panKey.Key)

        ' Clear the plain text account number from memory
        accountNumber = Nothing

        ' Build the request
        Dim request = New With {
            .paymentMethodType = accountType,
            .encryptedAccountNumber = encryptedAccount,
            .abaRoutingNumber = routingNumber,
            .accountHolderFirstName = firstName,
            .accountHolderLastName = lastName,
            .setAsDefaultPaymentMethod = setAsDefault
        }

        Dim endpoint As String = $"{_baseUrl}/v1/rest/merchants/{merchantId}/payors/{payorId}/savedPaymentMethods"
        Dim json As String = JsonSerializer.Serialize(request)
        Dim content As HttpContent = New StringContent(json, Encoding.UTF8, "application/json")

        Dim response As HttpResponseMessage = Await _httpClient.PostAsync(endpoint, content)
        response.EnsureSuccessStatusCode()

        Dim responseJson As String = Await response.Content.ReadAsStringAsync()
        Return JsonSerializer.Deserialize(Of SavedPaymentMethodResponse)(responseJson)
    End Function
End Class

' Usage example
Module Program
    Sub Main()
        MainAsync().Wait()
    End Sub

    Async Function MainAsync() As Task
        Dim baseUrl As String = "https://api.example.com"
        Dim bearerToken As String = "your-oauth-token-here"
        Dim merchantId As String = "12345678901"
        Dim payorId As String = "550e8400-e29b-41d4-a716-446655440000"

        Dim client As New PaymentMethodClient(baseUrl, bearerToken)

        ' Add a new credit card as backup payment method
        Dim newCard As SavedPaymentMethodResponse = Await client.AddCreditCardAsync(
            merchantId:=merchantId,
            payorId:=payorId,
            cardNumber:="4111111111111111",
            expiryMMYY:="1227",
            firstName:="Jane",
            lastName:="Doe",
            setAsDefault:=False)

        Console.WriteLine($"New payment method added: {newCard.MaskedAccountNumber}")
        Console.WriteLine($"Payment method ID: {newCard.SavedPaymentMethodId}")
        Console.WriteLine($"Is default: {newCard.IsDefault}")
    End Function
End Module

Public Class PublicKey
    Public Property Key As String
    Public Property KeyId As String
End Class

Public Class SavedPaymentMethodResponse
    Public Property PayorId As String
    Public Property SavedPaymentMethodId As String
    Public Property MaskedAccountNumber As String
    Public Property AccountType As String
    Public Property IsDefault As Boolean
    Public Property IsDeleted As Boolean
    Public Property SponsorKey As String
End Class
    
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import javax.crypto.Cipher;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonProperty;

public class PaymentMethodClient {
    private final HttpClient httpClient;
    private final String baseUrl;
    private final String bearerToken;
    private final ObjectMapper objectMapper;

    public PaymentMethodClient(String baseUrl, String bearerToken) {
        this.baseUrl = baseUrl;
        this.bearerToken = bearerToken;
        this.httpClient = HttpClient.newHttpClient();
        this.objectMapper = new ObjectMapper();
    }

    // Step 1: Get the PAN encryption key
    public PublicKeyResponse getPanKey() throws IOException, InterruptedException {
        String endpoint = baseUrl + "/v1/rest/keys/pan";
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(endpoint))
                .header("Authorization", "Bearer " + bearerToken)
                .GET()
                .build();

        HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new RuntimeException("Failed to get PAN key: " + response.statusCode());
        }

        return objectMapper.readValue(response.body(), PublicKeyResponse.class);
    }

    // Step 2: Encrypt account number with RSA
    public String encryptWithRSA(String plainText, String publicKeyPem) throws Exception {
        // Remove PEM headers and whitespace
        String publicKeyPEM = publicKeyPem
                .replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "")
                .replaceAll("\\s", "");

        byte[] keyBytes = Base64.getDecoder().decode(publicKeyPEM);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PublicKey publicKey = kf.generatePublic(spec);

        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    // Step 3: Add saved payment method to existing payor
    public SavedPaymentMethodResponse addCreditCard(
            String merchantId,
            String payorId,
            String cardNumber,
            String expiryMMYY,
            String firstName,
            String lastName,
            boolean setAsDefault) throws Exception {

        // Get encryption key
        PublicKeyResponse panKey = getPanKey();

        // Encrypt the card number
        String encryptedCard = encryptWithRSA(cardNumber, panKey.getKey());

        // Clear the plain text card number from memory
        cardNumber = null;

        // Build the request
        SavedPaymentMethodRequest request = new SavedPaymentMethodRequest();
        request.setPaymentMethodType("CreditCard");
        request.setEncryptedAccountNumber(encryptedCard);
        request.setCardExpiry(expiryMMYY);
        request.setAccountHolderFirstName(firstName);
        request.setAccountHolderLastName(lastName);
        request.setSetAsDefaultPaymentMethod(setAsDefault);

        String endpoint = String.format("%s/v1/rest/merchants/%s/payors/%s/savedPaymentMethods",
                baseUrl, merchantId, payorId);
        String json = objectMapper.writeValueAsString(request);

        HttpRequest httpRequest = HttpRequest.newBuilder()
                .uri(URI.create(endpoint))
                .header("Authorization", "Bearer " + bearerToken)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(json))
                .build();

        HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 201) {
            throw new RuntimeException("Failed to add payment method: " + response.statusCode() +
                    " - " + response.body());
        }

        return objectMapper.readValue(response.body(), SavedPaymentMethodResponse.class);
    }

    // Add ACH bank account
    public SavedPaymentMethodResponse addBankAccount(
            String merchantId,
            String payorId,
            String accountNumber,
            String routingNumber,
            String accountType,
            String firstName,
            String lastName,
            boolean setAsDefault) throws Exception {

        // Get encryption key
        PublicKeyResponse panKey = getPanKey();

        // Encrypt the account number
        String encryptedAccount = encryptWithRSA(accountNumber, panKey.getKey());

        // Clear the plain text account number from memory
        accountNumber = null;

        // Build the request
        SavedPaymentMethodRequest request = new SavedPaymentMethodRequest();
        request.setPaymentMethodType(accountType);
        request.setEncryptedAccountNumber(encryptedAccount);
        request.setAbaRoutingNumber(routingNumber);
        request.setAccountHolderFirstName(firstName);
        request.setAccountHolderLastName(lastName);
        request.setSetAsDefaultPaymentMethod(setAsDefault);

        String endpoint = String.format("%s/v1/rest/merchants/%s/payors/%s/savedPaymentMethods",
                baseUrl, merchantId, payorId);
        String json = objectMapper.writeValueAsString(request);

        HttpRequest httpRequest = HttpRequest.newBuilder()
                .uri(URI.create(endpoint))
                .header("Authorization", "Bearer " + bearerToken)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(json))
                .build();

        HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 201) {
            throw new RuntimeException("Failed to add bank account: " + response.statusCode() +
                    " - " + response.body());
        }

        return objectMapper.readValue(response.body(), SavedPaymentMethodResponse.class);
    }

    // Usage example
    public static void main(String[] args) {
        try {
            String baseUrl = "https://api.example.com";
            String bearerToken = "your-oauth-token-here";
            String merchantId = "12345678901";
            String payorId = "550e8400-e29b-41d4-a716-446655440000";

            PaymentMethodClient client = new PaymentMethodClient(baseUrl, bearerToken);

            // Add a new credit card as backup payment method
            SavedPaymentMethodResponse newCard = client.addCreditCard(
                    merchantId,
                    payorId,
                    "4111111111111111",
                    "1227",
                    "Jane",
                    "Doe",
                    false  // Keep current default, add this as backup
            );

            System.out.println("New payment method added: " + newCard.getMaskedAccountNumber());
            System.out.println("Payment method ID: " + newCard.getSavedPaymentMethodId());
            System.out.println("Is default: " + newCard.isDefault());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// Request model
class SavedPaymentMethodRequest {
    @JsonProperty("paymentMethodType")
    private String paymentMethodType;

    @JsonProperty("encryptedAccountNumber")
    private String encryptedAccountNumber;

    @JsonProperty("cardExpiry")
    private String cardExpiry;

    @JsonProperty("abaRoutingNumber")
    private String abaRoutingNumber;

    @JsonProperty("accountHolderFirstName")
    private String accountHolderFirstName;

    @JsonProperty("accountHolderLastName")
    private String accountHolderLastName;

    @JsonProperty("setAsDefaultPaymentMethod")
    private boolean setAsDefaultPaymentMethod;

    // Getters and setters
    public void setPaymentMethodType(String paymentMethodType) {
        this.paymentMethodType = paymentMethodType;
    }

    public void setEncryptedAccountNumber(String encryptedAccountNumber) {
        this.encryptedAccountNumber = encryptedAccountNumber;
    }

    public void setCardExpiry(String cardExpiry) {
        this.cardExpiry = cardExpiry;
    }

    public void setAbaRoutingNumber(String abaRoutingNumber) {
        this.abaRoutingNumber = abaRoutingNumber;
    }

    public void setAccountHolderFirstName(String firstName) {
        this.accountHolderFirstName = firstName;
    }

    public void setAccountHolderLastName(String lastName) {
        this.accountHolderLastName = lastName;
    }

    public void setSetAsDefaultPaymentMethod(boolean setAsDefault) {
        this.setAsDefaultPaymentMethod = setAsDefault;
    }
}

// Response models
class PublicKeyResponse {
    @JsonProperty("key")
    private String key;

    @JsonProperty("keyId")
    private String keyId;

    public String getKey() {
        return key;
    }
}

class SavedPaymentMethodResponse {
    @JsonProperty("payorId")
    private String payorId;

    @JsonProperty("savedPaymentMethodId")
    private String savedPaymentMethodId;

    @JsonProperty("maskedAccountNumber")
    private String maskedAccountNumber;

    @JsonProperty("accountType")
    private String accountType;

    @JsonProperty("isDefault")
    private boolean isDefault;

    @JsonProperty("isDeleted")
    private boolean isDeleted;

    @JsonProperty("sponsorKey")
    private String sponsorKey;

    public String getPayorId() { return payorId; }
    public String getSavedPaymentMethodId() { return savedPaymentMethodId; }
    public String getMaskedAccountNumber() { return maskedAccountNumber; }
    public String getAccountType() { return accountType; }
    public boolean isDefault() { return isDefault; }
    public boolean isDeleted() { return isDeleted; }
    public String getSponsorKey() { return sponsorKey; }
}
    
require 'net/http'
require 'json'
require 'openssl'
require 'base64'

class PaymentMethodClient
  def initialize(base_url, bearer_token)
    @base_url = base_url
    @bearer_token = bearer_token
  end

  # Step 1: Get the PAN encryption key
  def get_pan_key
    uri = URI("#{@base_url}/v1/rest/keys/pan")
    request = Net::HTTP::Get.new(uri)
    request['Authorization'] = "Bearer #{@bearer_token}"

    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
      http.request(request)
    end

    raise "Failed to get PAN key: #{response.code}" unless response.code == '200'

    JSON.parse(response.body)
  end

  # Step 2: Encrypt account number with RSA
  def encrypt_with_rsa(plain_text, public_key_pem)
    public_key = OpenSSL::PKey::RSA.new(public_key_pem)
    encrypted = public_key.public_encrypt(plain_text, OpenSSL::PKey::RSA::PKCS1_PADDING)
    Base64.strict_encode64(encrypted)
  end

  # Step 3: Add saved payment method to existing payor
  def add_credit_card(merchant_id:, payor_id:, card_number:, expiry_mmyy:,
                      first_name:, last_name:, set_as_default: false)
    # Get encryption key
    pan_key = get_pan_key

    # Encrypt the card number
    encrypted_card = encrypt_with_rsa(card_number, pan_key['key'])

    # Clear the plain text card number from memory
    card_number = nil

    # Build the request
    request_body = {
      paymentMethodType: 'CreditCard',
      encryptedAccountNumber: encrypted_card,
      cardExpiry: expiry_mmyy,
      accountHolderFirstName: first_name,
      accountHolderLastName: last_name,
      setAsDefaultPaymentMethod: set_as_default
    }

    uri = URI("#{@base_url}/v1/rest/merchants/#{merchant_id}/payors/#{payor_id}/savedPaymentMethods")
    request = Net::HTTP::Post.new(uri)
    request['Authorization'] = "Bearer #{@bearer_token}"
    request['Content-Type'] = 'application/json'
    request.body = request_body.to_json

    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
      http.request(request)
    end

    raise "Failed to add payment method: #{response.code} - #{response.body}" unless response.code == '201'

    JSON.parse(response.body)
  end

  # Add ACH bank account
  def add_bank_account(merchant_id:, payor_id:, account_number:, routing_number:,
                       account_type:, first_name:, last_name:, set_as_default: false)
    # Get encryption key
    pan_key = get_pan_key

    # Encrypt the account number
    encrypted_account = encrypt_with_rsa(account_number, pan_key['key'])

    # Clear the plain text account number from memory
    account_number = nil

    # Build the request
    request_body = {
      paymentMethodType: account_type,
      encryptedAccountNumber: encrypted_account,
      abaRoutingNumber: routing_number,
      accountHolderFirstName: first_name,
      accountHolderLastName: last_name,
      setAsDefaultPaymentMethod: set_as_default
    }

    uri = URI("#{@base_url}/v1/rest/merchants/#{merchant_id}/payors/#{payor_id}/savedPaymentMethods")
    request = Net::HTTP::Post.new(uri)
    request['Authorization'] = "Bearer #{@bearer_token}"
    request['Content-Type'] = 'application/json'
    request.body = request_body.to_json

    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
      http.request(request)
    end

    raise "Failed to add bank account: #{response.code} - #{response.body}" unless response.code == '201'

    JSON.parse(response.body)
  end
end

# Usage example
if __FILE__ == $0
  base_url = 'https://api.example.com'
  bearer_token = 'your-oauth-token-here'
  merchant_id = '12345678901'
  payor_id = '550e8400-e29b-41d4-a716-446655440000'

  client = PaymentMethodClient.new(base_url, bearer_token)

  begin
    # Add a new credit card as backup payment method
    new_card = client.add_credit_card(
      merchant_id: merchant_id,
      payor_id: payor_id,
      card_number: '4111111111111111',
      expiry_mmyy: '1227',
      first_name: 'Jane',
      last_name: 'Doe',
      set_as_default: false  # Keep current default, add this as backup
    )

    puts "New payment method added: #{new_card['maskedAccountNumber']}"
    puts "Payment method ID: #{new_card['savedPaymentMethodId']}"
    puts "Is default: #{new_card['isDefault']}"

  rescue => e
    puts "Error: #{e.message}"
  end
end
    

Common Use Cases

1. Customer's Card Expired - Add Replacement Card

// Keep existing card active until new card is verified, then set new card as default
SavedPaymentMethodResponse newCard = await client.AddCreditCardAsync(
    merchantId: "12345678901",
    payorId: existingPayor.PayorId,
    cardNumber: "4111111111111111",
    expiryMMYY: "1228",  // New expiration date
    firstName: "John",
    lastName: "Smith",
    setAsDefault: true   // Make this the new default since old card expired
);
    

2. Add Backup Payment Method

// Customer wants to keep current default but add a backup card
SavedPaymentMethodResponse backupCard = await client.AddCreditCardAsync(
    merchantId: "12345678901",
    payorId: existingPayor.PayorId,
    cardNumber: "5555555555554444",
    expiryMMYY: "0626",
    firstName: "John",
    lastName: "Smith",
    setAsDefault: false  // Keep current default, this is just a backup
);
    

3. Add ACH Account as Secondary Payment Option

// Customer has credit card but wants to add bank account for lower fees
SavedPaymentMethodResponse bankAccount = await client.AddBankAccountAsync(
    merchantId: "12345678901",
    payorId: existingPayor.PayorId,
    accountNumber: "1234567890",
    routingNumber: "021000021",
    accountType: "Checking",
    firstName: "John",
    lastName: "Smith",
    setAsDefault: false  // Keep card as default for now
);
    

Troubleshooting

Error: "Payor not found" (404)

Solution: Verify the payorId exists by calling GET /merchants/{merchantId}/payors/{payorId} first. The payor must exist before you can add payment methods to it.

Error: "Invalid encrypted account number" (400)

Solution: Ensure you're using the current PAN public key and PKCS#1 v1.5 padding. Keys should be refreshed every 4-6 hours. Try getting a fresh key and re-encrypting.

Error: "Invalid card expiry format" (400)

Solution: Card expiry must be exactly 4 digits in MMYY format. Examples: "0125" (January 2025), "1227" (December 2027). Do not use separators like "/" or "-".

Error: "Invalid routing number" (400)

Solution: ABA routing numbers must be exactly 9 digits. Verify the routing number is valid for ACH transactions. Some routing numbers are only valid for wire transfers.

Payment method created but not appearing as default

Solution: Only one payment method can be default at a time. If setAsDefaultPaymentMethod: true is specified, the API will automatically unset the previous default. Verify the response shows isDefault: true. If you need to change the default later, use PUT /savedPaymentMethods/{savedPaymentMethodId}.

Best Practices

  • Clear sensitive data: Set card numbers and account numbers to null immediately after encryption to minimize memory exposure
  • Refresh encryption keys: Cache PAN keys for 4-6 hours, then fetch fresh keys to handle key rotation
  • Validate before encrypting: Check card number format (Luhn algorithm) and expiry date before making the API call to provide faster feedback to users
  • Set default explicitly: If replacing an expired card, set setAsDefaultPaymentMethod: true so future transactions use the new card
  • Use customerAccountId: Include your system's customer ID to simplify lookups and reconciliation
  • Store savedPaymentMethodId: Save the returned savedPaymentMethodId in your database to reference this payment method in future transactions
  • Handle 408 timeouts: Implement retry logic with exponential backoff for timeout errors
  • Verify BIN: For credit cards, consider calling GET /bins/{binId} first to validate card brand and check surcharge rules

Security Considerations

PCI DSS Compliance:
  • Never log or store unencrypted card numbers or bank account numbers
  • Always use HTTPS for API calls
  • Implement proper key rotation by refreshing PAN keys regularly
  • Use secure memory handling - clear sensitive data immediately after encryption
  • Validate and sanitize all input before encryption
  • Store only the masked account number and savedPaymentMethodId for display purposes
  • Never decrypt account numbers client-side - the API handles decryption securely