diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index b3eb7b1f4a4fd..be715fbf3c059 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2433,7 +2433,9 @@ settings.event_workflow_job_desc = Gitea Actions Workflow job queued, waiting, i
 settings.event_package = Package
 settings.event_package_desc = Package created or deleted in a repository.
 settings.branch_filter = Branch filter
-settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or *, events for all branches are reported. See %[2]s documentation for syntax. Examples: master, {master,release*}.
+settings.branch_filter_desc_1 = Branch (and ref name) allowlist for push, branch creation and branch deletion events, specified as glob pattern. If empty or *, events for all branches and tags are reported.
+settings.branch_filter_desc_2 = Use refs/heads/ or refs/tags/ prefix to match full ref names.
+settings.branch_filter_desc_doc = See %[2]s documentation for syntax.
 settings.authorization_header = Authorization Header
 settings.authorization_header_desc = Will be included as authorization header for requests when present. Examples: %s.
 settings.active = Active
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index 2b276207a1396..a35997dda093a 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"strings"
 
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -46,21 +45,25 @@ func IsValidHookTaskType(name string) bool {
 // hookQueue is a global queue of web hooks
 var hookQueue *queue.WorkerPoolQueue[int64]
 
-// getPayloadBranch returns branch for hook event, if applicable.
-func getPayloadBranch(p api.Payloader) string {
+// getPayloadRef returns the full ref name for hook event, if applicable.
+func getPayloadRef(p api.Payloader) git.RefName {
 	switch pp := p.(type) {
 	case *api.CreatePayload:
-		if pp.RefType == "branch" {
-			return pp.Ref
+		switch pp.RefType {
+		case "branch":
+			return git.RefNameFromBranch(pp.Ref)
+		case "tag":
+			return git.RefNameFromTag(pp.Ref)
 		}
 	case *api.DeletePayload:
-		if pp.RefType == "branch" {
-			return pp.Ref
+		switch pp.RefType {
+		case "branch":
+			return git.RefNameFromBranch(pp.Ref)
+		case "tag":
+			return git.RefNameFromTag(pp.Ref)
 		}
 	case *api.PushPayload:
-		if strings.HasPrefix(pp.Ref, git.BranchPrefix) {
-			return pp.Ref[len(git.BranchPrefix):]
-		}
+		return git.RefName(pp.Ref)
 	}
 	return ""
 }
@@ -108,19 +111,22 @@ func enqueueHookTask(taskID int64) error {
 	return nil
 }
 
-func checkBranch(w *webhook_model.Webhook, branch string) bool {
-	if w.BranchFilter == "" || w.BranchFilter == "*" {
+func checkBranchFilter(branchFilter string, ref git.RefName) bool {
+	if branchFilter == "" || branchFilter == "*" || branchFilter == "**" {
 		return true
 	}
 
-	g, err := glob.Compile(w.BranchFilter)
+	g, err := glob.Compile(branchFilter)
 	if err != nil {
 		// should not really happen as BranchFilter is validated
-		log.Error("CheckBranch failed: %s", err)
+		log.Debug("checkBranchFilter failed to compile filer %q, err: %s", branchFilter, err)
 		return false
 	}
 
-	return g.Match(branch)
+	if ref.IsBranch() && g.Match(ref.BranchName()) {
+		return true
+	}
+	return g.Match(ref.String())
 }
 
 // PrepareWebhook creates a hook task and enqueues it for processing.
@@ -144,11 +150,10 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook
 		return nil
 	}
 
-	// If payload has no associated branch (e.g. it's a new tag, issue, etc.),
-	// branch filter has no effect.
-	if branch := getPayloadBranch(p); branch != "" {
-		if !checkBranch(w, branch) {
-			log.Info("Branch %q doesn't match branch filter %q, skipping", branch, w.BranchFilter)
+	// If payload has no associated branch (e.g. it's a new tag, issue, etc.), branch filter has no effect.
+	if ref := getPayloadRef(p); ref != "" {
+		// Check the payload's git ref against the webhook's branch filter.
+		if !checkBranchFilter(w.BranchFilter, ref) {
 			return nil
 		}
 	}
diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go
index 34bcfd3df9d9a..f4432cc3f1198 100644
--- a/services/webhook/webhook_test.go
+++ b/services/webhook/webhook_test.go
@@ -10,6 +10,7 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
@@ -90,3 +91,30 @@ func TestWebhookUserMail(t *testing.T) {
 	assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(t.Context(), user, nil).Email)
 	assert.Equal(t, user.Email, convert.ToUser(t.Context(), user, user).Email)
 }
+
+func TestCheckBranchFilter(t *testing.T) {
+	cases := []struct {
+		filter string
+		ref    git.RefName
+		match  bool
+	}{
+		{"", "any-ref", true},
+		{"*", "any-ref", true},
+		{"**", "any-ref", true},
+
+		{"main", git.RefNameFromBranch("main"), true},
+		{"main", git.RefNameFromTag("main"), false},
+
+		{"feature/*", git.RefNameFromBranch("feature"), false},
+		{"feature/*", git.RefNameFromBranch("feature/foo"), true},
+		{"feature/*", git.RefNameFromTag("feature/foo"), false},
+
+		{"{refs/heads/feature/*,refs/tags/release/*}", git.RefNameFromBranch("feature/foo"), true},
+		{"{refs/heads/feature/*,refs/tags/release/*}", git.RefNameFromBranch("main"), false},
+		{"{refs/heads/feature/*,refs/tags/release/*}", git.RefNameFromTag("release/bar"), true},
+		{"{refs/heads/feature/*,refs/tags/release/*}", git.RefNameFromTag("dev"), false},
+	}
+	for _, v := range cases {
+		assert.Equal(t, v.match, checkBranchFilter(v.filter, v.ref), "filter: %q ref: %q", v.filter, v.ref)
+	}
+}
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index a8ad1d6c9e5cf..1f71b9cfab7c4 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -44,7 +44,16 @@
 
main{main,feature/*}{refs/heads/feature/*,refs/tags/release/*}