Webhook
POST a signed JSON payload to your own endpoint. Verify with HMAC-SHA256.
For anyone who wants the raw lead delivered to their own server. JSON body, signed with HMAC-SHA256 so you can verify it came from us.
Setting up
- Open Integrations → New integration → Webhook.
- Enter the destination URL — anywhere that accepts a POST.
- Save.
- Copy the Signing secret shown after save and store it safely on your server.
What we send
Every new lead triggers a single POST to your URL:
POST https://your-server/webhook
Content-Type: application/json
X-Innkept-Event: lead.created
X-Innkept-Timestamp: 1747008000
X-Innkept-Signature: sha256=<hex digest>
User-Agent: Innkept/1.0
{ ... payload ... }
Full payload reference: Webhook payload v1.
Verifying the signature
The X-Innkept-Signature header is sha256= followed by the HMAC-SHA256
of <timestamp>.<raw body>, keyed with your signing secret. The timestamp
comes from the X-Innkept-Timestamp header and is the Unix seconds when we sent the
request. Reject requests where the timestamp drifts more than a few minutes from your server
clock — that's how you stop replays.
Node.js
import crypto from 'node:crypto';
function verify(rawBody, timestamp, header, secret) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(header)
);
}
Python
import hmac, hashlib, time
def verify(raw_body: bytes, timestamp: str, header: str, secret: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
signed = f"{timestamp}.".encode() + raw_body
expected = 'sha256=' + hmac.new(
secret.encode(), signed, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
PHP
function verify(string $rawBody, string $timestamp, string $header, string $secret): bool {
if (abs(time() - (int) $timestamp) > 300) return false;
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $header);
}
Always verify signatures on the raw body, not the parsed JSON. Re-serialising changes whitespace and breaks the hash.
Response expectations
- Return any 2xx status (
200 OKworks fine). - Anything else is treated as a failure and the push is retried up to 3 times with backoff.
- Respond within 10 seconds. Beyond that we time out and treat as failure.
Idempotency
Each push has a unique lead.uuid. If a retry results in a duplicate at your
end, dedupe on that UUID. Push retries can technically deliver the same lead twice during
transient failures.
Testing locally
Use ngrok or webhook.site to receive test pushes. Submit a lead via your widget and inspect what arrives.
Something missing or wrong? Tell us.
Updated regularly. UK English. No AI slop.