Webhook Signature Verification
Learn how to configure verification for your webhook listener endpoint
Webhooks are unauthenticated HTTP callbacks by default. When Mobile Text Alerts sends an event to your listener endpoint this event can be impersonated with an identically structured POST request. This can result in fabricated delivery receipts, injecting fake opt-ins, or triggering unintended business logic. With signature verification your server can distinguish a genuine request from Mobile Text Alerts from a spoofed one.
The OWASP (Open Web Application Security Project) publishes the API Security Top 10 — a widely referenced list of the most critical security risks for APIs. Webhook signature verification maps directly to the following categories:
API8:2023 — Security Misconfiguration
Exposing an unauthenticated callback endpoint is a misconfiguration. Verifying the
X-Signatureheader on every inbound request closes this gap.
API2:2023 — Broken Authentication
Accepting webhook payloads without authenticating their origin is a broken authentication pattern. HMAC-SHA256 verification gives your endpoint a cryptographically sound mechanism for confirming request sender.
How Mobile Text Alerts signs webhook requests
When registering a webhook, you are required to provide a secret value to Mobile Text Alerts. This is typically a 128-character hexadecimal string. Mobile Text Alerts stores this secret value and uses it to sign every subsequent request it delivers to your endpoint to be used for validation.
For each webhook event trigger, Mobile Text Alerts will:
Serialize the request body to raw bytes.
Computes HMAC-SHA256(secret, raw_body).
Encodes the digest as a lowercase hex string.
Sets that string as the value of the
X-SignatureHTTP request header in requests made to your webhook listener endpoint.
Example: How a signature is created by Mobile Text Alerts
signature = HMAC-SHA256(
key = secret, // the secret you provided at registration
message = raw_body_bytes // the exact bytes MTA will send as the POST body
)
hex_signature = lowercase_hex(signature) The result is placed in the X-Signature header. Mobile Text Alerts does not include a timestamp or nonce in the signed value.
How to verify a webhook request to your listener endpoint
Extract the signature header from the incoming request.
Read the raw request body (must be raw bytes, not parsed JSON).
Compute the expected signature using your stored secret.
Compare the signatures using a constant-time comparison function.
Return the correct HTTP Response:
Signature match →
200OK (processed)Signature absent / Signature mismatch →
401Unauthorized (do not process)Payload schema error →
400Bad RequestInternal server error →
500
Code Verification Examples
cURL Incoming Request Example - Local development testing
cURL Incoming Request Example - Local development testingA cURL test example can be useful for local development and CI testing to send test requests without needing to trigger a real Mobile Text Alerts event.
Compute the signature locally using
openssl dgst -sha256 -hmac "$SECRET"— the same HMAC-SHA256 operation Mobile Text Alerts performs on its end before dispatching a real webhook. (Theawk '{print $2}'strips the(stdin)=prefix thatopenssl dgstprepends, leaving just the lowercase hex digest.)Send it as a signed HTTP request to your endpoint with the resulting hex digest in the
X-Signatureheader.
Example request with valid signature:
Note The -d flag sends the body exactly as the string is defined, so what gets signed and what gets sent are byte-for-byte identical.
Example: Verification of incoming requests
You will then verify the incoming request using a constant-time comparison function. It should accept a correctly signed request and reject a tampered one.
Note: Use express.raw() (or bodyParser.raw()) so that req.body is a Buffer of the original bytes. Never pass req.body through JSON.parse before computing the HMAC.
Note: Access request.get_data() before Flask's JSON parsing to obtain the raw bytes. Use hmac.compare_digest for timing-safe comparison.
Troubleshooting
If you are having issues with the signature matching the expected result here are some common causes:
Signing the parsed body instead of raw bytes- If you automatically parse the incoming JSON before verification and you then re-serialise it to compute the HMAC, you may get a different byte sequence. Always read the raw request bytes before any parsing touches the body.
Trailing newline- This can be an issue when testing with
cURLor shell scripts.echo "$BODY"appends a\nto the string before piping it toopenssl, so the bytes being signed differ from the bytes being sent. Useprintf '%s'to avoid this.Character encoding mismatch- The HMAC must be computed over the UTF-8 byte representation of the body. If your runtime interprets the body as Latin-1 or re-encodes any characters, multi-byte characters (accented letters, emoji, non-ASCII phone number formats) will produce different bytes and a different digest.
Whitespace or formatting differences- Some HTTP middleware (reverse proxies, API gateways, logging) can normalize JSON in transit by pretty-printing it, stripping whitespace, or reordering keys. Any transformation between what Mobile Text Alerts sends and what you receive will break the signature.
Case mismatch- Mobile Text Alerts produces a lowercase hex string. If your code upper-cases either the received or computed value before comparing, they won't match. Keep both sides lowercase as a safe default.
Webhook Security Best Practices
Always verify request signatures for production environments.
Webhook endpoint is served over HTTPS only — reject HTTP.
Your
secretshould be stored in an environment variable or secrets manager. Not in source code, logged or included in error messages.Raw request bytes are signed, not the parsed JSON object.
Signature comparison uses a constant-time function (
timingSafeEqual,compare_digest,hash_equals, etc.).A missing or malformed
X-Signatureheader should return HTTP401immediately.
Last updated
Was this helpful?