Webhooks Signature
On this page, you will learn how MoneyHash's Webhooks Signatures work and how to use it to ensure the updates you receive from Webhooks are reliable.
With every Webhook event sent by MoneyHash, a header named MoneyHash-Signature
can be found with its content encrypted. 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.
MoneyHash's Signature Header
The MoneyHash-Signature
header in each webhook event is case-sensitive and contains a timestamp and the current version. The timestamp is prefixed by t=
and the version is prefixed by v=
:
t
: the timestamp this request was created.v
: the current version of the signature you will validate
Below, you find a signature example. Note that there are two signature versions, v1
and v2
. MoneyHash recommends you use the v2
once the v1
option will be deprecated soon.
MoneyHash-Signature t=1671980526,v1=6e7fba44f785115c1fd8be18c924b2337d6a83a889f2fdf290d64f81becc6761,v2=beaff64c5af40948fb1473f1e8909dc8320ae3e21402fefcbbea9d399c793268
Versions
Currently, we are transitioning from using version v1
to v2
to facilitate future
changes to the signature calculation. Your version should remain the same for backward compatibility. However,
if you are starting to use the Webhooks signature use the v2
.
Verifying signatures
Let's break down how to verify the MoneyHash's signature. Currently, there are two versions, so we will show you how to use both.
Recommended
Remember, using the latest version is always recommended, as the v1
will be deprecated soon.
Signature v2
Below are the steps needed to be completed to validate the signature using the version 2 (v2
).
- Extract the timestamp from the header: Start by extracting the value of the time
t
from theMoneyHash-Signature
header. Using the sample presented above, we would get the value oft
as:
time = "1671980526"
- Extract the signature from the header: Now, you need to extract the value of the signature of the correct version you are working on, in this case
v2
from the header. Using the sample presented above, we would get the value ofv2
as:
signature = "beaff64c5af40948fb1473f1e8909dc8320ae3e21402fefcbbea9d399c793268"
- Convert the JSON payload to a dictionary and sort it: At this point, with the JSON payload in hand, you need to convert it to a dictionary and sort it using
lexicographical sort
.
Check if it is sorted
Make sure the nested JSON values are sorted after the convertion into dictionary.
- Convert the sorted dictionary back to JSON: After ensuring all is correctly sorted, you need to convert the newly sorted dictionary back into a JSON. Below, you find examples using Python and NodeJs. Remember to save this new JSON, as you will need in the next steps. Next, we provide an example of a JSON before and after sorting.
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)
{
"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"
}
- Remove all spaces and new lines from the payload: You need to remove unnecessary spaces and lines from the payload. You can replace all spaces and new lines with empty strings, as shown below:
payload_without_spaces_or_lines = sorted_payload.replace(" ", "").replace("\n", "")
const payload_without_spaces_or_lines = sorted_payload.replace(/\s+/g, '');
- Create the hash message to validate: To create the message needed to validate the signature, you need to concatenate both the time recovered in Step 1 and the payload resulting from Step 5. Below are examples of how to do it using Python and NodeJS.
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);
- Calculate the signature: With a GET request to
/api/v1/organizations/get-webhook-signature-key/
passing your Account API Key as theX-Api-Key
header, you can get the organization secret key needed as a parameter of the HMAC function.
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
}
- Calculate the signature: Pass the
concatenated_msg
created in Step 6, along with theorganization secret key
recovered in Step 7 to the HMAC function to generate the signature to use next. The digest mode passed as a parameter to the HMAC function will always besha256
.
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');
- Compare the signatures: With your newly created signature in hand, you need to compare it with the
MoneyHash-Signature
header from the Webhook header. If they are equal (is_valid = true
), this means the event is valid and was sent by MoneyHash.
is_valid = digest.hexdigest() == signature
The following recipe contains the complete signature verification code. Click on it to open the recipe visualization and inspect the entire code, with step highlights and descriptions.
Signature v1
Below are the steps needed to be completed to validate the signature using the version 1 (v1
).
- Extract the timestamp from the header: Start by extracting the value of the time
t
from theMoneyHash-Signature
header. Using the sample presented above, we would get the value oft
as:
time = "1671980526"
- Extract the signature from the header: Now, you need to extract the value of the signature of the correct version you are working on, in this case
v1
from the header. Using the sample presented above, we would get the value ofv1
as:
signature = "6e7fba44f785115c1fd8be18c924b2337d6a83a889f2fdf290d64f81becc6761"
- Remove all spaces and new lines from the payload: You need to remove unnecessary spaces and lines from the payload. You can replace all spaces and new lines with empty strings, as shown below:
payload_without_spaces_or_lines = payload.replace(" ", "").replace("\n", "")
const payload_without_spaces_or_lines = sorted_payload.replace(/\s+/g, '');
- Create the hash message to validate: To create the message needed to validate the signature, you need to concatenate both the time recovered in Step 1 and the payload resulting from Step 3. Below, you find examples using Python and NodeJS.
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);
- Calculate the signature: Pass the
concatenated_msg
created in Step 5, along with yourAccount API Key
to the HMAC function to generate the signature to use next. The digest mode passed as a parameter to the HMAC function will always besha256
.
digest = hmac.new(
key=account_api_key.encode("utf-8"),
msg=concatenated_msg,
digestmod=hashlib.sha256,
)
const calculatedSignature = hmac.digest('hex');
- Compare the signatures: With your newly created signature in hand, you need to compare it with the
MoneyHash-Signature
header. If they are equal (is_valid = true
), this means the event is valid and was sent by MoneyHash.
is_valid = digest.hexdigest() == signature
const is_valid = calculatedSignature === signature;
Updated 8 days ago