Skip to content
KnockDevelopers

Handle events

Choose the event types your webhooks receive, read the envelope, and process deliveries safely, in any order and more than once.

Chat with a Developer

Every delivery is a JSON envelope with the event's details in data. The webhook events reference shows the envelope, the headers and every event type with its fields and an example payload. This page covers how to choose event types, what the envelope holds and how to process events.

Choose events

A webhook receives the event types listed in its events while it's active, plus webhook.ping when you ping it. One webhook can take several event types, so one URL can receive all of them: branch on the body's type. The same event type can also go to several URLs, one webhook each. To add or remove types later, change the webhook. webhook.ping can't be put in events: it's sent only when you ping a webhook or test a URL.

Event typeSent when
meeting.bookedA contact booked a meeting with your team through Knock.
meeting.rescheduledA meeting booked through Knock moved to a new time through Knock.
meeting.declinedA contact declined the calendar invitation of a meeting booked through Knock.
meeting.cancelledA meeting booked through Knock was cancelled, by the contact or by your team.
meeting.completedA meeting booked through Knock ended.
meeting.rejectedKnock did not let a contact book a meeting, because your routing rules disqualified them or they used a personal email.
outreach.linkedin_invite_sentKnock sent a LinkedIn connection invitation to a contact from a team member's account.
outreach.linkedin_invite_acceptedA contact accepted a LinkedIn connection invitation Knock sent from a team member's account.
member.linkedin_status_changedThe connection between a team member's LinkedIn account and Knock changed.
enrichment_request.completedAn enrichment request you submitted finished.
webhook.pingA test event Knock sends when you ping a webhook or test a URL.

Each one is documented in the webhook events reference, with its headers, every field of its body and an example payload. To read the same list from code, call GET /v1/webhooks/event-types: it returns each type a webhook can subscribe to, with a title, a one-sentence description, the JSON Schema of its body and an example.

Meeting events

meeting.booked and meeting.rescheduled carry the meeting in data, as GET /v1/meetings returns it, read when Knock sends the event. meeting.rescheduled adds previousStartAt and previousEndAt, the times before the move.

  • Not every booking is sent. Test bookings and meetings between members of your own team aren't sent, and nor is a meeting cancelled before Knock sent the event.
  • occurredAt can be later than bookedAt. occurredAt is when Knock announced the booking, which can be after the booking itself when Knock held it back, for example while it qualified the contact.
  • Only reschedules made through Knock are sent. A meeting moved directly in a calendar sends no event. Every reschedule through Knock is its own event, with its own id.
  • Keep the newest. Events can arrive in any order. For each meetingId, keep the event with the later occurredAt.

Enrichment events

enrichment_request.completed says that a request you submitted is finished, whether every item was enriched or not. It carries the request's status and item counts, not the results: read them with GET /v1/enrichment-requests/{enrichRequestId}. Its occurredAt is the request's completedAt.

A delivery can be late or fail for good, so keep a slow poll for requests you haven't heard about within 24 hours: an item still pending at the 24-hour deadline is settled when you read the request.

The envelope

Every delivery is a JSON body with the same envelope, whatever its type. Read type to know the shape of data.

enrichment_request.completed
{
  "id": "0b4c7f9e-1d2a-4e3b-8c5d-6f7a8b9c0d1e",
  "type": "enrichment_request.completed",
  "apiVersion": "2026-11-01",
  "occurredAt": "2026-11-02T12:00:05.000Z",
  "data": {
    "enrichRequestId": "7d0f4f1e-2c3b-4a8e-9a51-0e6f3b8c2d14",
    "type": "people",
    "status": "completed",
    "itemCounts": {
      "total": 3,
      "completed": 2,
      "notFound": 1,
      "failed": 0
    },
    "createdAt": "2026-11-02T11:58:35.000Z",
    "completedAt": "2026-11-02T12:00:05.000Z"
  }
}
FieldDescription
idID of the event, also sent as the webhook-id header. It's the same on every retry, and on every webhook the event is sent to.
typeThe event type, such as meeting.booked.
apiVersionVersion of the payload format: 2026-11-01 for every event type.
occurredAtWhen the event happened, as an ISO 8601 timestamp in UTC.
requestIdOptional. The X-Request-Id of the API call that caused the event. Absent when no API call did.
dataThe event's details. Its fields depend on type.

The webhook events reference lists every envelope field, every header and each event type's data.

Process events

Follow three rules:

  • Answer first, then process. Check the token, then answer 2xx within 5 seconds, before you process the event: Knock waits no longer, and doesn't read the response body. Store or queue the event, and do the work outside the request. See Respond fast, then process.
  • Process each event once. Delivery is at least once, so the same event can arrive again, for example when Knock retries because your answer took longer than 5 seconds. Deduplicate on the webhook-id header, or the body's id, which holds the same value: skip an event whose ID you've already processed. See Handle duplicates.
  • Don't rely on order. Events can arrive in any order. Compare occurredAt, not the time an event arrived. See Handle events out of order.

A minimal dispatcher. Call it after you've verified the delivery and answered 2xx, so slow work never delays your response. It skips events it has already processed, keeps the newest version of each meeting and reads enrichment results when a request completes. It ignores event types it doesn't know, so new types can't break it.

handle-event.mjs
// In production, keep both in your database.
const processedEventIds = new Set();
const meetings = new Map();

const MEETING_EVENT_TYPES = new Set(['meeting.booked', 'meeting.rescheduled']);

function saveMeeting(event) {
  const current = meetings.get(event.data.meetingId);
  if (current && Date.parse(current.occurredAt) >= Date.parse(event.occurredAt)) {
    return; // An older event arrived late.
  }
  meetings.set(event.data.meetingId, { occurredAt: event.occurredAt, meeting: event.data });
}

async function readEnrichmentRequest(enrichRequestId) {
  const res = await fetch(`https://app.knock-ai.com/v1/enrichment-requests/${enrichRequestId}`, {
    headers: { 'x-api-key': process.env.KNOCK_API_KEY },
    signal: AbortSignal.timeout(30_000),
  });

  if (!res.ok) {
    const error = await res.json();
    throw new Error(`Read failed: ${res.status} ${error.code}`);
  }

  return res.json();
}

export async function handleEvent(event) {
  if (processedEventIds.has(event.id)) return;

  if (MEETING_EVENT_TYPES.has(event.type)) {
    saveMeeting(event);
  } else if (event.type === 'enrichment_request.completed') {
    const request = await readEnrichmentRequest(event.data.enrichRequestId);
    console.log(request.status, request.items.length);
  }
  // webhook.ping and event types this code doesn't know yet need no action.

  processedEventIds.add(event.id);
}

Mark an event as processed only after its work succeeds, as above, so a duplicate delivery of a failed event runs the work again. Knock doesn't retry an event you answered with 2xx, so retry failed work from your own job queue.

New fields and new event types may be added over time, so ignore fields and types you don't recognize (see Versioning).

Next steps

Chat with a Developer

On this page