COPYandPAY
Introduction
COPYandPAY is a SAQ-A compliant payment-form solution, making it both secure and simple-to-integrate.
Known limitations
If FNB is your acquirer, you cannot use special characters in the merchantTransactionId or merchantInvoiceId parameters and must only use letters and numbers.
1. Prepare the checkout
First, send a server-to-server POST request to prepare the checkout with the required data, such as the order type, amount, and currency. If the request is successful, the response returns a JSON string containing an id. Use this id in the second step to create the payment form.
Sample request:
curl https://sandbox-card.peachpayments.com/v1/checkouts \
-d "entityId=8a8294174e735d0c014e78cf26461790" \
-d "amount=92.00" \
-d "currency=ZAR" \
-d "paymentType=DB" \
-d "integrity=true" \
-H "Authorization: Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ="public Dictionary<string, dynamic> Request() {
Dictionary<string, dynamic> responseData;
string data = "entityId=8a8294174e735d0c014e78cf26461790" +
"&amount=92.00" +
"¤cy=ZAR" +
"&paymentType=DB" +
"&integrity=true";
string url = "https://sandbox-card.peachpayments.com/v1/checkouts";
byte[] buffer = Encoding.ASCII.GetBytes(data);
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
request.Method = "POST";
request.Headers["Authorization"] = "Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=";
request.ContentType = "application/x-www-form-urlencoded";
Stream PostData = request.GetRequestStream();
PostData.Write(buffer, 0, buffer.Length);
PostData.Close();
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) {
Stream dataStream = response.GetResponseStream();
StreamReader reader = new StreamReader(dataStream);
var s = new JavaScriptSerializer();
responseData = s.Deserialize<Dictionary<string, dynamic>>(reader.ReadToEnd());
reader.Close();
dataStream.Close();
}
return responseData;
}
responseData = Request()["result"]["description"];import groovy.json.JsonSlurper
public static String request() {
def data = "entityId=8a8294174e735d0c014e78cf26461790" +
"&amount=92.00" +
"¤cy=ZAR" +
"&paymentType=DB" +
"&integrity=true"
def url = "https://sandbox-card.peachpayments.com/v1/checkouts".toURL()
def connection = url.openConnection()
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=")
connection.doOutput = true
connection.outputStream << data
def json = new JsonSlurper().parseText(connection.inputStream.text)
json
}
println request()private String request() throws IOException {
URL url = new URL("https://sandbox-card.peachpayments.com/v1/checkouts");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Authorization", "Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=");
conn.setDoInput(true);
conn.setDoOutput(true);
String data = ""
+ "entityId=8a8294174e735d0c014e78cf26461790"
+ "&amount=92.00"
+ "¤cy=ZAR"
+ "&paymentType=DB"
+ "&integrity=true";
DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
wr.writeBytes(data);
wr.flush();
wr.close();
int responseCode = conn.getResponseCode();
InputStream is;
if (responseCode >= 400)
is = conn.getErrorStream();
else
is = conn.getInputStream();
return IOUtils.toString(is);
}const https = require('https');
const querystring = require('querystring');
const request = async () => {
const path = '/v1/checkouts';
const data = querystring.stringify({
'entityId': '8a8294174e735d0c014e78cf26461790',
'amount': '92.00',
'currency': 'ZAR',
'paymentType': 'DB',
'integrity': 'true'
});
const options = {
port: 443,
host: 'sandbox-card.peachpayments.com',
path: path,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': data.length,
'Authorization': 'Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ='
}
};
return new Promise((resolve, reject) => {
const postRequest = https.request(options, function(res) {
const buf = [];
res.on('data', chunk => {
buf.push(Buffer.from(chunk));
});
res.on('end', () => {
const jsonString = Buffer.concat(buf).toString('utf8');
try {
resolve(JSON.parse(jsonString));
} catch (error) {
reject(error);
}
});
});
postRequest.on('error', reject);
postRequest.write(data);
postRequest.end();
});
};
request().then(console.log).catch(console.error);function request() {
$url = "https://sandbox-card.peachpayments.com/v1/checkouts";
$data = "entityId=8a8294174e735d0c014e78cf26461790" .
"&amount=92.00" .
"¤cy=ZAR" .
"&paymentType=DB" .
"&integrity=true";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization:Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ='
));
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // this should be set to true in production
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$responseData = curl_exec($ch);
if(curl_errno($ch)) {
return curl_error($ch);
}
curl_close($ch);
return $responseData;
}
$responseData = request();try:
from urllib.parse import urlencode
from urllib.request import build_opener, Request, HTTPHandler
from urllib.error import HTTPError, URLError
except ImportError:
from urllib import urlencode
from urllib2 import build_opener, Request, HTTPHandler, HTTPError, URLError
import json
def request():
url = "https://sandbox-card.peachpayments.com/v1/checkouts"
data = {
'entityId' : '8a8294174e735d0c014e78cf26461790',
'amount' : '92.00',
'currency' : 'ZAR',
'paymentType' : 'DB',
'integrity' : 'true'
}
try:
opener = build_opener(HTTPHandler)
request = Request(url, data=urlencode(data).encode('utf-8'))
request.add_header('Authorization', 'Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=')
request.get_method = lambda: 'POST'
response = opener.open(request)
return json.loads(response.read())
except HTTPError as e:
return json.loads(e.read())
except URLError as e:
return e.reason
responseData = request()
print(responseData)require 'net/https'
require 'uri'
require 'json'
def request()
uri = URI('https://sandbox-card.peachpayments.com/v1/checkouts')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Post.new(uri.path)
req.set_form_data({
'entityId' => '8a8294174e735d0c014e78cf26461790',
'amount' => '92.00',
'currency' => 'ZAR',
'paymentType' => 'DB',
'integrity' => 'true'
})
res = http.request(req)
return JSON.parse(res.body)
end
puts request()def initialPayment : String = {
val url = "https://sandbox-card.peachpayments.com/v1/checkouts"
val data = (""
+ "entityId=8a8294174e735d0c014e78cf26461790"
+ "&amount=92.00"
+ "¤cy=ZAR"
+ "&paymentType=DB"
+ "&integrity=true"
)
val conn = new URL(url).openConnection()
conn match {
case secureConn: HttpsURLConnection => secureConn.setRequestMethod("POST")
case _ => throw new ClassCastException
}
conn.setDoInput(true)
conn.setDoOutput(true)
IOUtils.write(data, conn.getOutputStream())
conn.setRequestProperty("Authorization", "Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=")
conn.connect()
if (conn.getResponseCode() >= 400) {
return IOUtils.toString(conn.getErrorStream())
}
else {
return IOUtils.toString(conn.getInputStream())
}
}Public Function Request() As Dictionary(Of String, Object)
Dim url As String = "https://sandbox-card.peachpayments.com/v1/checkouts"
Dim data As String = "" +
"entityId=8a8294174e735d0c014e78cf26461790" +
"&amount=92.00" +
"¤cy=ZAR" +
"&paymentType=DB" +
"&integrity=true"
Dim req As WebRequest = WebRequest.Create(url)
req.Method = "POST"
req.Headers.Add("Authorization", "Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=")
req.ContentType = "application/x-www-form-urlencoded"
Dim byteArray As Byte() = Encoding.UTF8.GetBytes(data)
req.ContentLength = byteArray.Length
Dim dataStream As Stream = req.GetRequestStream()
dataStream.Write(byteArray, 0, byteArray.Length)
dataStream.Close()
Dim res As WebResponse = req.GetResponse()
Dim resStream = res.GetResponseStream()
Dim reader As New StreamReader(resStream)
Dim response As String = reader.ReadToEnd()
reader.Close()
resStream.Close()
res.Close()
Dim jss As New System.Web.Script.Serialization.JavaScriptSerializer()
Dim dict As Dictionary(Of String, Object) = jss.Deserialize(Of Dictionary(Of String, Object))(response)
Return dict
End Function
responseData = Request()("result")("description"){
"result":{
"code":"000.200.100",
"description":"successfully created checkout"
},
"buildNumber":"9092e7a6af8301accda2f9a3a38f743f907dadd5@2026-03-23 16:50:06 +0000",
"timestamp":"2026-03-24 11:32:55+0000",
"ndc":"EBE5A9BFDF292A5F5666E3E0F403388E.uat01-vm-tx03",
"id":"EBE5A9BFDF292A5F5666E3E0F403388E.uat01-vm-tx03",
"integrity":"sha384-JVnCUMcZkc6QRJOxa27c2VjKkgsTrM1usl9EEwT0SEtNDb0yQ6korjvkYZaYGGu7"
}For a full list of parameters in the prepare checkout request, refer to the API reference.
For an HTTP POST request, all the parameters must go into the message body and not into the URL.
2. Create the payment form
To create the payment form, you need to add the following lines of HTML/JavaScript to your page and populate the following variables:
- Use the checkout's
idin the response from step 1.<script src="https://sandbox-card.peachpayments.com/v1/paymentWidgets.js?checkoutId={checkoutId}" integrity="{integrity}" crossorigin="anonymous"> </script> - The
shopperResultUrlis the page on your site where the Peach Payments system redirects the customer after processing the payment. It also specifies the brands available during the payment process.<form action="{shopperResultUrl}" class="paymentWidgets" data-brands="VISA MASTER AMEX"></form>
Sample form:
<form action="https://developer.peachpayments.com/docs/oppwa-integrations-copyandpay" class="paymentWidgets" data-brands="VISA MASTER AMEX"></form>body {background-color:#f6f6f5;}var wpwlOptions = {style:"card"}
Card form.
View the customisation guide for more information on customising the payment form. If using an SAQ-A compliant credit card form, press the Enter button on your keyboard to execute the payment.
You can now display multiple card forms for card brand promotion, grouping card brands across forms as needed. To use this feature, add any number of <form> tags. Configure the data-brands attribute of the rendered card forms according to your requirements.
<form action="{shopperResultUrl}" class="paymentWidgets" data-brands="VISA"></form><form action="{shopperResultUrl}" class="paymentWidgets" data-brands="AMEX MASTER DISCOVER CARTEBANCAIRE"></form>A checkout id expires when the user finalises a payment, or after 30 minutes. Before expiring, you can use it multiple times to retrieve a valid payment form. For example, if a user reloads the page or uses the back button without finishing the payment, you don't need to generate a new checkout id. However, such actions can create multiple transactions in the system, such as one failed and another successful, using the same checkout id.
3. Get the payment status
After the system processes the payment, it redirects the customer to the shopperResultUrl with a resourcePath GET parameter.
The baseUrl must end in a "/", for example, "https://sandbox-card.peachpayments.com/".
Then, to get the status of the payment, you should make a GET request to the baseUrl + resourcePath, including your authentication parameters.
Example resourcePath: resourcePath=/v1/checkouts/{checkoutId}/payment
When a status response is successful, the system can no longer use the checkout identifier. In this case, you can use the Transaction Reports endpoint to retrieve the transaction status using the payment id.
https://sandbox-card.peachpayments.com/v1/checkouts/EBE5A9BFDF292A5F5666E3E0F403388E.uat01-vm-tx03/payment
curl -G https://sandbox-card.peachpayments.com/v1/checkouts/{id}/payment \
-d "entityId=8a8294174e735d0c014e78cf26461790" \
-H "Authorization: Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=" public Dictionary<string, dynamic> Request() {
Dictionary<string, dynamic> responseData;
string data="entityId=8a8294174e735d0c014e78cf26461790";
string url = "https://sandbox-card.peachpayments.com/v1/checkouts/{id}/payment?" + data;
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
request.Method = "GET";
request.Headers["Authorization"] = "Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=";
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) {
Stream dataStream = response.GetResponseStream();
StreamReader reader = new StreamReader(dataStream);
var s = new JavaScriptSerializer();
responseData = s.Deserialize<Dictionary<string, dynamic>>(reader.ReadToEnd());
reader.Close();
dataStream.Close();
}
return responseData;
}
responseData = Request()["result"]["description"];import groovy.json.JsonSlurper
public static String request() {
def data = "entityId=8a8294174e735d0c014e78cf26461790"
def url = ("https://sandbox-card.peachpayments.com/v1/checkouts/{id}/payment?" + data).toURL()
def connection = url.openConnection()
connection.setRequestMethod("GET")
connection.setRequestProperty("Authorization","Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=")
def json = new JsonSlurper().parseText(connection.inputStream.text)
json
}
println request()private String request() throws IOException {
URL url = new URL("https://sandbox-card.peachpayments.com/v1/checkouts/{id}/payment?entityId=8a8294174e735d0c014e78cf26461790");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Authorization", "Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=");
int responseCode = conn.getResponseCode();
InputStream is;
if (responseCode >= 400) is = conn.getErrorStream();
else is = conn.getInputStream();
return IOUtils.toString(is);
}const https = require('https');
const querystring = require('querystring');
const request = async () => {
var path='/v1/checkouts/{id}/payment';
path += '?entityId=8a8294174e735d0c014e78cf26461790';
const options = {
port: 443,
host: 'sandbox-card.peachpayments.com',
path: path,
method: 'GET',
headers: {
'Authorization':'Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ='
}
};
return new Promise((resolve, reject) => {
const postRequest = https.request(options, function(res) {
const buf = [];
res.on('data', chunk => {
buf.push(Buffer.from(chunk));
});
res.on('end', () => {
const jsonString = Buffer.concat(buf).toString('utf8');
try {
resolve(JSON.parse(jsonString));
} catch (error) {
reject(error);
}
});
});
postRequest.on('error', reject);
postRequest.end();
});
};
request().then(console.log).catch(console.error);function request() {
$url = "https://sandbox-card.peachpayments.com/v1/checkouts/{id}/payment";
$url .= "?entityId=8a8294174e735d0c014e78cf26461790";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization:Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ='));
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // this should be set to true in production
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$responseData = curl_exec($ch);
if(curl_errno($ch)) {
return curl_error($ch);
}
curl_close($ch);
return $responseData;
}
$responseData = request();try:
from urllib.parse import urlencode
from urllib.request import build_opener, Request, HTTPHandler
from urllib.error import HTTPError, URLError
except ImportError:
from urllib import urlencode
from urllib2 import build_opener, Request, HTTPHandler, HTTPError, URLError
import json
def request():
url = "https://sandbox-card.peachpayments.com/v1/checkouts/{id}/payment"
url += '?entityId=8a8294174e735d0c014e78cf26461790'
try:
opener = build_opener(HTTPHandler)
request = Request(url, data=b'')
request.add_header('Authorization', 'Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=')
request.get_method = lambda: 'GET'
response = opener.open(request)
return json.loads(response.read())
except HTTPError as e:
return json.loads(e.read())
except URLError as e:
return e.reason
responseData = request()
print(responseData)require 'net/https'
require 'uri'
require 'json'
def request()
path = ("?entityId=8a8294174e735d0c014e78cf26461790")
uri = URI.parse('https://sandbox-card.peachpayments.com/v1/checkouts/{id}/payment' + path)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Get.new(uri)
req['Authorization'] = 'Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ='
res = http.request(req)
return JSON.parse(res.body)
end
puts request()def initialPayment: String = {
val url = "https://sandbox-card.peachpayments.com/v1/checkouts/{id}/payment"
url +="?entityId=8a8294174e735d0c014e78cf26461790"
val conn = new URL(url).openConnection()
conn match {
case secureConn: HttpsURLConnection => secureConn.setRequestMethod("GET")
case _ => throw new ClassCastException
}
conn.setRequestProperty("Authorization", "Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=")
conn.connect()
if (conn.getResponseCode() >= 400) {
return IOUtils.toString(conn.getErrorStream())
}
else {
return IOUtils.toString(conn.getInputStream())
}
}Public Function Request() As Dictionary(Of String, Object)
Dim url As String = "https://sandbox-card.peachpayments.com/v1/checkouts/{id}/payment" + "?entityId=8a8294174e735d0c014e78cf26461790"
Dim req As WebRequest = WebRequest.Create(url)
req.Method = "GET"
req.Headers.Add("Authorization", "Bearer OGE4Mjk0MTc0ZTczNWQwYzAxNGU3OGNmMjY2YjE3OTR8SFV3I3JGQTQ9bWpxaWYrPz9OWVQ=")
req.ContentType = "application/x-www-form-urlencoded"
Dim res As WebResponse = req.GetResponse()
Dim resStream = res.GetResponseStream()
Dim reader As New StreamReader(resStream)
Dim response As String = reader.ReadToEnd()
reader.Close()
resStream.Close()
res.Close()
Dim jss As New System.Web.Script.Serialization.JavaScriptSerializer()
Dim dict As Dictionary(Of String, Object) = jss.Deserialize(Of Dictionary(Of String, Object))(response)
Return dict
End Function
responseData = Request()("result")("description"){
"result":{
"code":"000.200.000",
"description":"transaction pending"
},
"buildNumber":"9092e7a6af8301accda2f9a3a38f743f907dadd5@2026-03-23 16:50:06 +0000",
"timestamp":"2026-03-24 11:37:22+0000",
"ndc":"EBE5A9BFDF292A5F5666E3E0F403388E.uat01-vm-tx03"
}A throttling rule applies for get payment status calls. Per checkout, you can send two get payment requests in a minute.
Verify the following fields from the Payment Status response by comparing the returned values with expected:
- IDs
- Amount
- Currency
- Brand
- Type
Backoffice operations
COPYandPAY securely accepts payment data. After processing payments, you can issue refunds or perform other back office operations by following the backoffice operations guide.
Updated about 4 hours ago