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 0babe84410..9afcebdc59 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -41,7 +41,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 33ea2c206b..718e2a0bc2 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -44,7 +44,7 @@ func Webhooks(ctx *context.Context) {
 	ctx.Data["PageIsSettingsHooks"] = true
 	ctx.Data["BaseLink"] = ctx.Repo.RepoLink + "/settings/hooks"
 	ctx.Data["BaseLinkNew"] = ctx.Repo.RepoLink + "/settings/hooks"
-	ctx.Data["Description"] = ctx.Tr("repo.settings.hooks_desc", "https://docs.gitea.com/usage/webhooks")
+	ctx.Data["Description"] = ctx.Tr("repo.settings.hooks_desc", "https://forgejo.org/docs/latest/user/webhooks/")
 
 	ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{RepoID: ctx.Repo.Repository.ID})
 	if err != nil {
@@ -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 54d2df2eed..f0f2faaf1d 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -390,6 +390,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)
@@ -404,6 +405,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 9d5dab85f7..f15c853331 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -70,7 +70,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]
@@ -177,7 +177,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/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl
index ed6e670d60..cafe88bb7c 100644
--- a/templates/repo/settings/webhook/base_list.tmpl
+++ b/templates/repo/settings/webhook/base_list.tmpl
@@ -4,6 +4,10 @@
 		<div class="ui jump dropdown">
 			<div class="ui primary tiny button">{{ctx.Locale.Tr "repo.settings.add_webhook"}}</div>
 			<div class="menu">
+				<a class="item" href="{{.BaseLinkNew}}/forgejo/new">
+					{{template "shared/webhook/icon" (dict "HookType" "forgejo" "Size" 20)}}
+					{{.locale.Tr "repo.settings.web_hook_name_forgejo"}}
+				</a>
 				<a class="item" href="{{.BaseLinkNew}}/gitea/new">
 					{{template "shared/webhook/icon" (dict "HookType" "gitea" "Size" 20)}}
 					{{ctx.Locale.Tr "repo.settings.web_hook_name_gitea"}}
diff --git a/templates/repo/settings/webhook/forgejo.tmpl b/templates/repo/settings/webhook/forgejo.tmpl
new file mode 100644
index 0000000000..320b064f93
--- /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://forgejo.org/docs/latest/user/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/gitea.tmpl b/templates/repo/settings/webhook/gitea.tmpl
index 4fda6a7b39..743c5169cf 100644
--- a/templates/repo/settings/webhook/gitea.tmpl
+++ b/templates/repo/settings/webhook/gitea.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "gitea"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gitea") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://forgejo.org/docs/latest/user/webhooks/" (ctx.Locale.Tr "repo.settings.web_hook_name_gitea") | Str2html}}</p>
 	<form class="ui form" action="{{.BaseLink}}/gitea/{{or .Webhook.ID "new"}}" method="post">
 		{{template "base/disable_form_autofill"}}
 		{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/gogs.tmpl b/templates/repo/settings/webhook/gogs.tmpl
index d2bd98c32c..62ca2e1d99 100644
--- a/templates/repo/settings/webhook/gogs.tmpl
+++ b/templates/repo/settings/webhook/gogs.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "gogs"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gogs") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://forgejo.org/docs/latest/user/webhooks/" (ctx.Locale.Tr "repo.settings.web_hook_name_gogs") | Str2html}}</p>
 	<form class="ui form" action="{{.BaseLink}}/gogs/{{or .Webhook.ID "new"}}" method="post">
 		{{template "base/disable_form_autofill"}}
 		{{.CsrfTokenHtml}}
diff --git a/templates/shared/webhook/icon.tmpl b/templates/shared/webhook/icon.tmpl
index 84f9de266f..a6507bab5b 100644
--- a/templates/shared/webhook/icon.tmpl
+++ b/templates/shared/webhook/icon.tmpl
@@ -2,8 +2,10 @@
 {{if .Size}}
 	{{$size = .Size}}
 {{end}}
-{{if eq .HookType "gitea"}}
-	<img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gitea.svg">
+{{if eq .HookType "forgejo"}}
+	<img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/forgejo.svg">
+{{else if eq .HookType "gitea"}}
+	<img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gitea-original.svg">
 {{else if eq .HookType "gogs"}}
 	<img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gogs.ico">
 {{else if eq .HookType "slack"}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 4a6aca7b96..fe70ead8f3 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -17736,6 +17736,7 @@
         "type": {
           "type": "string",
           "enum": [
+            "forgejo",
             "dingtalk",
             "discord",
             "gitea",
diff --git a/templates/webhook/new.tmpl b/templates/webhook/new.tmpl
index a185c42b51..60c729eee3 100644
--- a/templates/webhook/new.tmpl
+++ b/templates/webhook/new.tmpl
@@ -5,6 +5,7 @@
 	</div>
 </h4>
 <div class="ui attached segment">
+	{{template "repo/settings/webhook/forgejo" .ctxData}}
 	{{template "repo/settings/webhook/gitea" .ctxData}}
 	{{template "repo/settings/webhook/gogs" .ctxData}}
 	{{template "repo/settings/webhook/slack" .ctxData}}
diff --git a/tests/integration/links_test.go b/tests/integration/links_test.go
index a3937dd697..11e6146d07 100644
--- a/tests/integration/links_test.go
+++ b/tests/integration/links_test.go
@@ -176,6 +176,36 @@ 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)
+		}
+	}
+}
+
 func TestRepoLinks(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()