Skip to main content

Hash signature

Every request you send carries a hash that Payment Platform uses to authenticate it, and every callback Payment Platform sends carries a hash that you use to authenticate the callback. In all protocols the hash is a digest of a single concatenated string that includes your merchant password.

The recipe is not the same for every protocol. Checkout adds an outer SHA1 pass and reuses one field order across a whole class of operations. S2S CARD and S2S APM have no outer SHA1, reverse parts of the input, and change the input list per action. Reusing one helper across protocols is a frequent cause of 100000 Hash is not valid; the full list of causes, in order of frequency, is on the troubleshooting page.

Which password the formulas mean

Every formula on this page ends in - or contains - password. That is one specific secret: the merchant Password from the admin panel, under Merchants → your merchant → Password. It is not merchant_key, not client_key and not the Test key. Those identify you in the request body; password never travels in a request, it only goes into the hash input. Sandbox and production each have their own value. See Authentication and credentials for where to copy it from.

Which recipe applies to you

ProtocolOuter shapePer-action inputs
Checkoutsha1(md5(uppercase(concat)))Checkout signatures
Hosted Payment Fieldssha1(md5(uppercase(concat)))Session creation uses the Checkout authentication signature. The card calls that follow are authenticated by the session token and carry no hash. Callbacks are signed with the Checkout callback signature.
S2S CARDmd5(uppercase(concat))S2S CARD signatures: index, the eight formulas and a worked example each, in Appendix A
S2S APMmd5(uppercase(concat))S2S APM signatures: index, every signature and a worked example each, in Appendix A

Two protocols sharing an outer shape do not produce the same hash: what changes per protocol, and per action inside it, is the input list, set out in the protocol section below.

Digest mode: MD5 or SHA256

By default the digest is MD5. If the Use SHA256 encryption algorithm for hash option is enabled for your protocol mapping (admin panel → Configuration → Protocol Mapping, per protocol / merchant), the digest is SHA256 instead: your request hash is validated with SHA256 and the callbacks you receive are signed with SHA256.

// Checkout, default (MD5):
hash = sha1(md5(uppercase(concat)))
// Checkout, "Use SHA256 encryption algorithm for hash" enabled:
hash = sha1(sha256(uppercase(concat)))

// S2S CARD and S2S APM, default (MD5):
hash = md5(uppercase(concat))
// S2S CARD and S2S APM, "Use SHA256 encryption algorithm for hash" enabled:
hash = sha256(uppercase(concat))

The option is set per protocol mapping, so the same merchant can be on MD5 for one protocol and on SHA256 for another. The field order and the uppercase step are identical in both modes - only the digest function changes. Contact your administrator or account manager to confirm which mode is configured for your account.

warning

Compute hashes on your server, never in the browser. The merchant password is the secret that authenticates your requests; exposing it in client-side code is a complete compromise.

Uppercasing and non-ASCII values

Every recipe on this page uppercases the assembled string before it is digested. The platform does that with PHP strtoupper, which changes only a-z and leaves accented, Cyrillic and other non-ASCII letters as they are. Checkout request signatures are the one exception: they use mb_strtoupper, which uppercases non-ASCII letters too.

This matters only when a hashed value contains characters outside ASCII, most often order_description or a merchant-assigned order_id. JavaScript toUpperCase() and Python upper() behave like mb_strtoupper, so they match Checkout request signatures but not the other signatures. To reproduce strtoupper, uppercase the ASCII range only:

LanguageASCII-only uppercase
PHPstrtoupper($raw)
JavaScriptraw.replace(/[a-z]/g, c => c.toUpperCase())
Pythonre.sub(r"[a-z]", lambda m: m.group(0).upper(), raw)

The worked examples on this page and in both appendixes use ASCII-only sample values, where the two functions give the same result.

Checkout signatures

Concatenate the values in the order listed, append the password, uppercase the whole string, then take sha1(md5(...)). Nothing is reversed and nothing is delimited. All these calls live under the /api/v1 path.

Authentication signature

PositionValue
1order.number
2order.amount
3order.currency
4order.description
5password

Used when you create a payment session for the Checkout payment page, and when you create a Hosted Payment Fields session.

Worked example
ParameterExample value
order.numberorder-1234
order.amount10.00
order.currencyUSD
order.descriptionImportant gift
passwordm3rch4ntP4ss

The string that gets hashed:

ORDER-123410.00USDIMPORTANT GIFTM3RCH4NTP4SS

The resulting hash:

29478736ac36042734a48df28522e1ddb9255cf3

PHP

<?php
$order_number = "order-1234";
$order_amount = "10.00";
$order_currency = "USD";
$order_description = "Important gift";
$password = "m3rch4ntP4ss";

$raw = mb_strtoupper($order_number . $order_amount . $order_currency . $order_description . $password);
$hash = sha1(md5($raw));
echo $hash;

JavaScript (Node.js)

const crypto = require("crypto");

const order_number = "order-1234";
const order_amount = "10.00";
const order_currency = "USD";
const order_description = "Important gift";
const password = "m3rch4ntP4ss";

const raw = (order_number + order_amount + order_currency + order_description + password).toUpperCase();
const md5 = crypto.createHash("md5").update(raw).digest("hex");
const hash = crypto.createHash("sha1").update(md5).digest("hex");
console.log(hash);

Python

import hashlib

order_number = "order-1234"
order_amount = "10.00"
order_currency = "USD"
order_description = "Important gift"
password = "m3rch4ntP4ss"

raw = (order_number + order_amount + order_currency + order_description + password).upper()
hash_value = hashlib.sha1(hashlib.md5(raw.encode()).hexdigest().encode()).hexdigest()
print(hash_value)

Capture / Refund signature

PositionValue
1payment_id
2amount
3password
Worked example
ParameterExample value
payment_id50a1361a-7c2e-11f1-b8d4-0242ac120002
amount10.00
passwordm3rch4ntP4ss

The string that gets hashed:

50A1361A-7C2E-11F1-B8D4-0242AC12000210.00M3RCH4NTP4SS

The resulting hash:

202f45ba7913b4536f0591a9a1eeca8ffbd0c9da

PHP

<?php
$payment_id = "50a1361a-7c2e-11f1-b8d4-0242ac120002";
$amount = "10.00";
$password = "m3rch4ntP4ss";

$raw = mb_strtoupper($payment_id . $amount . $password);
$hash = sha1(md5($raw));
echo $hash;

JavaScript (Node.js)

const crypto = require("crypto");

const payment_id = "50a1361a-7c2e-11f1-b8d4-0242ac120002";
const amount = "10.00";
const password = "m3rch4ntP4ss";

const raw = (payment_id + amount + password).toUpperCase();
const md5 = crypto.createHash("md5").update(raw).digest("hex");
const hash = crypto.createHash("sha1").update(md5).digest("hex");
console.log(hash);

Python

import hashlib

payment_id = "50a1361a-7c2e-11f1-b8d4-0242ac120002"
amount = "10.00"
password = "m3rch4ntP4ss"

raw = (payment_id + amount + password).upper()
hash_value = hashlib.sha1(hashlib.md5(raw.encode()).hexdigest().encode()).hexdigest()
print(hash_value)

Void / Retry / Get-status-by-payment_id signature

PositionValue
1payment_id
2password
Worked example
ParameterExample value
payment_id50a1361a-7c2e-11f1-b8d4-0242ac120002
passwordm3rch4ntP4ss

The string that gets hashed:

50A1361A-7C2E-11F1-B8D4-0242AC120002M3RCH4NTP4SS

The resulting hash:

e505b0a63333c82df960b878530a4980c82cfc69

PHP

<?php
$payment_id = "50a1361a-7c2e-11f1-b8d4-0242ac120002";
$password = "m3rch4ntP4ss";

$raw = mb_strtoupper($payment_id . $password);
$hash = sha1(md5($raw));
echo $hash;

JavaScript (Node.js)

const crypto = require("crypto");

const payment_id = "50a1361a-7c2e-11f1-b8d4-0242ac120002";
const password = "m3rch4ntP4ss";

const raw = (payment_id + password).toUpperCase();
const md5 = crypto.createHash("md5").update(raw).digest("hex");
const hash = crypto.createHash("sha1").update(md5).digest("hex");
console.log(hash);

Python

import hashlib

payment_id = "50a1361a-7c2e-11f1-b8d4-0242ac120002"
password = "m3rch4ntP4ss"

raw = (payment_id + password).upper()
hash_value = hashlib.sha1(hashlib.md5(raw.encode()).hexdigest().encode()).hexdigest()
print(hash_value)

Get-status-by-order_id signature

PositionValue
1order_id
2password
Worked example
ParameterExample value
order_idorder-1234
passwordm3rch4ntP4ss

The string that gets hashed:

ORDER-1234M3RCH4NTP4SS

The resulting hash:

8d2d3032e471f6782aa070301d6de8aeb807f6fc

PHP

<?php
$order_id = "order-1234";
$password = "m3rch4ntP4ss";

$raw = mb_strtoupper($order_id . $password);
$hash = sha1(md5($raw));
echo $hash;

JavaScript (Node.js)

const crypto = require("crypto");

const order_id = "order-1234";
const password = "m3rch4ntP4ss";

const raw = (order_id + password).toUpperCase();
const md5 = crypto.createHash("md5").update(raw).digest("hex");
const hash = crypto.createHash("sha1").update(md5).digest("hex");
console.log(hash);

Python

import hashlib

order_id = "order-1234"
password = "m3rch4ntP4ss"

raw = (order_id + password).upper()
hash_value = hashlib.sha1(hashlib.md5(raw.encode()).hexdigest().encode()).hexdigest()
print(hash_value)

Recurring signature

PositionValue
1recurring_init_trans_id
2recurring_token
3order.number
4order.amount
5order.description
6password
note

The recurring signature does not include order.currency. This is the one Checkout signature where the order block is not taken whole.

Worked example
ParameterExample value
recurring_init_trans_id1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d
recurring_tokene7c1a94b6f2d48a1b0c35d7e9f180a62
order.numberorder-1234
order.amount10.00
order.descriptionImportant gift
passwordm3rch4ntP4ss

The string that gets hashed:

1A2B3C4D-5E6F-7A8B-9C0D-1E2F3A4B5C6DE7C1A94B6F2D48A1B0C35D7E9F180A62ORDER-123410.00IMPORTANT GIFTM3RCH4NTP4SS

The resulting hash:

49f61e04a193160cb02116274095c51e2fc3a9db

PHP

<?php
$recurring_init_trans_id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d";
$recurring_token = "e7c1a94b6f2d48a1b0c35d7e9f180a62";
$order_number = "order-1234";
$order_amount = "10.00";
$order_description = "Important gift";
$password = "m3rch4ntP4ss";

$raw = mb_strtoupper($recurring_init_trans_id . $recurring_token . $order_number . $order_amount . $order_description . $password);
$hash = sha1(md5($raw));
echo $hash;

JavaScript (Node.js)

const crypto = require("crypto");

const recurring_init_trans_id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d";
const recurring_token = "e7c1a94b6f2d48a1b0c35d7e9f180a62";
const order_number = "order-1234";
const order_amount = "10.00";
const order_description = "Important gift";
const password = "m3rch4ntP4ss";

const raw = (recurring_init_trans_id + recurring_token + order_number + order_amount + order_description + password).toUpperCase();
const md5 = crypto.createHash("md5").update(raw).digest("hex");
const hash = crypto.createHash("sha1").update(md5).digest("hex");
console.log(hash);

Python

import hashlib

recurring_init_trans_id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
recurring_token = "e7c1a94b6f2d48a1b0c35d7e9f180a62"
order_number = "order-1234"
order_amount = "10.00"
order_description = "Important gift"
password = "m3rch4ntP4ss"

raw = (recurring_init_trans_id + recurring_token + order_number + order_amount + order_description + password).upper()
hash_value = hashlib.sha1(hashlib.md5(raw.encode()).hexdigest().encode()).hexdigest()
print(hash_value)

Card credit (payout) signature

For POST /api/v1/payment/card/credit, see Card credit (payout) for the full parameter list.

PositionValue
1order_id
2order_amount
3order_currency
4order_description
5password

The field names here are the flat order_* request fields, not the nested order object used when creating a session. Neither card_number nor card_token takes part in this hash.

Worked example
ParameterExample value
order_idpayout-5678
order_amount25.00
order_currencyUSD
order_descriptionAffiliate payout
passwordm3rch4ntP4ss

The string that gets hashed:

PAYOUT-567825.00USDAFFILIATE PAYOUTM3RCH4NTP4SS

The resulting hash:

60f056560b3fe4383ab14eaf4d886212c84c7d6a

PHP

<?php
$order_id = "payout-5678";
$order_amount = "25.00";
$order_currency = "USD";
$order_description = "Affiliate payout";
$password = "m3rch4ntP4ss";

$raw = mb_strtoupper($order_id . $order_amount . $order_currency . $order_description . $password);
$hash = sha1(md5($raw));
echo $hash;

JavaScript (Node.js)

const crypto = require("crypto");

const order_id = "payout-5678";
const order_amount = "25.00";
const order_currency = "USD";
const order_description = "Affiliate payout";
const password = "m3rch4ntP4ss";

const raw = (order_id + order_amount + order_currency + order_description + password).toUpperCase();
const md5 = crypto.createHash("md5").update(raw).digest("hex");
const hash = crypto.createHash("sha1").update(md5).digest("hex");
console.log(hash);

Python

import hashlib

order_id = "payout-5678"
order_amount = "25.00"
order_currency = "USD"
order_description = "Affiliate payout"
password = "m3rch4ntP4ss"

raw = (order_id + order_amount + order_currency + order_description + password).upper()
hash_value = hashlib.sha1(hashlib.md5(raw.encode()).hexdigest().encode()).hexdigest()
print(hash_value)

Callback signature (verification on your side)

PositionValue
1id (= payment_public_id)
2order_number
3order_amount
4order_currency
5order_description
6password

The callback input is uppercased with strtoupper, not with a Unicode-aware function. See Uppercasing and non-ASCII values if your order_description can contain characters outside ASCII.

Worked example
ParameterExample value
id50a1361a-7c2e-11f1-b8d4-0242ac120002
order_numberorder-1234
order_amount10.00
order_currencyUSD
order_descriptionImportant gift
passwordm3rch4ntP4ss

The string that gets hashed:

50A1361A-7C2E-11F1-B8D4-0242AC120002ORDER-123410.00USDIMPORTANT GIFTM3RCH4NTP4SS

The resulting hash:

9eddb423ef8a8c2e36e8717eb5e638cdeb17d957

PHP

<?php
$id = "50a1361a-7c2e-11f1-b8d4-0242ac120002";
$order_number = "order-1234";
$order_amount = "10.00";
$order_currency = "USD";
$order_description = "Important gift";
$password = "m3rch4ntP4ss";

$raw = strtoupper($id . $order_number . $order_amount . $order_currency . $order_description . $password);
$hash = sha1(md5($raw));
echo $hash;

JavaScript (Node.js)

const crypto = require("crypto");

const id = "50a1361a-7c2e-11f1-b8d4-0242ac120002";
const order_number = "order-1234";
const order_amount = "10.00";
const order_currency = "USD";
const order_description = "Important gift";
const password = "m3rch4ntP4ss";

const raw = (id + order_number + order_amount + order_currency + order_description + password).toUpperCase();
const md5 = crypto.createHash("md5").update(raw).digest("hex");
const hash = crypto.createHash("sha1").update(md5).digest("hex");
console.log(hash);

Python

import hashlib

id_value = "50a1361a-7c2e-11f1-b8d4-0242ac120002"
order_number = "order-1234"
order_amount = "10.00"
order_currency = "USD"
order_description = "Important gift"
password = "m3rch4ntP4ss"

raw = (id_value + order_number + order_amount + order_currency + order_description + password).upper()
hash_value = hashlib.sha1(hashlib.md5(raw.encode()).hexdigest().encode()).hexdigest()
print(hash_value)

Customer-return signature (in success_url / cancel_url query parameters)

Same input as the callback signature: the same six values in the same order, so the worked example above applies unchanged, including the note on non-ASCII descriptions.

Two things differ when you verify it:

  • The query string carries payment_id, trans_id, order_id and hash, plus payment_method and brand if your Protocol Mapping asks for them. payment_id is position 1 of the hash input and order_id is position 2. Take positions 3 to 5 from your own order record: the URL does not carry the amount, the currency or the description.
  • Return parameters must be enabled in your Protocol Mapping. Without that setting the platform appends nothing to success_url and cancel_url.

S2S CARD signatures

S2S CARD does not use the Checkout recipe: there is no outer SHA1, parts of the input are reversed with strrev, and the input list changes per action.

Everything about S2S CARD signatures lives in one place, Appendix A: the per-action index, the eight literal formulas, and a runnable example for each of them.

What you needWhere
Which formula my action usesWhich formula for which action
The literal formulas, Formula 1 to Formula 8, each with a worked example in PHP, JavaScript and PythonHash formulas
A transaction that has no card data on fileNo card data on file

Transactions with no card data on file

See No card data on file in Appendix A.

Callbacks

Every S2S CARD callback is signed with Formula 2, where trans_id is the trans_id of the payment. The CREDIT2CARD callback is the exception and uses Formula 6.

S2S APM signatures

S2S APM has no outer SHA1 either, and its signatures are named rather than numbered.

Everything about S2S APM signatures lives in one place, Appendix A: the per-action index, every signature, and a runnable example for each.

What you needWhere
Which signature my action usesWhich signature for which action
The signatures themselves, each with a worked example in PHP, JavaScript and PythonAppendix A

Callbacks

S2S APM callbacks are not signed from a fixed field list: every value in the callback is reversed individually, the values are sorted by parameter name, concatenated, uppercased, and the password is appended. This applies to every callback on a SALE or DEBIT2VIRTUAL payment, not only the first one, so do not switch to a trans_id based signature after the initial callback. Rule and worked example: Callback signature.

CREDIT2VIRTUAL and CREDIT2CRYPTO are the exception, because they are payouts and not purchases: Credit2Virtual callback signature.

See also