# Webhook Signature Verification

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](https://owasp.org/) (Open Web Application Security Project) publishes the [API Security Top 10](https://owasp.org/API-Security/) — 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-Signature` header on every inbound request closes this gap.
* **API2:2023 — Broken Authentication**&#x20;
  * Accepting webhook payloads without authenticating their origin is a broken authentication pattern. [HMAC-SHA256](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.hmacsha256) verification gives your endpoint a cryptographically sound mechanism for confirming request sender.&#x20;

## 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:

1. Serialize the request body to raw bytes.
2. Computes [HMAC-SHA256](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.hmacsha256)(secret, raw\_body).
3. Encodes the digest as a lowercase hex string.
4. Sets that string as the value of the `X-Signature` HTTP 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

1. Extract the signature header from the incoming request.
2. Read the raw request body (must be raw bytes, not parsed JSON).
3. Compute the expected signature using your stored secret.
4. Compare the signatures using a constant-time comparison function.
5. Return the correct **HTTP Response**:
   * Signature match → <mark style="color:$success;">`200`</mark> OK (processed)
   * Signature absent / Signature mismatch  → <mark style="color:red;">`401`</mark> Unauthorized (do not process)
   * Payload schema error → <mark style="color:red;">`400`</mark> Bad Request&#x20;
   * Internal server error   → <mark style="color:$warning;">`500`</mark> &#x20;

### Code Verification Examples

#### `cURL` Incoming Request Example - Local development testing

A `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.

1. **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. (*The `awk '{print $2}'` strips the `(stdin)=` prefix that `openssl dgst` prepends, leaving just the lowercase hex digest.)*
2. **Send it as a signed HTTP request** to your endpoint with the resulting hex digest in the `X-Signature` header.

*Example request with valid signature:*

```bash
# Replace <SECRET> and destination URL with real values.
# This generates a valid signature and sends it to your endpoint.
 
SECRET="your_128_char_hex_secret"
BODY='{"event":"delivery-status","data":{"messageId":"test-123","status":"delivered"}}'
 
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
 
curl -X POST https://your-server.example.com/webhooks/mta \
     -H 'Content-Type: application/json' \
     -H "X-Signature: $SIG" \
     -d "$BODY"
```

*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.*&#x20;

#### Example: Verification of incoming requests&#x20;

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.

{% tabs %}
{% tab title="Node.js (Express)" %}
***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.*

```javascript
const express = require('express');
const crypto  = require('crypto');

const app = express();
const SECRET = process.env.MTA_WEBHOOK_SECRET; // store in env, never in source

// IMPORTANT: use raw body middleware on this route
app.post('/webhooks/mta',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const receivedSig = req.headers['x-signature'];
    if (!receivedSig) return res.status(401).send('Missing signature');

    const expectedSig = crypto
      .createHmac('sha256', SECRET)
      .update(req.body) // req.body is a buffer 
      .digest('hex');
      
  const sigBuf      = Buffer.from(receivedSig,  'hex');
  const expectedBuf = Buffer.from(expectedSig,  'hex');
  
  if (sigBuf.length !== expectedBuf.length ||
        !crypto.timingSafeEqual(sigBuf, expectedBuf)) {
      return res.status(401).send('Invalid signature');
      }
      
  const payload = JSON.parse(req.body.toString('utf8'));   
  // handle payload
    res.status(200).send('OK');
  }
);
```

{% endtab %}

{% tab title="Python (Flask)" %}
***Note**: Access request.get\_data() before Flask's JSON parsing to obtain the raw bytes. Use hmac.compare\_digest for timing-safe comparison.*

```python
import hmac, hashlib, os
from flask import Flask, request, abort
 
app    = Flask(__name__)
SECRET = os.environ['MTA_WEBHOOK_SECRET'].encode()  # bytes
 
@app.route('/webhooks/mta', methods=['POST'])
def mta_webhook():
    received_sig = request.headers.get('X-Signature', '')
    if not received_sig:
        abort(401)
 
    raw_body = request.get_data()  # bytes, before any parsing
 
    expected_sig = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
 
    # constant-time comparison
    if not hmac.compare_digest(expected_sig, received_sig):
        abort(401)
 
    payload = request.get_json()   # safe to parse now
    # handle payload ...
    return '', 200
```

{% endtab %}
{% endtabs %}

### 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 `cURL` or shell scripts. `echo "$BODY"` appends a `\n` to the string before piping it to `openssl`, so the bytes being signed differ from the bytes being sent. Use `printf '%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.&#x20;
* **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 `secret` should 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-Signature` header should return HTTP `401` immediately.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://developers.mobile-text-alerts.com/tutorials/webhooks/webhook-signature-verification.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
