Skip to content
KnockDevelopers

Delivery and retries

What counts as a delivered webhook, which failures Knock retries and on what schedule, what pausing or changing a webhook does to pending deliveries, and how to handle duplicates, ordering and debugging on your side.

Chat with a Developer

Knock sends each event to your webhook's URL as a POST and keeps trying until your endpoint accepts it or the retry schedule runs out. Delivery is at least once: an event can arrive more than once, and events can arrive out of order. This page covers what Knock expects from your endpoint, what it does when a delivery fails, and how to build a receiver that handles both safely.

What counts as delivered

A delivery succeeds when your endpoint answers with any 2xx status within 5 seconds. Anything else is a failed attempt: a different status, a slow answer, or no answer at all. Whether Knock tries again depends on how it failed (see Which failures are retried).

  • 5-second timeout. The 5 seconds cover the whole attempt: resolving your host, connecting, TLS, sending the body and receiving your status line. Every attempt has the same limit, retries, pings and URL tests included.
  • Redirects aren't followed. A 3xx is your endpoint's answer, and it isn't retried. Register the URL that answers directly.
  • The response body is never read. Only the status code counts, plus the Retry-After header on a 429 or 503. Knock closes the connection as soon as your status line and headers arrive, so whatever you put in the body is ignored. Leave it empty.

Answer as soon as you've stored the event, and do the real work afterwards. See Respond fast, then process.

What stays the same from one attempt of an event to the next, and what changes:

  • The same on every attempt: webhook-id (the event ID) and knock-webhook-id (the webhook this delivery is for).
  • New on every attempt: webhook-timestamp (when this attempt was sent).
  • The same body on every attempt: Knock builds the body once, when it accepts the event, so a retry never carries newer data than the first attempt.
  • Only when the webhook has a secret token: knock-webhook-token, that webhook's token exactly as you set it.
  • Read again for every attempt: the webhook's URL and its secret token. A retry made after you change the URL goes to the new URL, and one made after you change the token carries the new token.

Verify webhooks lists every header and how to check the token. Don't order events by webhook-timestamp: it's when the attempt was sent, not when the event happened. Use the body's occurredAt (see Handle events out of order).

Which failures are retried

Your endpoint's resultRetried?Notes
2xxNo, deliveredThe delivery is complete.
No response within 5 secondsYesCounts as a timeout.
Connection, DNS or TLS errorYesFor example, the host doesn't resolve or the connection is refused.
Your host resolves to an address that isn't publicNoKnock checks the address before every attempt, and doesn't send the request (blocked_destination).
408, 429YesOn 429, Knock honors Retry-After.
500 to 599YesOn 503, Knock honors Retry-After.
401, 403, 404YesCommon while you deploy a new endpoint or change your secret token, so Knock gives you time to fix them.
410NoThe delivery stops.
Any other 4xx, such as 400, 405, 409 or 422NoThe delivery stops.
Any 3xxNoKnock doesn't follow redirects. Register the final URL instead.

Return 401, not 400, when a delivery doesn't verify

A webhook's new secret token takes effect on its very next attempt. If your endpoint still holds the old value, answer 401 or 403: Knock retries those, so the event gets through once your endpoint has the new one. A 400 stops the delivery for good. To change the token without rejecting any attempt, accept both the old and the new value before you set the new one (see Rotate the token).

A 410 stops only that event's delivery to that webhook. The webhook stays, and later events are still sent to it: pause or delete the webhook to stop them.

Retry schedule

Knock tries the first time right away, then waits longer between each retry. A delivery gets up to 10 attempts over about 21 hours:

AttemptWait before this attempt (up to 20% shorter)Time since the first attempt (approx.)
1None0
230 seconds30 seconds
31.5 minutes2 minutes
44.5 minutes6.5 minutes
513.5 minutes20 minutes
640.5 minutesAbout 1 hour
72 hoursAbout 3 hours
86 hoursAbout 9 hours
96 hoursAbout 15 hours
106 hoursAbout 21 hours

Times are approximate: jitter can shorten each wait by up to 20%, so retries from many deliveries don't all arrive at once.

Retry-After

When your endpoint answers 429 or 503 with a Retry-After header, Knock waits at least that long, up to 6 hours, before the next attempt: the header can lengthen the scheduled wait, never shorten it. Both forms work, a number of seconds or an HTTP date. The attempt still counts toward the 10, and the 72-hour limit still applies. Knock ignores Retry-After on any other status. Send it when you're shedding load or under maintenance, so Knock comes back when you're ready.

503 Service Unavailable
HTTP/1.1 503 Service Unavailable
Retry-After: 120

The 72-hour limit

Knock never retries an event more than 72 hours after it accepted it. An attempt that would fall later than that isn't sent, and the delivery stops. The schedule above finishes well inside this limit.

Pause, change or delete a webhook

Changing a webhook changes what happens to its deliveries that aren't sent yet:

  • Pause it ("status": "paused" with PATCH /v1/webhooks/{webhookId}). Knock stops deliveries to it and drops every delivery not sent yet, retries included: they're never sent, even after you resume it. A request already in flight still completes. You can still ping a paused webhook.
  • Resume it ("status": "active"). Knock sends it the events it records from then on. An event from the pause that reaches Knock late can still be sent: its occurredAt shows when it happened.
  • Remove an event type from its events. Knock stops sending that type to it, retries included.
  • Change its URL or secret token. Events already accepted keep going to the webhook: their retries go to the new URL and carry the new token.
  • Delete it. Knock stops sending to it at once, including retries already scheduled.

A webhook created with "status": "paused" gets nothing until you set it to active. Deliveries Knock drops this way count as cancelled in the delivery stats.

When deliveries keep failing

When the last attempt fails, or a response isn't retried, Knock stops trying to deliver that event to that webhook. You can't replay a delivery. To catch up after an outage, read the current state from the API: GET /v1/meetings for meetings, and GET /v1/enrichment-requests/{enrichRequestId} for each enrichment request you haven't heard about.

Knock keeps the record of each delivery, every attempt and its result, for 3 days, the same as the 72-hour limit, and then deletes it. To find out why a delivery failed, contact Knock support with a webhook-id from your logs within those 3 days.

Deliveries to your other webhooks, including deliveries of the same event to other URLs, aren't affected: each webhook has its own retries.

To find out why an endpoint is failing, ping it. To stop deliveries to an endpoint for a while, pause the webhook; to stop them for good, delete it. Either is better than letting deliveries fail.

Knock doesn't pause or disable a webhook whose deliveries keep failing, and doesn't notify you. Watch its delivery stats and your endpoint's own error rate, and ping the webhook after an outage.

Watch delivery stats

GET /v1/webhooks/stats counts the delivery attempts of the last 24 hours (window=24h, the default) or 3 days (window=3d) by UTC hour, for one webhook (webhookId) or every webhook of your account. Pings and URL tests aren't counted, and the numbers are up to about 60 seconds behind. Get delivery stats lists every field.

A growing retrying count means your endpoint is failing now and Knock is still trying. failed counts events that webhook won't get.

Handle duplicates

Delivery is at least once, so the same event can reach your endpoint more than once. That happens when, for example, your endpoint processed an event but its 2xx didn't reach Knock in time, so Knock retried.

Every attempt of an event carries the same webhook-id, so make your receiver idempotent on it. A dependable way is an inbox table with a unique key: insert the event, ignore the insert if the key already exists, then answer 2xx either way.

Keep the IDs of the events you've handled for longer than 3 days. Knock's own record of a delivery lasts 3 days (see When deliveries keep failing), so an event Knock sends again after that, for example after an incident on Knock's side, arrives with the same webhook-id and only your inbox recognizes it as a duplicate.

Inbox table
CREATE TABLE knock_webhook_inbox (
  event_id     text PRIMARY KEY,
  type         text NOT NULL,
  occurred_at  timestamptz NOT NULL,
  payload      jsonb NOT NULL,
  received_at  timestamptz NOT NULL DEFAULT now(),
  processed_at timestamptz
);

The receivers below check the secret token in constant time, store the event once and answer right away. Set KNOCK_WEBHOOK_TOKEN to the secret token you set on the webhook, as secretToken on POST /v1/webhooks or PATCH /v1/webhooks/{webhookId} (see Verify webhooks).

receiver.mjs
// npm install express pg
import crypto from 'node:crypto';
import express from 'express';
import pg from 'pg';

const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const expected = crypto.createHash('sha256').update(process.env.KNOCK_WEBHOOK_TOKEN).digest();
const app = express();

function tokenMatches(received) {
  if (typeof received !== 'string') return false;
  return crypto.timingSafeEqual(crypto.createHash('sha256').update(received).digest(), expected);
}

app.post('/webhooks/knock', express.json(), async (req, res) => {
  if (!tokenMatches(req.get('knock-webhook-token'))) {
    return res.status(401).end(); // Retried, in case you're changing the token.
  }

  const event = req.body;
  try {
    await db.query(
      `INSERT INTO knock_webhook_inbox (event_id, type, occurred_at, payload)
       VALUES ($1, $2, $3, $4)
       ON CONFLICT (event_id) DO NOTHING`,
      [req.get('webhook-id'), event.type, event.occurredAt, event],
    );
  } catch (error) {
    console.error('Could not store Knock webhook', error);
    return res.status(500).end(); // Retried.
  }

  res.status(200).end(); // Stored now, or already stored by an earlier attempt.
});

app.listen(3000);

A worker then reads rows where processed_at is null, does the work and sets processed_at. If the worker can crash halfway, make each step safe to repeat too, for example by upserting rather than inserting.

Several URLs for one event type. The webhook-id is the same for every webhook that receives the event. If two of your webhooks receive the same event type and feed the same inbox, keying on webhook-id alone processes the event once. If each webhook should process it separately, key on webhook-id and knock-webhook-id together. Use knock-webhook-id to route and deduplicate, never as proof of where a request came from: only the secret token is.

Handle events out of order

Knock doesn't guarantee order. Retries, and events that happen close together, can arrive in any order: a meeting.rescheduled can arrive before the meeting.booked for the same meeting.

Order by the body's occurredAt, when the event happened in Knock, and never let an older event overwrite newer state. For example, keep the time of the last event you applied to each meeting, and only apply a later one:

Apply only newer events
INSERT INTO meetings (meeting_id, start_at, end_at, last_event_at)
VALUES ($1, $2, $3, $4)
ON CONFLICT (meeting_id) DO UPDATE
SET start_at      = EXCLUDED.start_at,
    end_at        = EXCLUDED.end_at,
    last_event_at = EXCLUDED.last_event_at
WHERE meetings.last_event_at < EXCLUDED.last_event_at;

Here $1 to $3 are data.meetingId, data.startAt and data.endAt, and $4 is the event's occurredAt. An older event that arrives late changes nothing.

Best practices

Respond fast, then process

  • Answer within 5 seconds. Check the secret token, store the event and return 2xx with an empty body. Anything slower counts as a failed attempt and is retried, even if your work later succeeds.
  • Queue the work. Do the processing in a background job or worker, not in the request. Calls to your CRM or other APIs can be slow, and a retry that arrives while the first attempt is still working creates a duplicate.
  • Be idempotent. Deduplicate on webhook-id, and make the processing itself safe to run twice. See Handle duplicates.

Answer with the status you mean

  • 2xx once the event is stored, including for a duplicate you've already stored and for webhook.ping.
  • 401 or 403 when the secret token doesn't match, so a delivery sent while you switch to a new token is retried.
  • 429 or 503 with Retry-After when you need Knock to back off.
  • 5xx for your own temporary failures, such as a database that's down.
  • Avoid other 4xx and 3xx. Knock doesn't retry them, so the event is lost for that webhook.

Network requirements

Your endpoint has to be reachable from the public internet over HTTPS on port 443. The full rules Knock checks when you create or change a webhook, or test a URL, are under URL requirements.

Knock doesn't follow 3xx responses, and doesn't retry them. Register the URL that answers directly, including the right host and trailing slash. Every attempt has 5 seconds, DNS lookup included, to get your status line back, and Knock never reads the response body.

Deliveries arrive with content-type: application/json and user-agent: Knock-Webhooks/1. A body is at most 32 KiB. If a firewall or WAF sits in front of your endpoint, allow requests with that user agent through to it. Don't treat the user agent as proof that a request came from Knock: anyone can send it, so always check the secret token.

Ping a webhook to debug

POST /v1/webhooks/{webhookId}/ping sends a webhook.ping event to that webhook's URL, with its secret token, and reports your endpoint's answer, so you can test the full path (network, TLS, token check and response) without waiting for a real event.

To check an endpoint before you create its webhook, POST /v1/webhooks/test sends the same webhook.ping to a url you pass, with the optional secretToken you pass (Knock doesn't keep it). There's no webhook yet, so the knock-webhook-id header and data.webhookId are 00000000-0000-0000-0000-000000000000.

Both are a single attempt with a 5-second timeout, are never retried, don't affect deliveries of real events and aren't counted in the delivery stats. You can ping each webhook once every 10 seconds, and your account can test a URL once every 10 seconds: retryAfterSeconds in the result says how long to wait before the next one.

What the result tells you

ResultLikely causeWhat to do
delivered: trueYour endpoint is reachable and accepted the event.Nothing. Real events take the same path.
statusCode: 401 or 403Your endpoint rejected the secret token.Check that it compares against the token you set last on that webhook, or the secretToken you passed to the URL test. See Verify webhooks.
statusCode: 404The path isn't deployed, or the URL has a typo.Compare the webhook's url with your route.
errorCode: redirectYour server redirected, for example to another host or to add a trailing slash. Knock doesn't follow redirects.Change the webhook's url to the final URL.
statusCode in 500 to 599Your handler failed.Check your logs for the eventId.
statusCode: nullNo HTTP response: errorCode is timeout, connection, dns or tls.Check DNS, your certificate and that port 443 is open to the internet.
errorCode: blocked_destinationThe host resolves to an address that isn't public.Point the host's DNS at a public address.
durationMs close to 5000Your endpoint is slow to answer.Store the event and answer before you process it.

Next steps

Chat with a Developer

On this page