Repository: xanzy/go-gitlab
Pull request #1951 (2024-06-05 18:12:34)

Add support for custom webhook headers
With 17.1, GitLab now supports the ability to set custom headers on webhooks. 

See the API documentation here: 

Project:
Add/Edit: https://docs.gitlab.com/ee/api/projects.html#edit-project-hook
Set Header: https://docs.gitlab.com/ee/api/projects.html#set-a-custom-header
Delete Header: https://docs.gitlab.com/ee/api/projects.html#delete-a-custom-header

Group:
Add/Edit: https://docs.gitlab.com/ee/api/groups.html#add-group-hook
Set Header: https://docs.gitlab.com/ee/api/groups.html#set-a-custom-header
Delete Header: https://docs.gitlab.com/ee/api/groups.html#delete-a-custom-header

Note that the `value` of the header will never come back from the API, you can only ever _set_ it (with these APIs). 

Note: Ignore the branch name please; I was intending to submit Group hooks as a separate PR, but just ended up adding it here so they could share the underlying structs in a couple cases :)


Patch:
diff --git a/group_hooks.go b/group_hooks.go
index 09e812a..414a8d0 100644
--- a/group_hooks.go
+++ b/group_hooks.go
@@ -26,29 +26,30 @@ import (
 //
 // GitLab API docs: https://docs.gitlab.com/ee/api/groups.html#list-group-hooks
 type GroupHook struct {
-	ID                        int        `json:"id"`
-	URL                       string     `json:"url"`
-	GroupID                   int        `json:"group_id"`
-	PushEvents                bool       `json:"push_events"`
-	PushEventsBranchFilter    string     `json:"push_events_branch_filter"`
-	IssuesEvents              bool       `json:"issues_events"`
-	ConfidentialIssuesEvents  bool       `json:"confidential_issues_events"`
-	ConfidentialNoteEvents    bool       `json:"confidential_note_events"`
-	MergeRequestsEvents       bool       `json:"merge_requests_events"`
-	TagPushEvents             bool       `json:"tag_push_events"`
-	NoteEvents                bool       `json:"note_events"`
-	JobEvents                 bool       `json:"job_events"`
-	PipelineEvents            bool       `json:"pipeline_events"`
-	WikiPageEvents            bool       `json:"wiki_page_events"`
-	DeploymentEvents          bool       `json:"deployment_events"`
-	ReleasesEvents            bool       `json:"releases_events"`
-	SubGroupEvents            bool       `json:"subgroup_events"`
-	MemberEvents              bool       `json:"member_events"`
-	EnableSSLVerification     bool       `json:"enable_ssl_verification"`
-	AlertStatus               string     `json:"alert_status"`
-	CreatedAt                 *time.Time `json:"created_at"`
-	CustomWebhookTemplate     string     `json:"custom_webhook_template"`
-	ResourceAccessTokenEvents bool       `json:"resource_access_token_events"`
+	ID                        int                 `json:"id"`
+	URL                       string              `json:"url"`
+	GroupID                   int                 `json:"group_id"`
+	PushEvents                bool                `json:"push_events"`
+	PushEventsBranchFilter    string              `json:"push_events_branch_filter"`
+	IssuesEvents              bool                `json:"issues_events"`
+	ConfidentialIssuesEvents  bool                `json:"confidential_issues_events"`
+	ConfidentialNoteEvents    bool                `json:"confidential_note_events"`
+	MergeRequestsEvents       bool                `json:"merge_requests_events"`
+	TagPushEvents             bool                `json:"tag_push_events"`
+	NoteEvents                bool                `json:"note_events"`
+	JobEvents                 bool                `json:"job_events"`
+	PipelineEvents            bool                `json:"pipeline_events"`
+	WikiPageEvents            bool                `json:"wiki_page_events"`
+	DeploymentEvents          bool                `json:"deployment_events"`
+	ReleasesEvents            bool                `json:"releases_events"`
+	SubGroupEvents            bool                `json:"subgroup_events"`
+	MemberEvents              bool                `json:"member_events"`
+	EnableSSLVerification     bool                `json:"enable_ssl_verification"`
+	AlertStatus               string              `json:"alert_status"`
+	CreatedAt                 *time.Time          `json:"created_at"`
+	CustomWebhookTemplate     string              `json:"custom_webhook_template"`
+	ResourceAccessTokenEvents bool                `json:"resource_access_token_events"`
+	CustomHeaders             []*HookCustomHeader `url:"custom_headers,omitempty" json:"custom_headers,omitempty"`
 }
 
 // ListGroupHooksOptions represents the available ListGroupHooks() options.
@@ -108,26 +109,27 @@ func (s *GroupsService) GetGroupHook(pid interface{}, hook int, options ...Reque
 //
 // GitLab API docs: https://docs.gitlab.com/ee/api/groups.html#add-group-hook
 type AddGroupHookOptions struct {
-	URL                       *string `url:"url,omitempty" json:"url,omitempty"`
-	PushEvents                *bool   `url:"push_events,omitempty"  json:"push_events,omitempty"`
-	PushEventsBranchFilter    *string `url:"push_events_branch_filter,omitempty"  json:"push_events_branch_filter,omitempty"`
-	IssuesEvents              *bool   `url:"issues_events,omitempty"  json:"issues_events,omitempty"`
-	ConfidentialIssuesEvents  *bool   `url:"confidential_issues_events,omitempty"  json:"confidential_issues_events,omitempty"`
-	ConfidentialNoteEvents    *bool   `url:"confidential_note_events,omitempty"  json:"confidential_note_events,omitempty"`
-	MergeRequestsEvents       *bool   `url:"merge_requests_events,omitempty"  json:"merge_requests_events,omitempty"`
-	TagPushEvents             *bool   `url:"tag_push_events,omitempty"  json:"tag_push_events,omitempty"`
-	NoteEvents                *bool   `url:"note_events,omitempty"  json:"note_events,omitempty"`
-	JobEvents                 *bool   `url:"job_events,omitempty"  json:"job_events,omitempty"`
-	PipelineEvents            *bool   `url:"pipeline_events,omitempty"  json:"pipeline_events,omitempty"`
-	WikiPageEvents            *bool   `url:"wiki_page_events,omitempty"  json:"wiki_page_events,omitempty"`
-	DeploymentEvents          *bool   `url:"deployment_events,omitempty" json:"deployment_events,omitempty"`
-	ReleasesEvents            *bool   `url:"releases_events,omitempty" json:"releases_events,omitempty"`
-	SubGroupEvents            *bool   `url:"subgroup_events,omitempty" json:"subgroup_events,omitempty"`
-	MemberEvents              *bool   `url:"member_events,omitempty" json:"member_events,omitempty"`
-	EnableSSLVerification     *bool   `url:"enable_ssl_verification,omitempty"  json:"enable_ssl_verification,omitempty"`
-	Token                     *string `url:"token,omitempty" json:"token,omitempty"`
-	ResourceAccessTokenEvents *bool   `url:"resource_access_token_events,omitempty" json:"resource_access_token_events,omitempty"`
-	CustomWebhookTemplate     *string `url:"custom_webhook_template,omitempty" json:"custom_webhook_template,omitempty"`
+	URL                       *string              `url:"url,omitempty" json:"url,omitempty"`
+	PushEvents                *bool                `url:"push_events,omitempty"  json:"push_events,omitempty"`
+	PushEventsBranchFilter    *string              `url:"push_events_branch_filter,omitempty"  json:"push_events_branch_filter,omitempty"`
+	IssuesEvents              *bool                `url:"issues_events,omitempty"  json:"issues_events,omitempty"`
+	ConfidentialIssuesEvents  *bool                `url:"confidential_issues_events,omitempty"  json:"confidential_issues_events,omitempty"`
+	ConfidentialNoteEvents    *bool                `url:"confidential_note_events,omitempty"  json:"confidential_note_events,omitempty"`
+	MergeRequestsEvents       *bool                `url:"merge_requests_events,omitempty"  json:"merge_requests_events,omitempty"`
+	TagPushEvents             *bool                `url:"tag_push_events,omitempty"  json:"tag_push_events,omitempty"`
+	NoteEvents                *bool                `url:"note_events,omitempty"  json:"note_events,omitempty"`
+	JobEvents                 *bool                `url:"job_events,omitempty"  json:"job_events,omitempty"`
+	PipelineEvents            *bool                `url:"pipeline_events,omitempty"  json:"pipeline_events,omitempty"`
+	WikiPageEvents            *bool                `url:"wiki_page_events,omitempty"  json:"wiki_page_events,omitempty"`
+	DeploymentEvents          *bool                `url:"deployment_events,omitempty" json:"deployment_events,omitempty"`
+	ReleasesEvents            *bool                `url:"releases_events,omitempty" json:"releases_events,omitempty"`
+	SubGroupEvents            *bool                `url:"subgroup_events,omitempty" json:"subgroup_events,omitempty"`
+	MemberEvents              *bool                `url:"member_events,omitempty" json:"member_events,omitempty"`
+	EnableSSLVerification     *bool                `url:"enable_ssl_verification,omitempty"  json:"enable_ssl_verification,omitempty"`
+	Token                     *string              `url:"token,omitempty" json:"token,omitempty"`
+	ResourceAccessTokenEvents *bool                `url:"resource_access_token_events,omitempty" json:"resource_access_token_events,omitempty"`
+	CustomWebhookTemplate     *string              `url:"custom_webhook_template,omitempty" json:"custom_webhook_template,omitempty"`
+	CustomHeaders             *[]*HookCustomHeader `url:"custom_headers,omitempty" json:"custom_headers,omitempty"`
 }
 
 // AddGroupHook create a new group scoped webhook.
@@ -159,26 +161,27 @@ func (s *GroupsService) AddGroupHook(gid interface{}, opt *AddGroupHookOptions,
 // GitLab API docs:
 // https://docs.gitlab.com/ee/api/groups.html#edit-group-hook
 type EditGroupHookOptions struct {
-	URL                       *string `url:"url,omitempty" json:"url,omitempty"`
-	PushEvents                *bool   `url:"push_events,omitempty" json:"push_events,omitempty"`
-	PushEventsBranchFilter    *string `url:"push_events_branch_filter,omitempty"  json:"push_events_branch_filter,omitempty"`
-	IssuesEvents              *bool   `url:"issues_events,omitempty" json:"issues_events,omitempty"`
-	ConfidentialIssuesEvents  *bool   `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"`
-	ConfidentialNoteEvents    *bool   `url:"confidential_note_events,omitempty" json:"confidential_note_events,omitempty"`
-	MergeRequestsEvents       *bool   `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"`
-	TagPushEvents             *bool   `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"`
-	NoteEvents                *bool   `url:"note_events,omitempty" json:"note_events,omitempty"`
-	JobEvents                 *bool   `url:"job_events,omitempty" json:"job_events,omitempty"`
-	PipelineEvents            *bool   `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"`
-	WikiPageEvents            *bool   `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"`
-	DeploymentEvents          *bool   `url:"deployment_events,omitempty" json:"deployment_events,omitempty"`
-	ReleasesEvents            *bool   `url:"releases_events,omitempty" json:"releases_events,omitempty"`
-	SubGroupEvents            *bool   `url:"subgroup_events,omitempty" json:"subgroup_events,omitempty"`
-	MemberEvents              *bool   `url:"member_events,omitempty" json:"member_events,omitempty"`
-	EnableSSLVerification     *bool   `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"`
-	Token                     *string `url:"token,omitempty" json:"token,omitempty"`
-	ResourceAccessTokenEvents *bool   `url:"resource_access_token_events,omitempty" json:"resource_access_token_events,omitempty"`
-	CustomWebhookTemplate     *string `url:"custom_webhook_template,omitempty" json:"custom_webhook_template,omitempty"`
+	URL                       *string              `url:"url,omitempty" json:"url,omitempty"`
+	PushEvents                *bool                `url:"push_events,omitempty" json:"push_events,omitempty"`
+	PushEventsBranchFilter    *string              `url:"push_events_branch_filter,omitempty"  json:"push_events_branch_filter,omitempty"`
+	IssuesEvents              *bool                `url:"issues_events,omitempty" json:"issues_events,omitempty"`
+	ConfidentialIssuesEvents  *bool                `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"`
+	ConfidentialNoteEvents    *bool                `url:"confidential_note_events,omitempty" json:"confidential_note_events,omitempty"`
+	MergeRequestsEvents       *bool                `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"`
+	TagPushEvents             *bool                `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"`
+	NoteEvents                *bool                `url:"note_events,omitempty" json:"note_events,omitempty"`
+	JobEvents                 *bool                `url:"job_events,omitempty" json:"job_events,omitempty"`
+	PipelineEvents            *bool                `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"`
+	WikiPageEvents            *bool                `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"`
+	DeploymentEvents          *bool                `url:"deployment_events,omitempty" json:"deployment_events,omitempty"`
+	ReleasesEvents            *bool                `url:"releases_events,omitempty" json:"releases_events,omitempty"`
+	SubGroupEvents            *bool                `url:"subgroup_events,omitempty" json:"subgroup_events,omitempty"`
+	MemberEvents              *bool                `url:"member_events,omitempty" json:"member_events,omitempty"`
+	EnableSSLVerification     *bool                `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"`
+	Token                     *string              `url:"token,omitempty" json:"token,omitempty"`
+	ResourceAccessTokenEvents *bool                `url:"resource_access_token_events,omitempty" json:"resource_access_token_events,omitempty"`
+	CustomWebhookTemplate     *string              `url:"custom_webhook_template,omitempty" json:"custom_webhook_template,omitempty"`
+	CustomHeaders             *[]*HookCustomHeader `url:"custom_headers,omitempty" json:"custom_headers,omitempty"`
 }
 
 // EditGroupHook edits a hook for a specified group.
@@ -225,3 +228,41 @@ func (s *GroupsService) DeleteGroupHook(pid interface{}, hook int, options ...Re
 
 	return s.client.Do(req, nil)
 }
+
+// SetGroupCustomHeader creates or updates a group custom webhook header.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/groups.html#set-a-custom-header
+func (s *GroupsService) SetGroupCustomHeader(gid interface{}, hook int, key string, opt *SetHookCustomHeaderOptions, options ...RequestOptionFunc) (*Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("groups/%s/hooks/%d/custom_headers/%s", PathEscape(group), hook, key)
+
+	req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
+// DeleteGroupCustomHeader deletes a group custom webhook header.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/groups.html#delete-a-custom-header
+func (s *GroupsService) DeleteGroupCustomHeader(gid interface{}, hook int, key string, options ...RequestOptionFunc) (*Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("groups/%s/hooks/%d/custom_headers/%s", PathEscape(group), hook, key)
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
diff --git a/projects.go b/projects.go
index 6c34de2..1b60129 100644
--- a/projects.go
+++ b/projects.go
@@ -1197,31 +1197,41 @@ type ProjectMember struct {
 	AvatarURL   string           `json:"avatar_url"`
 }
 
+// HookCustomHeader represents a project or group hook custom header
+// Note: "Key" is returned from the Get operation, but "Value" is not
+// The List operation doesn't return any headers at all for Projects,
+// but does return headers for Groups
+type HookCustomHeader struct {
+	Key   string `json:"key"`
+	Value string `json:"value"`
+}
+
 // ProjectHook represents a project hook.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/ee/api/projects.html#list-project-hooks
 type ProjectHook struct {
-	ID                        int        `json:"id"`
-	URL                       string     `json:"url"`
-	ConfidentialNoteEvents    bool       `json:"confidential_note_events"`
-	ProjectID                 int        `json:"project_id"`
-	PushEvents                bool       `json:"push_events"`
-	PushEventsBranchFilter    string     `json:"push_events_branch_filter"`
-	IssuesEvents              bool       `json:"issues_events"`
-	ConfidentialIssuesEvents  bool       `json:"confidential_issues_events"`
-	MergeRequestsEvents       bool       `json:"merge_requests_events"`
-	TagPushEvents             bool       `json:"tag_push_events"`
-	NoteEvents                bool       `json:"note_events"`
-	JobEvents                 bool       `json:"job_events"`
-	PipelineEvents            bool       `json:"pipeline_events"`
-	WikiPageEvents            bool       `json:"wiki_page_events"`
-	DeploymentEvents          bool       `json:"deployment_events"`
-	ReleasesEvents            bool       `json:"releases_events"`
-	EnableSSLVerification     bool       `json:"enable_ssl_verification"`
-	CreatedAt                 *time.Time `json:"created_at"`
-	ResourceAccessTokenEvents bool       `json:"resource_access_token_events"`
-	CustomWebhookTemplate     string     `json:"custom_webhook_template"`
+	ID                        int                 `json:"id"`
+	URL                       string              `json:"url"`
+	ConfidentialNoteEvents    bool                `json:"confidential_note_events"`
+	ProjectID                 int                 `json:"project_id"`
+	PushEvents                bool                `json:"push_events"`
+	PushEventsBranchFilter    string              `json:"push_events_branch_filter"`
+	IssuesEvents              bool                `json:"issues_events"`
+	ConfidentialIssuesEvents  bool                `json:"confidential_issues_events"`
+	MergeRequestsEvents       bool                `json:"merge_requests_events"`
+	TagPushEvents             bool                `json:"tag_push_events"`
+	NoteEvents                bool                `json:"note_events"`
+	JobEvents                 bool                `json:"job_events"`
+	PipelineEvents            bool                `json:"pipeline_events"`
+	WikiPageEvents            bool                `json:"wiki_page_events"`
+	DeploymentEvents          bool                `json:"deployment_events"`
+	ReleasesEvents            bool                `json:"releases_events"`
+	EnableSSLVerification     bool                `json:"enable_ssl_verification"`
+	CreatedAt                 *time.Time          `json:"created_at"`
+	ResourceAccessTokenEvents bool                `json:"resource_access_token_events"`
+	CustomWebhookTemplate     string              `json:"custom_webhook_template"`
+	CustomHeaders             []*HookCustomHeader `json:"custom_headers"`
 }
 
 // ListProjectHooksOptions represents the available ListProjectHooks() options.
@@ -1284,24 +1294,25 @@ func (s *ProjectsService) GetProjectHook(pid interface{}, hook int, options ...R
 // GitLab API docs:
 // https://docs.gitlab.com/ee/api/projects.html#add-project-hook
 type AddProjectHookOptions struct {
-	ConfidentialIssuesEvents  *bool   `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"`
-	ConfidentialNoteEvents    *bool   `url:"confidential_note_events,omitempty" json:"confidential_note_events,omitempty"`
-	DeploymentEvents          *bool   `url:"deployment_events,omitempty" json:"deployment_events,omitempty"`
-	EnableSSLVerification     *bool   `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"`
-	IssuesEvents              *bool   `url:"issues_events,omitempty" json:"issues_events,omitempty"`
-	JobEvents                 *bool   `url:"job_events,omitempty" json:"job_events,omitempty"`
-	MergeRequestsEvents       *bool   `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"`
-	NoteEvents                *bool   `url:"note_events,omitempty" json:"note_events,omitempty"`
-	PipelineEvents            *bool   `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"`
-	PushEvents                *bool   `url:"push_events,omitempty" json:"push_events,omitempty"`
-	PushEventsBranchFilter    *string `url:"push_events_branch_filter,omitempty" json:"push_events_branch_filter,omitempty"`
-	ReleasesEvents            *bool   `url:"releases_events,omitempty" json:"releases_events,omitempty"`
-	TagPushEvents             *bool   `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"`
-	Token                     *string `url:"token,omitempty" json:"token,omitempty"`
-	URL                       *string `url:"url,omitempty" json:"url,omitempty"`
-	WikiPageEvents            *bool   `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"`
-	ResourceAccessTokenEvents *bool   `url:"resource_access_token_events,omitempty" json:"resource_access_token_events,omitempty"`
-	CustomWebhookTemplate     *string `url:"custom_webhook_template,omitempty" json:"custom_webhook_template,omitempty"`
+	ConfidentialIssuesEvents  *bool                `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"`
+	ConfidentialNoteEvents    *bool                `url:"confidential_note_events,omitempty" json:"confidential_note_events,omitempty"`
+	DeploymentEvents          *bool                `url:"deployment_events,omitempty" json:"deployment_events,omitempty"`
+	EnableSSLVerification     *bool                `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"`
+	IssuesEvents              *bool                `url:"issues_events,omitempty" json:"issues_events,omitempty"`
+	JobEvents                 *bool                `url:"job_events,omitempty" json:"job_events,omitempty"`
+	MergeRequestsEvents       *bool                `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"`
+	NoteEvents                *bool                `url:"note_events,omitempty" json:"note_events,omitempty"`
+	PipelineEvents            *bool                `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"`
+	PushEvents                *bool                `url:"push_events,omitempty" json:"push_events,omitempty"`
+	PushEventsBranchFilter    *string              `url:"push_events_branch_filter,omitempty" json:"push_events_branch_filter,omitempty"`
+	ReleasesEvents            *bool                `url:"releases_events,omitempty" json:"releases_events,omitempty"`
+	TagPushEvents             *bool                `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"`
+	Token                     *string              `url:"token,omitempty" json:"token,omitempty"`
+	URL                       *string              `url:"url,omitempty" json:"url,omitempty"`
+	WikiPageEvents            *bool                `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"`
+	ResourceAccessTokenEvents *bool                `url:"resource_access_token_events,omitempty" json:"resource_access_token_events,omitempty"`
+	CustomWebhookTemplate     *string              `url:"custom_webhook_template,omitempty" json:"custom_webhook_template,omitempty"`
+	CustomHeaders             *[]*HookCustomHeader `url:"custom_headers,omitempty" json:"custom_headers,omitempty"`
 }
 
 // AddProjectHook adds a hook to a specified project.
@@ -1334,24 +1345,25 @@ func (s *ProjectsService) AddProjectHook(pid interface{}, opt *AddProjectHookOpt
 // GitLab API docs:
 // https://docs.gitlab.com/ee/api/projects.html#edit-project-hook
 type EditProjectHookOptions struct {
-	ConfidentialIssuesEvents  *bool   `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"`
-	ConfidentialNoteEvents    *bool   `url:"confidential_note_events,omitempty" json:"confidential_note_events,omitempty"`
-	DeploymentEvents          *bool   `url:"deployment_events,omitempty" json:"deployment_events,omitempty"`
-	EnableSSLVerification     *bool   `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"`
-	IssuesEvents              *bool   `url:"issues_events,omitempty" json:"issues_events,omitempty"`
-	JobEvents                 *bool   `url:"job_events,omitempty" json:"job_events,omitempty"`
-	MergeRequestsEvents       *bool   `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"`
-	NoteEvents                *bool   `url:"note_events,omitempty" json:"note_events,omitempty"`
-	PipelineEvents            *bool   `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"`
-	PushEvents                *bool   `url:"push_events,omitempty" json:"push_events,omitempty"`
-	PushEventsBranchFilter    *string `url:"push_events_branch_filter,omitempty" json:"push_events_branch_filter,omitempty"`
-	ReleasesEvents            *bool   `url:"releases_events,omitempty" json:"releases_events,omitempty"`
-	TagPushEvents             *bool   `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"`
-	Token                     *string `url:"token,omitempty" json:"token,omitempty"`
-	URL                       *string `url:"url,omitempty" json:"url,omitempty"`
-	WikiPageEvents            *bool   `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"`
-	ResourceAccessTokenEvents *bool   `url:"resource_access_token_events,omitempty" json:"resource_access_token_events,omitempty"`
-	CustomWebhookTemplate     *string `url:"custom_webhook_template,omitempty" json:"custom_webhook_template,omitempty"`
+	ConfidentialIssuesEvents  *bool                `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"`
+	ConfidentialNoteEvents    *bool                `url:"confidential_note_events,omitempty" json:"confidential_note_events,omitempty"`
+	DeploymentEvents          *bool                `url:"deployment_events,omitempty" json:"deployment_events,omitempty"`
+	EnableSSLVerification     *bool                `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"`
+	IssuesEvents              *bool                `url:"issues_events,omitempty" json:"issues_events,omitempty"`
+	JobEvents                 *bool                `url:"job_events,omitempty" json:"job_events,omitempty"`
+	MergeRequestsEvents       *bool                `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"`
+	NoteEvents                *bool                `url:"note_events,omitempty" json:"note_events,omitempty"`
+	PipelineEvents            *bool                `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"`
+	PushEvents                *bool                `url:"push_events,omitempty" json:"push_events,omitempty"`
+	PushEventsBranchFilter    *string              `url:"push_events_branch_filter,omitempty" json:"push_events_branch_filter,omitempty"`
+	ReleasesEvents            *bool                `url:"releases_events,omitempty" json:"releases_events,omitempty"`
+	TagPushEvents             *bool                `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"`
+	Token                     *string              `url:"token,omitempty" json:"token,omitempty"`
+	URL                       *string              `url:"url,omitempty" json:"url,omitempty"`
+	WikiPageEvents            *bool                `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"`
+	ResourceAccessTokenEvents *bool                `url:"resource_access_token_events,omitempty" json:"resource_access_token_events,omitempty"`
+	CustomWebhookTemplate     *string              `url:"custom_webhook_template,omitempty" json:"custom_webhook_template,omitempty"`
+	CustomHeaders             *[]*HookCustomHeader `url:"custom_headers,omitempty" json:"custom_headers,omitempty"`
 }
 
 // EditProjectHook edits a hook for a specified project.
@@ -1399,6 +1411,53 @@ func (s *ProjectsService) DeleteProjectHook(pid interface{}, hook int, options .
 	return s.client.Do(req, nil)
 }
 
+// SetHookCustomHeaderOptions represents a project or group hook custom header.
+// If the header isn't present, it will be created.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/projects.html#set-a-custom-header
+type SetHookCustomHeaderOptions struct {
+	Value *string `json:"value,omitempty"`
+}
+
+// SetProjectCustomHeader creates or updates a project custom webhook header.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/projects.html#set-a-custom-header
+func (s *ProjectsService) SetProjectCustomHeader(pid interface{}, hook int, key string, opt *SetHookCustomHeaderOptions, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/hooks/%d/custom_headers/%s", PathEscape(project), hook, key)
+
+	req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
+// DeleteProjectCustomHeader deletes a project custom webhook header.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/projects.html#delete-a-custom-header
+func (s *ProjectsService) DeleteProjectCustomHeader(pid interface{}, hook int, key string, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/hooks/%d/custom_headers/%s", PathEscape(project), hook, key)
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
 // ProjectForkRelation represents a project fork relationship.
 //
 // GitLab API docs:


Test patch:
diff --git a/group_hooks_test.go b/group_hooks_test.go
index cb434eb..06e5544 100644
--- a/group_hooks_test.go
+++ b/group_hooks_test.go
@@ -17,11 +17,15 @@
 package gitlab
 
 import (
+	"encoding/json"
 	"fmt"
+	"io"
 	"net/http"
 	"reflect"
 	"testing"
 	"time"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestListGroupHooks(t *testing.T) {
@@ -52,7 +56,11 @@ func TestListGroupHooks(t *testing.T) {
 		"enable_ssl_verification": true,
 		"alert_status": "executable",
 		"created_at": "2012-10-12T17:04:47Z",
-		"resource_access_token_events": true
+		"resource_access_token_events": true,
+		"custom_headers": [
+			{"key": "Authorization"},
+			{"key": "OtherHeader"}
+		]
 	}
 ]`)
 	})
@@ -85,6 +93,14 @@ func TestListGroupHooks(t *testing.T) {
 		AlertStatus:               "executable",
 		CreatedAt:                 &datePointer,
 		ResourceAccessTokenEvents: true,
+		CustomHeaders: []*HookCustomHeader{
+			{
+				Key: "Authorization",
+			},
+			{
+				Key: "OtherHeader",
+			},
+		},
 	}}
 
 	if !reflect.DeepEqual(groupHooks, want) {
@@ -119,7 +135,11 @@ func TestGetGroupHook(t *testing.T) {
 	"enable_ssl_verification": true,
 	"alert_status": "executable",
 	"created_at": "2012-10-12T17:04:47Z",
-	"resource_access_token_events": true
+	"resource_access_token_events": true,
+	"custom_headers": [
+		{"key": "Authorization"},
+		{"key": "OtherHeader"}
+	]
 }`)
 	})
 
@@ -151,6 +171,14 @@ func TestGetGroupHook(t *testing.T) {
 		AlertStatus:               "executable",
 		CreatedAt:                 &datePointer,
 		ResourceAccessTokenEvents: true,
+		CustomHeaders: []*HookCustomHeader{
+			{
+				Key: "Authorization",
+			},
+			{
+				Key: "OtherHeader",
+			},
+		},
 	}
 
 	if !reflect.DeepEqual(groupHook, want) {
@@ -185,7 +213,11 @@ func TestAddGroupHook(t *testing.T) {
 	"enable_ssl_verification": true,
 	"created_at": "2012-10-12T17:04:47Z",
 	"custom_webhook_template": "addTestValue",
-	"resource_access_token_events": true
+	"resource_access_token_events": true,
+	"custom_headers": [
+		{"key": "Authorization", "value": "testMe"},
+		{"key": "OtherHeader", "value": "otherTest"}
+	]
 }`)
 	})
 
@@ -223,6 +255,16 @@ func TestAddGroupHook(t *testing.T) {
 		CreatedAt:                 &datePointer,
 		CustomWebhookTemplate:     "addTestValue",
 		ResourceAccessTokenEvents: true,
+		CustomHeaders: []*HookCustomHeader{
+			{
+				Key:   "Authorization",
+				Value: "testMe",
+			},
+			{
+				Key:   "OtherHeader",
+				Value: "otherTest",
+			},
+		},
 	}
 
 	if !reflect.DeepEqual(groupHooks, want) {
@@ -257,7 +299,11 @@ func TestEditGroupHook(t *testing.T) {
 	"enable_ssl_verification": true,
 	"created_at": "2012-10-12T17:04:47Z",
 	"custom_webhook_template": "testValue",
-	"resource_access_token_events": true
+	"resource_access_token_events": true,
+	"custom_headers": [
+		{"key": "Authorization", "value": "testMe"},
+		{"key": "OtherHeader", "value": "otherTest"}
+	]
 }`)
 	})
 
@@ -295,6 +341,16 @@ func TestEditGroupHook(t *testing.T) {
 		CreatedAt:                 &datePointer,
 		CustomWebhookTemplate:     "testValue",
 		ResourceAccessTokenEvents: true,
+		CustomHeaders: []*HookCustomHeader{
+			{
+				Key:   "Authorization",
+				Value: "testMe",
+			},
+			{
+				Key:   "OtherHeader",
+				Value: "otherTest",
+			},
+		},
 	}
 
 	if !reflect.DeepEqual(groupHooks, want) {
@@ -314,3 +370,76 @@ func TestDeleteGroupHook(t *testing.T) {
 		t.Error(err)
 	}
 }
+
+func TestGetGroupWebhookHeader(t *testing.T) {
+	mux, client := setup(t)
+
+	// Removed most of the arguments to keep test slim
+	mux.HandleFunc("/api/v4/groups/1/hooks/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprint(w, `{
+			"id": 1,
+			"custom_webhook_template": "{\"event\":\"{{object_kind}}\"}",
+			"custom_headers": [
+			  {
+				"key": "Authorization"
+			  },
+			  {
+				"key": "OtherKey"
+			  }
+			]
+		  }`)
+	})
+
+	hook, _, err := client.Groups.GetGroupHook(1, 1)
+	if err != nil {
+		t.Errorf("Projects.GetGroupHook returned error: %v", err)
+	}
+
+	want := &GroupHook{
+		ID:                    1,
+		CustomWebhookTemplate: "{\"event\":\"{{object_kind}}\"}",
+		CustomHeaders: []*HookCustomHeader{
+			{
+				Key: "Authorization",
+			},
+			{
+				Key: "OtherKey",
+			},
+		},
+	}
+
+	if !reflect.DeepEqual(want, hook) {
+		t.Errorf("Projects.GetGroupHook returned %+v, want %+v", hook, want)
+	}
+}
+
+func TestSetGroupWebhookHeader(t *testing.T) {
+	mux, client := setup(t)
+	var bodyJson map[string]interface{}
+
+	// Removed most of the arguments to keep test slim
+	mux.HandleFunc("/api/v4/groups/1/hooks/1/custom_headers/Authorization", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPut)
+		w.WriteHeader(http.StatusNoContent)
+
+		// validate that the `value` body is sent properly
+		body, err := io.ReadAll(r.Body)
+		if err != nil {
+			t.Fatalf("Unable to read body properly. Error: %v", err)
+		}
+
+		// Unmarshal the body into JSON so we can check it
+		_ = json.Unmarshal(body, &bodyJson)
+
+		fmt.Fprint(w, ``)
+	})
+
+	req, err := client.Groups.SetGroupCustomHeader(1, 1, "Authorization", &SetHookCustomHeaderOptions{Value: Ptr("testValue")})
+	if err != nil {
+		t.Errorf("Groups.SetGroupCustomHeader returned error: %v", err)
+	}
+
+	assert.Equal(t, bodyJson["value"], "testValue")
+	assert.Equal(t, http.StatusNoContent, req.StatusCode)
+}
diff --git a/projects_test.go b/projects_test.go
index 7a1f5cd..21f6d7e 100644
--- a/projects_test.go
+++ b/projects_test.go
@@ -1470,9 +1470,10 @@ func TestProjectModelsOptionalMergeAttribute(t *testing.T) {
 }
 
 // Test that the "CustomWebhookTemplate" serializes properly
-func TestProjectAddWebhook_CustomTemplate(t *testing.T) {
+func TestProjectAddWebhook_CustomTemplateStuff(t *testing.T) {
 	mux, client := setup(t)
 	customWebhookSet := false
+	authValueSet := false
 
 	mux.HandleFunc("/api/v4/projects/1/hooks",
 		func(w http.ResponseWriter, r *http.Request) {
@@ -1484,27 +1485,49 @@ func TestProjectAddWebhook_CustomTemplate(t *testing.T) {
 				t.Fatalf("Unable to read body properly. Error: %v", err)
 			}
 			customWebhookSet = strings.Contains(string(body), "custom_webhook_template")
+			authValueSet = strings.Contains(string(body), `"value":"stuff"`)
 
 			fmt.Fprint(w, `{
-				"custom_webhook_template": "testValue"
+				"custom_webhook_template": "testValue",
+				"custom_headers": [
+					{
+						"key": "Authorization"
+					},
+					{
+						"key": "Favorite-Pet"
+					}
+				]
 			}`)
 		},
 	)
 
 	hook, resp, err := client.Projects.AddProjectHook(1, &AddProjectHookOptions{
 		CustomWebhookTemplate: Ptr(`{"example":"{{object_kind}}"}`),
+		CustomHeaders: &[]*HookCustomHeader{
+			{
+				Key:   "Authorization",
+				Value: "stuff",
+			},
+			{
+				Key:   "Favorite-Pet",
+				Value: "Cats",
+			},
+		},
 	})
 
 	assert.NoError(t, err)
 	assert.Equal(t, http.StatusCreated, resp.StatusCode)
 	assert.Equal(t, true, customWebhookSet)
+	assert.Equal(t, true, authValueSet)
 	assert.Equal(t, "testValue", hook.CustomWebhookTemplate)
+	assert.Equal(t, 2, len(hook.CustomHeaders))
 }
 
 // Test that the "CustomWebhookTemplate" serializes properly when editing
-func TestProjectEditWebhook_CustomTemplate(t *testing.T) {
+func TestProjectEditWebhook_CustomTemplateStuff(t *testing.T) {
 	mux, client := setup(t)
 	customWebhookSet := false
+	authValueSet := false
 
 	mux.HandleFunc("/api/v4/projects/1/hooks/1",
 		func(w http.ResponseWriter, r *http.Request) {
@@ -1516,18 +1539,41 @@ func TestProjectEditWebhook_CustomTemplate(t *testing.T) {
 				t.Fatalf("Unable to read body properly. Error: %v", err)
 			}
 			customWebhookSet = strings.Contains(string(body), "custom_webhook_template")
+			authValueSet = strings.Contains(string(body), `"value":"stuff"`)
 
-			fmt.Fprint(w, "{}")
+			fmt.Fprint(w, `{
+				"custom_webhook_template": "testValue",
+				"custom_headers": [
+					{
+						"key": "Authorization"
+					},
+					{
+						"key": "Favorite-Pet"
+					}
+				]}`)
 		},
 	)
 
-	_, resp, err := client.Projects.EditProjectHook(1, 1, &EditProjectHookOptions{
+	hook, resp, err := client.Projects.EditProjectHook(1, 1, &EditProjectHookOptions{
 		CustomWebhookTemplate: Ptr(`{"example":"{{object_kind}}"}`),
+		CustomHeaders: &[]*HookCustomHeader{
+			{
+				Key:   "Authorization",
+				Value: "stuff",
+			},
+			{
+				Key:   "Favorite-Pet",
+				Value: "Cats",
+			},
+		},
 	})
 
 	assert.NoError(t, err)
 	assert.Equal(t, http.StatusOK, resp.StatusCode)
 	assert.Equal(t, true, customWebhookSet)
+	assert.Equal(t, true, authValueSet)
+	assert.Equal(t, "testValue", hook.CustomWebhookTemplate)
+	assert.Equal(t, 2, len(hook.CustomHeaders))
 }
 
 func TestGetProjectPushRules(t *testing.T) {
@@ -1703,3 +1749,94 @@ func TestEditProjectPushRules(t *testing.T) {
 		t.Errorf("Projects.EditProjectPushRule returned %+v, want %+v", rule, want)
 	}
 }
+
+func TestGetProjectWebhookHeader(t *testing.T) {
+	mux, client := setup(t)
+
+	// Removed most of the arguments to keep test slim
+	mux.HandleFunc("/api/v4/projects/1/hooks/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprint(w, `{
+			"id": 1,
+			"custom_webhook_template": "{\"event\":\"{{object_kind}}\"}",
+			"custom_headers": [
+			  {
+				"key": "Authorization"
+			  },
+			  {
+				"key": "OtherKey"
+			  }
+			]
+		  }`)
+	})
+
+	hook, _, err := client.Projects.GetProjectHook(1, 1)
+	if err != nil {
+		t.Errorf("Projects.GetProjectHook returned error: %v", err)
+	}
+
+	want := &ProjectHook{
+		ID:                    1,
+		CustomWebhookTemplate: "{\"event\":\"{{object_kind}}\"}",
+		CustomHeaders: []*HookCustomHeader{
+			{
+				Key: "Authorization",
+			},
+			{
+				Key: "OtherKey",
+			},
+		},
+	}
+
+	if !reflect.DeepEqual(want, hook) {
+		t.Errorf("Projects.GetProjectHook returned %+v, want %+v", hook, want)
+	}
+}
+
+func TestSetProjectWebhookHeader(t *testing.T) {
+	mux, client := setup(t)
+	var bodyJson map[string]interface{}
+
+	// Removed most of the arguments to keep test slim
+	mux.HandleFunc("/api/v4/projects/1/hooks/1/custom_headers/Authorization", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPut)
+		w.WriteHeader(http.StatusNoContent)
+
+		// validate that the `value` body is sent properly
+		body, err := io.ReadAll(r.Body)
+		if err != nil {
+			t.Fatalf("Unable to read body properly. Error: %v", err)
+		}
+
+		// Unmarshal the body into JSON so we can check it
+		_ = json.Unmarshal(body, &bodyJson)
+
+		fmt.Fprint(w, ``)
+	})
+
+	req, err := client.Projects.SetProjectCustomHeader(1, 1, "Authorization", &SetHookCustomHeaderOptions{Value: Ptr("testValue")})
+	if err != nil {
+		t.Errorf("Projects.SetProjectCustomHeader returned error: %v", err)
+	}
+
+	assert.Equal(t, bodyJson["value"], "testValue")
+	assert.Equal(t, http.StatusNoContent, req.StatusCode)
+}
+
+func TestDeleteProjectWebhookHeader(t *testing.T) {
+	mux, client := setup(t)
+
+	// Removed most of the arguments to keep test slim
+	mux.HandleFunc("/api/v4/projects/1/hooks/1/custom_headers/Authorization", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+		w.WriteHeader(http.StatusNoContent)
+		fmt.Fprint(w, ``)
+	})
+
+	req, err := client.Projects.DeleteProjectCustomHeader(1, 1, "Authorization")
+	if err != nil {
+		t.Errorf("Projects.DeleteProjectCustomHeader returned error: %v", err)
+	}
+
+	assert.Equal(t, http.StatusNoContent, req.StatusCode)
+}