Webhook security

Every request Mobile Sasa POSTs at your servers carries headers that prove it came from us. Verify them, and nobody who merely discovers your callback URL can feed your system fake traffic.

This applies to every callback we send: inbound SMS forwards on keywords and shortcodes, USSD session steps, and delivery reports. Your team's signing secret is generated automatically and lives in the portal under Settings → Webhooks, where an admin can reveal, copy or rotate it.

The headers

Parameters

FieldTypeRequiredDescription
X-MobileSasa-SecretstringNoYour team's webhook secret, verbatim. Compare it for equality — the simplest check, sufficient for most integrations over HTTPS.
X-MobileSasa-SignaturestringNoFormat t=<unix seconds>,v1=<hex> where the hex is HMAC-SHA256 of <t>.<raw request body> keyed with your secret. The secret never travels, and the timestamp lets you reject replays.
Example request headers
POST /your/callback HTTP/1.1
Content-Type: application/json
X-MobileSasa-Secret: whsec_1f4c...9ab0
X-MobileSasa-Signature: t=1756400000,v1=5257a869e7...bd42

Verifying the signature

Read the raw request body exactly as received (before any JSON or form parsing), rebuild <t>.<body>, HMAC it with your secret, and compare against v1 with a constant-time comparison. Reject requests whose timestamp is older than a few minutes if you want replay protection.

const crypto = require("crypto");

function verify(req, rawBody, secret) {
  const header = req.headers["x-mobilesasa-signature"] || "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (!parts.t || !parts.v1) return false;

  // Optional replay window: reject anything older than 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(parts.t + "." + rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Which body do we sign?

Whatever we send: JSON for inbound SMS forwards and delivery reports, form-encoded (application/x-www-form-urlencoded) for USSD steps. Always sign-check the raw bytes you received, not a re-encoded version — re-encoding can reorder fields and change the bytes.

Good practice

  • Use an HTTPS callback URL. Over plain HTTP the headers travel unencrypted.
  • Start with the equality check on X-MobileSasa-Secret; add the HMAC check when you can.
  • After rotating the secret in the portal, update your servers immediately — new callbacks sign with the new secret within a few minutes.
  • The Send test buttons on the keyword and USSD pages sign their test requests exactly like live traffic, so you can verify your implementation end to end.

Related