Zep
A complete, production-grade Elixir client for the Zep
AI memory API — Threads, Users, Context Templates, the temporal Knowledge
Graph, Batch ingestion, Projects, and Tasks — with typed structs, automatic
retries, :telemetry instrumentation, and a strict credo --strict /
dialyzer toolchain.
Installation
def deps do
[
{:zep, "~> 1.0.0"}
]
end
Configuration
# config/config.exs
config :zep,
api_key: System.get_env("ZEP_API_KEY"),
base_url: "https://api.getzep.com",
receive_timeout: 30_000,
max_retries: 2
Or build a client explicitly (useful for multi-tenant apps talking to more than one Zep project):
client = Zep.Client.new(api_key: "z_...")
Every resource function accepts either a %Zep.Client{} or a plain keyword
list of client options as its first argument — the latter resolves
configuration from Application env / the ZEP_API_KEY environment
variable on each call. See Zep.Config for the full precedence order.
Quick start
client = Zep.Client.new(api_key: "z_...")
{:ok, user} = Zep.User.add(client, "user-1", first_name: "Ada", last_name: "Lovelace")
{:ok, thread} = Zep.Thread.create(client, "thread-1", "user-1")
{:ok, _} =
Zep.Thread.add_messages(client, "thread-1", [
%{role: "user", content: "Hi, I'm Ada. I love analytical engines."}
])
{:ok, %{context: context}} = Zep.Thread.get_user_context(client, "thread-1")
Resources
| Module | Covers |
|---|---|
Zep.Thread | list, create, delete, get/add messages, user context, rolling summary |
Zep.Thread.Message | update an individual message by uuid |
Zep.User | add, get, update, delete, list_ordered, get_threads, summary instructions |
Zep.Context | context-rendering templates (list/get/create/update/delete) |
Zep.Graph | create/get/update/delete/list, add, add_batch, add_fact_triple, clone, search, ontology, pattern detection, cache warming |
Zep.Graph.Edge | individual fact/edge CRUD |
Zep.Graph.Episode | individual episode CRUD + streaming |
Zep.Graph.Node | individual node CRUD + connected edges |
Zep.Graph.CustomInstructions | per-graph extraction instructions |
Zep.Graph.Observations | standalone timestamped graph notes |
Zep.Graph.ThreadSummaries | thread summaries folded into a graph |
Zep.Project | current project settings |
Zep.Task | async task status + polling (await/3) |
Zep.Batch | bulk ingestion jobs: create → add → process → poll/list |
Zep.Webhook | verify incoming Zep webhook deliveries (HMAC-SHA256 / Svix) |
Pagination
Paginated list endpoints (Zep.Thread.list/2, Zep.User.list_ordered/2,
Zep.Graph.list_all/2) each have a lazy stream/2 counterpart that walks
every page automatically:
Zep.Thread.stream(client, page_size: 100)
|> Stream.filter(&(&1.user_id == "user-1"))
|> Enum.to_list()
Error handling
Every function returns {:ok, result} or {:error, error}, where error
is either a %Zep.Error{} (the API responded with a non-2xx status) or a
%Zep.TransportError{} (the request never reached the API):
case Zep.Thread.get_summary(client, "thread-1") do
{:ok, summary} -> summary
{:error, %Zep.Error{reason: :not_found}} -> nil
{:error, %Zep.Error{reason: :forbidden}} -> {:error, :plan_upgrade_required}
{:error, error} -> raise error
end
Zep.Error implements the Exception behaviour, so it can also be raised
directly. 429 and 5xx responses are retried automatically with exponential
backoff (:max_retries, default 2); 4xx errors are not retried.
Telemetry
Every request emits [:zep, :request, :start | :stop | :exception] events
via :telemetry.span/3, tagged with :resource and :action metadata —
attach a handler for logging, metrics, or tracing:
:telemetry.attach(
"zep-logger",
[:zep, :request, :stop],
fn _event, measurements, metadata, _config ->
Logger.info("zep.#{metadata.resource}.#{metadata.action} -> #{metadata.status} (#{measurements.duration})")
end,
nil
)
Batch ingestion
The Batch API is a three-step lifecycle - create an empty batch, add up to 500 items per call (up to 50,000 per batch), then start processing:
{:ok, batch} = Zep.Batch.create(client, metadata: %{description: "Support backfill"})
{:ok, _} =
Zep.Batch.add(client, batch.batch_id, [
%{type: :graph_episode, user_id: "alice", data: "Alice upgraded to Pro.", data_type: :text},
%{type: :thread_message, thread_id: "alice-support-42", content: "Dashboard won't load.", role: "user", name: "Alice"}
])
{:ok, _} = Zep.Batch.process(client, batch.batch_id)
{:ok, final} = Zep.Batch.await(client, batch.batch_id)
Zep.Graph.add_batch/3 remains fine for small (≤20 episodes), same-graph,
order-independent batches. Zep.Thread.add_messages_batch/4 and the
pre-2026 single-call Zep.Graph.add_batch/3 for large jobs are both
deprecated in favor of Zep.Batch for anything larger or mixing
destinations. For batches with thousands of items, prefer subscribing to
the ingest.batch.completed webhook over polling await/3.
Webhooks
Zep signs webhook deliveries via Svix (HMAC-SHA256). Endpoint management
(creating/rotating endpoints) happens in the Zep dashboard; Zep.Webhook
verifies deliveries your server receives:
{:ok, raw_body, conn} = Plug.Conn.read_body(conn)
headers = %{
"svix-id" => List.first(Plug.Conn.get_req_header(conn, "svix-id")),
"svix-timestamp" => List.first(Plug.Conn.get_req_header(conn, "svix-timestamp")),
"svix-signature" => List.first(Plug.Conn.get_req_header(conn, "svix-signature"))
}
case Zep.Webhook.verify(raw_body, headers, signing_secret) do
:ok -> handle_event(Jason.decode!(raw_body))
{:error, reason} -> send_resp(conn, 400, "invalid signature: #{reason}")
end
Always verify against the raw request body - many web frameworks parse JSON before your handler runs, which breaks verification.
Development
mix deps.get
mix test
mix credo --strict
mix dialyzer