Documentation

Receiving Webhooks

Stand up an endpoint that accepts Davi deliveries and verifies their signatures.

This page shows you how to receive an organization's events on your own server and verify them. Webhooks are configured in the Davi dashboard; this page covers the receiving side. The event list, delivery envelope and header names are in Webhook Events.

1. Expose an endpoint

Point the webhook at a publicly reachable URL. Private, loopback and .local addresses are rejected when the webhook is created.

The signing secret is shown once, when you create the webhook. Store it where your handler can read it and treat it as a credential. If you lose it, rotate it instead of recreating the webhook.

2. Verify the signature

Every delivery is signed with HMAC-SHA256 over the raw request body, and the signature is sent in X-Webhook-Signature as sha256=<hex>. The HMAC key is the SHA-256 hex digest of your signing secret, not the secret itself: hash the secret first, then use the resulting hex string as the key. Verify before you read the body, and compare in constant time.

import hashlib
import hmac

def verify(secret: str, raw_body: bytes, signature_header: str) -> bool:
    key = hashlib.sha256(secret.encode()).hexdigest()
    expected = "sha256=" + hmac.new(
        key.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)
import { createHash, createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, rawBody, signatureHeader) {
  const key = createHash("sha256").update(secret).digest("hex");
  const expected =
    "sha256=" + createHmac("sha256", key).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader);
  return a.length === b.length && timingSafeEqual(a, b);
}

Compute the signature over the exact bytes you received. Re-serializing a parsed JSON object changes key order and whitespace, and the signature will not match. Use your framework's raw-body accessor before its JSON parser runs.

3. Respond fast, work later

Return a 2xx as soon as you have verified and stored the delivery. Deliveries time out after 30 seconds, and a timeout counts as a failure. Put slow work (sending mail, calling another API, generating a file) on a queue.

Failed deliveries are retried with increasing backoff, except a 4xx other than 429, which fails the delivery at once. Answer 4xx only for a request you never want again, such as a bad signature. Design your handler for two consequences:

  • The same event can arrive more than once. Make the handler idempotent, and use X-Webhook-Delivery to recognize a repeat.
  • Deliveries can arrive out of order when retries are involved. The envelope's timestamp is when that attempt was sent, not when the event happened, so it cannot restore the order. Where order matters, re-read the resource from the API and act on its current state.

The retry schedule and delivery states are in Webhook Events.

Next