Verifying Webhook Signatures
All webhooks include a cryptographic signature that you must verify to ensure the request originated from Minteo and wasn't tampered with.
Security considerations
Your webhook secret is different from your API key.
- The secret is used only for webhook signature verification
- Store the secret securely (environment variables, secret manager)
- Never commit the secret to version control
- Never expose the secret in client-side code
- Rotate the secret if the secret is compromised
Minteo stores your secret encrypted in our database and only decrypts it when needed.
Getting your secret
You can view your current webhook secret at any time in the Board:
- Production
- Sandbox
How signature verification works
The signature.checksum field is a SHA256 hash computed from:
- Property values (extracted from
dataobject in the order specified insignature.properties) - Timestamp (the
timestampfield from the payload) - Your secret (shared only between you and Minteo)
Verification process
Step 1: Extract signature properties
The signature.properties array specifies which fields were used to generate the checksum.
The properties in signature.properties may vary over time and between events.
Do NOT hardcode this array in your code. Always extract properties dynamically from each webhook.
Step 2: Extract property values from data
For each property in the array, extract the value from the data object using dot notation.
Example: order.id → data.order.id
Step 3: Concatenate values
Concatenate all extracted values as strings, in the order they appear in signature.properties.
Step 4: Append timestamp
Append the timestamp value (as a string) to the concatenated string.
Step 5: Append your secret
Append your webhook secret to the concatenated string.
Step 6: Compute SHA256 hash
Compute the SHA256 hash of the final concatenated string.
Step 7: Compare checksums
Compare your computed hash with the signature.checksum value from the webhook.
If they do not match, reject the webhook immediately.
Implementation example (Node.js)
import crypto from 'node:crypto';
interface WebhookEvent {
event_id: string;
hook_id: string;
event_type: string;
data: Record<string, unknown>;
signature: {
properties: string[];
checksum: string;
};
timestamp: number;
sent_at: string;
}
function validateChecksum(event: WebhookEvent, secret: string): boolean {
const { signature, data, timestamp } = event;
// Steps 1-3: Extract and concatenate property values
const propertyValues = signature.properties
.map(prop => getNestedValue(data, prop))
.join('');
// Steps 4-5: Append timestamp and secret
const payload = `${propertyValues}${timestamp.toString()}${secret}`;
// Step 6: Compute SHA256 hash
const computedChecksum = crypto
.createHash('sha256')
.update(payload)
.digest('hex')
.toUpperCase();
// Step 7: Compare checksums
return computedChecksum === signature.checksum;
}
function getNestedValue(obj: Record<string, unknown>, path: string): string {
const value = path.split('.').reduce((acc, key) => acc?.[key], obj);
return `${value ?? ''}`.trim();
}
// Usage in your webhook handler
app.post('/webhooks/minteo', (req, res) => {
const event = req.body;
const secret = process.env.MINTEO_WEBHOOK_SECRET!;
if (!validateChecksum(event, secret)) {
console.error('Invalid webhook signature');
return res.status(401).send('Unauthorized');
}
// Process the webhook
console.log('Valid webhook received:', event.event_type);
res.status(200).send('OK');
});
Calculation example
Given this webhook:
{
"data": {
"order": {
"id": "1234-1610641025-49201",
"status": "SUCCEEDED",
"amount": "4490000"
}
},
"signature": {
"properties": ["order.id", "order.status", "order.amount"],
"checksum": "124F3E92EA81EAC6DAB684035557433BA1922A7A47FED49F2001E831B5185C7E"
},
"timestamp": 1530291411
}
And secret: whsec_abc123xyz
The concatenated string would be:
"1234-1610641025-49201" + "SUCCEEDED" + "4490000" + "1530291411" + "whsec_abc123xyz"
= "1234-1610641025-49201SUCCEEDED44900001530291411whsec_abc123xyz"
Then compute: SHA256("1234-1610641025-49201SUCCEEDED44900001530291411whsec_abc123xyz")
If the computed checksum does NOT match the signature.checksum from the event, you MUST reject the webhook. A mismatch indicates the payload may have been tampered with or the request is not from Minteo.
Secret rotation
When you rotate your webhook secret in the Board:
- The current secret is immediately rotated
- A new secret is generated
- Existing events being retried continue using the old secret (snapshotted at event creation)
- New events use the new secret
Recommended rotation process
To rotate your secret without downtime:
- Update your code to validate against multiple secrets (current + past secrets visible in Board)
- Deploy the updated code
- Rotate the secret in Board
- Existing retry hooks (~64 hours) will use the old secret
- New events will use the new secret
- After ~64 hours, all hooks use the new secret
- Remove old secret validation from your code