Webhooks Signature
MoneyHash can sign all webhook events sent to your endpoints with a signature. This signature appears in each event's MoneyHash-Signature
header. It allows you to verify that the events were sent by MoneyHash rather than a third party.
Our Signature verification makes use of the Hash-based Message Authentication Code (HMAC) for authenticating and validating webhooks with (SHA256). The result HMAC is calculated using a secret key and the event payload.
Verifying signatures
The MoneyHash-Signature
header included in each webhook event contains a timestamp and the current version. The timestamp is prefixed by t=
and the version is prefixed by v=
Our Current signature versions are 1, 2 and 3
Header sample
t=1698744604,v1=5e92be4afca58c407f59c71e763021caf0df4140e79b36e53f6201aa99e29edc,v2=23021d841c21af5e7deb1f5b79f13dcf2cdefab55124aac78460fd884c864ecb,v3=5ae747e562f95b621859f7846edf6b3546e02dca4ad53d26bc788535d2f4660b
Header information
The header will contain t
for time and v
for the version:
t
: the timestamp this request was createdv
: the current version of signature you will validate.
Currently, we are passingv1
,v2
andv3
and the version used to allow us to change the signature calculation in the future, your version should stay the same (For backward compatibility)
Note
MoneyHash-Signature
header is case-sensitive, so make sure to handle it properly.
Verification steps
Warning
Please make sure to use the latest signature version to stay up to date with our enhancements.
Signature v3
Step 1 Extract the signature and the time from the header
timestamp = "1698744604"
signature = "5ae747e562f95b621859f7846edf6b3546e02dca4ad53d26bc788535d2f4660b"
Step 2: Get your organization signature key
You will need to get your organization secret key to pass it to HMAC function to be used as your secret key, to get this key you can set GET request to /api/v1/organizations/get-webhook-signature-key/
and pass your account API key as header
curl --location --request GET 'https://web.moneyhash.io/api/v1/organizations/get-webhook-signature-key/' \
--header 'X-Api-Key: YOUR_ACCOUNT_API_KEY'
{
"status": {
"code": 200,
"message": "success",
"errors": []
},
"data": {
"webhook_signature_secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"count": 1,
"next": null,
"previous": null
}
Signature key doesn't change, so you can store it as an environment variable instead of calling get signature endpoint on each signature verification
Step 3: Calculate the signature
import hashlib
import hmac
secret = "<your-organization-webhook-secret>"
timestamp = "<request-timestamp>"
decoded_body = base64.b64encode(request.body).decode('utf-8')
to_sign = f"{decoded_body}{timestamp}".encode('utf-8')
signature = hmac.new(
secret.encode(),
to_sign,
digestmod=hashlib.sha256,
)
calculated_signature = signature.hexdigest()
const crypto = require('crypto');
const secret = '<your-organization-webhook-secret>';
const timestamp = '<request-timestamp>';
const decoded_body = Buffer.from(request.body).toString('base64');
const to_sign = Buffer.from(decoded_body + timestamp, 'utf-8');
const signature = crypto.createHmac('sha256', secret)
.update(to_sign)
.digest('hex');
const calculated_signature = signature;
Signature v2
Step 1: Extract the timestamp from the header
You will need to extract the value of the time by getting the value of t
from the header will be 1658594891
in our sample
time = "1658594891"
Step 2: Extract the signature from the header
You will need to extract the value of the signature by getting the value of v1 from the header will be 3b6ed5f3ecadcb1c60d9e7684ac9a80581a55d805d678a074c6fc2fbe5378878
in our sample
signature = "3b6ed5f3ecadcb1c60d9e7684ac9a80581a55d805d678a074c6fc2fbe5378878"
Step 3: Convert the json payload to dictionary and sort it
You will need to convert the json payload to dictionary, and sort this dictionary using lexicographical sort
, you should make sure the nested json values are sorted as well after converting it to a dictionary.
After converting the payload to dictionary and sorting it, you should convert the dictionary back to json object and save this json object to use it in next steps.
sorted_payload = json.dumps(
payload_as_dict, cls=DjangoJSONEncoder, separators=(",", ":"), sort_keys=True
)
function sortJson(jsonObj) {
if (typeof jsonObj !== "object" || jsonObj === null) {
return jsonObj; // Return non-objects as is
}
if (Array.isArray(jsonObj)) {
return jsonObj.map((item) => sortJson(item)); // If it's an array, sort its elements recursively
}
const sortedKeys = Object.keys(jsonObj).sort(); // Sort the keys
const sortedObject = {};
for (const key of sortedKeys) {
sortedObject[key] = sortJson(jsonObj[key]); // Recursively sort nested objects
}
return sortedObject;
}
const sorted_payload = sortJson(payload)
Example:
{
"type": "transaction.purchase.successful",
"status_id": "8xgNYLk",
"intent": {
"id": "ldLWMZD",
"created": "2022-12-25 15:45:25.659438+00:00",
"custom_fields": null,
"custom_form_answers": null,
"amount": {
"value": 50,
"currency": "USD"
}
},
"account": {
"id": "YVglAZx"
},
"transaction": {
"type": "payment",
"id": "a1f90648-aa49-4798-b7cb-07a866009522",
"created": "2022-12-25 15:45:31.615820+00:00",
"status": "purchase.successful",
"billing_data": null,
"external_action_message": [],
"operations": [
{
"id": "K1ZvzZB",
"type": "purchase",
"status": "successful",
"amount": {
"value": 50,
"currency": "USD"
},
"statuses": [
{
"id": "8xgNYLk",
"value": "successful",
"created": "2022-12-25 15:45:31.664841+00:00"
}
]
}
],
"custom_message": "",
"method": {
"id": "k9m63Lb",
"display_name": "Kashier - Card",
"service_provider": {
"id": "ldLWMZD"
}
}
},
"api_version": "1.1"
}
{
"account": {
"id": "YVglAZx"
},
"api_version": "1.1",
"intent": {
"amount": {
"currency": "USD",
"value": 50
},
"created": "2022-12-25 15:45:25.659438+00:00",
"custom_fields": null,
"custom_form_answers": null,
"id": "ldLWMZD"
},
"status_id": "8xgNYLk",
"transaction": {
"billing_data": null,
"created": "2022-12-25 15:45:31.615820+00:00",
"custom_message": "",
"external_action_message": [],
"id": "a1f90648-aa49-4798-b7cb-07a866009522",
"method": {
"display_name": "Kashier - Card",
"id": "k9m63Lb",
"service_provider": {
"id": "ldLWMZD"
}
},
"operations": [
{
"amount": {
"currency": "USD",
"value": 50
},
"id": "K1ZvzZB",
"status": "successful",
"statuses": [
{
"created": "2022-12-25 15:45:31.664841+00:00",
"id": "8xgNYLk",
"value": "successful"
}
],
"type": "purchase"
}
],
"status": "purchase.successful",
"type": "payment"
},
"type": "transaction.purchase.successful"
}
Step 4: Remove all spaces and new lines from the payload
You will need to remove all spaces and new lines sent on the event payload, you can achieve that by replacing all spaces and new lines with empty string
payload_without_spaces_or_lines = sorted_payload.replace(" ", "").replace("\n", "")
const payload_without_spaces_or_lines = sorted_payload.replace(/\s+/g, '');
Step 5: Create the hash message to validate
You will need to create the message that will be used to validate the signature by concatenating both the time and the payload
concatenated_msg = f"{payload_without_spaces_or_lines}{time}".encode("utf-8")
const concatenated_msg = Buffer.from(payload_without_spaces_or_lines + time, 'utf-8');
const hmac = crypto.createHmac('sha256', organization_key);
hmac.update(concatenated_msg);
Step 6: Get your organization signature key
You will need to get your organization secret key to pass it to HMAC function to be used as your secret key, to get this key you can set GET request to /api/v1/organizations/get-webhook-signature-key/
and pass your account API key as header
curl --location --request GET 'https://web.moneyhash.io/api/v1/organizations/get-webhook-signature-key/' \
--header 'X-Api-Key: YOUR_ACCOUNT_API_KEY'
{
"status": {
"code": 200,
"message": "success",
"errors": []
},
"data": {
"webhook_signature_secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"count": 1,
"next": null,
"previous": null
}
Step 7: Calculate the signature
Pass the output of the concatenated_msg
and your organization api key key to the HMAC
function to generate the signature and the mode should be sha256
digest = hmac.new(
key=organization_key.encode("utf-8"),
msg=concatenated_msg,
digestmod=hashlib.sha256,
)
const crypto = require('crypto');
const organization_key = '<YOUR_ORGANIZATION_KEY>';
const concatenated_msg = '<YOU_CONCATENATED_MESSAGE';
const hmac = crypto.createHmac('sha256', organization_key);
hmac.update(concatenated_msg);
const digest = hmac.digest('hex');
Step 8: Compare the signatures
Compare the signature in the header to the expected signature, if they are equal then the event is valid and received from MoneyHash
is_valid = digest.hexdigest() == signature
Signature v1
Step 1: Extract the timestamp from the header
You will need to extract the value of the time by getting the value of t
from the header will be 1658594891
in our sample
time = "1658594891"
Step 2: Extract the signature from the header
You will need to extract the value of the signature by getting the value of v1 from the header will be 3b6ed5f3ecadcb1c60d9e7684ac9a80581a55d805d678a074c6fc2fbe5378878
in our sample
signature = "3b6ed5f3ecadcb1c60d9e7684ac9a80581a55d805d678a074c6fc2fbe5378878"
Step 3: Remove all spaces and new lines from the payload
You will need to remove all spaces and new lines sent on the event payload, you can achieve that by replacing all spaces and new lines with empty string
payload_without_spaces_or_lines = payload.replace(" ", "").replace("\n", "")
const payload_without_spaces_or_lines = sorted_payload.replace(/\s+/g, '');
Step 4: Create the hash message to validate
You will need to create the message that will be used to validate the signature by concatenating both the time and the payload
concatenated_msg = f"{payload_without_spaces_or_lines}{time}".encode("utf-8")
const concatenated_msg = Buffer.from(payload_without_spaces_or_lines + time, 'utf-8');
const hmac = crypto.createHmac('sha256', organization_key);
hmac.update(concatenated_msg);
Step 5: Calculate the signature
Pass the output of the concatenatedmsg and your Account API key_ to the HMAC
function to generate the signature and the mode should be sha256
digest = hmac.new(
key=account_api_key.encode("utf-8"),
msg=concatenated_msg,
digestmod=hashlib.sha256,
)
const calculatedSignature = hmac.digest('hex');
Step 6: Compare the signatures
Compare the signature in the header to the expected signature, if they are equal then the event is valid and received from MoneyHash
is_valid = digest.hexdigest() == signature
const is_valid = calculatedSignature === signature;
Examples
import json
import hashlib
import hmac
import requests
# Step 1: Extract the timestamp from the header
time = "1697640557"
# Step 2: Extract the signature from the header
signature_v2 = "f02a4404b41aae5535eb8622b02deeda6823893c8582367d22b1688dc44d043d"
# Step 3: Convert the JSON payload to a dictionary and sort it
payload = '{"type":"intent.processed","intent_type":"PAYMENT","data":{"intent_id":"LWWAyzL","intent":{"id":"LWWAyzL","status":"PROCESSED","amount":50,"amount_currency":"USD","type":"Payin","account":"YVglAZx","custom_fields":null,"billing_data":{"first_name":"","last_name":"","email":"","phone_number":""},"active_transaction":{"id":"ebcdadbc-44c4-4814-9c7e-1e62a064bb67","custom_fields":null,"created":"2023-10-18T14:49:12.476917Z","status":"SUCCESSFUL","amount":"50.00","amount_currency":"USD","billing_data":null,"external_action_message":[],"payment_method":"CARD","payment_method_name":"Card","custom_form_answers":null,"custom_message":"","account":"YVglAZx","provider_transaction_fields":{},"provider_signature_match":false},"transactions_history":[{"id":"ebcdadbc-44c4-4814-9c7e-1e62a064bb67","custom_fields":null,"created":"2023-10-18T14:49:12.476917Z","status":"SUCCESSFUL","amount":"50.00","amount_currency":"USD","billing_data":null,"external_action_message":[],"payment_method":"CARD","payment_method_name":"Card","custom_form_answers":null,"custom_message":"","account":"YVglAZx","provider_transaction_fields":{},"provider_signature_match":false}],"method":{"display_name":"Stripe - Card","id":"jgxny9r"},"flow":null,"is_live":false,"created":"2023-10-18T14:49:03.132515Z"}},"api_version":"1.1"}'
payload = json.loads(payload)
# Sort the dictionary
sorted_payload = json.dumps(payload, separators=(",", ":"), sort_keys=True)
# Step 4: Remove all spaces and new lines from the payload
payload_without_spaces_or_lines = sorted_payload.replace(" ", "").replace("\n", "")
# Step 5: Create the hash message to validate
concatenated_msg = f"{payload_without_spaces_or_lines}{time}".encode("utf-8")
# Step 6: Get the organization secret key via API (replace with your actual API key)
api_key = "<YOUR_ACCOUNT_API_KEY>"
key_response = requests.get(
"https://web.moneyhash.io/api/v1/organizations/get-webhook-signature-key/",
headers={"X-Api-Key": api_key},
)
if key_response.status_code == 200:
organization_key = key_response.json()["data"]["webhook_signature_secret"]
else:
print("Failed to retrieve the organization secret key.")
exit()
# Step 7: Calculate the signature
digest = hmac.new(
key=organization_key.encode("utf-8"),
msg=concatenated_msg,
digestmod=hashlib.sha256,
)
# Step 8: Compare the signatures
is_valid = digest.hexdigest() == signature_v2
if is_valid:
print("Signature is valid. The event is received from MoneyHash.")
else:
print("Signature is not valid. The event may be tampered with or invalid.")
import crypto from "crypto";
import fetch from "node-fetch";
function sortJson(jsonObj) {
if (typeof jsonObj !== "object" || jsonObj === null) {
return jsonObj; // Return non-objects as is
}
if (Array.isArray(jsonObj)) {
return jsonObj.map((item) => sortJson(item)); // If it's an array, sort its elements recursively
}
const sortedKeys = Object.keys(jsonObj).sort(); // Sort the keys
const sortedObject = {};
for (const key of sortedKeys) {
sortedObject[key] = sortJson(jsonObj[key]); // Recursively sort nested objects
}
return sortedObject;
}
// Step 1: Extract the timestamp from the header
const time = "1697640557";
// Step 2: Extract the signature from the header
const signature_v2 =
"f02a4404b41aae5535eb8622b02deeda6823893c8582367d22b1688dc44d043d";
// Step 3: Convert the JSON payload to an object and sort it
const payload = JSON.parse(
'{"type":"intent.processed","intent_type":"PAYMENT","data":{"intent_id":"LWWAyzL","intent":{"id":"LWWAyzL","status":"PROCESSED","amount":50,"amount_currency":"USD","type":"Payin","account":"YVglAZx","custom_fields":null,"billing_data":{"first_name":"","last_name":"","email":"","phone_number":""},"active_transaction":{"id":"ebcdadbc-44c4-4814-9c7e-1e62a064bb67","custom_fields":null,"created":"2023-10-18T14:49:12.476917Z","status":"SUCCESSFUL","amount":"50.00","amount_currency":"USD","billing_data":null,"external_action_message":[],"payment_method":"CARD","payment_method_name":"Card","custom_form_answers":null,"custom_message":"","account":"YVglAZx","provider_transaction_fields":{},"provider_signature_match":false},"transactions_history":[{"id":"ebcdadbc-44c4-4814-9c7e-1e62a064bb67","custom_fields":null,"created":"2023-10-18T14:49:12.476917Z","status":"SUCCESSFUL","amount":"50.00","amount_currency":"USD","billing_data":null,"external_action_message":[],"payment_method":"CARD","payment_method_name":"Card","custom_form_answers":null,"custom_message":"","account":"YVglAZx","provider_transaction_fields":{},"provider_signature_match":false}],"method":{"display_name":"Stripe - Card","id":"jgxny9r"},"flow":null,"is_live":false,"created":"2023-10-18T14:49:03.132515Z"}},"api_version":"1.1"}'
);
const sorted_payload = sortJson(payload);
// Step 4: Remove all spaces and new lines from the payload
const payload_without_spaces_or_lines = JSON.stringify(sorted_payload).replace(
/\s+/g,
""
);
// Step 5: Create the hash message to validate
const concatenated_msg = Buffer.from(
payload_without_spaces_or_lines + time,
"utf-8"
);
// Step 6: Get the organization secret key via API (replace with your actual API key)
const api_key = "<YOUR_ACCOUNT_API_KEY>";
fetch(
"https://web.moneyhash.io/api/v1/organizations/get-webhook-signature-key/",
{
method: "GET",
headers: { "X-Api-Key": api_key },
}
)
.then((key_response) => {
if (key_response.status === 200) {
return key_response.json();
} else {
throw new Error("Failed to retrieve the organization secret key.");
}
})
.then((keyData) => {
const organization_key = keyData.data.webhook_signature_secret;
// Step 7: Calculate the signature
const digest = crypto
.createHmac("sha256", organization_key)
.update(concatenated_msg.toString())
.digest("hex");
// Step 8: Compare the signatures
const is_valid = digest === signature_v2;
if (is_valid) {
console.log("Signature is valid. The event is received from MoneyHash.");
} else {
console.log(
"Signature is not valid. The event may be tampered with or invalid."
);
}
})
.catch((error) => {
console.error(error);
});
Updated 9 months ago