> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs-beta.getzep.com/v3/webhooks/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs-beta.getzep.com/_mcp/server. # Webhooks Webhooks allow your application to receive real-time notifications when specific events occur in Zep, such as when an episode finishes processing or a batch ingestion completes. Instead of polling for changes, Zep pushes event data directly to your server as HTTP POST requests. > **Note** > > Webhooks are only available on certain plans. See the [Zep pricing page](https://www.getzep.com/pricing) for details. ## Why use webhooks Webhooks enable event-driven architectures where your application reacts immediately to changes: * **Episode processed notifications:** Trigger downstream processing when new data is added to a graph * **Batch completion alerts:** Know when large data imports finish so you can start using the data * **BYOM monitoring:** Receive aggregated alerts when your LLM credentials or provider requests fail * **Reduced polling:** Eliminate the need to continuously check for updates ## Setting up webhooks Webhooks are configured per project within the Webhooks page in the Zep Dashboard sidebar. ### Navigate to webhooks Open the Webhooks page from the sidebar in the Zep Dashboard. ### Create an endpoint Add a new endpoint by providing: * **Endpoint URL:** The HTTPS URL on your server that will receive webhook events * **Subscribed events:** Select which events you want to receive (e.g., `episode.processed`, `ingest.batch.completed`) ### Save your signing secret After creating an endpoint, you'll see a signing secret. Copy and securely store this secret—you'll need it to verify that incoming webhooks are genuinely from Zep. ## Available events ### Graph events | Event | Description | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `episode.processed` | Fired when an episode finishes processing and is added to the graph | | `ingest.batch.completed` | Fired when a [Batch API](/adding-batch-data) batch completes. The deprecated `graph.add_batch` and `thread.add_messages_batch` methods also create Batch API batches. See [Batch completion payloads](#batch-completion-payloads) for when Zep sends this event | ### BYOM events These events apply to [Bring Your Own LLM (BYOM)](/bring-your-own-llm) configurations. They report credential and provider failures for requests that use your credentials. Zep aggregates BYOM webhook events instead of sending one webhook per request. Zep reports occurrences from a 60-second window in one webhook. | Event | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `byom.request_failed` | Fired when BYOM requests fail due to credential or provider issues (invalid credentials, expired credentials, provider unavailable, unsupported model) | ## Webhook payload schemas ### Episode processed payload `episode.processed` events include the following fields: | Field | Description | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `event_name` | The type of event (`episode.processed`) | | `account_uuid` | Your Zep account identifier | | `project_uuid` | The project where the event occurred | | `episode_uuid` | The UUID of the episode that finished processing | | `source` | The source of the episode (e.g., `text`, `json`, `message`) | | `completed_at` | RFC3339 timestamp of when processing completed | | `graph_type` | Either `user` or `graph`, indicating the graph type | | `graph_uuid` | The canonical UUID of the graph. Present in every payload that identifies a graph. The value matches `graph_uuid` on the user for a user graph, and `canonical_graph_uuid` on the graph for a named graph. It does not match the `uuid` field of a graph. | | `user_uuid` | The `uuid` of the user. Present only when `graph_type` is `user`. | | `graph_id` | The graph identifier\* | | `user_id` | The user identifier\* | \*A payload for a user graph can include `user_id`. A payload for a graph with the `graph` type can include `graph_id`. Zep sends each field only when the user or graph has a developer-assigned identifier. A user or graph that the v4 API creates has no developer-assigned identifier. Its payload identifies the graph with `graph_uuid` and, for a user graph, with `user_uuid`. ### Batch completion payloads Zep sends one payload for every batch, including a batch that a deprecated batch method creates. #### Batch API payload Sent when a batch created via the [Batch API](/adding-batch-data) finishes as `succeeded`, `canceled`, or `partial` from canceled items. Zep does not send this payload when a run ends as `failed`, or as `partial` because items failed. The `batch_id` matches the value returned by `batch.create()`. | Field | Description | | -------------- | -------------------------------------------------------------------------- | | `event_name` | `ingest.batch.completed` | | `account_uuid` | Your Zep account identifier | | `project_uuid` | The project where the event occurred | | `batch_id` | The UUID of the completed batch (matches `batch.create()` / `batch.get()`) | | `completed_at` | RFC3339 timestamp of when the worker finished rollup | Example payload: ```json { "event_name": "ingest.batch.completed", "account_uuid": "1811f9c5-336c-402a-964b-1a62faddc9c0", "project_uuid": "a3627577-88b1-46ed-bcb9-0e90ff32dba3", "batch_id": "52caee86-759e-4bc2-8d20-c5c1a8212368", "completed_at": "2026-05-05T18:16:34Z" } ``` #### Deprecated batch methods The deprecated `graph.add_batch()` and `thread.add_messages_batch()` methods create a Batch API batch. When that batch completes, Zep sends the Batch API payload above. The payload does not include `task_id`, `episode_uuids`, or graph identifiers. To match the payload to a call, compare `batch_id` with the `batch_uuid` value in the `params` of the task that the call returns. ### BYOM event payload `byom.request_failed` events include the following fields: | Field | Description | | ------------------ | ---------------------------------------------------------- | | `event_name` | The event type (`byom.request_failed`) | | `account_uuid` | Your Zep account identifier | | `project_uuid` | The project where the event occurred | | `provider` | The LLM provider (e.g., `openai`, `anthropic`) | | `model` | The model name (e.g., `gpt-5.6-terra`, `claude-sonnet-5`) | | `count` | Number of occurrences in the aggregation window | | `first_occurrence` | ISO 8601 timestamp of the first event in the window | | `last_occurrence` | ISO 8601 timestamp of the last event in the window | | `window_seconds` | Duration of the aggregation window in seconds (minimum 60) | | `error_code` | The specific error code (`byom.request_failed` only) | #### BYOM error codes The `error_code` field in `byom.request_failed` events indicates the specific failure reason: | Error code | Description | | --------------------------- | ------------------------------------------------------------------- | | `BYOM_CREDENTIALS_INVALID` | The provided LLM credentials are invalid or missing required fields | | `BYOM_CREDENTIALS_EXPIRED` | The provided LLM credentials have expired | | `BYOM_PROVIDER_UNAVAILABLE` | The LLM provider is unavailable or unreachable | | `BYOM_MODEL_NOT_SUPPORTED` | The requested model is not supported by the provider | ## Receiving webhooks Your webhook endpoint must: * Accept HTTP POST requests * Return a `2xx` status code before the request times out * Disable CSRF protection for the webhook route if your framework enables it by default **`Python`** ```python Python from flask import Flask, request app = Flask(__name__) @app.route("/webhooks/zep", methods=["POST"]) def handle_webhook(): payload = request.get_data(as_text=True) headers = request.headers # Verify the webhook signature (see next section) # Process the event event = request.json event_name = event.get("event_name") if event_name == "episode.processed": # Handle episode processed event pass elif event_name == "ingest.batch.completed": # Handle batch completion event pass elif event_name == "byom.request_failed": # Handle BYOM request failure error_code = event.get("error_code") pass return "", 200 ``` **`TypeScript`** ```typescript TypeScript import express from "express"; const app = express(); // Important: Use raw body for signature verification app.post("/webhooks/zep", express.raw({ type: "application/json" }), (req, res) => { const payload = req.body.toString(); const headers = req.headers; // Verify the webhook signature (see next section) // Process the event const event = JSON.parse(payload); const eventName = event.event_name; if (eventName === "episode.processed") { // Handle episode processed event } else if (eventName === "ingest.batch.completed") { // Handle batch completion event } else if (eventName === "byom.request_failed") { // Handle BYOM request failure const errorCode = event.error_code; } res.status(200).send(); }); ``` **`Go`** ```go Go package main import ( "encoding/json" "io" "net/http" ) func handleWebhook(w http.ResponseWriter, r *http.Request) { payload, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Error reading body", http.StatusBadRequest) return } // Verify the webhook signature (see next section) // Process the event var event map[string]interface{} json.Unmarshal(payload, &event) eventName := event["event_name"].(string) switch eventName { case "episode.processed": // Handle episode processed event case "ingest.batch.completed": // Handle batch completion event case "byom.request_failed": // Handle BYOM request failure errorCode := event["error_code"].(string) _ = errorCode } w.WriteHeader(http.StatusOK) } ``` ## Verifying webhook signatures > **Warning** > > Verifying webhook signatures is **recommended**. Without verification, attackers could send fake HTTP POST requests to your endpoint, potentially causing your application to process fraudulent data. Zep signs every webhook with a cryptographic signature using your endpoint's signing secret. Verifying this signature ensures that: * The webhook genuinely came from Zep * The payload hasn't been tampered with in transit ### Using the Svix libraries (recommended) Zep uses [Svix](https://www.svix.com/) to manage webhooks. The easiest way to verify signatures is with the Svix client libraries. First, install the Svix library: **`Python`** ```bash Python pip install svix ``` **`TypeScript`** ```bash TypeScript npm install svix ``` **`Go`** ```bash Go go get github.com/svix/svix-webhooks/go ``` Then verify incoming webhooks: **`Python`** ```python Python from svix.webhooks import Webhook # Your signing secret from the Zep Dashboard WEBHOOK_SECRET = "whsec_..." def verify_webhook(payload: str, headers: dict) -> dict: wh = Webhook(WEBHOOK_SECRET) # This will raise an exception if verification fails return wh.verify(payload, headers) # In your webhook handler: @app.route("/webhooks/zep", methods=["POST"]) def handle_webhook(): payload = request.get_data(as_text=True) headers = { "svix-id": request.headers.get("svix-id"), "svix-timestamp": request.headers.get("svix-timestamp"), "svix-signature": request.headers.get("svix-signature"), } try: event = verify_webhook(payload, headers) # Process the verified event return "", 200 except Exception as e: print(f"Webhook verification failed: {e}") return "", 400 ``` **`TypeScript`** ```typescript TypeScript import { Webhook } from "svix"; // Your signing secret from the Zep Dashboard const WEBHOOK_SECRET = "whsec_..."; function verifyWebhook(payload: string, headers: Record): any { const wh = new Webhook(WEBHOOK_SECRET); // This will throw if verification fails return wh.verify(payload, headers); } // In your webhook handler: app.post("/webhooks/zep", express.raw({ type: "application/json" }), (req, res) => { const payload = req.body.toString(); const headers = { "svix-id": req.headers["svix-id"] as string, "svix-timestamp": req.headers["svix-timestamp"] as string, "svix-signature": req.headers["svix-signature"] as string, }; try { const event = verifyWebhook(payload, headers); // Process the verified event res.status(200).send(); } catch (err) { console.error("Webhook verification failed:", err); res.status(400).send(); } }); ``` **`Go`** ```go Go package main import ( "io" "net/http" svix "github.com/svix/svix-webhooks/go" ) // Your signing secret from the Zep Dashboard var webhookSecret = "whsec_..." func handleWebhook(w http.ResponseWriter, r *http.Request) { payload, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Error reading body", http.StatusBadRequest) return } wh, err := svix.NewWebhook(webhookSecret) if err != nil { http.Error(w, "Error creating webhook verifier", http.StatusInternalServerError) return } err = wh.Verify(payload, r.Header) if err != nil { http.Error(w, "Webhook verification failed", http.StatusBadRequest) return } // Process the verified event w.WriteHeader(http.StatusOK) } ``` > **Info** > > The verification process requires the **raw request body** exactly as received. Many web frameworks automatically parse JSON bodies, which can break signature verification. Make sure to access the raw body before any parsing middleware runs. ### Manual verification If you prefer not to use the Svix libraries, you can verify signatures manually using HMAC-SHA256. Every webhook includes three headers for verification: * `svix-id`: Unique message identifier * `svix-timestamp`: Unix timestamp (seconds since epoch) * `svix-signature`: Base64-encoded signatures (may include multiple, comma-separated) ### Construct the signed content Concatenate the `svix-id`, `svix-timestamp`, and the raw request body, separated by periods: ``` {svix-id}.{svix-timestamp}.{raw-body} ``` ### Calculate the expected signature Use HMAC-SHA256 with your signing secret (base64-decoded, excluding the `whsec_` prefix) to sign the content: ```javascript const crypto = require('crypto'); const signedContent = `${svixId}.${svixTimestamp}.${rawBody}`; const secret = "whsec_..."; const secretBytes = Buffer.from(secret.split('_')[1], "base64"); const expectedSignature = crypto .createHmac('sha256', secretBytes) .update(signedContent) .digest('base64'); ``` ### Compare signatures The `svix-signature` header may contain multiple signatures prefixed with version numbers (e.g., `v1,abc123`). Remove the version prefix and compare against your calculated signature. > **Warning** > > Use constant-time string comparison to prevent timing attacks. ### Validate the timestamp Compare the `svix-timestamp` against your server's current time. Reject webhooks with timestamps more than 5 minutes old to prevent replay attacks. For more details on manual verification, see the [Svix documentation on manual verification](https://docs.svix.com/receiving/verifying-payloads/how-manual). ## Managing webhooks The Webhooks tab in the Zep Dashboard provides several management features: * **Disable/Enable:** Temporarily stop receiving events without deleting your endpoint configuration * **Activity logs:** View the history of webhook deliveries and their status * **Replay messages:** Re-send failed webhooks for debugging or recovery * **Rate limiting:** Configure throttling to control the rate of incoming webhooks * **Delete:** Remove an endpoint entirely > **Note** > > Webhook configuration is **project-specific**. Each project has its own set of webhook endpoints and subscriptions. If you have multiple projects, you'll need to configure webhooks separately for each one. > **Info** > > Changes to webhook configuration (including creating, updating, or deleting endpoints) take **5-10 minutes to propagate** and take effect. This delay is due to configuration caching. ## Pricing Webhook deliveries consume credits. See the [Zep pricing page](https://www.getzep.com/pricing) for costs. ## Best practices * **Always verify signatures:** Treat unverified webhooks as potentially malicious * **Acknowledge the request:** Return a `2xx` response before the request times out * **Process asynchronously:** If handling takes longer than a few seconds, acknowledge receipt immediately and process the event in a background job * **Handle duplicates:** Webhooks may occasionally be delivered more than once; use the `svix-id` header to deduplicate * **Monitor failures:** Check the activity logs in the Dashboard to identify and fix delivery issues ## Further reading For additional information on consuming webhooks: * [Why verify webhooks](https://docs.svix.com/receiving/verifying-payloads/why) - Security considerations explained * [Svix webhook verification libraries](https://docs.svix.com/receiving/verifying-payloads/how) - Full library documentation * [Manual verification guide](https://docs.svix.com/receiving/verifying-payloads/how-manual) - Detailed manual verification steps > Receive real-time notifications when events occur in Zep