Verifying MANTL Webhooks
This guide explains how to verify webhooks received from MANTL's systems to ensure they are authentic and haven't been tampered with.
Table of Contents
Overview
When MANTL sends a webhook to your system, we include several security elements that allow you to verify the authenticity of the request:
- A signature in the
MANTL-Signatureheader - A unique message ID in both the
MANTL-Msg-IDheader and request body -
Your unique consumer ID in the request body
Request Format
Headers
Every webhook request includes the following headers:
Accept: */*
Content-Type: application/json
MANTL-Signature: t:1698537600,v1:hGHgh76tr4=,v1:L8uKNXZ5X2w=
MANTL-Msg-ID: 123e4567-e89b-12d3-a456-426614174000
User-Agent: MANTL-Webhooks/2024-04-08Additional headers can be configured per consumer with static header names and values. For example, Authorization or API-Key headers can be configured to be included on every request.
Request Body
All webhook events use this standard envelope format:
{
"messageId": "123e4567-e89b-12d3-a456-426614174000",
"consumerId": "987fcdeb-51d3-a456-426614174000",
"timestamp": "2024-10-25T15:30:00Z",
"eventType": "application.booked",
"data": {
// Event-specific data structure
}
}The envelope fields are:
-
messageId: A UUID that uniquely identifies this webhook event. Remains the same across retry attempts. -
consumerId: Your MANTL UUID as the intended recipient of this webhook. -
timestamp: When the original event occurred (in ISO 8601 format). -
eventType: Identifies the type of event, which determines the structure of thedataobject. -
data: Contains the event-specific payload.
Message Retries
If a webhook delivery fails, we may retry the delivery. In retry attempts:
- The
MANTL-Msg-IDand request body remain exactly the same - The
MANTL-Signatureheader will have a new timestamp and signature - You can use the message ID to deduplicate repeated deliveries
- Failed deliveries will retry up to nine more times using an exponential backoff strategy for roughly three days
- Messages may be manually retried in the MANTL dashboard
- A
Retry-Afterheader may be provided with error responses to define a minimum delay
Handling Asynchronous Data
MANTL's webhooks utilize an eventually consistent, at least once event pattern.
- Consumers should be idempotent, using the message ID to deduplicate requests
- Even if you responded with a success, we may send the message again in the future because we didn't receive the success or the message was manually retried
- Consumers should not assume the event just occurred, instead rely on the timestamp in the request envelope
- A downtime in either the MANTL system or your consumer may cause a message to be received days after the event occurred
- Events may be received out of order
- Consumers should process message requests quickly, handle any non-trivial tasks in an asynchronous queue
-
Our clients will timeout any request taking longer than ten seconds and retry the request
-
How to Verify Webhooks
Step 1: Parse the Signature Header
The MANTL-Signature header contains comma separated values:
- A timestamp:
t:1234567890- A Unix Timestamp (in seconds) of when the request was sent
- Retried messages will be sent with an updated timestamp, and therefore, different signatures
- One or more signatures:
v1:abc123...- "v1" denotes the signature was created with HMAC-SHA256, and is our only supported algorithm currently
- Transmitted using base64 encoding
- A webhook consumer, configured in MANTL, can have more than one signing key active at a time. Allows for zero-downtime key rotation.
- Each active key will result in a signature in the header
Example Python code to parse the header:
parts = signature_header.split(',')
timestamp = next((p for p in parts if p.startswith('t:')), None)
signatures = [p for p in parts if p.startswith('v1:')]Step 2: Verify the Timestamp
Check that the signature timestamp is within your allowed tolerance (typically 5 minutes):
def verify_timestamp(timestamp_str: str, tolerance: int = 300) - bool:
timestamp = int(timestamp_str.replace('t:', ''))
now = int(time.time())
return abs(now - timestamp) <= toleranceStep 3: Verify the Signature
For each signature:
- Combine the timestamp and raw request body with a period (.)
- Create an HMAC-SHA256 hash using your signing key
- Signing keys are shared encoded as base64. The following example assumes they are still base64, adjust as your environment requires.
- Compare with the provided signature
def verify_signature(timestamp: str, body: str, signatures: List[str], signing_key: str) -> bool:
payload = f"{timestamp.replace('t:', '')}.{body}"
computed = hmac.new(
base64.b64decode(signing_key),
payload.encode('utf-8'),
hashlib.sha256
).digest()
expected = base64.b64encode(computed).decode('utf-8')
return any(
hmac.compare_digest(sig.replace('v1:', ''), expected)
for sig in signatures
)
Security Checklist
- Always use constant-time comparison when verifying signatures to prevent timing attacks
- Verify timestamps to prevent replay attacks
- Keep signing keys secure and never expose them in client-side code
- Support multiple signing keys to facilitate key rotation
- Store raw request bodies for verification, as any modification will invalidate the signature
- Verify message IDs match between header and body
- Verify consumer ID matches your MANTL ID
- Implement idempotency using message IDs to handle retries safely
Need help? Contact MANTL support for assistance with webhook integration.