Verify MyFatoorah Webhook Signatures

MyFatoorah v2 webhooks don't sign the raw body — they build a canonical Key=Value,… string from specific Data fields in a fixed per-event order, HMAC-SHA256 it with your webhook secret key, and send the base64 result in the MyFatoorah-Signature header. Construct that string (see the steps below), then paste it with your secret and the header value.

MyFatoorah v2 signs a comma-joined Key=Value string built from specific Data fields in a fixed order per event — not the raw body. Build that string (see below) and paste it here.

Paste the MyFatoorah-Signature value above to compare

Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the webhook signature verifier hub or the generic HMAC generator.

How MyFatoorah signs webhooks

  1. Take the fields from the webhook's Data object in the exact order documented for that event. For a payment status change: Invoice.Id, Invoice.Status, Transaction.Status, Transaction.PaymentId, Invoice.ExternalIdentifier.
  2. Join them as Key=Value pairs separated by commas, replacing any null value with an empty string. This is the signed string (the order is fixed per event — it is not alphabetical).
  3. Compute HMAC-SHA256 of that string using your webhook secret key (UTF-8) as the key.
  4. Base64-encode the digest and constant-time compare it to the MyFatoorah-Signature header.

Verify MyFatoorah signatures in code

Node.js
const crypto = require('crypto');

// Fixed field order per MyFatoorah v2 event (dot-paths into payload.Data).
const FIELDS = {
  PAYMENT_STATUS_CHANGED: ['Invoice.Id','Invoice.Status','Transaction.Status','Transaction.PaymentId','Invoice.ExternalIdentifier'],
  REFUND_STATUS_CHANGED:  ['Refund.Id','Refund.Status','Amount.ValueInBaseCurrency','ReferencedInvoice.Id'],
};

const get = (o, path) => {
  const v = path.split('.').reduce((x, k) => (x == null ? undefined : x[k]), o);
  return v == null ? '' : String(v);          // null -> empty string
};

const event = JSON.parse(rawBody);
const signed = FIELDS[event.Event.Name]
  .map(f => `${f}=${get(event.Data, f)}`).join(',');

const expected = crypto
  .createHmac('sha256', process.env.MYFATOORAH_WEBHOOK_SECRET)
  .update(signed).digest('base64');

const valid = crypto.timingSafeEqual(
  Buffer.from(expected), Buffer.from(req.headers['myfatoorah-signature']));
Python
import hmac, hashlib, base64, json

FIELDS = {
    "PAYMENT_STATUS_CHANGED": ["Invoice.Id","Invoice.Status","Transaction.Status","Transaction.PaymentId","Invoice.ExternalIdentifier"],
    "REFUND_STATUS_CHANGED":  ["Refund.Id","Refund.Status","Amount.ValueInBaseCurrency","ReferencedInvoice.Id"],
}

def get(data, path):
    cur = data
    for k in path.split("."):
        cur = cur.get(k) if isinstance(cur, dict) else None
    return "" if cur is None else str(cur)      # null -> empty string

event = json.loads(raw_body)
signed = ",".join(f"{f}={get(event['Data'], f)}"
                  for f in FIELDS[event["Event"]["Name"]])

expected = base64.b64encode(hmac.new(
    secret.encode(), signed.encode(), hashlib.sha256).digest()).decode()

valid = hmac.compare_digest(expected, request.headers["MyFatoorah-Signature"])

Frequently asked questions

What does MyFatoorah actually sign?

Not the raw JSON body. It signs a comma-separated Key=Value string built from a fixed set of Data fields, in the exact order documented for that event type, with any null value replaced by an empty string.

Which fields go into the signed string?

It depends on the event. For PAYMENT_STATUS_CHANGED it is Invoice.Id, Invoice.Status, Transaction.Status, Transaction.PaymentId, Invoice.ExternalIdentifier. Each event's data-model page lists its own order — follow it exactly, it is not alphabetical.

Where is the webhook secret key?

In the MyFatoorah portal under Integration Settings → Webhook Settings, where you also choose v1 or v2. v2 requires the secret key. It is used as a raw UTF-8 string, not decoded.

Verify other providers

Receiving MyFatoorah webhooks on a server behind a firewall or on localhost? Webhook Relay can forward them to your internal service and even verify or transform them before delivery.