Webhooks

Konektor webhooks are sent asynchronously when lead changes occur. Delivery uses queue-based dispatch, so you should assume at-least-once delivery semantics.

Webhook Headers

Each webhook request includes:

  1. Content-Type: application/json
  2. X-Konektor-Signature: t=<unix_timestamp>,v1=<hex_hmac_sha256>
  3. X-Konektor-Event: <event_name>
  4. User-Agent: Konektor-Webhook/1.0

Emitted Events

Lead flow currently emits:

  1. lead.created
  2. lead.updated

The HTTP body contains the event payload directly, without an extra wrapper.

lead.created Example

{
  "lead": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "uniqueCode": "API_1A2B3C4D",
    "externalRef": "crm-001",
    "firstName": "John",
    "email": "john@example.com",
    "phone": "6281234567890",
    "status": "new",
    "priority": "medium",
    "city": null,
    "country": null
  },
  "source": "api"
}

lead.updated Example

{
  "lead": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "uniqueCode": "API_1A2B3C4D",
    "status": "qualified"
  },
  "leadId": "550e8400-e29b-41d4-a716-446655440000",
  "previousStatus": "new",
  "newStatus": "qualified",
  "changes": ["status"],
  "source": "api"
}

Signature Verification (Timing-Safe)

Always verify using the raw request body. Do not re-stringify parsed JSON before hashing.

import crypto from 'node:crypto'

function verifyKonektorSignature(rawBody, signatureHeader, secret) {
  const parts = signatureHeader.split(',')
  const timestamp = parts.find(p => p.startsWith('t='))?.slice(2)
  const signature = parts.find(p => p.startsWith('v1='))?.slice(3)

  if (!timestamp || !signature) return false

  const now = Math.floor(Date.now() / 1000)
  if (Math.abs(now - Number(timestamp)) > 300) return false

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')

  const providedBuf = Buffer.from(signature, 'hex')
  const expectedBuf = Buffer.from(expected, 'hex')

  if (providedBuf.length !== expectedBuf.length) return false
  return crypto.timingSafeEqual(providedBuf, expectedBuf)
}

Idempotency Requirements

Because queue retries can resend events, your receiver must be idempotent:

  1. Store an internal event key (for example leadId + updatedAt + event).
  2. Ignore duplicate events that already completed.
  3. Return 2xx for valid duplicates to stop further retries.

Timeout and Receiver Behavior

Konektor sends webhook requests with a short timeout (5s). A reliable receiver should:

  1. Verify signature.
  2. Enqueue heavy processing internally.
  3. Respond 200 OK quickly.

Security Notes

  1. Store webhook secrets in server-side secret storage.
  2. Rotate secrets on a fixed schedule.
  3. Reject missing or invalid signature headers.
  4. Apply infrastructure-level IP allowlists if required by your environment.

Need More Help?

Our team is ready to help you maximize ad tracking and business attribution.

Email Supportsupport@konektor.id
YouTubeYouTube

© 2026 Konektor. All rights reserved.