innkept

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

  1. Open Integrations → New integration → Webhook.
  2. Enter the destination URL — anywhere that accepts a POST.
  3. Save.
  4. 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 OK works 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.