diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go
index c24404c42c..64119f1494 100644
--- a/models/webhook/webhook.go
+++ b/models/webhook/webhook.go
@@ -463,41 +463,6 @@ func CountWebhooksByOpts(opts *ListWebhookOptions) (int64, error) {
 	return db.GetEngine(db.DefaultContext).Where(opts.toCond()).Count(&Webhook{})
 }
 
-// GetDefaultWebhooks returns all admin-default webhooks.
-func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) {
-	webhooks := make([]*Webhook, 0, 5)
-	return webhooks, db.GetEngine(ctx).
-		Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, false).
-		Find(&webhooks)
-}
-
-// GetSystemOrDefaultWebhook returns admin system or default webhook by given ID.
-func GetSystemOrDefaultWebhook(id int64) (*Webhook, error) {
-	webhook := &Webhook{ID: id}
-	has, err := db.GetEngine(db.DefaultContext).
-		Where("repo_id=? AND org_id=?", 0, 0).
-		Get(webhook)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return nil, ErrWebhookNotExist{ID: id}
-	}
-	return webhook, nil
-}
-
-// GetSystemWebhooks returns all admin system webhooks.
-func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webhook, error) {
-	webhooks := make([]*Webhook, 0, 5)
-	if isActive.IsNone() {
-		return webhooks, db.GetEngine(ctx).
-			Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, true).
-			Find(&webhooks)
-	}
-	return webhooks, db.GetEngine(ctx).
-		Where("repo_id=? AND org_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()).
-		Find(&webhooks)
-}
-
 // UpdateWebhook updates information of webhook.
 func UpdateWebhook(w *Webhook) error {
 	_, err := db.GetEngine(db.DefaultContext).ID(w.ID).AllCols().Update(w)
@@ -545,44 +510,3 @@ func DeleteWebhookByOrgID(orgID, id int64) error {
 		OrgID: orgID,
 	})
 }
-
-// DeleteDefaultSystemWebhook deletes an admin-configured default or system webhook (where Org and Repo ID both 0)
-func DeleteDefaultSystemWebhook(id int64) error {
-	ctx, committer, err := db.TxContext(db.DefaultContext)
-	if err != nil {
-		return err
-	}
-	defer committer.Close()
-
-	count, err := db.GetEngine(ctx).
-		Where("repo_id=? AND org_id=?", 0, 0).
-		Delete(&Webhook{ID: id})
-	if err != nil {
-		return err
-	} else if count == 0 {
-		return ErrWebhookNotExist{ID: id}
-	}
-
-	if _, err := db.DeleteByBean(ctx, &HookTask{HookID: id}); err != nil {
-		return err
-	}
-
-	return committer.Commit()
-}
-
-// CopyDefaultWebhooksToRepo creates copies of the default webhooks in a new repo
-func CopyDefaultWebhooksToRepo(ctx context.Context, repoID int64) error {
-	ws, err := GetDefaultWebhooks(ctx)
-	if err != nil {
-		return fmt.Errorf("GetDefaultWebhooks: %w", err)
-	}
-
-	for _, w := range ws {
-		w.ID = 0
-		w.RepoID = repoID
-		if err := CreateWebhook(ctx, w); err != nil {
-			return fmt.Errorf("CreateWebhook: %w", err)
-		}
-	}
-	return nil
-}
diff --git a/models/webhook/webhook_system.go b/models/webhook/webhook_system.go
new file mode 100644
index 0000000000..21dc0406a0
--- /dev/null
+++ b/models/webhook/webhook_system.go
@@ -0,0 +1,81 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package webhook
+
+import (
+	"context"
+	"fmt"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/util"
+)
+
+// GetDefaultWebhooks returns all admin-default webhooks.
+func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) {
+	webhooks := make([]*Webhook, 0, 5)
+	return webhooks, db.GetEngine(ctx).
+		Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, false).
+		Find(&webhooks)
+}
+
+// GetSystemOrDefaultWebhook returns admin system or default webhook by given ID.
+func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error) {
+	webhook := &Webhook{ID: id}
+	has, err := db.GetEngine(ctx).
+		Where("repo_id=? AND org_id=?", 0, 0).
+		Get(webhook)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrWebhookNotExist{ID: id}
+	}
+	return webhook, nil
+}
+
+// GetSystemWebhooks returns all admin system webhooks.
+func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webhook, error) {
+	webhooks := make([]*Webhook, 0, 5)
+	if isActive.IsNone() {
+		return webhooks, db.GetEngine(ctx).
+			Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, true).
+			Find(&webhooks)
+	}
+	return webhooks, db.GetEngine(ctx).
+		Where("repo_id=? AND org_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()).
+		Find(&webhooks)
+}
+
+// DeleteDefaultSystemWebhook deletes an admin-configured default or system webhook (where Org and Repo ID both 0)
+func DeleteDefaultSystemWebhook(ctx context.Context, id int64) error {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		count, err := db.GetEngine(ctx).
+			Where("repo_id=? AND org_id=?", 0, 0).
+			Delete(&Webhook{ID: id})
+		if err != nil {
+			return err
+		} else if count == 0 {
+			return ErrWebhookNotExist{ID: id}
+		}
+
+		_, err = db.DeleteByBean(ctx, &HookTask{HookID: id})
+		return err
+	})
+}
+
+// CopyDefaultWebhooksToRepo creates copies of the default webhooks in a new repo
+func CopyDefaultWebhooksToRepo(ctx context.Context, repoID int64) error {
+	ws, err := GetDefaultWebhooks(ctx)
+	if err != nil {
+		return fmt.Errorf("GetDefaultWebhooks: %v", err)
+	}
+
+	for _, w := range ws {
+		w.ID = 0
+		w.RepoID = repoID
+		if err := CreateWebhook(ctx, w); err != nil {
+			return fmt.Errorf("CreateWebhook: %v", err)
+		}
+	}
+	return nil
+}
diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go
new file mode 100644
index 0000000000..2aed4139f3
--- /dev/null
+++ b/routers/api/v1/admin/hooks.go
@@ -0,0 +1,174 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/setting"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/routers/api/v1/utils"
+	webhook_service "code.gitea.io/gitea/services/webhook"
+)
+
+// ListHooks list system's webhooks
+func ListHooks(ctx *context.APIContext) {
+	// swagger:operation GET /admin/hooks admin adminListHooks
+	// ---
+	// summary: List system's webhooks
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results
+	//   type: integer
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/HookList"
+
+	sysHooks, err := webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err)
+		return
+	}
+	hooks := make([]*api.Hook, len(sysHooks))
+	for i, hook := range sysHooks {
+		h, err := webhook_service.ToHook(setting.AppURL+"/admin", hook)
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
+			return
+		}
+		hooks[i] = h
+	}
+	ctx.JSON(http.StatusOK, hooks)
+}
+
+// GetHook get an organization's hook by id
+func GetHook(ctx *context.APIContext) {
+	// swagger:operation GET /admin/hooks/{id} admin adminGetHook
+	// ---
+	// summary: Get a hook
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: id
+	//   in: path
+	//   description: id of the hook to get
+	//   type: integer
+	//   format: int64
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/Hook"
+
+	hookID := ctx.ParamsInt64(":id")
+	hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
+		return
+	}
+	h, err := webhook_service.ToHook("/admin/", hook)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
+		return
+	}
+	ctx.JSON(http.StatusOK, h)
+}
+
+// CreateHook create a hook for an organization
+func CreateHook(ctx *context.APIContext) {
+	// swagger:operation POST /admin/hooks admin adminCreateHook
+	// ---
+	// summary: Create a hook
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: body
+	//   in: body
+	//   required: true
+	//   schema:
+	//     "$ref": "#/definitions/CreateHookOption"
+	// responses:
+	//   "201":
+	//     "$ref": "#/responses/Hook"
+
+	form := web.GetForm(ctx).(*api.CreateHookOption)
+	// TODO in body params
+	if !utils.CheckCreateHookOption(ctx, form) {
+		return
+	}
+	utils.AddSystemHook(ctx, form)
+}
+
+// EditHook modify a hook of a repository
+func EditHook(ctx *context.APIContext) {
+	// swagger:operation PATCH /admin/hooks/{id} admin adminEditHook
+	// ---
+	// summary: Update a hook
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: id
+	//   in: path
+	//   description: id of the hook to update
+	//   type: integer
+	//   format: int64
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/EditHookOption"
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/Hook"
+
+	form := web.GetForm(ctx).(*api.EditHookOption)
+
+	// TODO in body params
+	hookID := ctx.ParamsInt64(":id")
+	utils.EditSystemHook(ctx, form, hookID)
+}
+
+// DeleteHook delete a system hook
+func DeleteHook(ctx *context.APIContext) {
+	// swagger:operation DELETE /amdin/hooks/{id} admin adminDeleteHook
+	// ---
+	// summary: Delete a hook
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: id
+	//   in: path
+	//   description: id of the hook to delete
+	//   type: integer
+	//   format: int64
+	//   required: true
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+
+	hookID := ctx.ParamsInt64(":id")
+	if err := webhook.DeleteDefaultSystemWebhook(ctx, hookID); err != nil {
+		if webhook.IsErrWebhookNotExist(err) {
+			ctx.NotFound()
+		} else {
+			ctx.Error(http.StatusInternalServerError, "DeleteDefaultSystemWebhook", err)
+		}
+		return
+	}
+	ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 21bc2e2de4..eef2a64244 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1222,6 +1222,13 @@ func Routes(ctx gocontext.Context) *web.Route {
 				m.Post("/{username}/{reponame}", admin.AdoptRepository)
 				m.Delete("/{username}/{reponame}", admin.DeleteUnadoptedRepository)
 			})
+			m.Group("/hooks", func() {
+				m.Combo("").Get(admin.ListHooks).
+					Post(bind(api.CreateHookOption{}), admin.CreateHook)
+				m.Combo("/{id}").Get(admin.GetHook).
+					Patch(bind(api.EditHookOption{}), admin.EditHook).
+					Delete(admin.DeleteHook)
+			})
 		}, reqToken(auth_model.AccessTokenScopeSudo), reqSiteAdmin())
 
 		m.Group("/topics", func() {
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index 44fba22b5a..f6aaf74aff 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -11,6 +11,7 @@ import (
 	"code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -67,6 +68,19 @@ func CheckCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption)
 	return true
 }
 
+// AddSystemHook add a system hook
+func AddSystemHook(ctx *context.APIContext, form *api.CreateHookOption) {
+	hook, ok := addHook(ctx, form, 0, 0)
+	if ok {
+		h, err := webhook_service.ToHook(setting.AppSubURL+"/admin", hook)
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
+			return
+		}
+		ctx.JSON(http.StatusCreated, h)
+	}
+}
+
 // AddOrgHook add a hook to an organization. Writes to `ctx` accordingly
 func AddOrgHook(ctx *context.APIContext, form *api.CreateHookOption) {
 	org := ctx.Org.Organization
@@ -196,6 +210,30 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID
 	return w, true
 }
 
+// EditSystemHook edit system webhook `w` according to `form`. Writes to `ctx` accordingly
+func EditSystemHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64) {
+	hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
+		return
+	}
+	if !editHook(ctx, form, hook) {
+		ctx.Error(http.StatusInternalServerError, "editHook", err)
+		return
+	}
+	updated, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
+		return
+	}
+	h, err := webhook_service.ToHook(setting.AppURL+"/admin", updated)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
+		return
+	}
+	ctx.JSON(http.StatusOK, h)
+}
+
 // EditOrgHook edit webhook `w` according to `form`. Writes to `ctx` accordingly
 func EditOrgHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64) {
 	org := ctx.Org.Organization
diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go
index e8db9a3ded..57cf5f49e5 100644
--- a/routers/web/admin/hooks.go
+++ b/routers/web/admin/hooks.go
@@ -60,7 +60,7 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
 
 // DeleteDefaultOrSystemWebhook handler to delete an admin-defined system or default webhook
 func DeleteDefaultOrSystemWebhook(ctx *context.Context) {
-	if err := webhook.DeleteDefaultSystemWebhook(ctx.FormInt64("id")); err != nil {
+	if err := webhook.DeleteDefaultSystemWebhook(ctx, ctx.FormInt64("id")); err != nil {
 		ctx.Flash.Error("DeleteDefaultWebhook: " + err.Error())
 	} else {
 		ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go
index 96261af674..d3826c3f3d 100644
--- a/routers/web/repo/webhook.go
+++ b/routers/web/repo/webhook.go
@@ -591,7 +591,7 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) {
 	} else if orCtx.OrgID > 0 {
 		w, err = webhook.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
 	} else if orCtx.IsAdmin {
-		w, err = webhook.GetSystemOrDefaultWebhook(ctx.ParamsInt64(":id"))
+		w, err = webhook.GetSystemOrDefaultWebhook(ctx, ctx.ParamsInt64(":id"))
 	}
 	if err != nil || w == nil {
 		if webhook.IsErrWebhookNotExist(err) {
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index cd64b7070f..726b771cfc 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -138,6 +138,127 @@
         }
       }
     },
+    "/admin/hooks": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "admin"
+        ],
+        "summary": "List system's webhooks",
+        "operationId": "adminListHooks",
+        "parameters": [
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results",
+            "name": "limit",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/HookList"
+          }
+        }
+      },
+      "post": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "admin"
+        ],
+        "summary": "Create a hook",
+        "operationId": "adminCreateHook",
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateHookOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "$ref": "#/responses/Hook"
+          }
+        }
+      }
+    },
+    "/admin/hooks/{id}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "admin"
+        ],
+        "summary": "Get a hook",
+        "operationId": "adminGetHook",
+        "parameters": [
+          {
+            "type": "integer",
+            "format": "int64",
+            "description": "id of the hook to get",
+            "name": "id",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/Hook"
+          }
+        }
+      },
+      "patch": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "admin"
+        ],
+        "summary": "Update a hook",
+        "operationId": "adminEditHook",
+        "parameters": [
+          {
+            "type": "integer",
+            "format": "int64",
+            "description": "id of the hook to update",
+            "name": "id",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/EditHookOption"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/Hook"
+          }
+        }
+      }
+    },
     "/admin/orgs": {
       "get": {
         "produces": [
@@ -601,6 +722,33 @@
         }
       }
     },
+    "/amdin/hooks/{id}": {
+      "delete": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "admin"
+        ],
+        "summary": "Delete a hook",
+        "operationId": "adminDeleteHook",
+        "parameters": [
+          {
+            "type": "integer",
+            "format": "int64",
+            "description": "id of the hook to delete",
+            "name": "id",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          }
+        }
+      }
+    },
     "/markdown": {
       "post": {
         "consumes": [