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 v2once 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).

  1. Extract the timestamp from the header: Start by extracting the value of the time t from the MoneyHash-Signature header. Using the sample presented above, we would get the value of t as:
time = "1671980526"
  1. 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 of v2 as:
signature = "beaff64c5af40948fb1473f1e8909dc8320ae3e21402fefcbbea9d399c793268"
  1. 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.

  1. 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"
}
  1. 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, '');
  1. 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);
  1. Calculate the signature: With a GET request to /api/v1/organizations/get-webhook-signature-key/ passing your Account API Key as the X-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
}
  1. Calculate the signature: Pass the concatenated_msg created in Step 6, along with the organization 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 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');
  1. 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).

  1. Extract the timestamp from the header: Start by extracting the value of the time t from the MoneyHash-Signature header. Using the sample presented above, we would get the value of t as:
time = "1671980526"
  1. 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 of v1 as:
signature = "6e7fba44f785115c1fd8be18c924b2337d6a83a889f2fdf290d64f81becc6761"
  1. 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, '');
  1. 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);
  1. Calculate the signature: Pass the concatenated_msg created in Step 5, along with your Account 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 be sha256.
digest = hmac.new(
    key=account_api_key.encode("utf-8"),
    msg=concatenated_msg,
    digestmod=hashlib.sha256,
)
const calculatedSignature = hmac.digest('hex');

  1. 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;