mMoney Docs

Webhooks

Verify signatures

Each webhook delivery carries an MMoney-Signature header:

MMoney-Signature: t=1747000000,v1=<hex-hmac-sha256>

The v1 value is an HMAC-SHA256 over the string "<t>.<raw_body>" with your webhook signing secret. Verify it on every delivery:

  1. Compute the HMAC over the raw request body. Do not re-encode the JSON first.
  2. Compare with a constant-time function.
  3. Reject deliveries where t is more than 5 minutes old. This stops replays.

Node.js

import crypto from "crypto";

function verifySignature(rawBody, headerValue, secret) {
  const parts = Object.fromEntries(
    headerValue.split(",").map(s => s.split("="))
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1)
  );
}

Python

import hashlib
import hmac
import time

def verify_signature(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(
        secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, parts["v1"]):
        return False
    return abs(time.time() - int(parts["t"])) <= 300

PHP

function verify_signature($raw_body, $header, $secret) {
    parse_str(str_replace(',', '&', $header), $parts);
    $expected = hash_hmac('sha256', $parts['t'] . '.' . $raw_body, $secret);
    return hash_equals($expected, $parts['v1']);
}

Elixir

def verify(raw_body, header, secret) do
  parts =
    header
    |> String.split(",")
    |> Map.new(fn p -> List.to_tuple(String.split(p, "=")) end)

  expected =
    :crypto.mac(:hmac, :sha256, secret, "#{parts["t"]}.#{raw_body}")
    |> Base.encode16(case: :lower)

  Plug.Crypto.secure_compare(expected, parts["v1"])
end

On a mismatch

Return 401. mMoney retries up to 5 times with exponential backoff.

Secret rotation

Rotation takes effect immediately. There is no overlap window. Deploy the new secret to all backend instances before you click rotate in the dashboard.

Log in to send a test event to your webhook endpoints.