diff --git a/modules/structs/repo_compare.go b/modules/structs/repo_compare.go
new file mode 100644
index 0000000000..8a12498705
--- /dev/null
+++ b/modules/structs/repo_compare.go
@@ -0,0 +1,10 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package structs
+
+// Compare represents a comparison between two commits.
+type Compare struct {
+	TotalCommits int       `json:"total_commits"` // Total number of commits in the comparison.
+	Commits      []*Commit `json:"commits"`       // List of commits in the comparison.
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 80f2243ef0..a15210ac51 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -983,6 +983,8 @@ func Routes() *web.Route {
 			m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate)
 
 			m.Group("/{username}/{reponame}", func() {
+				m.Get("/compare/*", reqRepoReader(unit.TypeCode), repo.CompareDiff)
+
 				m.Combo("").Get(reqAnyRepoReader(), repo.Get).
 					Delete(reqToken(), reqOwner(), repo.Delete).
 					Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go
new file mode 100644
index 0000000000..549b9b7fa9
--- /dev/null
+++ b/routers/api/v1/repo/compare.go
@@ -0,0 +1,99 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+	"net/http"
+	"strings"
+
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/gitrepo"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/convert"
+)
+
+// CompareDiff compare two branches or commits
+func CompareDiff(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} Get commit comparison information
+	// ---
+	// summary: Get commit comparison information
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// - name: basehead
+	//   in: path
+	//   description: compare two branches or commits
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/Compare"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	if ctx.Repo.GitRepo == nil {
+		gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
+			return
+		}
+		ctx.Repo.GitRepo = gitRepo
+		defer gitRepo.Close()
+	}
+
+	infoPath := ctx.Params("*")
+	infos := []string{ctx.Repo.Repository.DefaultBranch, ctx.Repo.Repository.DefaultBranch}
+	if infoPath != "" {
+		infos = strings.SplitN(infoPath, "...", 2)
+		if len(infos) != 2 {
+			if infos = strings.SplitN(infoPath, "..", 2); len(infos) != 2 {
+				infos = []string{ctx.Repo.Repository.DefaultBranch, infoPath}
+			}
+		}
+	}
+
+	_, _, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{
+		Base: infos[0],
+		Head: infos[1],
+	})
+	if ctx.Written() {
+		return
+	}
+	defer headGitRepo.Close()
+
+	verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
+	files := ctx.FormString("files") == "" || ctx.FormBool("files")
+
+	apiCommits := make([]*api.Commit, 0, len(ci.Commits))
+	userCache := make(map[string]*user_model.User)
+	for i := 0; i < len(ci.Commits); i++ {
+		apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ci.Commits[i], userCache,
+			convert.ToCommitOptions{
+				Stat:         true,
+				Verification: verification,
+				Files:        files,
+			})
+		if err != nil {
+			ctx.ServerError("toCommit", err)
+			return
+		}
+		apiCommits = append(apiCommits, apiCommit)
+	}
+
+	ctx.JSON(http.StatusOK, &api.Compare{
+		TotalCommits: len(ci.Commits),
+		Commits:      apiCommits,
+	})
+}
diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go
index c7fa98a697..b55ea1d0a9 100644
--- a/routers/api/v1/swagger/repo.go
+++ b/routers/api/v1/swagger/repo.go
@@ -421,3 +421,9 @@ type swaggerBlockedUserList struct {
 	// in:body
 	Body []api.BlockedUser `json:"body"`
 }
+
+// swagger:response Compare
+type swaggerCompare struct {
+	// in:body
+	Body api.Compare `json:"body"`
+}
diff --git a/routers/common/compare.go b/routers/common/compare.go
new file mode 100644
index 0000000000..4d1cc2f0d8
--- /dev/null
+++ b/routers/common/compare.go
@@ -0,0 +1,21 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package common
+
+import (
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+)
+
+// CompareInfo represents the collected results from ParseCompareInfo
+type CompareInfo struct {
+	HeadUser         *user_model.User
+	HeadRepo         *repo_model.Repository
+	HeadGitRepo      *git.Repository
+	CompareInfo      *git.CompareInfo
+	BaseBranch       string
+	HeadBranch       string
+	DirectComparison bool
+}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 93c224b7be..b07209c779 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -35,6 +35,7 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/typesniffer"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/routers/common"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/gitdiff"
@@ -185,21 +186,10 @@ func setCsvCompareContext(ctx *context.Context) {
 	}
 }
 
-// CompareInfo represents the collected results from ParseCompareInfo
-type CompareInfo struct {
-	HeadUser         *user_model.User
-	HeadRepo         *repo_model.Repository
-	HeadGitRepo      *git.Repository
-	CompareInfo      *git.CompareInfo
-	BaseBranch       string
-	HeadBranch       string
-	DirectComparison bool
-}
-
 // ParseCompareInfo parse compare info between two commit for preparing comparing references
-func ParseCompareInfo(ctx *context.Context) *CompareInfo {
+func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
 	baseRepo := ctx.Repo.Repository
-	ci := &CompareInfo{}
+	ci := &common.CompareInfo{}
 
 	fileOnly := ctx.FormBool("file-only")
 
@@ -576,7 +566,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
 // PrepareCompareDiff renders compare diff page
 func PrepareCompareDiff(
 	ctx *context.Context,
-	ci *CompareInfo,
+	ci *common.CompareInfo,
 	whitespaceBehavior git.TrustedCmdArgs,
 ) bool {
 	var (
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index fcb60f4174..cc4e50e60d 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -5210,6 +5210,51 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/compare/{basehead}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "Get",
+          "commit",
+          "comparison"
+        ],
+        "summary": "Get commit comparison information",
+        "operationId": "information",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "compare two branches or commits",
+            "name": "basehead",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/Compare"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/contents": {
       "get": {
         "produces": [
@@ -19017,6 +19062,25 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "Compare": {
+      "type": "object",
+      "title": "Compare represents a comparison between two commits.",
+      "properties": {
+        "commits": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Commit"
+          },
+          "x-go-name": "Commits"
+        },
+        "total_commits": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "TotalCommits"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "ContentsResponse": {
       "description": "ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content",
       "type": "object",
@@ -25088,6 +25152,12 @@
         }
       }
     },
+    "Compare": {
+      "description": "",
+      "schema": {
+        "$ref": "#/definitions/Compare"
+      }
+    },
     "ContentsListResponse": {
       "description": "ContentsListResponse",
       "schema": {
diff --git a/tests/integration/api_repo_compare_test.go b/tests/integration/api_repo_compare_test.go
new file mode 100644
index 0000000000..f3188eb49f
--- /dev/null
+++ b/tests/integration/api_repo_compare_test.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"net/http"
+	"testing"
+
+	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAPICompareBranches(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	// Login as User2.
+	session := loginUser(t, user.Name)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+	repoName := "repo20"
+
+	req := NewRequestf(t, "GET", "/api/v1/repos/user2/%s/compare/add-csv...remove-files-b", repoName).
+		AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+
+	var apiResp *api.Compare
+	DecodeJSON(t, resp, &apiResp)
+
+	assert.Equal(t, 2, apiResp.TotalCommits)
+	assert.Len(t, apiResp.Commits, 2)
+}