Appendix A
Hash formulas
The hash field is the signature that Payment Platform uses to validate requests from your system, and that your system uses to validate callbacks from Payment Platform. Calculate the hash value with MD5 using the per-action formulas below.
These formulas are specific to S2S CARD. They are not the Checkout recipe: there is no outer sha1 pass. For the same information laid out per protocol, and for Checkout and S2S APM, see Hash signature.
Every formula uppercases its assembled input with PHP strtoupper, which changes only a-z. If a value that goes into a hash can contain characters outside ASCII, see Uppercasing and non-ASCII values.
SHA256 hash mode
The digest is MD5 by default and SHA256 when the Use SHA256 encryption algorithm for hash option is enabled for your protocol mapping. The inputs and the order below do not change; only the digest function does. The setting, and what it does to every protocol, is described once under Digest mode.
Notation used in the formulas
| In the formulas | What it is |
|---|---|
email | The payer email for the transaction: the payer_email you send in the request, or the one Payment Platform stored for the initiating payment when the call carries only trans_id. |
PASSWORD | Your merchant password, the Password field of the merchant in the admin panel. It is not the client_key and not the merchant key, and it is never sent in the request - it only goes into the hash input. |
card_number | The PAN the card fragment is taken from. Only the first 6 and the last 4 digits take part. On calls that carry only trans_id, the fragment is the one Payment Platform stored for that transaction, returned to you in the card parameter. |
Which formula for which action
| Action or callback | Formula |
|---|---|
SALE, SALE with auth=Y, DEBIT, CARD2CARD | Formula 1 |
SALE, DEBIT sent with payment_token (digital wallet) | Formula 8 |
RECURRING_SALE, RECURRING_DEBIT | Formula 1 |
CAPTURE, VOID, CREDITVOID, RETRY, GET_TRANS_STATUS, GET_TRANS_DETAILS | Formula 2 |
the same actions on a transaction that has no payer email, for example a CREDIT2CARD payout | Formula 6 |
| the same actions on a transaction that has no card data stored | no card data on file |
GET_TRANS_STATUS_BY_ORDER | Formula 7 |
CREATE_SCHEDULE | Formula 3 |
UPDATE_SCHEDULE, PAUSE_SCHEDULE, RUN_SCHEDULE, DELETE_SCHEDULE, SCHEDULE_INFO, DESCHEDULE | Formula 4 |
CREDIT2CARD request | Formula 5 |
CREDIT2CARD callback | Formula 6 |
| every other callback | Formula 2 |
If a formula contains a parameter that is optional and you do not send it - payer_email in a DEBIT request, for example - leave that term out of the hash input entirely. Do not put an empty placeholder in its place and do not keep a separator for it. Formula 6 and Formula 8 are this rule already applied to the two cases you are most likely to meet.
Formula 1
Used for SALE, SALE with auth=Y, DEBIT, CARD2CARD, RECURRING_SALE and RECURRING_DEBIT.
hash for SALE is calculated by the formula:
md5(strtoupper(strrev(email).PASSWORD.strrev(substr(card_number,0,6).substr(card_number,-4))))
if parameter card_token is specified hash is calculated by the formula:
md5(strtoupper(strrev(email).PASSWORD.strrev(card_token)))
hash for RECURRING_SALE is calculated by the formula:
md5(strtoupper(strrev(email).PASSWORD.strrev(substr(card_number,0,6).substr(card_number,-4))))
For RECURRING_SALE and RECURRING_DEBIT, Payment Platform takes email and the card fragment from the stored initiating payment, not from the parameters you send in that call.
For CARD2CARD, the card fragment is taken from the sender card, that is from payer_card_number, and never from payee_card_number.
Worked example
| Parameter | Example value |
|---|---|
email | [email protected] |
password | m3rch4ntP4ss |
card_number | 4111111111111111 |
The string that gets hashed:
MOC.ELPMAXE@NHOJM3RCH4NTP4SS1111111114
The resulting hash:
adff769832661477ef1e2d6393b57546
PHP
<?php
$email = "[email protected]";
$password = "m3rch4ntP4ss";
$card_number = "4111111111111111";
$raw = strtoupper(strrev($email) . $password . strrev(substr($card_number,0,6) . substr($card_number,-4)));
$hash = md5($raw);
echo $hash;
JavaScript (Node.js)
const crypto = require("crypto");
const rev = s => s.split("").reverse().join("");
const email = "[email protected]";
const password = "m3rch4ntP4ss";
const card_number = "4111111111111111";
const raw = (rev(email) + password + rev(card_number.slice(0,6) + card_number.slice(-4))).toUpperCase();
const hash = crypto.createHash("md5").update(raw).digest("hex");
console.log(hash);
Python
import hashlib
email = "[email protected]"
password = "m3rch4ntP4ss"
card_number = "4111111111111111"
raw = (email[::-1] + password + (card_number[:6] + card_number[-4:])[::-1]).upper()
hash_value = hashlib.md5(raw.encode()).hexdigest()
print(hash_value)
Worked example
| Parameter | Example value |
|---|---|
email | [email protected] |
password | m3rch4ntP4ss |
card_token | 9b74c9897bac770ffc029102a200c5de |
The string that gets hashed:
MOC.ELPMAXE@NHOJM3RCH4NTP4SSED5C002A201920CFF077CAB7989C47B9
The resulting hash:
7dd38ef934c425bafb3ff1b9346f0142
PHP
<?php
$email = "[email protected]";
$password = "m3rch4ntP4ss";
$card_token = "9b74c9897bac770ffc029102a200c5de";
$raw = strtoupper(strrev($email) . $password . strrev($card_token));
$hash = md5($raw);
echo $hash;
JavaScript (Node.js)
const crypto = require("crypto");
const rev = s => s.split("").reverse().join("");
const email = "[email protected]";
const password = "m3rch4ntP4ss";
const card_token = "9b74c9897bac770ffc029102a200c5de";
const raw = (rev(email) + password + rev(card_token)).toUpperCase();
const hash = crypto.createHash("md5").update(raw).digest("hex");
console.log(hash);
Python
import hashlib
email = "[email protected]"
password = "m3rch4ntP4ss"
card_token = "9b74c9897bac770ffc029102a200c5de"
raw = (email[::-1] + password + card_token[::-1]).upper()
hash_value = hashlib.md5(raw.encode()).hexdigest()
print(hash_value)
Formula 2
Used for CAPTURE, VOID, CREDITVOID, RETRY, GET_TRANS_STATUS and GET_TRANS_DETAILS, and for every callback except the CREDIT2CARD callback.
RETRY sends only trans_id, so it is signed with this formula and not with Formula 1: the email and the card fragment come from the stored initiating payment.
hash is calculated by the formula:
md5(strtoupper(strrev(email).PASSWORD.trans_id.strrev(substr(card_number,0,6).substr(card_number,-4))))
The card fragment is the one Payment Platform stored for that transaction, not card data you send in the call. In a callback, trans_id is the trans_id of the payment. If the transaction has no payer email, use Formula 6; if it has no card data stored at all, see no card data on file - which applies to every action listed here except CREDITVOID.
Worked example
| Parameter | Example value |
|---|---|
email | [email protected] |
password | m3rch4ntP4ss |
trans_id | 1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d |
card_number | 4111111111111111 |
The string that gets hashed:
MOC.ELPMAXE@NHOJM3RCH4NTP4SS1A2B3C4D-5E6F-7A8B-9C0D-1E2F3A4B5C6D1111111114
The resulting hash:
08e0d6cf105fb585e5bfacb0852f6ab3
PHP
<?php
$email = "[email protected]";
$password = "m3rch4ntP4ss";
$trans_id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d";
$card_number = "4111111111111111";
$raw = strtoupper(strrev($email) . $password . $trans_id . strrev(substr($card_number,0,6) . substr($card_number,-4)));
$hash = md5($raw);
echo $hash;
JavaScript (Node.js)
const crypto = require("crypto");
const rev = s => s.split("").reverse().join("");
const email = "[email protected]";
const password = "m3rch4ntP4ss";
const trans_id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d";
const card_number = "4111111111111111";
const raw = (rev(email) + password + trans_id + rev(card_number.slice(0,6) + card_number.slice(-4))).toUpperCase();
const hash = crypto.createHash("md5").update(raw).digest("hex");
console.log(hash);
Python
import hashlib
email = "[email protected]"
password = "m3rch4ntP4ss"
trans_id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
card_number = "4111111111111111"
raw = (email[::-1] + password + trans_id + (card_number[:6] + card_number[-4:])[::-1]).upper()
hash_value = hashlib.md5(raw.encode()).hexdigest()
print(hash_value)
Formula 3
Used for CREATE_SCHEDULE.
hash for Create a schedule is calculated by the formula:
md5(strtoupper(strrev(PASSWORD)))
Worked example
| Parameter | Example value |
|---|---|
password | m3rch4ntP4ss |
The string that gets hashed:
SS4PTN4HCR3M
The resulting hash:
35b7c8b50a964eca3fcd89ab6386d711
PHP
<?php
$password = "m3rch4ntP4ss";
$raw = strtoupper(strrev($password));
$hash = md5($raw);
echo $hash;
JavaScript (Node.js)
const crypto = require("crypto");
const rev = s => s.split("").reverse().join("");
const password = "m3rch4ntP4ss";
const raw = rev(password).toUpperCase();
const hash = crypto.createHash("md5").update(raw).digest("hex");
console.log(hash);
Python
import hashlib
password = "m3rch4ntP4ss"
raw = password[::-1].upper()
hash_value = hashlib.md5(raw.encode()).hexdigest()
print(hash_value)
Formula 4
Used for UPDATE_SCHEDULE, PAUSE_SCHEDULE, RUN_SCHEDULE, DELETE_SCHEDULE, SCHEDULE_INFO and DESCHEDULE.
hash for Other schedules is calculated by the formula:
md5(strtoupper(strrev(schedule_id + PASSWORD)))
schedule_id and PASSWORD are concatenated first and reversed together, so this is not the same as reversing schedule_id and then appending PASSWORD.
Worked example
| Parameter | Example value |
|---|---|
schedule_id | 7f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0 |
password | m3rch4ntP4ss |
The string that gets hashed:
SS4PTN4HCR3M0F1E2D3C4B5A-6978-8796-A5B4-C3D2E1F7
The resulting hash:
cf82f235dcb2dcc178e248fb746a9eab
PHP
<?php
$schedule_id = "7f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0";
$password = "m3rch4ntP4ss";
$raw = strtoupper(strrev($schedule_id . $password));
$hash = md5($raw);
echo $hash;
JavaScript (Node.js)
const crypto = require("crypto");
const rev = s => s.split("").reverse().join("");
const schedule_id = "7f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0";
const password = "m3rch4ntP4ss";
const raw = rev(schedule_id + password).toUpperCase();
const hash = crypto.createHash("md5").update(raw).digest("hex");
console.log(hash);
Python
import hashlib
schedule_id = "7f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0"
password = "m3rch4ntP4ss"
raw = (schedule_id + password)[::-1].upper()
hash_value = hashlib.md5(raw.encode()).hexdigest()
print(hash_value)
Formula 5
Used for the CREDIT2CARD request.
hash for CREDIT2CARD request is calculated by the formula:
md5(strtoupper(PASSWORD.strrev(substr(card_number,0,6).substr(card_number,-4))))
if card_token is specified hash is calculated by the formula:
md5(strtoupper(PASSWORD. strrev(card_token)))
Worked example
| Parameter | Example value |
|---|---|
password | m3rch4ntP4ss |
card_number | 4111111111111111 |
The string that gets hashed:
M3RCH4NTP4SS1111111114
The resulting hash:
eb43518dd1b49c127457ac142044f454
PHP
<?php
$password = "m3rch4ntP4ss";
$card_number = "4111111111111111";
$raw = strtoupper($password . strrev(substr($card_number,0,6) . substr($card_number,-4)));
$hash = md5($raw);
echo $hash;
JavaScript (Node.js)
const crypto = require("crypto");
const rev = s => s.split("").reverse().join("");
const password = "m3rch4ntP4ss";
const card_number = "4111111111111111";
const raw = (password + rev(card_number.slice(0,6) + card_number.slice(-4))).toUpperCase();
const hash = crypto.createHash("md5").update(raw).digest("hex");
console.log(hash);
Python
import hashlib
password = "m3rch4ntP4ss"
card_number = "4111111111111111"
raw = (password + (card_number[:6] + card_number[-4:])[::-1]).upper()
hash_value = hashlib.md5(raw.encode()).hexdigest()
print(hash_value)
Worked example
| Parameter | Example value |
|---|---|
password | m3rch4ntP4ss |
card_token | 9b74c9897bac770ffc029102a200c5de |
The string that gets hashed:
M3RCH4NTP4SSED5C002A201920CFF077CAB7989C47B9
The resulting hash:
3093f20bad8e2e05d80237b8a5789627
PHP
<?php
$password = "m3rch4ntP4ss";
$card_token = "9b74c9897bac770ffc029102a200c5de";
$raw = strtoupper($password . strrev($card_token));
$hash = md5($raw);
echo $hash;
JavaScript (Node.js)
const crypto = require("crypto");
const rev = s => s.split("").reverse().join("");
const password = "m3rch4ntP4ss";
const card_token = "9b74c9897bac770ffc029102a200c5de";
const raw = (password + rev(card_token)).toUpperCase();
const hash = crypto.createHash("md5").update(raw).digest("hex");
console.log(hash);
Python
import hashlib
password = "m3rch4ntP4ss"
card_token = "9b74c9897bac770ffc029102a200c5de"
raw = (password + card_token[::-1]).upper()
hash_value = hashlib.md5(raw.encode()).hexdigest()
print(hash_value)
Formula 6
Used for the CREDIT2CARD callback, and for CAPTURE, VOID, CREDITVOID, GET_TRANS_STATUS and GET_TRANS_DETAILS on a transaction that has no payer email - a CREDIT2CARD payout, for example, carries payee_email and no payer email.
hash is calculated by the formula:
md5(strtoupper(PASSWORD.trans_id.strrev(substr(card_number,0,6).substr(card_number,-4))))
This is Formula 2 without the strrev(email) term, following the Pay attention rule above.
Worked example
| Parameter | Example value |
|---|---|
password | m3rch4ntP4ss |
trans_id | 1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d |
card_number | 4111111111111111 |
The string that gets hashed:
M3RCH4NTP4SS1A2B3C4D-5E6F-7A8B-9C0D-1E2F3A4B5C6D1111111114
The resulting hash:
349bd331411b799a74578c660323107a
PHP
<?php
$password = "m3rch4ntP4ss";
$trans_id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d";
$card_number = "4111111111111111";
$raw = strtoupper($password . $trans_id . strrev(substr($card_number,0,6) . substr($card_number,-4)));
$hash = md5($raw);
echo $hash;
JavaScript (Node.js)
const crypto = require("crypto");
const rev = s => s.split("").reverse().join("");
const password = "m3rch4ntP4ss";
const trans_id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d";
const card_number = "4111111111111111";
const raw = (password + trans_id + rev(card_number.slice(0,6) + card_number.slice(-4))).toUpperCase();
const hash = crypto.createHash("md5").update(raw).digest("hex");
console.log(hash);
Python
import hashlib
password = "m3rch4ntP4ss"
trans_id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
card_number = "4111111111111111"
raw = (password + trans_id + (card_number[:6] + card_number[-4:])[::-1]).upper()
hash_value = hashlib.md5(raw.encode()).hexdigest()
print(hash_value)
Formula 7
Used for GET_TRANS_STATUS_BY_ORDER.
hash is calculated by the formula:
md5(strtoupper(strrev(email).PASSWORD.order_id.strrev(substr(card_number,0,6).substr(card_number,-4))))
This is Formula 2 with order_id in place of trans_id. On a transaction with no payer email - a CREDIT2CARD payout, for example - drop the strrev(email) term, as in Formula 6. If Payment Platform holds no card data for the order, see no card data on file.
Worked example
| Parameter | Example value |
|---|---|
email | [email protected] |
password | m3rch4ntP4ss |
order_id | order-1234 |
card_number | 4111111111111111 |
The string that gets hashed:
MOC.ELPMAXE@NHOJM3RCH4NTP4SSORDER-12341111111114
The resulting hash:
98fe9590aca61eb784a59810990277dc
PHP
<?php
$email = "[email protected]";
$password = "m3rch4ntP4ss";
$order_id = "order-1234";
$card_number = "4111111111111111";
$raw = strtoupper(strrev($email) . $password . $order_id . strrev(substr($card_number,0,6) . substr($card_number,-4)));
$hash = md5($raw);
echo $hash;
JavaScript (Node.js)
const crypto = require("crypto");
const rev = s => s.split("").reverse().join("");
const email = "[email protected]";
const password = "m3rch4ntP4ss";
const order_id = "order-1234";
const card_number = "4111111111111111";
const raw = (rev(email) + password + order_id + rev(card_number.slice(0,6) + card_number.slice(-4))).toUpperCase();
const hash = crypto.createHash("md5").update(raw).digest("hex");
console.log(hash);
Python
import hashlib
email = "[email protected]"
password = "m3rch4ntP4ss"
order_id = "order-1234"
card_number = "4111111111111111"
raw = (email[::-1] + password + order_id + (card_number[:6] + card_number[-4:])[::-1]).upper()
hash_value = hashlib.md5(raw.encode()).hexdigest()
print(hash_value)
Formula 8
Used for SALE, SALE with auth=Y and DEBIT when the card is presented as a digital wallet token, that is when you send payment_token instead of card_number or card_token.
hash is calculated by the formula:
md5(strtoupper(strrev(email).PASSWORD))
This is Formula 1 with no card term, because there is no PAN and no card token in the request to build one from. See Appendix C for the digital wallet parameters.
Worked example
| Parameter | Example value |
|---|---|
email | [email protected] |
password | m3rch4ntP4ss |
The string that gets hashed:
MOC.ELPMAXE@NHOJM3RCH4NTP4SS
The resulting hash:
1c40b8905bd426df293be7e91a8de181
PHP
<?php
$email = "[email protected]";
$password = "m3rch4ntP4ss";
$raw = strtoupper(strrev($email) . $password);
$hash = md5($raw);
echo $hash;
JavaScript (Node.js)
const crypto = require("crypto");
const rev = s => s.split("").reverse().join("");
const email = "[email protected]";
const password = "m3rch4ntP4ss";
const raw = (rev(email) + password).toUpperCase();
const hash = crypto.createHash("md5").update(raw).digest("hex");
console.log(hash);
Python
import hashlib
email = "[email protected]"
password = "m3rch4ntP4ss"
raw = (email[::-1] + password).upper()
hash_value = hashlib.md5(raw.encode()).hexdigest()
print(hash_value)
Transactions with no card data on file
CAPTURE, VOID, RETRY, GET_TRANS_STATUS, GET_TRANS_DETAILS and GET_TRANS_STATUS_BY_ORDER are signed with the card fragment that Payment Platform stored for the transaction. If Payment Platform holds no card fragment for that transaction, the card term cannot be built and a shorter input is used instead:
// CAPTURE, VOID, RETRY, GET_TRANS_STATUS, GET_TRANS_DETAILS
md5(strtoupper(strrev(trans_id)).PASSWORD)
// GET_TRANS_STATUS_BY_ORDER
md5(strtoupper(strrev(order_id)).PASSWORD)
CREDITVOID is the exception and does not switch to this shorter input: it keeps the Formula 2 shape and simply omits the card term.
This is not Formula 2 with empty terms. Three things differ: there is no email term, the identifier itself is reversed, and only the reversed identifier is uppercased - PASSWORD is appended as it is.
This applies to a digital wallet payment that stayed a wallet payment, that is Apple Pay or Google Pay where the wallet token was not decrypted into card data. When the wallet token is decrypted on our side, the card is stored, its fragment comes back to you in the card parameter of the response and the callback, and Formula 2 applies as usual. If you are not sure which case applies to your account, check whether card is populated in the response and callback of the initiating payment.
Worked example
| Parameter | Example value |
|---|---|
trans_id | 1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d |
password | m3rch4ntP4ss |
The string that gets hashed:
D6C5B4A3F2E1-D0C9-B8A7-F6E5-D4C3B2A1m3rch4ntP4ss
The resulting hash:
d868b91ebb3652361ad1f361aaf61530
PHP
<?php
$trans_id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d";
$password = "m3rch4ntP4ss";
$raw = strtoupper(strrev($trans_id)) . $password;
$hash = md5($raw);
echo $hash;
JavaScript (Node.js)
const crypto = require("crypto");
const rev = s => s.split("").reverse().join("");
const trans_id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d";
const password = "m3rch4ntP4ss";
const raw = rev(trans_id).toUpperCase() + password;
const hash = crypto.createHash("md5").update(raw).digest("hex");
console.log(hash);
Python
import hashlib
trans_id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
password = "m3rch4ntP4ss"
raw = trans_id[::-1].upper() + password
hash_value = hashlib.md5(raw.encode()).hexdigest()
print(hash_value)
See also
For related S2S Card documentation, see the following pages:
- Hash signature: the same formulas mapped per protocol and per action, including Checkout and S2S APM.
- S2S Card overview: integration overview and action list.
- Payment operation types: per-action request and response shapes. The CREDIT2CARD request uses Formula 5; its callback uses Formula 6.
- Appendix B: worked request and response examples.
- Appendix C: additional parameters for digital wallets and alternative methods.