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" +
                  "&currency=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" +
               "&currency=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"
        + "&currency=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" .
            "&currency=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"
        + "&currency=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" +
        "&currency=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 id in the response from step 1.
    <script 
        src="https://sandbox-card.peachpayments.com/v1/paymentWidgets.js?checkoutId={checkoutId}"
        integrity="{integrity}"
        crossorigin="anonymous">
    </script>
  • The shopperResultUrl is 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.

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.