Eklow
API & Webhooks

API & Webhooks

Outbound webhooks and signature verification

Subscribing to events, the delivery and retry contract, and verifying the HMAC signature with a working code sample.

2 min read

Outbound webhooks push events to your endpoint as they happen, so your CRM or data warehouse stays in sync without polling. Configure them under Settings → Webhooks: an HTTPS URL, the events you want, and the signing secret Eklow generates for the endpoint.

Events

EventFires when
message.sentA campaign or warmup message was handed to the provider
email.openedA tracked open was recorded
email.clickedA tracked link was clicked
email.repliedA reply was detected and classified as human
email.bouncedA hard or soft bounce was recorded
contact.unsubscribedAn unsubscribe was processed and suppressed
placement_test.completedA placement test finished, expired, failed or was stopped; status says which
Example payload — email.replied
{
  "id": "evt_01j9x7v2m8",
  "type": "email.replied",
  "created_at": "2026-08-04T14:22:07Z",
  "workspace_id": "b1f4...",
  "data": {
    "message_id": "a2c8...",
    "campaign_id": "6b9d...",
    "contact_id": "e91f...",
    "mailbox_id": "77aa..."
  }
}

Delivery and retries

Deliveries are POST requests with a JSON body. Your endpoint should return a 2xx within 10 seconds — do real processing async and respond fast. Non-2xx responses and timeouts are retried with exponential backoff — 8 attempts at 30s, 1m, 2m, 4m, 8m, 16m, 32m, so roughly an hour — before the delivery is marked failed. After 10 failed deliveries in a rolling 7 days the endpoint is switched off, and turning it back on is a manual step in Settings → Webhooks. Deliveries can arrive out of order and, rarely, more than once — dedupe on the event id.

Verifying the signature

Every delivery is signed with your endpoint's secret so you can prove it came from Eklow and wasn't tampered with. The X-Eklow-Signature header reads t=<unix seconds>,v1=<signature>, where the signature is the hex HMAC-SHA256 of t + "." + rawBody. Verify like this:

Node / TypeScript verification
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook(
  rawBody: string,
  header: string,
  secret: string
): boolean {
  const parts = new Map(header.split(",").map((part) => part.split("=") as [string, string]));
  const timestamp = parts.get("t") ?? "";
  const signature = parts.get("v1") ?? "";
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(timestamp + "." + rawBody)
    .digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(signature, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}
  • Compute the HMAC over the raw request body exactly as received — parse-then-restringify changes whitespace and breaks the signature. In most frameworks that means reading the body before any JSON middleware touches it.
  • The timestamp check (5-minute window) defeats replay of captured deliveries.
  • Use a constant-time comparison (timingSafeEqual above), never === on the hex strings.
  • Rotating the secret from Settings → Webhooks invalidates the old one after a 24-hour overlap window in which deliveries are signed with both (two signatures, comma-separated) so you can roll without downtime.

Was this page helpful?

Related articles