Webhook Signature Verification
Every webhook call from Incapon's servers carries an HMAC-SHA256 signature in the X-Incapon-Signature header. Verify that signature on your side to keep forged and unauthorised requests out.
Get Your Webhook Secret
When you configure your webhook endpoint, a shared secret key is issued to you alone. That key is the cryptographic trust between our servers and your application.
Copy the secret that starts with whsec_... from the Webhook settings in your developer dashboard. Never expose it on the client side (frontend) or in public repositories.
Read the Raw Payload of the Incoming Request
For the signature check to hold, the byte sequence delivered to your server must be untouched. Capture the raw body (a raw payload buffer or string) before any JSON parsing happens.
express.raw({ type: 'application/json' }) middleware.Compute and Compare the HMAC-SHA256 Hash
The hash produced from the raw body and your secret must match the X-Incapon-Signature value in the request header exactly.
To resist timing attacks, use a constant-time comparison (crypto.timingSafeEqual) instead of a plain string equality check. This is critical.
Implementation Examples
const crypto = require('crypto');
const express = require('express');
const app = express();
// 1. Capture the raw body on the webhook endpoint
app.post('/webhooks/incapon', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-incapon-signature'];
const webhookSecret = process.env.INCAPON_WEBHOOK_SECRET;
// 2. Compute the expected hash
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(req.body)
.digest('hex');
// 3. Compare in constant time (timing-safe)
const isValid = crypto.timingSafeEqual(
Buffer.from(signature || ''),
Buffer.from(expectedSignature)
);
if (!isValid) {
return res.status(401).send('Invalid signature!');
}
// 4. Carry on once the signature checks out
const event = JSON.parse(req.body.toString());
console.log(event.event_type);
res.status(200).json({ received: true });
});import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
SECRET = 'whsec_sample_key_98234'.encode('utf-8')
@app.post('/webhooks/incapon')
async def verify_webhook(request: Request):
signature = request.headers.get('X-Incapon-Signature')
raw_body = await request.body()
computed_sig = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature or '', computed_sig):
raise HTTPException(status_code=401, detail='Signature mismatch')
return {'status': 'success'}curl -X POST https://api.incapon.com/v2.4/webhooks/test-dispatch \
-H 'Content-Type: application/json' \
-H 'X-Incapon-Signature: 5d41402abc4b2a76b9719d911017c592' \
-H 'X-Incapon-Timestamp: 1718029100' \
-d '{"event_type": "price.changed", "payload": {"monitor_id": "mon_42"}}'Header & Body Parameters
Every webhook payload carries the following standard HTTP headers and data model:
| Parameter / Header | Type | Required | Description |
|---|---|---|---|
| X-Incapon-Signature | String | Yes | Hex digest signed with HMAC SHA-256. |
| X-Incapon-Timestamp | Integer | Yes | UNIX timestamp (seconds) when the request was sent. |
| event_type | String | Yes | The event that fired (price.changed, document.redacted). |
| payload | Object | Yes | A complete JSON object holding the event data. |
Additional Security Recommendations
Compare the X-Incapon-Timestamp header against your local server clock. Reject any request older than 5 minutes (300 s) outright.
Every call leaves Incapon from a fixed set of egress IP blocks. Allow only those IPs in your firewall (WAF).