diff --git a/modules/setting/webhook.go b/modules/setting/webhook.go
index c01261dbbd..b56c55c439 100644
--- a/modules/setting/webhook.go
+++ b/modules/setting/webhook.go
@@ -35,7 +35,7 @@ func loadWebhookFrom(rootCfg ConfigProvider) {
 	Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
 	Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
 	Webhook.AllowedHostList = sec.Key("ALLOWED_HOST_LIST").MustString("")
-	Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork", "packagist"}
+	Webhook.Types = []string{"forgejo", "gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork", "packagist"}
 	Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
 	Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("")
 	if Webhook.ProxyURL != "" {
diff --git a/modules/structs/hook.go b/modules/structs/hook.go
index cd91d4bc46..120df34915 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -40,7 +40,7 @@ type CreateHookOptionConfig map[string]string
 // CreateHookOption options when create a hook
 type CreateHookOption struct {
 	// required: true
-	// enum: dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist
+	// enum: forgejo,dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist
 	Type string `json:"type" binding:"Required"`
 	// required: true
 	Config              CreateHookOptionConfig `json:"config" binding:"Required"`
diff --git a/modules/webhook/type.go b/modules/webhook/type.go
index 7042d391b7..7f427f2ea8 100644
--- a/modules/webhook/type.go
+++ b/modules/webhook/type.go
@@ -72,6 +72,7 @@ type HookType = string
 
 // Types of webhooks
 const (
+	FORGEJO    HookType = "forgejo"
 	GITEA      HookType = "gitea"
 	GOGS       HookType = "gogs"
 	SLACK      HookType = "slack"
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index d85d5c8b07..7c581dc343 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -309,6 +309,34 @@ func editWebhook(ctx *context.Context, params webhookParams) {
 	ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
 }
 
+// ForgejoHooksNewPost response for creating Forgejo webhook
+func ForgejoHooksNewPost(ctx *context.Context) {
+	createWebhook(ctx, forgejoHookParams(ctx))
+}
+
+// ForgejoHooksEditPost response for editing Forgejo webhook
+func ForgejoHooksEditPost(ctx *context.Context) {
+	editWebhook(ctx, forgejoHookParams(ctx))
+}
+
+func forgejoHookParams(ctx *context.Context) webhookParams {
+	form := web.GetForm(ctx).(*forms.NewWebhookForm)
+
+	contentType := webhook.ContentTypeJSON
+	if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm {
+		contentType = webhook.ContentTypeForm
+	}
+
+	return webhookParams{
+		Type:        webhook_module.FORGEJO,
+		URL:         form.PayloadURL,
+		ContentType: contentType,
+		Secret:      form.Secret,
+		HTTPMethod:  form.HTTPMethod,
+		WebhookForm: form.WebhookForm,
+	}
+}
+
 // GiteaHooksNewPost response for creating Gitea webhook
 func GiteaHooksNewPost(ctx *context.Context) {
 	createWebhook(ctx, giteaHookParams(ctx))
diff --git a/routers/web/web.go b/routers/web/web.go
index 4328272e20..4455c64cfd 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -282,6 +282,7 @@ func registerRoutes(m *web.Route) {
 
 	addWebhookAddRoutes := func() {
 		m.Get("/{type}/new", repo_setting.WebhooksNew)
+		m.Post("/forgejo/new", web.Bind(forms.NewWebhookForm{}), repo_setting.ForgejoHooksNewPost)
 		m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo_setting.GiteaHooksNewPost)
 		m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo_setting.GogsHooksNewPost)
 		m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo_setting.SlackHooksNewPost)
@@ -296,6 +297,7 @@ func registerRoutes(m *web.Route) {
 	}
 
 	addWebhookEditRoutes := func() {
+		m.Post("/forgejo/{id}", web.Bind(forms.NewWebhookForm{}), repo_setting.ForgejoHooksEditPost)
 		m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo_setting.GiteaHooksEditPost)
 		m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo_setting.GogsHooksEditPost)
 		m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo_setting.SlackHooksEditPost)
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index 3cd9deafd8..4f77e23efc 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -69,7 +69,7 @@ var webhooks = map[webhook_module.HookType]*webhook{
 
 // IsValidHookTaskType returns true if a webhook registered
 func IsValidHookTaskType(name string) bool {
-	if name == webhook_module.GITEA || name == webhook_module.GOGS {
+	if name == webhook_module.FORGEJO || name == webhook_module.GITEA || name == webhook_module.GOGS {
 		return true
 	}
 	_, ok := webhooks[name]
@@ -172,7 +172,7 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook
 	// Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.).
 	// Integration webhooks (e.g. drone) still receive the required data.
 	if pushEvent, ok := p.(*api.PushPayload); ok &&
-		w.Type != webhook_module.GITEA && w.Type != webhook_module.GOGS &&
+		w.Type != webhook_module.FORGEJO && w.Type != webhook_module.GITEA && w.Type != webhook_module.GOGS &&
 		len(pushEvent.Commits) == 0 {
 		return nil
 	}
diff --git a/templates/admin/hook_new.tmpl b/templates/admin/hook_new.tmpl
index 1abdd4c813..f1ec14f68c 100644
--- a/templates/admin/hook_new.tmpl
+++ b/templates/admin/hook_new.tmpl
@@ -11,8 +11,10 @@
 				{{.locale.Tr "admin.defaulthooks.update_webhook"}}
 			{{end}}
 			<div class="ui right">
-				{{if eq .HookType "gitea"}}
-					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea.svg">
+				{{if eq .HookType "forgejo"}}
+					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/forgejo.svg">
+				{{else if eq .HookType "gitea"}}
+					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea-original.svg">
 				{{else if eq .HookType "gogs"}}
 					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gogs.ico">
 				{{else if eq .HookType "slack"}}
@@ -37,6 +39,7 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
+			{{template "repo/settings/webhook/forgejo" .}}
 			{{template "repo/settings/webhook/gitea" .}}
 			{{template "repo/settings/webhook/gogs" .}}
 			{{template "repo/settings/webhook/slack" .}}
diff --git a/templates/org/settings/hook_new.tmpl b/templates/org/settings/hook_new.tmpl
index 1bf74fb7ba..da1813b001 100644
--- a/templates/org/settings/hook_new.tmpl
+++ b/templates/org/settings/hook_new.tmpl
@@ -3,8 +3,10 @@
 				<h4 class="ui top attached header">
 					{{if .PageIsSettingsHooksNew}}{{.locale.Tr "repo.settings.add_webhook"}}{{else}}{{.locale.Tr "repo.settings.update_webhook"}}{{end}}
 					<div class="ui right">
-						{{if eq .HookType "gitea"}}
-							<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea.svg">
+						{{if eq .HookType "forgejo"}}
+							<img width="26" height="26" src="{{AssetUrlPrefix}}/img/forgejo.svg">
+						{{else if eq .HookType "gitea"}}
+							<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea-original.svg">
 						{{else if eq .HookType "gogs"}}
 							<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gogs.ico">
 						{{else if eq .HookType "slack"}}
diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl
index 8db9a9263e..479bcd1e62 100644
--- a/templates/repo/settings/webhook/base_list.tmpl
+++ b/templates/repo/settings/webhook/base_list.tmpl
@@ -4,8 +4,11 @@
 		<div class="ui jump dropdown">
 			<div class="ui primary tiny button">{{.locale.Tr "repo.settings.add_webhook"}}</div>
 			<div class="menu">
+				<a class="item" href="{{.BaseLinkNew}}/forgejo/new">
+					<img width="20" height="20" src="{{AssetUrlPrefix}}/img/forgejo.svg">{{.locale.Tr "repo.settings.web_hook_name_forgejo"}}
+				</a>
 				<a class="item" href="{{.BaseLinkNew}}/gitea/new">
-					<img width="20" height="20" src="{{AssetUrlPrefix}}/img/gitea.svg">{{.locale.Tr "repo.settings.web_hook_name_gitea"}}
+					<img width="20" height="20" src="{{AssetUrlPrefix}}/img/gitea-original.svg">{{.locale.Tr "repo.settings.web_hook_name_gitea"}}
 				</a>
 				<a class="item" href="{{.BaseLinkNew}}/gogs/new">
 					<img width="20" height="20" src="{{AssetUrlPrefix}}/img/gogs.ico">{{.locale.Tr "repo.settings.web_hook_name_gogs"}}
diff --git a/templates/repo/settings/webhook/forgejo.tmpl b/templates/repo/settings/webhook/forgejo.tmpl
new file mode 100644
index 0000000000..0d733f3362
--- /dev/null
+++ b/templates/repo/settings/webhook/forgejo.tmpl
@@ -0,0 +1,40 @@
+{{if eq .HookType "forgejo"}}
+	<p>{{.locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.io/en-us/webhooks/" (.locale.Tr "repo.settings.web_hook_name_forgejo") | Str2html}}</p>
+	<form class="ui form" action="{{.BaseLink}}/forgejo/{{or .Webhook.ID "new"}}" method="post">
+		{{template "base/disable_form_autofill"}}
+		{{.CsrfTokenHtml}}
+		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
+			<label for="payload_url">{{.locale.Tr "repo.settings.payload_url"}}</label>
+			<input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required>
+		</div>
+		<div class="field">
+			<label>{{.locale.Tr "repo.settings.http_method"}}</label>
+			<div class="ui selection dropdown">
+				<input type="hidden" id="http_method" name="http_method" value="{{if .Webhook.HTTPMethod}}{{.Webhook.HTTPMethod}}{{else}}POST{{end}}">
+				<div class="default text"></div>
+				{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+				<div class="menu">
+					<div class="item" data-value="POST">POST</div>
+					<div class="item" data-value="GET">GET</div>
+				</div>
+			</div>
+		</div>
+		<div class="field">
+			<label>{{.locale.Tr "repo.settings.content_type"}}</label>
+			<div class="ui selection dropdown">
+				<input type="hidden" id="content_type" name="content_type" value="{{if .Webhook.ContentType}}{{.Webhook.ContentType}}{{else}}1{{end}}">
+				<div class="default text"></div>
+				{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+				<div class="menu">
+					<div class="item" data-value="1">application/json</div>
+					<div class="item" data-value="2">application/x-www-form-urlencoded</div>
+				</div>
+			</div>
+		</div>
+		<div class="field {{if .Err_Secret}}error{{end}}">
+			<label for="secret">{{.locale.Tr "repo.settings.secret"}}</label>
+			<input id="secret" name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off">
+		</div>
+		{{template "repo/settings/webhook/settings" .}}
+	</form>
+{{end}}
diff --git a/templates/repo/settings/webhook/new.tmpl b/templates/repo/settings/webhook/new.tmpl
index 73d6726a57..c5be063024 100644
--- a/templates/repo/settings/webhook/new.tmpl
+++ b/templates/repo/settings/webhook/new.tmpl
@@ -3,8 +3,10 @@
 		<h4 class="ui top attached header">
 			{{if .PageIsSettingsHooksNew}}{{.locale.Tr "repo.settings.add_webhook"}}{{else}}{{.locale.Tr "repo.settings.update_webhook"}}{{end}}
 			<div class="ui right">
-				{{if eq .HookType "gitea"}}
-					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea.svg">
+				{{if eq .HookType "forgejo"}}
+					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/forgejo.svg">
+				{{else if eq .HookType "gitea"}}
+					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea-original.svg">
 				{{else if eq .HookType "gogs"}}
 					<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gogs.ico">
 				{{else if eq .HookType "slack"}}
@@ -29,6 +31,7 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
+			{{template "repo/settings/webhook/forgejo" .}}
 			{{template "repo/settings/webhook/gitea" .}}
 			{{template "repo/settings/webhook/gogs" .}}
 			{{template "repo/settings/webhook/slack" .}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 42aa54e1fb..907d99bb41 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -16904,6 +16904,7 @@
         "type": {
           "type": "string",
           "enum": [
+            "forgejo",
             "dingtalk",
             "discord",
             "gitea",
diff --git a/templates/user/settings/hook_new.tmpl b/templates/user/settings/hook_new.tmpl
index ac32cdd8c8..ab6d55bc64 100644
--- a/templates/user/settings/hook_new.tmpl
+++ b/templates/user/settings/hook_new.tmpl
@@ -4,7 +4,9 @@
 				<h4 class="ui top attached header">
 				{{if .PageIsSettingsHooksNew}}{{.locale.Tr "repo.settings.add_webhook"}}{{else}}{{.locale.Tr "repo.settings.update_webhook"}}{{end}}
 				<div class="ui right">
-					{{if eq .HookType "gitea"}}
+					{{if eq .HookType "forgejo"}}
+						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/forgejo.svg">
+					{{else if eq .HookType "gitea"}}
 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea.svg">
 					{{else if eq .HookType "gogs"}}
 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gogs.ico">
diff --git a/tests/integration/links_test.go b/tests/integration/links_test.go
index 9136f8f915..74d9fe20cb 100644
--- a/tests/integration/links_test.go
+++ b/tests/integration/links_test.go
@@ -173,3 +173,33 @@ func TestLinksLogin(t *testing.T) {
 
 	testLinksAsUser("user2", t)
 }
+
+func TestRedirectsWebhooks(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	//
+	// A redirect means the route exists but not if it performs as intended.
+	//
+	for _, kind := range []string{"forgejo", "gitea"} {
+		redirects := []struct {
+			from string
+			to   string
+			verb string
+		}{
+			{from: "/user2/repo1/settings/hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
+			{from: "/user/settings/hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
+			{from: "/admin/system-hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
+			{from: "/admin/default-hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
+			{from: "/user2/repo1/settings/hooks/" + kind + "/new", to: "/", verb: "POST"},
+			{from: "/admin/system-hooks/" + kind + "/new", to: "/", verb: "POST"},
+			{from: "/admin/default-hooks/" + kind + "/new", to: "/", verb: "POST"},
+			{from: "/user2/repo1/settings/hooks/" + kind + "/1", to: "/", verb: "POST"},
+			{from: "/admin/hooks/" + kind + "/1", to: "/", verb: "POST"},
+		}
+		for _, info := range redirects {
+			req := NewRequest(t, info.verb, info.from)
+			resp := MakeRequest(t, req, http.StatusSeeOther)
+			assert.EqualValues(t, path.Join(setting.AppSubURL, info.to), test.RedirectURL(resp), info.from)
+		}
+	}
+}