Add Saved Payment Method
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.
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
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.
GET /v1/rest/keys/pan to retrieve the encryption key. Never send unencrypted account numbers.
Path Parameters
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 |
|
| 404 | Not Found |
|
| 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: trueso 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
- 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