> ## Documentation Index
> Fetch the complete documentation index at: https://developer.vanta.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Subscribe to webhook events

> Receive real-time HTTP notifications when events happen in Vanta — register an endpoint, verify Svix signatures, and handle retries — instead of polling the API.

export function vibePromptBuildText(prompt, pageTitle) {
  const url = typeof window !== "undefined" && window.location ? window.location.href : "";
  const ref = url ? "\n\nReference: " + (pageTitle || "Vanta docs") + " — " + url : "";
  return prompt + ref;
}

export function vibePromptOpen(e) {
  e.preventDefault();
  e.stopPropagation();
  const link = e.currentTarget;
  const target = link.getAttribute("data-target");
  const prompt = link.getAttribute("data-prompt") || "";
  const pageTitle = link.getAttribute("data-page-title") || "";
  const URL_LIMIT = 8000;
  const text = vibePromptBuildText(prompt, pageTitle);
  let enc = encodeURIComponent(text);
  if (enc.length > URL_LIMIT) enc = encodeURIComponent(text.slice(0, 2400));
  let dest = "";
  if (target === "cursor") dest = "cursor://anysphere.cursor-deeplink/prompt?text=" + enc; else if (target === "claude") dest = "https://claude.ai/new?q=" + enc; else if (target === "chatgpt") dest = "https://chatgpt.com/?q=" + enc;
  if (!dest) return;
  if (target === "cursor") {
    window.location.href = dest;
  } else {
    window.open(dest, "_blank", "noopener,noreferrer");
  }
}

export function vibePromptCopy(e) {
  e.preventDefault();
  e.stopPropagation();
  const btn = e.currentTarget;
  const prompt = btn.getAttribute("data-prompt") || "";
  const pageTitle = btn.getAttribute("data-page-title") || "";
  const original = btn.getAttribute("data-label") || btn.innerText;
  btn.setAttribute("data-label", original);
  const text = vibePromptBuildText(prompt, pageTitle);
  navigator.clipboard.writeText(text).then(function () {
    btn.innerText = "Copied";
    setTimeout(function () {
      btn.innerText = original;
    }, 1500);
  }, function () {
    btn.innerText = "Copy failed";
    setTimeout(function () {
      btn.innerText = original;
    }, 1500);
  });
}

export function vibePromptToggle(e) {
  e.preventDefault();
  const summary = e.currentTarget;
  const panel = summary.parentElement;
  if (!panel) return;
  const body = panel.querySelector(".vibe-prompt__body");
  const chevron = summary.querySelector(".vibe-prompt__chevron");
  const isOpen = panel.getAttribute("data-open") === "true";
  const next = isOpen ? "false" : "true";
  panel.setAttribute("data-open", next);
  summary.setAttribute("aria-expanded", next);
  if (body) body.style.display = isOpen ? "none" : "block";
  if (chevron) chevron.style.transform = isOpen ? "rotate(0deg)" : "rotate(180deg)";
}

export const BuildPrompt = ({prompt, pageTitle, defaultOpen}) => <div className="vibe-prompt__panel" data-open={defaultOpen ? "true" : "false"} style={{
  marginTop: "0.85rem",
  borderRadius: "12px",
  border: "1px solid var(--vanta-border, rgba(120, 120, 130, 0.18))",
  background: "color-mix(in srgb, #5E05C4 4%, transparent)",
  overflow: "hidden"
}}>
    <button type="button" onClick={vibePromptToggle} aria-expanded={defaultOpen ? "true" : "false"} style={{
  display: "flex",
  alignItems: "center",
  gap: "0.75rem",
  width: "100%",
  padding: "0.9rem 1.1rem",
  background: "transparent",
  border: "none",
  textAlign: "left",
  cursor: "pointer",
  font: "inherit",
  color: "inherit"
}}>
      <span style={{
  display: "inline-flex",
  alignItems: "center",
  justifyContent: "center",
  gap: "0.35rem",
  padding: "0.2rem 0.55rem",
  borderRadius: "9999px",
  fontSize: "0.7rem",
  fontWeight: 700,
  letterSpacing: "0.06em",
  textTransform: "uppercase",
  color: "#5E05C4",
  background: "color-mix(in srgb, #5E05C4 12%, transparent)",
  border: "1px solid color-mix(in srgb, #5E05C4 30%, transparent)",
  whiteSpace: "nowrap",
  flexShrink: 0,
  minWidth: "9rem"
}}>
        Code this
      </span>
      <span style={{
  flex: 1,
  minWidth: 0
}}>
        <span className="text-gray-600 dark:text-gray-300" style={{
  display: "block",
  fontSize: "0.85rem",
  lineHeight: 1.45
}}>
          Generate a script or app that performs this function using the Vanta API.
        </span>
      </span>
      <span className="vibe-prompt__chevron" aria-hidden="true" style={{
  flexShrink: 0,
  display: "inline-flex",
  alignItems: "center",
  justifyContent: "center",
  width: "24px",
  height: "24px",
  color: "#5E05C4",
  transform: defaultOpen ? "rotate(180deg)" : "rotate(0deg)",
  transition: "transform 200ms ease"
}}>
        <Icon icon="chevron-down" iconType="regular" size={14} color="#5E05C4" />
      </span>
    </button>
    <div className="vibe-prompt__body" style={{
  display: defaultOpen ? "block" : "none",
  padding: "0 1.1rem 1.1rem"
}}>
      <div style={{
  margin: 0,
  padding: "0.85rem 1rem",
  borderRadius: "8px",
  background: "rgba(15, 17, 21, 0.92)",
  color: "#f4f4f5",
  fontSize: "0.78rem",
  lineHeight: 1.55,
  fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
  whiteSpace: "pre-wrap",
  wordBreak: "break-word",
  overflowWrap: "anywhere",
  overflowX: "hidden",
  overflowY: "auto",
  maxHeight: "22rem"
}}>
        {prompt}
      </div>
      <div className="not-prose" style={{
  display: "flex",
  flexWrap: "wrap",
  gap: "0.5rem",
  marginTop: "0.75rem"
}}>
        <button type="button" onClick={vibePromptCopy} data-prompt={prompt} data-page-title={pageTitle} style={{
  display: "inline-flex",
  alignItems: "center",
  gap: "0.4rem",
  padding: "0.4rem 0.85rem",
  borderRadius: "8px",
  border: "none",
  cursor: "pointer",
  fontSize: "0.8rem",
  fontWeight: 600,
  color: "#ffffff",
  background: "#5E05C4"
}}>
          <Icon icon="copy" iconType="regular" size={13} color="#ffffff" />
          Copy prompt
        </button>
        <a href="#" onClick={vibePromptOpen} data-target="cursor" data-prompt={prompt} data-page-title={pageTitle} className="vibe-prompt__link" style={{
  display: "inline-flex",
  alignItems: "center",
  gap: "0.4rem",
  padding: "0.4rem 0.75rem",
  borderRadius: "8px",
  border: "1px solid var(--vanta-border, rgba(120, 120, 130, 0.25))",
  fontSize: "0.8rem",
  fontWeight: 500,
  textDecoration: "none",
  color: "inherit",
  background: "transparent"
}}>
          <Icon icon="arrow-up-right-from-square" iconType="regular" size={11} color="#5E05C4" /><span>Cursor</span>
        </a>
        <a href="#" onClick={vibePromptOpen} data-target="claude" data-prompt={prompt} data-page-title={pageTitle} className="vibe-prompt__link" style={{
  display: "inline-flex",
  alignItems: "center",
  gap: "0.4rem",
  padding: "0.4rem 0.75rem",
  borderRadius: "8px",
  border: "1px solid var(--vanta-border, rgba(120, 120, 130, 0.25))",
  fontSize: "0.8rem",
  fontWeight: 500,
  textDecoration: "none",
  color: "inherit",
  background: "transparent"
}}>
          <Icon icon="arrow-up-right-from-square" iconType="regular" size={11} color="#5E05C4" /><span>Claude</span>
        </a>
        <a href="#" onClick={vibePromptOpen} data-target="chatgpt" data-prompt={prompt} data-page-title={pageTitle} className="vibe-prompt__link" style={{
  display: "inline-flex",
  alignItems: "center",
  gap: "0.4rem",
  padding: "0.4rem 0.75rem",
  borderRadius: "8px",
  border: "1px solid var(--vanta-border, rgba(120, 120, 130, 0.25))",
  fontSize: "0.8rem",
  fontWeight: 500,
  textDecoration: "none",
  color: "inherit",
  background: "transparent"
}}>
          <Icon icon="arrow-up-right-from-square" iconType="regular" size={11} color="#5E05C4" /><span>ChatGPT</span>
        </a>
      </div>
    </div>
  </div>;

export const VibePrompts = ({children}) => <section className="vibe-prompts" style={{
  marginTop: "2.5rem",
  marginBottom: "2rem",
  padding: "1.5rem 1.5rem 1.25rem",
  borderRadius: "16px",
  border: "1px solid color-mix(in srgb, #5E05C4 22%, transparent)",
  background: "linear-gradient(180deg, color-mix(in srgb, #5E05C4 6%, transparent) 0%, transparent 100%)"
}}>
    <div style={{
  display: "flex",
  alignItems: "center",
  gap: "0.65rem",
  marginBottom: "0.25rem"
}}>
      <div style={{
  display: "inline-flex",
  alignItems: "center",
  justifyContent: "center",
  width: "32px",
  height: "32px",
  borderRadius: "9px",
  background: "color-mix(in srgb, #5E05C4 14%, transparent)"
}}>
        <Icon icon="wand-magic-sparkles" iconType="regular" size={16} color="#5E05C4" />
      </div>
      <div>
        <div className="text-gray-900 dark:text-white" style={{
  fontFamily: "Reckless, Georgia, serif",
  fontSize: "1.25rem",
  fontWeight: 500,
  lineHeight: 1.15
}}>
          Let AI do this for you
        </div>
        <div className="text-gray-600 dark:text-gray-300" style={{
  fontSize: "0.85rem",
  lineHeight: 1.45,
  marginTop: "0.15rem"
}}>
          Copy the prompt to run this live via the <a href="/docs/vanta-mcp" style={{
  color: "#5E05C4"
}}> MCP server</a> or have AI generate a runnable script.
        </div>
      </div>
    </div>
    {children}
  </section>;

export const WEBHOOK_BUILD = "You are writing a production-quality Node.js 18+ webhook receiver using Express that ingests Vanta webhook events, verifies their signatures, and processes them idempotently. Vanta delivers webhooks through Svix, so use the official svix library to verify signatures.\n\nSteps:\n\n1. Read VANTA_WEBHOOK_SIGNING_SECRET (the endpoint signing secret, starts with whsec_) and PORT (default 3000) from env. Fail fast on stderr if the signing secret is missing — never hard-code it.\n2. Use only express and svix (npm i express svix). On the webhook route, use express.raw({ type: 'application/json' }) so the RAW request body is preserved as a Buffer. Signature verification fails if the body has been parsed or re-serialized, so do NOT use express.json() on this route.\n3. Expose POST /webhooks. Build a Svix Webhook from the signing secret and call wh.verify(rawBody, headers), passing the svix-id, svix-timestamp, and svix-signature headers. If verification throws, respond 400 and stop.\n4. Deduplicate on the svix-id header (delivery is at-least-once). Keep a bounded in-memory Set (or a persistent store in production) of processed svix-id values; if the id was already seen, respond 200 and stop without reprocessing.\n5. Acknowledge fast: respond 200 within 15 seconds, BEFORE doing any slow work. Hand the verified event to an async queue or handler — do not block the HTTP response on processing.\n6. In the async handler, branch on the event type (for example v1.questionnaire.status-changed, v1.vendor.decision.created, v1.trust-center.access-request.approved). The verified JSON body matches the payload documented for that event type in the Vanta webhook event reference. Treat all IDs as opaque strings; to fetch the full object, call the matching Manage Vanta API endpoint.\n7. Log one structured line per event: svix-id | event type | outcome (processed / duplicate / error). Never log the signing secret.\n8. Add a GET /healthz route that returns 200 for liveness checks.\n\nMissing-field handling: if an expected payload field is absent, log a warning and continue — never throw inside the request handler in a way that prevents the 200 acknowledgement.\n\nError handling:\n- Invalid signature: respond 400, log the svix-id, do not process.\n- Processing failure in the async handler: log it and rely on Vanta's automatic retries — do NOT return a non-2xx after you have already acknowledged.\n- Malformed or oversized body: respond 400.\n\nDo not:\n- Add dependencies beyond express and svix.\n- Use express.json() on the webhook route, or verify against a parsed or re-serialized body.\n- Block the 200 response on downstream processing.\n- Hard-code or log the signing secret.\n- Disable signature verification in production.\n\nDone when: a Svix \"Send Example\" test event is received on POST /webhooks, passes signature verification, is acknowledged with 200 within 15 seconds, processed exactly once (a redelivery with the same svix-id is skipped), and logged.\n\nRequires: an HTTPS-reachable endpoint and the endpoint signing secret from Settings → Webhooks.";

This guide sets up a webhook integration so your server receives real-time `POST` notifications when events happen in Vanta instead of polling the API. You'll register an endpoint, implement a handler that verifies signatures, and confirm an event end to end.

<VibePrompts>
  <BuildPrompt prompt={WEBHOOK_BUILD} />
</VibePrompts>

## Before you begin

This guide is for developers building a server that receives Vanta webhooks.

You'll need:

* A publicly accessible **HTTPS** endpoint on your server to receive webhook `POST` requests.
* Access to **Settings → Webhooks** in the Vanta dashboard, to register the endpoint and copy its signing secret.
* The official [Svix](https://www.svix.com/) library for your language — recommended for signature verification.

<Info>
  Webhooks are powered by [Svix](https://www.svix.com/), an enterprise webhook delivery platform, so you get automatic retries, delivery guarantees, and signature verification out of the box. For the full event catalog, payloads, and schemas, see the [Webhook event reference](/reference/webhooks/overview).
</Info>

<Steps>
  <Step title="Register your endpoint">
    In the Vanta dashboard, register the URL that will receive events. An endpoint is a URL on your server that receives webhook `POST` requests from Vanta.

    1. Navigate to **Settings → Webhooks**.
    2. Click **Add Endpoint**.
    3. Enter your endpoint URL (must be HTTPS).
    4. Browse the available event types and select the ones relevant to your integration — see the [Webhook event reference](/reference/webhooks/overview) for the full catalog of events, payloads, and schemas. Leave the selection blank to receive all events.
    5. Click **Create** to register the endpoint.

    Then open the endpoint and copy its **Signing Secret** (it starts with `whsec_`) from the **Signing Secret** section — you'll use it to verify signatures in the next steps.
  </Step>

  <Step title="Implement your handler">
    Implement a server-side handler that can receive the webhook request you just configured.

    <Warning>
      Signature verification requires the **raw request body** as a string, not a parsed object. Make sure your framework preserves the raw body on the webhook route. For example, in Express use `express.raw({ type: 'application/json' })` instead of `express.json()`.
    </Warning>

    Keep the following in mind as you build:

    <AccordionGroup>
      <Accordion title="Expose your endpoint over HTTPS">
        Your endpoint must be publicly accessible over HTTPS.
      </Accordion>

      <Accordion title="Respond with a 2xx within 15 seconds">
        Return a `2xx` status code within 15 seconds to acknowledge receipt. If you don't, the delivery is marked as failed and [retried](/reference/webhooks/overview#retry-schedule).
      </Accordion>

      <Accordion title="Disable CSRF protection on the webhook route">
        Webhook requests won't include CSRF tokens, so CSRF protection must be disabled for the webhook endpoint.
      </Accordion>

      <Accordion title="Process payloads asynchronously">
        Return a `2xx` immediately, then handle the event in a background job or queue. This prevents timeouts on long-running operations.
      </Accordion>

      <Accordion title="Implement idempotent handling">
        Webhook delivery is "at least once," so your endpoint may receive the same event more than once. Use the `svix-id` header to deduplicate events.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Verify webhook signatures">
    Webhook signatures let you confirm that messages are actually sent by Vanta and not a malicious third party. Verification isn't strictly required, but **always verify signatures in production**.

    Each webhook message includes `svix-id`, `svix-timestamp`, and `svix-signature` headers used for verification — see [Delivery format](/reference/webhooks/overview#delivery-format) in the event reference for what each header contains.

    The simplest way to verify signatures is to use the official Svix libraries. Install the library for your language and use the `Webhook.verify` method.

    <Tip>
      You can find your endpoint's signing secret in the Vanta webhook dashboard by clicking the endpoint and looking in the **Signing Secret** section.
    </Tip>

    <CodeGroup>
      ```javascript Node.js theme={"system"}
      import { Webhook } from "svix";

      const secret = "whsec_..."; // Your signing secret

      const wh = new Webhook(secret);

      app.post("/webhook", (req, res) => {
        try {
          const payload = wh.verify(req.body, req.headers);
          // payload is the verified JSON body
          console.log("Verified webhook:", payload);
          res.status(200).send("OK");
        } catch (err) {
          console.error("Verification failed:", err.message);
          res.status(400).send("Invalid signature");
        }
      });
      ```

      ```python Python theme={"system"}
      from svix.webhooks import Webhook

      secret = "whsec_..."  # Your signing secret

      wh = Webhook(secret)

      def handle_webhook(request):
          try:
              payload = wh.verify(request.body, request.headers)
              # payload is the verified dict
              print("Verified webhook:", payload)
              return HttpResponse(status=200)
          except Exception as e:
              print("Verification failed:", e)
              return HttpResponse(status=400)
      ```

      ```go Go theme={"system"}
      import svix "github.com/svix/svix-webhooks/go"

      secret := "whsec_..." // Your signing secret

      wh, _ := svix.NewWebhook(secret)

      func handleWebhook(w http.ResponseWriter, r *http.Request) {
          payload, err := io.ReadAll(r.Body)
          if err != nil {
              http.Error(w, "Bad request", http.StatusBadRequest)
              return
          }

          err = wh.Verify(payload, r.Header)
          if err != nil {
              http.Error(w, "Invalid signature", http.StatusBadRequest)
              return
          }

          // payload is verified
          w.WriteHeader(http.StatusOK)
      }
      ```

      ```ruby Ruby theme={"system"}
      require "svix"

      secret = "whsec_..." # Your signing secret

      wh = Svix::Webhook.new(secret)

      post "/webhook" do
        begin
          payload = wh.verify(request.body.read, request.env)
          # payload is the verified hash
          puts "Verified webhook: #{payload}"
          status 200
        rescue Svix::WebhookVerificationError => e
          puts "Verification failed: #{e.message}"
          status 400
        end
      end
      ```
    </CodeGroup>

    If you prefer not to use a library, you can verify signatures manually:

    1. Extract the `svix-id`, `svix-timestamp`, and `svix-signature` headers.
    2. Concatenate `{svix-id}.{svix-timestamp}.{body}` (the raw request body as a string).
    3. Base64-decode the signing secret (remove the `whsec_` prefix first).
    4. Compute an HMAC-SHA256 of the signed content using the decoded secret.
    5. Base64-encode the result and compare it against the signature(s) in the `svix-signature` header (split by space, each prefixed with `v1,`).

    <Warning>
      Also verify that the `svix-timestamp` is recent (within 5 minutes) to prevent replay attacks.
    </Warning>
  </Step>

  <Step title="Test your endpoint">
    Before going to production, confirm your endpoint can receive and process webhooks correctly.

    1. Go to **Settings → Webhooks** in the Vanta dashboard.
    2. Select the endpoint you want to test.
    3. Navigate to the **Testing** tab.
    4. Choose an event type and click **Send Example**.

    This sends a test message with an example payload to your endpoint, letting you confirm your server verifies and handles it correctly.

    <AccordionGroup>
      <Accordion title="Webhook requests are failing with 4xx errors">
        * Verify that your endpoint URL is correct and publicly accessible over HTTPS.
        * Ensure that CSRF protection is disabled for the webhook endpoint.
        * Check that your server is returning a `2xx` status code.
      </Accordion>

      <Accordion title="Signature verification is failing">
        * Make sure you are using the **raw request body** (not a parsed JSON object) when verifying the signature.
        * Confirm that the signing secret matches the one displayed in the webhook dashboard.
        * Check that you haven't accidentally modified or re-serialized the request body before verification.
      </Accordion>

      <Accordion title="Webhook requests are timing out">
        Your endpoint must respond within 15 seconds. If your processing takes longer, acknowledge the webhook immediately with a `200` response and handle the event asynchronously in a background job or queue.
      </Accordion>

      <Accordion title="Recovering missed events">
        If your endpoint was down for an extended period, recover missed events through the webhook dashboard:

        1. Go to **Settings → Webhooks**.
        2. Select the affected endpoint.
        3. Browse the message history to find failed deliveries.
        4. Click **Retry** on individual messages, or use **Bulk Retry** to replay all failed messages within a time range.
      </Accordion>
    </AccordionGroup>
  </Step>
</Steps>

## Congratulations

You've built a webhook integration. Your endpoint now receives verified, real-time events from Vanta, acknowledges them within the retry window, and processes them idempotently — no polling required. As you add event types to your subscription, consult the [Webhook event reference](/reference/webhooks/overview) for each one's payload and schema.

## Next steps

<CardGroup cols={2}>
  <Card title="Webhook event reference" icon="webhook" href="/reference/webhooks/overview">
    Browse every event type, with payloads, schemas, and examples.
  </Card>

  <Card title="Manage Vanta" icon="bolt" href="/docs/quickstart/manage-vanta">
    Use webhooks alongside the Manage Vanta API to react to events in real time.
  </Card>

  <Card title="Build an Integration" icon="plug" href="/docs/quickstart/build-integration">
    Become a Vanta partner and push resources into customers' Vanta accounts.
  </Card>
</CardGroup>
