diff --git a/docs/user/api/json.md b/docs/user/api/json.md
index 98ac7680884d..03314a848104 100644
--- a/docs/user/api/json.md
+++ b/docs/user/api/json.md
@@ -482,6 +482,44 @@ For example, here is what a withdrawn vulnerability might look like:
 }
 ```
 
+### Get a user
+
+Route: `GET /user/<username>/json`
+
+Returns the same information found in the HTML profile page (`/user/<username>`), but in a JSON format. 
+It contains the time of account creation, the username, the name (if present), and a list of the user's projects.
+
+Status codes:
+
+* `200 OK` - no error
+* `404 Not Found` - User was not found
+
+Example request:
+
+```http
+GET /user/someuser/json HTTP/1.1
+Host: pypi.org
+Accept: application/json
+
+??? "Example JSON response"
+
+    ```http
+    HTTP/1.1 200 OK
+    Content-Type: application/json; charset="UTF-8"
+
+    {
+        "joined_at": "2025-01-01T06:35:12",
+        "name": "Some User",
+        "projects": [
+            {
+                "last_released": "2025-01-04T08:47:36",
+                "name": "sampleproject",
+                "summary": "sample project"
+            }
+        ],
+        "username": "someuser"
+    }
+    ```
 
 [Index API]: ./index-api.md
 [known vulnerabilities]: https://github.com/pypa/advisory-database
diff --git a/tests/unit/legacy/api/test_json.py b/tests/unit/legacy/api/test_json.py
index 3a7b029e8c7e..b409b711a012 100644
--- a/tests/unit/legacy/api/test_json.py
+++ b/tests/unit/legacy/api/test_json.py
@@ -774,3 +774,51 @@ def test_normalizing_redirects(self, db_request):
         assert db_request.current_route_path.calls == [
             pretend.call(name=release.project.normalized_name)
         ]
+
+
+class TestJSONUser:
+    def test_no_projects(self, db_request):
+        user = UserFactory.create()
+        resp = json.json_user(user, db_request)
+        assert resp["projects"] == []
+
+    def test_has_projects(self, db_request):
+        user = UserFactory.create()
+        project = ProjectFactory.create()
+        release = ReleaseFactory.create(project=project, version="1.0")
+
+        user.projects.append(project)
+
+        resp = json.json_user(user, db_request)
+        assert resp["projects"][0] == {
+            "name": project.name,
+            "last_released": release.created.strftime("%Y-%m-%dT%H:%M:%S"),
+            "summary": release.summary,
+        }
+
+    def test_has_name(self, db_request):
+        user = UserFactory.create()
+        resp = json.json_user(user, db_request)
+        assert resp["name"] == user.name
+        assert resp["username"] == user.username
+
+    def test_has_no_name(self, db_request):
+        user = UserFactory.create(name="")
+        resp = json.json_user(user, db_request)
+        assert resp["name"] is None
+        assert resp["username"] == user.username
+
+    def test_project_without_releases(self, db_request):
+        user = UserFactory.create()
+        project = ProjectFactory.create()
+        user.projects.append(project)
+
+        resp = json.json_user(user, db_request)
+        assert resp["projects"] == []
+
+
+class TestJSONUserSlash:
+    def test_redirect(self, db_request):
+        user = UserFactory.create()
+        resp = json.json_user_slash(user, db_request)
+        assert resp["username"] == user.username
diff --git a/tests/unit/test_routes.py b/tests/unit/test_routes.py
index 0de9e3f61694..d31a2ee6f64b 100644
--- a/tests/unit/test_routes.py
+++ b/tests/unit/test_routes.py
@@ -623,6 +623,20 @@ def add_redirect_rule(*args, **kwargs):
             factory="warehouse.legacy.api.json.release_factory",
             domain=warehouse,
         ),
+        pretend.call(
+            "legacy.api.json.user",
+            "/user/{username}/json",
+            factory="warehouse.accounts.models:UserFactory",
+            traverse="/{username}",
+            domain=warehouse,
+        ),
+        pretend.call(
+            "legacy.api.json.user_slash",
+            "/user/{username}/json/",
+            factory="warehouse.accounts.models:UserFactory",
+            traverse="/{username}",
+            domain=warehouse,
+        ),
         pretend.call("legacy.docs", docs_route_url),
     ]
 
diff --git a/warehouse/legacy/api/json.py b/warehouse/legacy/api/json.py
index 513699cbd665..49403a015854 100644
--- a/warehouse/legacy/api/json.py
+++ b/warehouse/legacy/api/json.py
@@ -16,6 +16,7 @@
 from sqlalchemy.exc import MultipleResultsFound, NoResultFound
 from sqlalchemy.orm import Load, contains_eager, joinedload
 
+from warehouse.accounts.models import User
 from warehouse.cache.http import cache_control
 from warehouse.cache.origin import origin_cache
 from warehouse.packaging.models import (
@@ -345,3 +346,42 @@ def json_release(release, request):
 )
 def json_release_slash(release, request):
     return json_release(release, request)
+
+
+@view_config(
+    route_name="legacy.api.json.user",
+    context=User,
+    renderer="json",
+)
+def json_user(user, request):
+    projects = []
+    for project in user.projects:
+        latest_release = max(project.releases, key=lambda r: r.created, default=None)
+        if latest_release:
+            projects.append(
+                {
+                    "name": project.name,
+                    "last_released": latest_release.created.strftime(
+                        "%Y-%m-%dT%H:%M:%S"
+                    ),
+                    "summary": latest_release.summary,
+                }
+            )
+
+    # Apply CORS headers.
+    request.response.headers.update(_CORS_HEADERS)
+    return {
+        "username": user.username,
+        "name": user.name or None,
+        "joined_at": user.date_joined.strftime("%Y-%m-%dT%H:%M:%S"),
+        "projects": projects,
+    }
+
+
+@view_config(
+    route_name="legacy.api.json.user_slash",
+    context=User,
+    renderer="json",
+)
+def json_user_slash(user, request):
+    return json_user(user, request)
diff --git a/warehouse/routes.py b/warehouse/routes.py
index c31158dbab82..3e929f680b17 100644
--- a/warehouse/routes.py
+++ b/warehouse/routes.py
@@ -640,6 +640,22 @@ def includeme(config):
         domain=warehouse,
     )
 
+    config.add_route(
+        "legacy.api.json.user",
+        "/user/{username}/json",
+        factory="warehouse.accounts.models:UserFactory",
+        traverse="/{username}",
+        domain=warehouse,
+    )
+
+    config.add_route(
+        "legacy.api.json.user_slash",
+        "/user/{username}/json/",
+        factory="warehouse.accounts.models:UserFactory",
+        traverse="/{username}",
+        domain=warehouse,
+    )
+
     # Legacy Action URLs
     # TODO: We should probably add Warehouse routes for these that just error
     #       and direct people to use upload.pypi.org