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
| Protocol | Outer shape | Per-action inputs |
|---|---|---|
| Checkout | sha1(md5(uppercase(concat))) | Checkout signatures |
| Hosted Payment Fields | sha1(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 CARD | md5(uppercase(concat)) | S2S CARD signatures: index, the eight formulas and a worked example each, in Appendix A |
| S2S APM | md5(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.
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:
| Language | ASCII-only uppercase |
|---|---|
| PHP | strtoupper($raw) |
| JavaScript | raw.replace(/[a-z]/g, c => c.toUpperCase()) |
| Python | re.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
| Position | Value |
|---|---|
| 1 | order.number |
| 2 | order.amount |
| 3 | order.currency |
| 4 | order.description |
| 5 | password |
Used when you create a payment session for the Checkout payment page, and when you create a Hosted Payment Fields session.
Worked example
| Parameter | Example value |
|---|---|
order.number | order-1234 |
order.amount | 10.00 |
order.currency | USD |
order.description | Important gift |
password | m3rch4ntP4ss |
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
| Position | Value |
|---|---|
| 1 | payment_id |
| 2 | amount |
| 3 | password |
Worked example
| Parameter | Example value |
|---|---|
payment_id | 50a1361a-7c2e-11f1-b8d4-0242ac120002 |
amount | 10.00 |
password | m3rch4ntP4ss |
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
| Position | Value |
|---|---|
| 1 | payment_id |
| 2 | password |
Worked example
| Parameter | Example value |
|---|---|
payment_id | 50a1361a-7c2e-11f1-b8d4-0242ac120002 |
password | m3rch4ntP4ss |
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
| Position | Value |
|---|---|
| 1 | order_id |
| 2 | password |
Worked example
| Parameter | Example value |
|---|---|
order_id | order-1234 |
password | m3rch4ntP4ss |
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
| Position | Value |
|---|---|
| 1 | recurring_init_trans_id |
| 2 | recurring_token |
| 3 | order.number |
| 4 | order.amount |
| 5 | order.description |
| 6 | password |
The recurring signature does not include order.currency. This is the one Checkout signature where the order block is not taken whole.
Worked example
| Parameter | Example value |
|---|---|
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 |
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.
| Position | Value |
|---|---|
| 1 | order_id |
| 2 | order_amount |
| 3 | order_currency |
| 4 | order_description |
| 5 | password |
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
| Parameter | Example value |
|---|---|
order_id | payout-5678 |
order_amount | 25.00 |
order_currency | USD |
order_description | Affiliate payout |
password | m3rch4ntP4ss |
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)
| Position | Value |
|---|---|
| 1 | id (= payment_public_id) |
| 2 | order_number |
| 3 | order_amount |
| 4 | order_currency |
| 5 | order_description |
| 6 | password |
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
| Parameter | Example value |
|---|---|
id | 50a1361a-7c2e-11f1-b8d4-0242ac120002 |
order_number | order-1234 |
order_amount | 10.00 |
order_currency | USD |
order_description | Important gift |
password | m3rch4ntP4ss |
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_idandhash, pluspayment_methodandbrandif your Protocol Mapping asks for them.payment_idis position 1 of the hash input andorder_idis 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_urlandcancel_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 need | Where |
|---|---|
| Which formula my action uses | Which formula for which action |
| The literal formulas, Formula 1 to Formula 8, each with a worked example in PHP, JavaScript and Python | Hash formulas |
| A transaction that has no card data on file | No 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 need | Where |
|---|---|
| Which signature my action uses | Which signature for which action |
| The signatures themselves, each with a worked example in PHP, JavaScript and Python | Appendix 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
- S2S CARD Appendix A: the literal S2S CARD formulas, Formula 1 to Formula 8, each with a worked example in PHP, JavaScript and Python.
- S2S APM Appendix A: the literal S2S APM signatures, each with a worked example in PHP, JavaScript and Python.
- Troubleshooting: hash signature failures: the most common hash-recipe mistakes, with a known-good shell helper.