diff --git a/docs/scripts/api.py b/docs/scripts/api.py
new file mode 100755
index 000000000..e2af27450
--- /dev/null
+++ b/docs/scripts/api.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+import os
+import shutil
+from pathlib import Path
+
+import pdoc.render
+
+here = Path(__file__).parent
+
+if os.environ.get("DOCS_ARCHIVE", False):
+ edit_url_map = {}
+else:
+ edit_url_map = {
+ "mitmproxy": "https://github.com/mitmproxy/mitmproxy/blob/master/mitmproxy/",
+ }
+
+pdoc.render.configure(
+ template_directory=here / "pdoc-template",
+ edit_url_map=edit_url_map,
+)
+
+modules = [
+ "mitmproxy.proxy.context",
+ "mitmproxy.http",
+ "mitmproxy.flow",
+ "mitmproxy.tcp",
+ "mitmproxy.websocket",
+]
+
+pdoc.pdoc(
+ *modules,
+ output_directory=here / ".." / "src" / "generated" / "api"
+)
+
+api_content = here / ".." / "src" / "content" / "api"
+if api_content.exists():
+ shutil.rmtree(api_content)
+
+api_content.mkdir()
+
+for module in modules:
+ filename = f"api/{ module.replace('.','/') }.html"
+ (api_content / f"{module}.md").write_text(f"""
+---
+title: "{module}"
+url: "{filename}"
+
+menu:
+ addons:
+ parent: 'API Reference'
+---
+
+{{{{< readfile file="/generated/{filename}" >}}}}
+""")
+
+(api_content / f"_index.md").write_text(f"""
+---
+title: "API Reference"
+layout: single
+menu:
+ addons:
+ weight: 5
+---
+
+# API Reference
+""")
diff --git a/docs/scripts/pdoc-template/frame.html.jinja2 b/docs/scripts/pdoc-template/frame.html.jinja2
new file mode 100644
index 000000000..576dd761b
--- /dev/null
+++ b/docs/scripts/pdoc-template/frame.html.jinja2
@@ -0,0 +1,2 @@
+{% block style %}{% endblock %}
+{% block body %}{% endblock %}
diff --git a/docs/scripts/pdoc-template/module.html.jinja2 b/docs/scripts/pdoc-template/module.html.jinja2
new file mode 100644
index 000000000..728a9b4dc
--- /dev/null
+++ b/docs/scripts/pdoc-template/module.html.jinja2
@@ -0,0 +1,3 @@
+{% extends "default/module.html.jinja2" %}
+{% block nav %}{% endblock %}
+{% block style_layout %}{% endblock %}
diff --git a/docs/src/content/api/_index.md b/docs/src/content/api/_index.md
new file mode 100644
index 000000000..911e96639
--- /dev/null
+++ b/docs/src/content/api/_index.md
@@ -0,0 +1,10 @@
+
+---
+title: "API Reference"
+layout: single
+menu:
+ addons:
+ weight: 5
+---
+
+# API Reference
diff --git a/docs/src/content/api/mitmproxy.flow.md b/docs/src/content/api/mitmproxy.flow.md
new file mode 100644
index 000000000..0837998c1
--- /dev/null
+++ b/docs/src/content/api/mitmproxy.flow.md
@@ -0,0 +1,11 @@
+
+---
+title: "mitmproxy.flow"
+url: "api/mitmproxy/flow.html"
+
+menu:
+ addons:
+ parent: 'API Reference'
+---
+
+{{< readfile file="/generated/api/mitmproxy/flow.html" >}}
diff --git a/docs/src/content/api/mitmproxy.http.md b/docs/src/content/api/mitmproxy.http.md
new file mode 100644
index 000000000..69496d5c2
--- /dev/null
+++ b/docs/src/content/api/mitmproxy.http.md
@@ -0,0 +1,11 @@
+
+---
+title: "mitmproxy.http"
+url: "api/mitmproxy/http.html"
+
+menu:
+ addons:
+ parent: 'API Reference'
+---
+
+{{< readfile file="/generated/api/mitmproxy/http.html" >}}
diff --git a/docs/src/content/api/mitmproxy.proxy.context.md b/docs/src/content/api/mitmproxy.proxy.context.md
new file mode 100644
index 000000000..e11c6c494
--- /dev/null
+++ b/docs/src/content/api/mitmproxy.proxy.context.md
@@ -0,0 +1,11 @@
+
+---
+title: "mitmproxy.proxy.context"
+url: "api/mitmproxy/proxy/context.html"
+
+menu:
+ addons:
+ parent: 'API Reference'
+---
+
+{{< readfile file="/generated/api/mitmproxy/proxy/context.html" >}}
diff --git a/docs/src/content/api/mitmproxy.tcp.md b/docs/src/content/api/mitmproxy.tcp.md
new file mode 100644
index 000000000..59553d49d
--- /dev/null
+++ b/docs/src/content/api/mitmproxy.tcp.md
@@ -0,0 +1,11 @@
+
+---
+title: "mitmproxy.tcp"
+url: "api/mitmproxy/tcp.html"
+
+menu:
+ addons:
+ parent: 'API Reference'
+---
+
+{{< readfile file="/generated/api/mitmproxy/tcp.html" >}}
diff --git a/docs/src/content/api/mitmproxy.websocket.md b/docs/src/content/api/mitmproxy.websocket.md
new file mode 100644
index 000000000..0f70619d5
--- /dev/null
+++ b/docs/src/content/api/mitmproxy.websocket.md
@@ -0,0 +1,11 @@
+
+---
+title: "mitmproxy.websocket"
+url: "api/mitmproxy/websocket.html"
+
+menu:
+ addons:
+ parent: 'API Reference'
+---
+
+{{< readfile file="/generated/api/mitmproxy/websocket.html" >}}
diff --git a/docs/src/layouts/partials/edit-on-github.html b/docs/src/layouts/partials/edit-on-github.html
index d2c3098c2..a5de09108 100644
--- a/docs/src/layouts/partials/edit-on-github.html
+++ b/docs/src/layouts/partials/edit-on-github.html
@@ -1,4 +1,4 @@
-{{ if and .IsPage (not (getenv "DOCS_ARCHIVE")) }}
+{{ if and .IsPage (ne .Type "api") (not (getenv "DOCS_ARCHIVE")) }}
{{ end }}
-
diff --git a/docs/src/layouts/partials/sidemenu.html b/docs/src/layouts/partials/sidemenu.html
index 035cc59e8..919abf3cf 100644
--- a/docs/src/layouts/partials/sidemenu.html
+++ b/docs/src/layouts/partials/sidemenu.html
@@ -3,9 +3,19 @@
{{ $currentPage := .ctx }}
{{ $menuname := .menuname }}
{{ range $menu.ByWeight }}
-