Webhook Signature Verification
MeSomb signs every webhook request sent to your endpoint. This allows your application to verify that the webhook was really sent by MeSomb and that the payload was not modified in transit.
Webhook signature verification is strongly recommended for all production integrations.
How it works
When you create a webhook endpoint in MeSomb, a unique webhook signing secret is generated for that endpoint.
Example:
<span><span style="color: undefined">whsec_8d7f2c9a4b6e1...</span></span>
<span><span style="color: undefined"></span></span>
You must store this secret securely on your server. MeSomb will only show it once when the webhook endpoint is created.
For every webhook delivery, MeSomb sends signature information in HTTP headers.
Example:
<span><span style="color: undefined">X-MeSomb-Webhook-Event-Id: evt_01HX9K7Y6N4V2X8QJ4K2M9ZP3A</span></span>
<span><span style="color: undefined">X-MeSomb-Webhook-Request-Timestamp: 1778192130</span></span>
<span><span style="color: undefined">X-MeSomb-Webhook-Signature: t=1778192130,v1=6f1a0c8e8d7f2c9a4b6e1...</span></span>
<span><span style="color: undefined"></span></span>
Your server must use these headers, the raw request body, and your webhook signing secret to verify the request.
Signature headers
MeSomb includes the following headers in each webhook request.
| Header | Description |
|---|---|
| X-MeSomb-Webhook-Event-Id | Unique ID of the webhook event. Use it for deduplication. |
| X-MeSomb-Webhook-Request-Timestamp | Unix timestamp indicating when the webhook was signed. |
| X-MeSomb-Webhook-Signature | Signature header containing the timestamp and HMAC signature. |
Example:
<span><span style="color: undefined">X-MeSomb-Webhook-Event-Id: evt_01HX9K7Y6N4V2X8QJ4K2M9ZP3A</span></span>
<span><span style="color: undefined">X-MeSomb-Webhook-Request-Timestamp: 1778192130</span></span>
<span><span style="color: undefined">X-MeSomb-Webhook-Signature: t=1778192130,v1=6f1a0c8e8d7f2c9a4b6e1...</span></span>
<span><span style="color: undefined"></span></span>
Signature format
The X-MeSomb-Webhook-Signature header has the following format:
<span><span style="color: undefined">t=<timestamp>,v1=<signature></span></span>
<span><span style="color: undefined"></span></span>
Example:
<span><span style="color: undefined">t=1778192130,v1=6f1a0c8e8d7f2c9a4b6e1...</span></span>
<span><span style="color: undefined"></span></span>
Where:
| Part | Description |
|---|---|
| t | Unix timestamp used when generating the signature. |
| v1 | HMAC-SHA256 signature of the webhook payload. |
How to verify a webhook
To verify a webhook request, your server must:
- Extract the
X-MeSomb-Webhook-Signatureheader. - Extract the timestamp
t. - Extract the signature value
v1. - Get the raw request body exactly as received.
- Build the signed payload using this format:
<span><span style="color: undefined"><timestamp>.<raw_body></span></span>
<span><span style="color: undefined"></span></span>
- Compute the HMAC-SHA256 signature using your webhook signing secret.
- Compare your computed signature with the
v1signature from the header. - Reject the webhook if the signature does not match.
- Reject the webhook if the timestamp is too old.
Signed payload format
The signed payload is created by joining the timestamp, a dot, and the raw request body.
<span><span style="color: undefined">timestamp + "." + raw_body</span></span>
<span><span style="color: undefined"></span></span>
Example:
<span><span style="color: undefined">1778192130.{"id":"evt_01HX...","object":"event","type":"payment.transaction.succeeded"}</span></span>
<span><span style="color: undefined"></span></span>
Important: use the raw request body, not a parsed and re-serialized JSON object.
This is important because small changes in spaces, line breaks, or field ordering will produce a different signature.
Signature algorithm
MeSomb uses HMAC with SHA-256.
<span><span style="color: undefined">HMAC-SHA256(webhook_signing_secret, signed_payload)</span></span>
<span><span style="color: undefined"></span></span>
The result is encoded as a lowercase hexadecimal string.
Timestamp tolerance
To help protect against replay attacks, your server should reject webhook requests with an old timestamp.
Recommended tolerance:
<span><span style="color: undefined">5 minutes</span></span>
<span><span style="color: undefined"></span></span>
For example, if the timestamp in the webhook is older than 5 minutes compared to your server time, reject the request.
Your server time should be synchronized using NTP or another reliable time source.
Deduplication
MeSomb may send the same webhook event more than once.
This can happen when:
- your server does not respond with a
2xxstatus code; - the network fails before MeSomb receives your response;
- the webhook is manually replayed;
- MeSomb retries a failed delivery.
Your integration must be idempotent.
Use the X-MeSomb-Webhook-Event-Id header or the id field in the webhook body to detect duplicate events.
Example:
<span><span style="color: undefined">X-MeSomb-Webhook-Event-Id: evt_01HX9K7Y6N4V2X8QJ4K2M9ZP3A</span></span>
<span><span style="color: undefined"></span></span>
Store processed event IDs in your database. If the same event ID is received again, return a successful 2xx response without processing the business action again.
Recommended verification flow
Example webhook request
<span><span style="color: undefined">POST /webhooks/mesomb HTTP/1.1</span></span>
<span><span style="color: undefined">Host: merchant.example.com</span></span>
<span><span style="color: undefined">Content-Type: application/json</span></span>
<span><span style="color: undefined">X-MeSomb-Webhook-Event-Id: evt_01HX9K7Y6N4V2X8QJ4K2M9ZP3A</span></span>
<span><span style="color: undefined">X-MeSomb-Webhook-Request-Timestamp: 1778192130</span></span>
<span><span style="color: undefined">X-MeSomb-Webhook-Signature: t=1778192130,v1=6f1a0c8e8d7f2c9a4b6e1...</span></span>
<span><span style="color: undefined"></span></span>
<span><span style="color: undefined">{</span></span>
<span><span style="color: undefined"> "id": "evt_01HX9K7Y6N4V2X8QJ4K2M9ZP3A",</span></span>
<span><span style="color: undefined"> "object_type": "event",</span></span>
<span><span style="color: undefined"> "event_type": "payment.transaction.succeeded",</span></span>
<span><span style="color: undefined"> "api_version": "2026-05-01",</span></span>
<span><span style="color: undefined"> "created_at": "2026-05-07T22:15:30Z",</span></span>
<span><span style="color: undefined"> "livemode": true,</span></span>
<span><span style="color: undefined"> "data": {</span></span>
<span><span style="color: undefined"> "object": {</span></span>
<span><span style="color: undefined"> "id": "txn_01HX9K7A1R6F8MZK2Q9V7N4P5B",</span></span>
<span><span style="color: undefined"> "object": "payment_transaction",</span></span>
<span><span style="color: undefined"> "status": "succeeded",</span></span>
<span><span style="color: undefined"> "amount": 15000,</span></span>
<span><span style="color: undefined"> "currency": "XAF",</span></span>
<span><span style="color: undefined"> "reference": "ORDER-10045"</span></span>
<span><span style="color: undefined"> }</span></span>
<span><span style="color: undefined"> }</span></span>
<span><span style="color: undefined">}</span></span>
<span><span style="color: undefined"></span></span>
Code examples
Example
<span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">crypto</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">require</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">"crypto"</span><span style="color: var(--shiki-color-text)">);</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">function</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">verifyMeSombWebhook</span><span style="color: var(--shiki-color-text)">({</span></span> <span><span style="color: var(--shiki-color-text)">rawBody</span><span style="color: var(--shiki-token-punctuation)">,</span></span> <span><span style="color: var(--shiki-color-text)">signatureHeader</span><span style="color: var(--shiki-token-punctuation)">,</span></span> <span><span style="color: var(--shiki-color-text)">webhookSecret</span><span style="color: var(--shiki-token-punctuation)">,</span></span> <span><span style="color: var(--shiki-color-text)">toleranceSeconds </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">300</span><span style="color: var(--shiki-token-punctuation)">,</span></span> <span><span style="color: var(--shiki-color-text)">}) {</span></span> <span><span style="color: var(--shiki-token-keyword)">if</span><span style="color: var(--shiki-color-text)"> (</span><span style="color: var(--shiki-token-keyword)">!</span><span style="color: var(--shiki-color-text)">signatureHeader) {</span></span> <span><span style="color: var(--shiki-token-keyword)">throw</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">new</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">Error</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">"Missing X-MeSomb-Webhook-Signature header"</span><span style="color: var(--shiki-color-text)">);</span></span> <span><span style="color: var(--shiki-color-text)">}</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">parts</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">signatureHeader</span><span style="color: var(--shiki-token-function)">.split</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">","</span><span style="color: var(--shiki-color-text)">);</span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">timestampPart</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">parts</span><span style="color: var(--shiki-token-function)">.find</span><span style="color: var(--shiki-color-text)">((part) </span><span style="color: var(--shiki-token-keyword)">=></span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">part</span><span style="color: var(--shiki-token-function)">.startsWith</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">"t="</span><span style="color: var(--shiki-color-text)">));</span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">signaturePart</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">parts</span><span style="color: var(--shiki-token-function)">.find</span><span style="color: var(--shiki-color-text)">((part) </span><span style="color: var(--shiki-token-keyword)">=></span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">part</span><span style="color: var(--shiki-token-function)">.startsWith</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">"v1="</span><span style="color: var(--shiki-color-text)">));</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">if</span><span style="color: var(--shiki-color-text)"> (</span><span style="color: var(--shiki-token-keyword)">!</span><span style="color: var(--shiki-color-text)">timestampPart </span><span style="color: var(--shiki-token-keyword)">||</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">!</span><span style="color: var(--shiki-color-text)">signaturePart) {</span></span> <span><span style="color: var(--shiki-token-keyword)">throw</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">new</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">Error</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">"Invalid X-MeSomb-Webhook-Signature header format"</span><span style="color: var(--shiki-color-text)">);</span></span> <span><span style="color: var(--shiki-color-text)">}</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">timestamp</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">timestampPart</span><span style="color: var(--shiki-token-function)">.slice</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">2</span><span style="color: var(--shiki-color-text)">);</span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">receivedSignature</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">signaturePart</span><span style="color: var(--shiki-token-function)">.slice</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">3</span><span style="color: var(--shiki-color-text)">);</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">timestampNumber</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">Number</span><span style="color: var(--shiki-color-text)">(timestamp);</span></span> <span><span style="color: var(--shiki-token-keyword)">if</span><span style="color: var(--shiki-color-text)"> (</span><span style="color: var(--shiki-token-keyword)">!</span><span style="color: var(--shiki-token-constant)">Number</span><span style="color: var(--shiki-token-function)">.isFinite</span><span style="color: var(--shiki-color-text)">(timestampNumber)) {</span></span> <span><span style="color: var(--shiki-token-keyword)">throw</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">new</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">Error</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">"Invalid signature timestamp"</span><span style="color: var(--shiki-color-text)">);</span></span> <span><span style="color: var(--shiki-color-text)">}</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">now</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">Math</span><span style="color: var(--shiki-token-function)">.floor</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">Date</span><span style="color: var(--shiki-token-function)">.now</span><span style="color: var(--shiki-color-text)">() </span><span style="color: var(--shiki-token-keyword)">/</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">1000</span><span style="color: var(--shiki-color-text)">);</span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">age</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">Math</span><span style="color: var(--shiki-token-function)">.abs</span><span style="color: var(--shiki-color-text)">(now </span><span style="color: var(--shiki-token-keyword)">-</span><span style="color: var(--shiki-color-text)"> timestampNumber);</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">if</span><span style="color: var(--shiki-color-text)"> (age </span><span style="color: var(--shiki-token-keyword)">></span><span style="color: var(--shiki-color-text)"> toleranceSeconds) {</span></span> <span><span style="color: var(--shiki-token-keyword)">throw</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">new</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">Error</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">"Webhook timestamp is outside the allowed tolerance"</span><span style="color: var(--shiki-color-text)">);</span></span> <span><span style="color: var(--shiki-color-text)">}</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">signedPayload</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">`</span><span style="color: var(--shiki-token-keyword)">${</span><span style="color: var(--shiki-color-text)">timestamp</span><span style="color: var(--shiki-token-keyword)">}</span><span style="color: var(--shiki-token-string-expression)">.</span><span style="color: var(--shiki-token-keyword)">${</span><span style="color: var(--shiki-color-text)">rawBody</span><span style="color: var(--shiki-token-keyword)">}</span><span style="color: var(--shiki-token-string-expression)">`</span><span style="color: var(--shiki-color-text)">;</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">expectedSignature</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> crypto</span></span> <span><span style="color: var(--shiki-token-function)">.createHmac</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">"sha256"</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> webhookSecret)</span></span> <span><span style="color: var(--shiki-token-function)">.update</span><span style="color: var(--shiki-color-text)">(signedPayload</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">"utf8"</span><span style="color: var(--shiki-color-text)">)</span></span> <span><span style="color: var(--shiki-token-function)">.digest</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">"hex"</span><span style="color: var(--shiki-color-text)">);</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">receivedBuffer</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">Buffer</span><span style="color: var(--shiki-token-function)">.from</span><span style="color: var(--shiki-color-text)">(receivedSignature</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">"hex"</span><span style="color: var(--shiki-color-text)">);</span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">expectedBuffer</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">Buffer</span><span style="color: var(--shiki-token-function)">.from</span><span style="color: var(--shiki-color-text)">(expectedSignature</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">"hex"</span><span style="color: var(--shiki-color-text)">);</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">if</span><span style="color: var(--shiki-color-text)"> (</span></span> <span><span style="color: var(--shiki-token-constant)">receivedBuffer</span><span style="color: var(--shiki-color-text)">.</span><span style="color: var(--shiki-token-constant)">length</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">!==</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">expectedBuffer</span><span style="color: var(--shiki-color-text)">.</span><span style="color: var(--shiki-token-constant)">length</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">||</span></span> <span><span style="color: var(--shiki-token-keyword)">!</span><span style="color: var(--shiki-token-constant)">crypto</span><span style="color: var(--shiki-token-function)">.timingSafeEqual</span><span style="color: var(--shiki-color-text)">(receivedBuffer</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> expectedBuffer)</span></span> <span><span style="color: var(--shiki-color-text)">) {</span></span> <span><span style="color: var(--shiki-token-keyword)">throw</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">new</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">Error</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">"Invalid webhook signature"</span><span style="color: var(--shiki-color-text)">);</span></span> <span><span style="color: var(--shiki-color-text)">}</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">return</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">true</span><span style="color: var(--shiki-color-text)">;</span></span> <span><span style="color: var(--shiki-color-text)">}</span></span> <span></span>