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

ModuleCovers
Zep.Threadlist, create, delete, get/add messages, user context, rolling summary
Zep.Thread.Messageupdate an individual message by uuid
Zep.Useradd, get, update, delete, list_ordered, get_threads, summary instructions
Zep.Contextcontext-rendering templates (list/get/create/update/delete)
Zep.Graphcreate/get/update/delete/list, add, add_batch, add_fact_triple, clone, search, ontology, pattern detection, cache warming
Zep.Graph.Edgeindividual fact/edge CRUD
Zep.Graph.Episodeindividual episode CRUD + streaming
Zep.Graph.Nodeindividual node CRUD + connected edges
Zep.Graph.CustomInstructionsper-graph extraction instructions
Zep.Graph.Observationsstandalone timestamped graph notes
Zep.Graph.ThreadSummariesthread summaries folded into a graph
Zep.Projectcurrent project settings
Zep.Taskasync task status + polling (await/3)
Zep.Batchbulk ingestion jobs: create → add → process → poll/list
Zep.Webhookverify 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