diff --git a/Build/Sources/Sass/component/_table.scss b/Build/Sources/Sass/component/_table.scss
index 56230e3584e51bbd0e83847bf90b7c0ed2e6fb70..3d5d0874f87f60e2411255c25f9e09bb7a3286de 100644
--- a/Build/Sources/Sass/component/_table.scss
+++ b/Build/Sources/Sass/component/_table.scss
@@ -119,6 +119,7 @@
     .col-selector {
         span.form-check {
             display: inline-block;
+            margin-bottom: 0;
         }
     }
 }
diff --git a/composer.json b/composer.json
index 250a2ce4681561073b32bcca46bcb70e0beecc46..ecac9f03237437f08ae966c70dced8d9a37cc546 100644
--- a/composer.json
+++ b/composer.json
@@ -216,6 +216,7 @@
 		"typo3/cms-t3editor": "self.version",
 		"typo3/cms-tstemplate": "self.version",
 		"typo3/cms-viewpage": "self.version",
+		"typo3/cms-webhooks": "self.version",
 		"typo3/cms-workspaces": "self.version"
 	},
 	"autoload": {
@@ -253,6 +254,7 @@
 			"TYPO3\\CMS\\T3editor\\": "typo3/sysext/t3editor/Classes/",
 			"TYPO3\\CMS\\Tstemplate\\": "typo3/sysext/tstemplate/Classes/",
 			"TYPO3\\CMS\\Viewpage\\": "typo3/sysext/viewpage/Classes/",
+			"TYPO3\\CMS\\Webhooks\\": "typo3/sysext/webhooks/Classes/",
 			"TYPO3\\CMS\\Workspaces\\": "typo3/sysext/workspaces/Classes/"
 		},
 		"classmap": [
@@ -294,6 +296,7 @@
 			"TYPO3\\CMS\\Seo\\Tests\\": "typo3/sysext/seo/Tests/",
 			"TYPO3\\CMS\\Setup\\Tests\\": "typo3/sysext/setup/Tests/",
 			"TYPO3\\CMS\\SysNote\\Tests\\": "typo3/sysext/sys_note/Tests/",
+			"TYPO3\\CMS\\Webhooks\\Tests\\": "typo3/sysext/webhooks/Tests/",
 			"TYPO3\\CMS\\Workspaces\\Tests\\": "typo3/sysext/workspaces/Tests/",
 			"TYPO3\\CMS\\Recycler\\Tests\\": "typo3/sysext/recycler/Tests/",
 			"TYPO3\\CMS\\T3editor\\Tests\\": "typo3/sysext/t3editor/Tests/",
diff --git a/composer.lock b/composer.lock
index e525df56dd4f96554f9f6a66bc593a6b24c6a2da..eb69d330a75293abcf8a4ea84f1d0e6b00e92822 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "ffa2bec04e4f097057a4b6b5847fc8b7",
+    "content-hash": "4ab387b2c2c70ed65bc6b34cca55675b",
     "packages": [
         {
             "name": "bacon/bacon-qr-code",
diff --git a/typo3/sysext/backend/Resources/Public/Css/backend.css b/typo3/sysext/backend/Resources/Public/Css/backend.css
index fd30f82c64034275c144104e9a47e70f3e979277..f04332d62227e6a7fd6a57066a150756f66c4501 100644
--- a/typo3/sysext/backend/Resources/Public/Css/backend.css
+++ b/typo3/sysext/backend/Resources/Public/Css/backend.css
@@ -49,7 +49,7 @@
     margin-bottom: 0 !important;
   }
 }
-.table .table{margin:0}.table .table+.table{margin-top:6px}.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>th{white-space:nowrap;vertical-align:middle}.table>tbody>tr>td,.table>tfoot>tr>td,.table>thead>tr>td{vertical-align:middle}.table>thead>tr th.col-checkbox+th.col-title label{margin-bottom:0}.table>thead>tr td i,.table>thead>tr th i{font-weight:400}.table>tbody>tr{border-color:#ccc}.table .pagination{margin:0}.table td:first-child,.table th:first-child{padding-left:var(--typo3-component-padding-x)}.table td:last-child,.table th:last-child{padding-right:var(--typo3-component-padding-x)}.table .col-icon{text-align:center}.table .col-checkbox,.table .col-icon{padding-right:0}.table .col-task,.table .col-title{width:99%}.table .col-state{min-width:120px}.table .col-task{min-width:400px}.table .col-clipboard,.table .col-control,.table .col-nowrap{white-space:nowrap!important}.table .col-clipboard,.table .col-control{text-align:right}.table .col-border-left{border-left:1px solid var(--bs-table-border-color)}.table .col-min{min-width:150px}.table .col-responsive{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media (min-width:768px){.table .col-word-break{word-wrap:break-word;word-break:break-all}}.table .col-selector span.form-check{display:inline-block}.table-transparent{--bs-table-bg:transparent}.table-vertical-top td,.table-vertical-top th{vertical-align:top}.table-center td,.table-center th{text-align:center}.table-fit{width:100%;border-radius:var(--typo3-component-border-radius);box-shadow:var(--typo3-component-box-shadow);margin-bottom:var(--typo3-component-spacing);overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ccc}.table-fit .table-bordered>:not(caption)>*{border-width:0}.table-fit .table-bordered>:not(caption)>*>*{border-width:1px}.table-fit>.table{margin-bottom:0}.table-fit>.table colgroup:first-child+tbody tr:first-child td,.table-fit>.table colgroup:first-child+tbody tr:first-child th,.table-fit>.table tbody:first-child tr:first-child td,.table-fit>.table tbody:first-child tr:first-child th{border-top:0!important}.table-fit>.table tr>td:first-child,.table-fit>.table tr>th:first-child{border-left:0!important}.table-fit>.table tr>td:last-child,.table-fit>.table tr>th:last-child{border-right:0!important}.table-fit>.table tr:last-child td{border-bottom:0!important}.table-fit-wrap td,.table-fit-wrap th{white-space:normal}.table-fit-inline-block{max-width:100%;width:auto;display:inline-block;margin:0}.table-fit-inline-block>.table{width:auto}.media{display:grid;grid-template-columns:2rem auto;grid-gap:1rem}.media .media-body{grid-column:1/3}.media .media-left+.media-body{grid-column:2/3;align-self:center}.modal .close{background:0 0;border:none;color:inherit;padding:0;margin:0;text-shadow:rgba(0,0,0,.5) 0 0 3px;opacity:.5;-webkit-user-select:none;-moz-user-select:none;user-select:none}.modal .close:active,.modal .close:hover{box-shadow:none;outline:0;background:0 0;opacity:1}.modal-dialog{display:flex;flex-direction:column;align-items:stretch;justify-content:center;margin:0 auto!important;height:100vh;width:100vw}.modal-content{display:flex;flex-direction:column;margin:0 auto;max-height:calc(100vh - 40px);max-width:calc(100vw - 40px)}.modal-content .help-block{margin-bottom:0}.modal-body{overflow-y:auto}.modal-body>:first-child{margin-top:0}.modal-body>:last-child{margin-bottom:0}.modal-footer,.modal-header{padding:calc(var(--bs-modal-padding)/ 2) var(--bs-modal-padding)}.modal-footer .btn{margin-right:0;padding-left:1em;padding-right:1em;display:inline-flex;align-items:center}.modal-footer .btn typo3-backend-icon{margin-left:-2px;margin-right:4px}.modal-footer .progress{flex-basis:100%;margin-right:0;margin-left:0}.modal-footer .modal-btn-group{margin-right:0;margin-left:0}.modal-content{transition:margin-top .1s ease-in;border:none}.modal-content .modal-loading{height:100%;display:flex;flex:1 0 auto;justify-content:center;align-items:center}.modal-image-manipulation .modal-body{padding:0}@media (min-width:768px){.modal-image-manipulation .modal-body{display:flex;flex-direction:row}}.modal-image-manipulation .modal-panel-main{overflow:visible;background-image:url(../Images/cropper-background.png);display:flex;align-items:center;justify-content:center;padding:20px;width:100%}@media (min-width:768px){.modal-image-manipulation .modal-panel-main{width:calc(100% - 250px)}}@media (min-width:992px){.modal-image-manipulation .modal-panel-main{width:calc(100% - 300px)}}.modal-image-manipulation .modal-panel-main img{max-width:100%;max-height:100%;height:auto}.modal-image-manipulation .modal-panel-sidebar{padding:15px;flex-shrink:0;border-left:1px solid rgba(0,0,0,.25);position:relative;overflow:auto;-webkit-overflow-scrolling:touch;width:100%}@media (min-width:768px){.modal-image-manipulation .modal-panel-sidebar{width:250px}}@media (min-width:992px){.modal-image-manipulation .modal-panel-sidebar{width:300px}}.modal-image-manipulation .panel-heading .is-active{pointer-events:none}.modal-image-manipulation .panel{margin-bottom:0}.modal-image-manipulation .panel-body{border-left:2px solid #ff8700}.modal-image-manipulation .panel-body label{margin-bottom:4px}.modal-type-iframe{padding:0}.modal-type-iframe .modal-body{padding:0}.modal-iframe{display:block;border:0;height:100%;width:100%;position:absolute;top:0;left:0}.modal-size-small .modal-content{width:440px}.modal-size-default .modal-content{width:600px}.modal-size-medium .modal-content{width:800px;height:520px}.modal-size-large .modal-content{width:1000px;height:800px}.modal-size-full .modal-content{width:100%;height:100%}.modal-severity-primary .modal-header{background-color:#0078e6;color:#fff;border-bottom-color:#006ccf}.modal-severity-secondary .modal-header{background-color:#737373;color:#fff;border-bottom-color:#686868}.modal-severity-success .modal-header{background-color:#107c10;color:#fff;border-bottom-color:#0e700e}.modal-severity-info .modal-header{background-color:#6daae0;color:#000;border-bottom-color:#6299ca}.modal-severity-warning .modal-header{background-color:#e8a33d;color:#000;border-bottom-color:#d19337}.modal-severity-danger .modal-header{background-color:#c83c3c;color:#fff;border-bottom-color:#b43636}.modal-severity-light .modal-header{background-color:#f5f5f5;color:#000;border-bottom-color:#ddd}.modal-severity-default .modal-header{background-color:#f5f5f5;color:#000;border-bottom-color:#ddd}.modal-severity-notice .modal-header{background-color:#333;color:#fff;border-bottom-color:#2e2e2e}.modal-severity-dark .modal-header{background-color:#1e1e1e;color:#fff;border-bottom-color:#1b1b1b}.modal-style-dark{color:#fff}.modal-style-dark .modal-header{color:#fff;background-color:#484848;border-bottom-color:#000}.modal-style-dark .modal-content{overflow:hidden;background-color:#292929}.modal-style-dark .modal-body,.modal-style-dark .modal-footer{background-color:#292929;color:#fff}.modal-style-dark .modal-footer{border-top:1px solid #000}.t3js-modal-footer .form-inline{display:block;margin:1em 0;width:100%}.t3js-modal-footer label{margin-right:10px}.modal-multi-step-wizard .modal-body .carousel.slide{min-height:21em}.modal-multi-step-wizard .modal-body .carousel-inner{width:auto;margin:0 -5px;padding:0 5px}.modal-multi-step-wizard .modal-footer .btn+.btn{margin-left:.5em}.modal-multi-step-wizard .modal-footer .progress-bar.inactive{background:0 0;color:#000}:root{--note-primary-color:#000;--note-primary-bg:#d9ebfb;--note-primary-header-color:#000;--note-primary-header-bg:#99c9f5;--note-secondary-color:#000;--note-secondary-bg:#eaeaea;--note-secondary-header-color:#000;--note-secondary-header-bg:#c7c7c7;--note-success-color:#000;--note-success-bg:#dbebdb;--note-success-header-color:#000;--note-success-header-bg:#9fcb9f;--note-info-color:#000;--note-info-bg:#e9f2fa;--note-info-header-color:#000;--note-info-header-bg:#c5ddf3;--note-warning-color:#000;--note-warning-bg:#fcf1e2;--note-warning-header-color:#000;--note-warning-header-bg:#f6dab1;--note-danger-color:#000;--note-danger-bg:#f7e2e2;--note-danger-header-color:#000;--note-danger-header-bg:#e9b1b1;--note-light-color:#000;--note-light-bg:#fefefe;--note-light-header-color:#000;--note-light-header-bg:#fbfbfb;--note-default-color:#000;--note-default-bg:#fefefe;--note-default-header-color:#000;--note-default-header-bg:#fbfbfb;--note-notice-color:#000;--note-notice-bg:#e0e0e0;--note-notice-header-color:#000;--note-notice-header-bg:#adadad;--note-dark-color:#000;--note-dark-bg:#dddddd;--note-dark-header-color:#000;--note-dark-header-bg:#a5a5a5;--note-light-bg:rgb(245, 245, 245);--note-light-header-bg:#dddddd;--note-default-bg:rgb(238, 238, 238);--note-default-header-bg:#d6d6d6}.note-list{display:grid;gap:calc(var(--typo3-component-spacing)/ 2);margin-bottom:var(--typo3-component-spacing)}.note-list .note{margin-bottom:0}.note{overflow:hidden;position:relative;z-index:1;color:var(--note-color);background-color:var(--note-bg);border-radius:var(--typo3-component-border-radius);margin-bottom:var(--typo3-component-spacing);box-shadow:var(--typo3-component-box-shadow);--note-color:var(--note-default-color);--note-bg:var(--note-default-bg);--note-header-color:var(--note-default-header-color);--note-header-bg:var(--note-default-header-bg)}.note-header{color:var(--note-header-color);background-color:var(--note-header-bg);padding:.5rem 1rem}.note-header-bar{display:flex;align-items:center;flex-wrap:wrap;gap:.5rem}.note-actions{margin-left:auto}.note-body{padding:1rem}.note-body>:first-child{margin-top:0}.note-body>:last-child{margin-bottom:0}.note-primary{--note-color:var(--note-primary-color);--note-bg:var(--note-primary-bg);--note-header-color:var(--note-primary-header-color);--note-header-bg:var(--note-primary-header-bg)}.note-secondary{--note-color:var(--note-secondary-color);--note-bg:var(--note-secondary-bg);--note-header-color:var(--note-secondary-header-color);--note-header-bg:var(--note-secondary-header-bg)}.note-success{--note-color:var(--note-success-color);--note-bg:var(--note-success-bg);--note-header-color:var(--note-success-header-color);--note-header-bg:var(--note-success-header-bg)}.note-info{--note-color:var(--note-info-color);--note-bg:var(--note-info-bg);--note-header-color:var(--note-info-header-color);--note-header-bg:var(--note-info-header-bg)}.note-warning{--note-color:var(--note-warning-color);--note-bg:var(--note-warning-bg);--note-header-color:var(--note-warning-header-color);--note-header-bg:var(--note-warning-header-bg)}.note-danger{--note-color:var(--note-danger-color);--note-bg:var(--note-danger-bg);--note-header-color:var(--note-danger-header-color);--note-header-bg:var(--note-danger-header-bg)}.note-light{--note-color:var(--note-light-color);--note-bg:var(--note-light-bg);--note-header-color:var(--note-light-header-color);--note-header-bg:var(--note-light-header-bg)}.note-default{--note-color:var(--note-default-color);--note-bg:var(--note-default-bg);--note-header-color:var(--note-default-header-color);--note-header-bg:var(--note-default-header-bg)}.note-notice{--note-color:var(--note-notice-color);--note-bg:var(--note-notice-bg);--note-header-color:var(--note-notice-header-color);--note-header-bg:var(--note-notice-header-bg)}.note-dark{--note-color:var(--note-dark-color);--note-bg:var(--note-dark-bg);--note-header-color:var(--note-dark-header-color);--note-header-bg:var(--note-dark-header-bg)}.note-category-1{--note-color:var(--note-info-color);--note-bg:var(--note-info-bg);--note-header-color:var(--note-info-header-color);--note-header-bg:var(--note-info-header-bg)}.note-category-2{--note-color:var(--note-warning-color);--note-bg:var(--note-warning-bg);--note-header-color:var(--note-warning-header-color);--note-header-bg:var(--note-warning-header-bg)}.note-category-3{--note-color:var(--note-notice-color);--note-bg:var(--note-notice-bg);--note-header-color:var(--note-notice-header-color);--note-header-bg:var(--note-notice-header-bg)}.note-category-4{--note-color:var(--note-success-color);--note-bg:var(--note-success-bg);--note-header-color:var(--note-success-header-color);--note-header-bg:var(--note-success-header-bg)}.card{overflow:hidden;box-shadow:0 1px 1px rgba(0,0,0,.2);border-color:#ccc;margin-bottom:20px;transition:all .2s ease-in-out;transition-property:box-shadow,border,transform}a.card:hover{text-decoration:none;border:1px solid #b3b3b3;transform:translate(0,-1px);box-shadow:0 2px 1px rgba(0,0,0,.3)}.card-container{display:flex;flex-wrap:wrap;margin:10px -10px}.card-container .card{margin-left:10px;margin-right:10px}.card-size-large,.card-size-medium,.card-size-small{width:calc(100% - 20px)}@media (min-width:768px){.card-size-small{width:calc(50% - 20px)}}@media (min-width:992px){.card-size-small{width:calc(25% - 20px)}}@media (min-width:768px){.card-size-medium{width:calc(50% - 20px)}}.card-size-fixed-small{width:calc(100% - 20px)}@media (min-width:624px){.card-size-fixed-small{width:calc(50% - 20px)}}@media (min-width:768px){.card-size-fixed-small{width:300px}}.card-disabled{opacity:.4}.card-body,.card-footer,.card-header,.card-image{padding:1.5em 1.5em 0 1.5em}.card-body:last-child,.card-footer:last-child,.card-header:last-child,.card-image:last-child{padding-bottom:1.5em}.card-body :first-child,.card-footer :first-child,.card-header :first-child,.card-image :first-child{margin-top:0}.card-body :last-child,.card-footer :last-child,.card-header :last-child,.card-image :last-child{margin-bottom:0}.card-image{position:relative;padding-left:0;padding-right:0}.card-image:first-child{padding-top:0}.card-image:first-child .card-image-badge{top:.75em}.card-image:last-child{padding-bottom:0}.card-image .card-image-badge{position:absolute;top:1.5em;right:.75em}.card-image img{display:block;height:auto;width:100%;margin:0 auto}.card-header{border-bottom:none}.card-header .card-icon{float:left;margin-right:.75em}.card-header .card-header-body{display:block;overflow:hidden}.card-header .card-title{font-family:inherit;font-weight:500;display:block;font-size:1.35em;line-height:1.2em;margin:0}.card-header .card-subtitle{display:block;margin-top:.5em;font-size:1em;line-height:1.2em;opacity:.5}.card-header .card-longdesc{margin-top:1em}.card-footer{border-top:none}.card-table td:first-child,.card-table th:first-child{padding-left:1.5em}.card-table td:last-child,.card-table th:last-child{padding-right:1.5em}.form{margin-bottom:var(--typo3-component-spacing)}.form-wizard-icon-list{color:var(--typo3-component-color);background:var(--typo3-component-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:.125rem;margin-top:.25rem;padding:calc(var(--typo3-spacing)/ 4);display:flex;flex-wrap:wrap;gap:2px}.form-wizard-icon-list-item a{display:flex;height:100%;color:var(--typo3-component-color);border-radius:calc(var(--typo3-component-border-radius)/ 2);padding:calc(var(--typo3-spacing)/ 2);align-items:center;justify-content:center;outline-offset:-1px}.form-wizard-icon-list-item a:hover{color:var(--typo3-list-item-hover-color);background-color:var(--typo3-list-item-hover-bg);outline:1px solid var(--typo3-list-item-hover-border-color)}.form-wizard-icon-list-item a:focus{color:var(--typo3-list-item-focus-color);background-color:var(--typo3-list-item-focus-bg);outline:1px solid var(--typo3-list-item-focus-border-color)}.form-wizard-icon-list-item a.active,.form-wizard-icon-list-item a:active{color:var(--typo3-list-item-active-color);background-color:var(--typo3-list-item-active-bg);outline:1px solid var(--typo3-list-item-active-border-color)}.form-wizard-icon-list-item a,.form-wizard-icon-list-item a>span[title]{display:block;line-height:1}.form-wizard-icon-list-item img{display:block;min-width:16px;max-width:128px;max-height:128px}.context-menu{font-size:var(--typo3-component-font-size);line-height:var(--typo3-component-line-height);padding:2px;position:absolute;z-index:310;color:var(--typo3-component-color);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);background-color:var(--typo3-component-bg);box-shadow:var(--typo3-component-box-shadow);border-radius:var(--typo3-component-border-radius)}.context-menu-group{position:relative;display:flex;flex-direction:column;gap:1px;list-style:none;padding:0;margin:0;min-width:150px}.context-menu-item{position:relative;display:flex;border-radius:calc(var(--typo3-component-border-radius) - var(--typo3-component-border-width));gap:.5em;padding:var(--typo3-list-item-padding-y) var(--typo3-list-item-padding-x);cursor:pointer;text-decoration:none}.context-menu-item:focus,.context-menu-item:hover{z-index:1;outline-offset:-1px}.context-menu-item:hover{color:var(--typo3-component-hover-color);background-color:var(--typo3-component-hover-bg);outline:1px solid var(--typo3-component-hover-border-color)}.context-menu-item:focus{color:var(--typo3-component-focus-color);background-color:var(--typo3-component-focus-bg);outline:1px solid var(--typo3-component-focus-border-color)}.context-menu-item-icon{flex-shrink:0;flex-grow:0;width:var(--icon-size-small)}.context-menu-item-label{flex-grow:1}.context-menu-item-indicator{flex-shrink:0;flex-grow:0;width:var(--icon-size-small)}.context-menu-item-divider{padding:0;height:0;margin-top:var(--typo3-list-item-padding-y);margin-bottom:var(--typo3-list-item-padding-y);border-top:var(--typo3-component-border-width) solid var(--typo3-component-border-color)}typo3-backend-live-search{display:flex;flex-direction:column;height:100%}typo3-backend-live-search .sticky-form-actions{z-index:50}typo3-backend-live-search .search-option-badge{position:absolute!important;transform:translate(-50%,-50%);top:0!important;left:100%;--bs-badge-border-radius:.65rem}typo3-backend-live-search-result-item-container,typo3-backend-live-search-result-item-detail-container{position:relative;flex-grow:1;flex-basis:50%;overflow:auto;padding:var(--bs-modal-padding)}typo3-backend-live-search-result-item-container{padding-top:0}typo3-backend-live-search-result-container{--livesearch-item-opacity:.5;--livesearch-preamble-delimiter-border-width:1px;--livesearch-preamble-delimiter-border-color:rgb(215, 215, 215);display:flex;flex-direction:row;margin:calc(var(--bs-modal-padding) * -1);height:100%;overflow:auto}typo3-backend-live-search-result-action-list,typo3-backend-live-search-result-list{display:flex;flex-direction:column;gap:1px}typo3-backend-live-search-result-list .livesearch-result-item-group-label{font-weight:700;line-height:inherit;padding:var(--typo3-list-item-padding-y) 0;border-bottom:1px solid rgba(0,0,0,.1);margin-bottom:var(--typo3-list-item-padding-y);background-color:#fff;z-index:20}typo3-backend-live-search-result-list .livesearch-result-item-group-label.sticky{position:sticky;top:0;z-index:15}typo3-backend-live-search-result-item-detail-container .livesearch-detail-preamble{text-align:center;padding-bottom:1em;margin-bottom:1em;border-bottom:var(--livesearch-preamble-delimiter-border-width) solid var(--livesearch-preamble-delimiter-border-color)}typo3-backend-live-search-result-item-detail-container .livesearch-detail-preamble .livesearch-detail-preamble-type{opacity:var(--livesearch-item-opacity)}typo3-backend-live-search-result-item,typo3-backend-live-search-result-item-action{display:flex;gap:1.5em;font-size:var(--typo3-component-font-size);line-height:var(--typo3-component-line-height);padding:var(--typo3-list-item-padding-y) var(--typo3-list-item-padding-x);border-radius:calc(var(--typo3-component-border-radius) - var(--typo3-component-border-width));color:#000;background-color:#fff;cursor:pointer}typo3-backend-live-search-result-item-action.active,typo3-backend-live-search-result-item-action:focus,typo3-backend-live-search-result-item-action:hover,typo3-backend-live-search-result-item.active,typo3-backend-live-search-result-item:focus,typo3-backend-live-search-result-item:hover{z-index:1;outline-offset:-1px}typo3-backend-live-search-result-item-action:hover,typo3-backend-live-search-result-item:hover{background-color:#f2f8fe;outline:1px solid #d9ebfb}typo3-backend-live-search-result-item-action.active,typo3-backend-live-search-result-item-action:focus,typo3-backend-live-search-result-item.active,typo3-backend-live-search-result-item:focus{background-color:#f2f8fe;outline:1px solid #3393eb}typo3-backend-live-search-result-item .livesearch-expand-action,typo3-backend-live-search-result-item-action .livesearch-expand-action{flex:0;align-items:center;margin:calc(var(--typo3-list-item-padding-y) * -1) calc(var(--typo3-list-item-padding-x) * -1);padding:var(--typo3-list-item-padding-y) calc(var(--typo3-list-item-padding-x)/ 2);border-left:1px solid transparent}typo3-backend-live-search-result-item .livesearch-expand-action:hover,typo3-backend-live-search-result-item-action .livesearch-expand-action:hover{border-left:1px solid #d9ebfb}typo3-backend-live-search-result-item-action>*,typo3-backend-live-search-result-item>*{display:flex;gap:.5em;flex:1}typo3-backend-live-search-result-item-action>* .livesearch-result-item-icon,typo3-backend-live-search-result-item>* .livesearch-result-item-icon{display:flex;gap:.5em;flex-grow:0;flex-shrink:0;align-items:center}typo3-backend-live-search-result-item-action>* .livesearch-result-item-title,typo3-backend-live-search-result-item>* .livesearch-result-item-title{flex-grow:1;word-break:break-word}typo3-backend-live-search-result-item-action>* .livesearch-result-item-title .small,typo3-backend-live-search-result-item-action>* .livesearch-result-item-title small,typo3-backend-live-search-result-item>* .livesearch-result-item-title .small,typo3-backend-live-search-result-item>* .livesearch-result-item-title small{opacity:var(--livesearch-item-opacity)}.recordlist{overflow:hidden;background:var(--panel-bg);box-shadow:var(--panel-box-shadow);border-radius:var(--panel-border-radius);border:var(--panel-border-width) solid var(--panel-default-border-color);margin-bottom:var(--panel-spacing)}.recordlist table tr td.deletePlaceholder{text-decoration:line-through}.recordlist .table-fit{box-shadow:none;border-radius:0;border-left:0;border-right:0;border-bottom:0;margin-bottom:0}.recordlist .pagination{display:inline-flex}.recordlist-heading{display:flex;align-items:center;flex-wrap:wrap;color:var(--panel-default-heading-color);background:var(--panel-default-heading-bg);padding:var(--panel-header-padding-y) var(--panel-header-padding-x);gap:var(--panel-header-padding-y) var(--panel-header-padding-x)}.recordlist-heading-row{flex-grow:1;display:flex;align-items:center;flex-wrap:wrap;max-width:100%;gap:var(--panel-header-padding-y) var(--panel-header-padding-x)}.recordlist-heading-title{font-weight:700;flex-grow:1;width:250px;max-width:100%}.recordlist-heading-actions,.recordlist-heading-selection{display:flex;align-items:center;flex-wrap:wrap;gap:.25rem}.recordlist-heading-actions [data-recordlist-action=new]{order:1}.recordlist-heading-actions [data-recordlist-action=download]{order:2}.recordlist-heading-actions [data-recordlist-action=columns]{order:3}.recordlist-heading-actions [data-recordlist-action=toggle]{order:99}.resource-tiles{--resource-tiles-grid-spacing:.5rem;--resource-tiles-grid-width:150px;--resource-tile-spacing:1rem;--resource-tile-border-radius:4px;--resource-tile-nameplate-size:12px;--resource-tile-nameplate-activity-size:10px;--resource-tile-checkbox-size:16px;--resource-tile-color:#313131;--resource-tile-bg:#fff;--resource-tile-border-color:rgb(215, 215, 215);--resource-tile-hover-color:var(--resource-tile-color);--resource-tile-hover-bg:rgba(0, 0, 0, .05);--resource-tile-hover-border-color:rgb(215, 215, 215);--resource-tile-focus-color:var(--resource-tile-color);--resource-tile-focus-bg:rgba(0, 0, 0, .05);--resource-tile-focus-border-color:rgb(187, 187, 187);--resource-tile-active-color:var(--resource-tile-color);--resource-tile-active-bg:#f2f8fe;--resource-tile-active-border-color:#3393eb;--resource-tile-success-color:var(--resource-tile-color);--resource-tile-success-bg:#f3f8f3;--resource-tile-success-border-color:#409640;--resource-tile-info-color:var(--resource-tile-color);--resource-tile-info-bg:#f8fbfd;--resource-tile-info-border-color:#8abbe6;--resource-tile-danger-color:var(--resource-tile-color);--resource-tile-danger-bg:#fcf5f5;--resource-tile-danger-border-color:#d36363;--resource-tile-warning-color:var(--resource-tile-color);--resource-tile-warning-bg:#fefaf5;--resource-tile-warning-border-color:#edb564;display:grid;grid-template-columns:repeat(auto-fill,var(--resource-tiles-grid-width));gap:var(--resource-tiles-grid-spacing);-webkit-user-select:none;-moz-user-select:none;user-select:none}
+.table .table{margin:0}.table .table+.table{margin-top:6px}.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>th{white-space:nowrap;vertical-align:middle}.table>tbody>tr>td,.table>tfoot>tr>td,.table>thead>tr>td{vertical-align:middle}.table>thead>tr th.col-checkbox+th.col-title label{margin-bottom:0}.table>thead>tr td i,.table>thead>tr th i{font-weight:400}.table>tbody>tr{border-color:#ccc}.table .pagination{margin:0}.table td:first-child,.table th:first-child{padding-left:var(--typo3-component-padding-x)}.table td:last-child,.table th:last-child{padding-right:var(--typo3-component-padding-x)}.table .col-icon{text-align:center}.table .col-checkbox,.table .col-icon{padding-right:0}.table .col-task,.table .col-title{width:99%}.table .col-state{min-width:120px}.table .col-task{min-width:400px}.table .col-clipboard,.table .col-control,.table .col-nowrap{white-space:nowrap!important}.table .col-clipboard,.table .col-control{text-align:right}.table .col-border-left{border-left:1px solid var(--bs-table-border-color)}.table .col-min{min-width:150px}.table .col-responsive{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media (min-width:768px){.table .col-word-break{word-wrap:break-word;word-break:break-all}}.table .col-selector span.form-check{display:inline-block;margin-bottom:0}.table-transparent{--bs-table-bg:transparent}.table-vertical-top td,.table-vertical-top th{vertical-align:top}.table-center td,.table-center th{text-align:center}.table-fit{width:100%;border-radius:var(--typo3-component-border-radius);box-shadow:var(--typo3-component-box-shadow);margin-bottom:var(--typo3-component-spacing);overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ccc}.table-fit .table-bordered>:not(caption)>*{border-width:0}.table-fit .table-bordered>:not(caption)>*>*{border-width:1px}.table-fit>.table{margin-bottom:0}.table-fit>.table colgroup:first-child+tbody tr:first-child td,.table-fit>.table colgroup:first-child+tbody tr:first-child th,.table-fit>.table tbody:first-child tr:first-child td,.table-fit>.table tbody:first-child tr:first-child th{border-top:0!important}.table-fit>.table tr>td:first-child,.table-fit>.table tr>th:first-child{border-left:0!important}.table-fit>.table tr>td:last-child,.table-fit>.table tr>th:last-child{border-right:0!important}.table-fit>.table tr:last-child td{border-bottom:0!important}.table-fit-wrap td,.table-fit-wrap th{white-space:normal}.table-fit-inline-block{max-width:100%;width:auto;display:inline-block;margin:0}.table-fit-inline-block>.table{width:auto}.media{display:grid;grid-template-columns:2rem auto;grid-gap:1rem}.media .media-body{grid-column:1/3}.media .media-left+.media-body{grid-column:2/3;align-self:center}.modal .close{background:0 0;border:none;color:inherit;padding:0;margin:0;text-shadow:rgba(0,0,0,.5) 0 0 3px;opacity:.5;-webkit-user-select:none;-moz-user-select:none;user-select:none}.modal .close:active,.modal .close:hover{box-shadow:none;outline:0;background:0 0;opacity:1}.modal-dialog{display:flex;flex-direction:column;align-items:stretch;justify-content:center;margin:0 auto!important;height:100vh;width:100vw}.modal-content{display:flex;flex-direction:column;margin:0 auto;max-height:calc(100vh - 40px);max-width:calc(100vw - 40px)}.modal-content .help-block{margin-bottom:0}.modal-body{overflow-y:auto}.modal-body>:first-child{margin-top:0}.modal-body>:last-child{margin-bottom:0}.modal-footer,.modal-header{padding:calc(var(--bs-modal-padding)/ 2) var(--bs-modal-padding)}.modal-footer .btn{margin-right:0;padding-left:1em;padding-right:1em;display:inline-flex;align-items:center}.modal-footer .btn typo3-backend-icon{margin-left:-2px;margin-right:4px}.modal-footer .progress{flex-basis:100%;margin-right:0;margin-left:0}.modal-footer .modal-btn-group{margin-right:0;margin-left:0}.modal-content{transition:margin-top .1s ease-in;border:none}.modal-content .modal-loading{height:100%;display:flex;flex:1 0 auto;justify-content:center;align-items:center}.modal-image-manipulation .modal-body{padding:0}@media (min-width:768px){.modal-image-manipulation .modal-body{display:flex;flex-direction:row}}.modal-image-manipulation .modal-panel-main{overflow:visible;background-image:url(../Images/cropper-background.png);display:flex;align-items:center;justify-content:center;padding:20px;width:100%}@media (min-width:768px){.modal-image-manipulation .modal-panel-main{width:calc(100% - 250px)}}@media (min-width:992px){.modal-image-manipulation .modal-panel-main{width:calc(100% - 300px)}}.modal-image-manipulation .modal-panel-main img{max-width:100%;max-height:100%;height:auto}.modal-image-manipulation .modal-panel-sidebar{padding:15px;flex-shrink:0;border-left:1px solid rgba(0,0,0,.25);position:relative;overflow:auto;-webkit-overflow-scrolling:touch;width:100%}@media (min-width:768px){.modal-image-manipulation .modal-panel-sidebar{width:250px}}@media (min-width:992px){.modal-image-manipulation .modal-panel-sidebar{width:300px}}.modal-image-manipulation .panel-heading .is-active{pointer-events:none}.modal-image-manipulation .panel{margin-bottom:0}.modal-image-manipulation .panel-body{border-left:2px solid #ff8700}.modal-image-manipulation .panel-body label{margin-bottom:4px}.modal-type-iframe{padding:0}.modal-type-iframe .modal-body{padding:0}.modal-iframe{display:block;border:0;height:100%;width:100%;position:absolute;top:0;left:0}.modal-size-small .modal-content{width:440px}.modal-size-default .modal-content{width:600px}.modal-size-medium .modal-content{width:800px;height:520px}.modal-size-large .modal-content{width:1000px;height:800px}.modal-size-full .modal-content{width:100%;height:100%}.modal-severity-primary .modal-header{background-color:#0078e6;color:#fff;border-bottom-color:#006ccf}.modal-severity-secondary .modal-header{background-color:#737373;color:#fff;border-bottom-color:#686868}.modal-severity-success .modal-header{background-color:#107c10;color:#fff;border-bottom-color:#0e700e}.modal-severity-info .modal-header{background-color:#6daae0;color:#000;border-bottom-color:#6299ca}.modal-severity-warning .modal-header{background-color:#e8a33d;color:#000;border-bottom-color:#d19337}.modal-severity-danger .modal-header{background-color:#c83c3c;color:#fff;border-bottom-color:#b43636}.modal-severity-light .modal-header{background-color:#f5f5f5;color:#000;border-bottom-color:#ddd}.modal-severity-default .modal-header{background-color:#f5f5f5;color:#000;border-bottom-color:#ddd}.modal-severity-notice .modal-header{background-color:#333;color:#fff;border-bottom-color:#2e2e2e}.modal-severity-dark .modal-header{background-color:#1e1e1e;color:#fff;border-bottom-color:#1b1b1b}.modal-style-dark{color:#fff}.modal-style-dark .modal-header{color:#fff;background-color:#484848;border-bottom-color:#000}.modal-style-dark .modal-content{overflow:hidden;background-color:#292929}.modal-style-dark .modal-body,.modal-style-dark .modal-footer{background-color:#292929;color:#fff}.modal-style-dark .modal-footer{border-top:1px solid #000}.t3js-modal-footer .form-inline{display:block;margin:1em 0;width:100%}.t3js-modal-footer label{margin-right:10px}.modal-multi-step-wizard .modal-body .carousel.slide{min-height:21em}.modal-multi-step-wizard .modal-body .carousel-inner{width:auto;margin:0 -5px;padding:0 5px}.modal-multi-step-wizard .modal-footer .btn+.btn{margin-left:.5em}.modal-multi-step-wizard .modal-footer .progress-bar.inactive{background:0 0;color:#000}:root{--note-primary-color:#000;--note-primary-bg:#d9ebfb;--note-primary-header-color:#000;--note-primary-header-bg:#99c9f5;--note-secondary-color:#000;--note-secondary-bg:#eaeaea;--note-secondary-header-color:#000;--note-secondary-header-bg:#c7c7c7;--note-success-color:#000;--note-success-bg:#dbebdb;--note-success-header-color:#000;--note-success-header-bg:#9fcb9f;--note-info-color:#000;--note-info-bg:#e9f2fa;--note-info-header-color:#000;--note-info-header-bg:#c5ddf3;--note-warning-color:#000;--note-warning-bg:#fcf1e2;--note-warning-header-color:#000;--note-warning-header-bg:#f6dab1;--note-danger-color:#000;--note-danger-bg:#f7e2e2;--note-danger-header-color:#000;--note-danger-header-bg:#e9b1b1;--note-light-color:#000;--note-light-bg:#fefefe;--note-light-header-color:#000;--note-light-header-bg:#fbfbfb;--note-default-color:#000;--note-default-bg:#fefefe;--note-default-header-color:#000;--note-default-header-bg:#fbfbfb;--note-notice-color:#000;--note-notice-bg:#e0e0e0;--note-notice-header-color:#000;--note-notice-header-bg:#adadad;--note-dark-color:#000;--note-dark-bg:#dddddd;--note-dark-header-color:#000;--note-dark-header-bg:#a5a5a5;--note-light-bg:rgb(245, 245, 245);--note-light-header-bg:#dddddd;--note-default-bg:rgb(238, 238, 238);--note-default-header-bg:#d6d6d6}.note-list{display:grid;gap:calc(var(--typo3-component-spacing)/ 2);margin-bottom:var(--typo3-component-spacing)}.note-list .note{margin-bottom:0}.note{overflow:hidden;position:relative;z-index:1;color:var(--note-color);background-color:var(--note-bg);border-radius:var(--typo3-component-border-radius);margin-bottom:var(--typo3-component-spacing);box-shadow:var(--typo3-component-box-shadow);--note-color:var(--note-default-color);--note-bg:var(--note-default-bg);--note-header-color:var(--note-default-header-color);--note-header-bg:var(--note-default-header-bg)}.note-header{color:var(--note-header-color);background-color:var(--note-header-bg);padding:.5rem 1rem}.note-header-bar{display:flex;align-items:center;flex-wrap:wrap;gap:.5rem}.note-actions{margin-left:auto}.note-body{padding:1rem}.note-body>:first-child{margin-top:0}.note-body>:last-child{margin-bottom:0}.note-primary{--note-color:var(--note-primary-color);--note-bg:var(--note-primary-bg);--note-header-color:var(--note-primary-header-color);--note-header-bg:var(--note-primary-header-bg)}.note-secondary{--note-color:var(--note-secondary-color);--note-bg:var(--note-secondary-bg);--note-header-color:var(--note-secondary-header-color);--note-header-bg:var(--note-secondary-header-bg)}.note-success{--note-color:var(--note-success-color);--note-bg:var(--note-success-bg);--note-header-color:var(--note-success-header-color);--note-header-bg:var(--note-success-header-bg)}.note-info{--note-color:var(--note-info-color);--note-bg:var(--note-info-bg);--note-header-color:var(--note-info-header-color);--note-header-bg:var(--note-info-header-bg)}.note-warning{--note-color:var(--note-warning-color);--note-bg:var(--note-warning-bg);--note-header-color:var(--note-warning-header-color);--note-header-bg:var(--note-warning-header-bg)}.note-danger{--note-color:var(--note-danger-color);--note-bg:var(--note-danger-bg);--note-header-color:var(--note-danger-header-color);--note-header-bg:var(--note-danger-header-bg)}.note-light{--note-color:var(--note-light-color);--note-bg:var(--note-light-bg);--note-header-color:var(--note-light-header-color);--note-header-bg:var(--note-light-header-bg)}.note-default{--note-color:var(--note-default-color);--note-bg:var(--note-default-bg);--note-header-color:var(--note-default-header-color);--note-header-bg:var(--note-default-header-bg)}.note-notice{--note-color:var(--note-notice-color);--note-bg:var(--note-notice-bg);--note-header-color:var(--note-notice-header-color);--note-header-bg:var(--note-notice-header-bg)}.note-dark{--note-color:var(--note-dark-color);--note-bg:var(--note-dark-bg);--note-header-color:var(--note-dark-header-color);--note-header-bg:var(--note-dark-header-bg)}.note-category-1{--note-color:var(--note-info-color);--note-bg:var(--note-info-bg);--note-header-color:var(--note-info-header-color);--note-header-bg:var(--note-info-header-bg)}.note-category-2{--note-color:var(--note-warning-color);--note-bg:var(--note-warning-bg);--note-header-color:var(--note-warning-header-color);--note-header-bg:var(--note-warning-header-bg)}.note-category-3{--note-color:var(--note-notice-color);--note-bg:var(--note-notice-bg);--note-header-color:var(--note-notice-header-color);--note-header-bg:var(--note-notice-header-bg)}.note-category-4{--note-color:var(--note-success-color);--note-bg:var(--note-success-bg);--note-header-color:var(--note-success-header-color);--note-header-bg:var(--note-success-header-bg)}.card{overflow:hidden;box-shadow:0 1px 1px rgba(0,0,0,.2);border-color:#ccc;margin-bottom:20px;transition:all .2s ease-in-out;transition-property:box-shadow,border,transform}a.card:hover{text-decoration:none;border:1px solid #b3b3b3;transform:translate(0,-1px);box-shadow:0 2px 1px rgba(0,0,0,.3)}.card-container{display:flex;flex-wrap:wrap;margin:10px -10px}.card-container .card{margin-left:10px;margin-right:10px}.card-size-large,.card-size-medium,.card-size-small{width:calc(100% - 20px)}@media (min-width:768px){.card-size-small{width:calc(50% - 20px)}}@media (min-width:992px){.card-size-small{width:calc(25% - 20px)}}@media (min-width:768px){.card-size-medium{width:calc(50% - 20px)}}.card-size-fixed-small{width:calc(100% - 20px)}@media (min-width:624px){.card-size-fixed-small{width:calc(50% - 20px)}}@media (min-width:768px){.card-size-fixed-small{width:300px}}.card-disabled{opacity:.4}.card-body,.card-footer,.card-header,.card-image{padding:1.5em 1.5em 0 1.5em}.card-body:last-child,.card-footer:last-child,.card-header:last-child,.card-image:last-child{padding-bottom:1.5em}.card-body :first-child,.card-footer :first-child,.card-header :first-child,.card-image :first-child{margin-top:0}.card-body :last-child,.card-footer :last-child,.card-header :last-child,.card-image :last-child{margin-bottom:0}.card-image{position:relative;padding-left:0;padding-right:0}.card-image:first-child{padding-top:0}.card-image:first-child .card-image-badge{top:.75em}.card-image:last-child{padding-bottom:0}.card-image .card-image-badge{position:absolute;top:1.5em;right:.75em}.card-image img{display:block;height:auto;width:100%;margin:0 auto}.card-header{border-bottom:none}.card-header .card-icon{float:left;margin-right:.75em}.card-header .card-header-body{display:block;overflow:hidden}.card-header .card-title{font-family:inherit;font-weight:500;display:block;font-size:1.35em;line-height:1.2em;margin:0}.card-header .card-subtitle{display:block;margin-top:.5em;font-size:1em;line-height:1.2em;opacity:.5}.card-header .card-longdesc{margin-top:1em}.card-footer{border-top:none}.card-table td:first-child,.card-table th:first-child{padding-left:1.5em}.card-table td:last-child,.card-table th:last-child{padding-right:1.5em}.form{margin-bottom:var(--typo3-component-spacing)}.form-wizard-icon-list{color:var(--typo3-component-color);background:var(--typo3-component-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:.125rem;margin-top:.25rem;padding:calc(var(--typo3-spacing)/ 4);display:flex;flex-wrap:wrap;gap:2px}.form-wizard-icon-list-item a{display:flex;height:100%;color:var(--typo3-component-color);border-radius:calc(var(--typo3-component-border-radius)/ 2);padding:calc(var(--typo3-spacing)/ 2);align-items:center;justify-content:center;outline-offset:-1px}.form-wizard-icon-list-item a:hover{color:var(--typo3-list-item-hover-color);background-color:var(--typo3-list-item-hover-bg);outline:1px solid var(--typo3-list-item-hover-border-color)}.form-wizard-icon-list-item a:focus{color:var(--typo3-list-item-focus-color);background-color:var(--typo3-list-item-focus-bg);outline:1px solid var(--typo3-list-item-focus-border-color)}.form-wizard-icon-list-item a.active,.form-wizard-icon-list-item a:active{color:var(--typo3-list-item-active-color);background-color:var(--typo3-list-item-active-bg);outline:1px solid var(--typo3-list-item-active-border-color)}.form-wizard-icon-list-item a,.form-wizard-icon-list-item a>span[title]{display:block;line-height:1}.form-wizard-icon-list-item img{display:block;min-width:16px;max-width:128px;max-height:128px}.context-menu{font-size:var(--typo3-component-font-size);line-height:var(--typo3-component-line-height);padding:2px;position:absolute;z-index:310;color:var(--typo3-component-color);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);background-color:var(--typo3-component-bg);box-shadow:var(--typo3-component-box-shadow);border-radius:var(--typo3-component-border-radius)}.context-menu-group{position:relative;display:flex;flex-direction:column;gap:1px;list-style:none;padding:0;margin:0;min-width:150px}.context-menu-item{position:relative;display:flex;border-radius:calc(var(--typo3-component-border-radius) - var(--typo3-component-border-width));gap:.5em;padding:var(--typo3-list-item-padding-y) var(--typo3-list-item-padding-x);cursor:pointer;text-decoration:none}.context-menu-item:focus,.context-menu-item:hover{z-index:1;outline-offset:-1px}.context-menu-item:hover{color:var(--typo3-component-hover-color);background-color:var(--typo3-component-hover-bg);outline:1px solid var(--typo3-component-hover-border-color)}.context-menu-item:focus{color:var(--typo3-component-focus-color);background-color:var(--typo3-component-focus-bg);outline:1px solid var(--typo3-component-focus-border-color)}.context-menu-item-icon{flex-shrink:0;flex-grow:0;width:var(--icon-size-small)}.context-menu-item-label{flex-grow:1}.context-menu-item-indicator{flex-shrink:0;flex-grow:0;width:var(--icon-size-small)}.context-menu-item-divider{padding:0;height:0;margin-top:var(--typo3-list-item-padding-y);margin-bottom:var(--typo3-list-item-padding-y);border-top:var(--typo3-component-border-width) solid var(--typo3-component-border-color)}typo3-backend-live-search{display:flex;flex-direction:column;height:100%}typo3-backend-live-search .sticky-form-actions{z-index:50}typo3-backend-live-search .search-option-badge{position:absolute!important;transform:translate(-50%,-50%);top:0!important;left:100%;--bs-badge-border-radius:.65rem}typo3-backend-live-search-result-item-container,typo3-backend-live-search-result-item-detail-container{position:relative;flex-grow:1;flex-basis:50%;overflow:auto;padding:var(--bs-modal-padding)}typo3-backend-live-search-result-item-container{padding-top:0}typo3-backend-live-search-result-container{--livesearch-item-opacity:.5;--livesearch-preamble-delimiter-border-width:1px;--livesearch-preamble-delimiter-border-color:rgb(215, 215, 215);display:flex;flex-direction:row;margin:calc(var(--bs-modal-padding) * -1);height:100%;overflow:auto}typo3-backend-live-search-result-action-list,typo3-backend-live-search-result-list{display:flex;flex-direction:column;gap:1px}typo3-backend-live-search-result-list .livesearch-result-item-group-label{font-weight:700;line-height:inherit;padding:var(--typo3-list-item-padding-y) 0;border-bottom:1px solid rgba(0,0,0,.1);margin-bottom:var(--typo3-list-item-padding-y);background-color:#fff;z-index:20}typo3-backend-live-search-result-list .livesearch-result-item-group-label.sticky{position:sticky;top:0;z-index:15}typo3-backend-live-search-result-item-detail-container .livesearch-detail-preamble{text-align:center;padding-bottom:1em;margin-bottom:1em;border-bottom:var(--livesearch-preamble-delimiter-border-width) solid var(--livesearch-preamble-delimiter-border-color)}typo3-backend-live-search-result-item-detail-container .livesearch-detail-preamble .livesearch-detail-preamble-type{opacity:var(--livesearch-item-opacity)}typo3-backend-live-search-result-item,typo3-backend-live-search-result-item-action{display:flex;gap:1.5em;font-size:var(--typo3-component-font-size);line-height:var(--typo3-component-line-height);padding:var(--typo3-list-item-padding-y) var(--typo3-list-item-padding-x);border-radius:calc(var(--typo3-component-border-radius) - var(--typo3-component-border-width));color:#000;background-color:#fff;cursor:pointer}typo3-backend-live-search-result-item-action.active,typo3-backend-live-search-result-item-action:focus,typo3-backend-live-search-result-item-action:hover,typo3-backend-live-search-result-item.active,typo3-backend-live-search-result-item:focus,typo3-backend-live-search-result-item:hover{z-index:1;outline-offset:-1px}typo3-backend-live-search-result-item-action:hover,typo3-backend-live-search-result-item:hover{background-color:#f2f8fe;outline:1px solid #d9ebfb}typo3-backend-live-search-result-item-action.active,typo3-backend-live-search-result-item-action:focus,typo3-backend-live-search-result-item.active,typo3-backend-live-search-result-item:focus{background-color:#f2f8fe;outline:1px solid #3393eb}typo3-backend-live-search-result-item .livesearch-expand-action,typo3-backend-live-search-result-item-action .livesearch-expand-action{flex:0;align-items:center;margin:calc(var(--typo3-list-item-padding-y) * -1) calc(var(--typo3-list-item-padding-x) * -1);padding:var(--typo3-list-item-padding-y) calc(var(--typo3-list-item-padding-x)/ 2);border-left:1px solid transparent}typo3-backend-live-search-result-item .livesearch-expand-action:hover,typo3-backend-live-search-result-item-action .livesearch-expand-action:hover{border-left:1px solid #d9ebfb}typo3-backend-live-search-result-item-action>*,typo3-backend-live-search-result-item>*{display:flex;gap:.5em;flex:1}typo3-backend-live-search-result-item-action>* .livesearch-result-item-icon,typo3-backend-live-search-result-item>* .livesearch-result-item-icon{display:flex;gap:.5em;flex-grow:0;flex-shrink:0;align-items:center}typo3-backend-live-search-result-item-action>* .livesearch-result-item-title,typo3-backend-live-search-result-item>* .livesearch-result-item-title{flex-grow:1;word-break:break-word}typo3-backend-live-search-result-item-action>* .livesearch-result-item-title .small,typo3-backend-live-search-result-item-action>* .livesearch-result-item-title small,typo3-backend-live-search-result-item>* .livesearch-result-item-title .small,typo3-backend-live-search-result-item>* .livesearch-result-item-title small{opacity:var(--livesearch-item-opacity)}.recordlist{overflow:hidden;background:var(--panel-bg);box-shadow:var(--panel-box-shadow);border-radius:var(--panel-border-radius);border:var(--panel-border-width) solid var(--panel-default-border-color);margin-bottom:var(--panel-spacing)}.recordlist table tr td.deletePlaceholder{text-decoration:line-through}.recordlist .table-fit{box-shadow:none;border-radius:0;border-left:0;border-right:0;border-bottom:0;margin-bottom:0}.recordlist .pagination{display:inline-flex}.recordlist-heading{display:flex;align-items:center;flex-wrap:wrap;color:var(--panel-default-heading-color);background:var(--panel-default-heading-bg);padding:var(--panel-header-padding-y) var(--panel-header-padding-x);gap:var(--panel-header-padding-y) var(--panel-header-padding-x)}.recordlist-heading-row{flex-grow:1;display:flex;align-items:center;flex-wrap:wrap;max-width:100%;gap:var(--panel-header-padding-y) var(--panel-header-padding-x)}.recordlist-heading-title{font-weight:700;flex-grow:1;width:250px;max-width:100%}.recordlist-heading-actions,.recordlist-heading-selection{display:flex;align-items:center;flex-wrap:wrap;gap:.25rem}.recordlist-heading-actions [data-recordlist-action=new]{order:1}.recordlist-heading-actions [data-recordlist-action=download]{order:2}.recordlist-heading-actions [data-recordlist-action=columns]{order:3}.recordlist-heading-actions [data-recordlist-action=toggle]{order:99}.resource-tiles{--resource-tiles-grid-spacing:.5rem;--resource-tiles-grid-width:150px;--resource-tile-spacing:1rem;--resource-tile-border-radius:4px;--resource-tile-nameplate-size:12px;--resource-tile-nameplate-activity-size:10px;--resource-tile-checkbox-size:16px;--resource-tile-color:#313131;--resource-tile-bg:#fff;--resource-tile-border-color:rgb(215, 215, 215);--resource-tile-hover-color:var(--resource-tile-color);--resource-tile-hover-bg:rgba(0, 0, 0, .05);--resource-tile-hover-border-color:rgb(215, 215, 215);--resource-tile-focus-color:var(--resource-tile-color);--resource-tile-focus-bg:rgba(0, 0, 0, .05);--resource-tile-focus-border-color:rgb(187, 187, 187);--resource-tile-active-color:var(--resource-tile-color);--resource-tile-active-bg:#f2f8fe;--resource-tile-active-border-color:#3393eb;--resource-tile-success-color:var(--resource-tile-color);--resource-tile-success-bg:#f3f8f3;--resource-tile-success-border-color:#409640;--resource-tile-info-color:var(--resource-tile-color);--resource-tile-info-bg:#f8fbfd;--resource-tile-info-border-color:#8abbe6;--resource-tile-danger-color:var(--resource-tile-color);--resource-tile-danger-bg:#fcf5f5;--resource-tile-danger-border-color:#d36363;--resource-tile-warning-color:var(--resource-tile-color);--resource-tile-warning-bg:#fefaf5;--resource-tile-warning-border-color:#edb564;display:grid;grid-template-columns:repeat(auto-fill,var(--resource-tiles-grid-width));gap:var(--resource-tiles-grid-spacing);-webkit-user-select:none;-moz-user-select:none;user-select:none}
 .resource-tiles-container {
   container-type: inline-size;
   margin-bottom: var(--typo3-component-spacing);
diff --git a/typo3/sysext/core/Classes/Attribute/RemoteEvent.php b/typo3/sysext/core/Classes/Attribute/WebhookMessage.php
similarity index 66%
rename from typo3/sysext/core/Classes/Attribute/RemoteEvent.php
rename to typo3/sysext/core/Classes/Attribute/WebhookMessage.php
index 6756bbf7c86eca00c93751c683d03cad1eba3195..1ba72d1d0710f81a17918f3f9965b31f48677f35 100644
--- a/typo3/sysext/core/Classes/Attribute/RemoteEvent.php
+++ b/typo3/sysext/core/Classes/Attribute/WebhookMessage.php
@@ -20,17 +20,17 @@ namespace TYPO3\CMS\Core\Attribute;
 use Attribute;
 
 /**
- * Service tag to mark an event as remote event (Webhook)
+ * Service tag to mark a message as a webhook-compatible message
  */
 #[Attribute(Attribute::TARGET_CLASS)]
-class RemoteEvent
+class WebhookMessage
 {
-    public function __construct(private string $description)
-    {
-    }
+    public const TAG_NAME = 'core.webhook_message';
 
-    public function getDescription(): string
-    {
-        return $this->description;
+    public function __construct(
+        public string $identifier,
+        public string $description,
+        public ?string $method = null,
+    ) {
     }
 }
diff --git a/typo3/sysext/core/Classes/Messaging/WebhookMessageInterface.php b/typo3/sysext/core/Classes/Messaging/WebhookMessageInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..e87fd627b69ffd4c7bb2b3ab7065a05854066554
--- /dev/null
+++ b/typo3/sysext/core/Classes/Messaging/WebhookMessageInterface.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Messaging;
+
+/**
+ * A semantic interface for messages that can be put into
+ * a message bus in order to be serialized.
+ *
+ * Recommendations for webhook messages:
+ * - POPOs like DTOs or custom message objects
+ * - No services, events, requests, or models
+ */
+interface WebhookMessageInterface extends \JsonSerializable
+{
+}
diff --git a/typo3/sysext/core/Documentation/Changelog/12.2/Feature-99632-IntroducePHPAttributeToMarkAWebhookMessage.rst b/typo3/sysext/core/Documentation/Changelog/12.2/Feature-99632-IntroducePHPAttributeToMarkAWebhookMessage.rst
new file mode 100644
index 0000000000000000000000000000000000000000..c6a1b391f5638f6e0a9baf27df8b79b55fa5f288
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.2/Feature-99632-IntroducePHPAttributeToMarkAWebhookMessage.rst
@@ -0,0 +1,49 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-99632-1674121967:
+
+===================================================================
+Feature: #99632 - Introduce PHP attribute to mark a webhook message
+===================================================================
+
+See :issue:`99632`
+
+Description
+===========
+
+A new custom PHP attribute :php:`\TYPO3\CMS\Core\Attribute\WebhookMessage` has
+been added in order to register a message as a specific webhook message,
+to send as remote status.
+
+The attribute must have an identifier for the webhook type (unique),
+and a description that explains the purpose of the message.
+
+Optionally, a property method can be set for the attribute,
+that contains the factory method. By default this is `createFromEvent`,
+which is typically used when creating a message by a event listener, see
+webhooks documentation for more details.
+
+Example
+-------
+
+..  code-block:: php
+
+    use TYPO3\CMS\Core\Attribute\WebhookMessage;
+
+    #[WebhookMessage(
+        identifier: 'typo3/file-updated',
+        description: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.webhook_type.typo3-file-updated'
+    )]
+    final class AnyKindOfMessage
+    {
+        // ...
+    }
+
+
+Impact
+======
+
+It is now possible to tag any PHP class as webhook message by the PHP attribute
+:php:`\TYPO3\CMS\Core\Attribute\WebhookMessage`.
+
+.. index:: Backend, Frontend, PHP-API, ext:core
diff --git a/typo3/sysext/core/Documentation/Changelog/12.2/Feature-99632-IntroducePHPAttributeToMarkAnEventAsRemoteEventWebhook.rst b/typo3/sysext/core/Documentation/Changelog/12.2/Feature-99632-IntroducePHPAttributeToMarkAnEventAsRemoteEventWebhook.rst
deleted file mode 100644
index 4a2d6cce74f99e6a89845fe5cf9eaf30f1a6957d..0000000000000000000000000000000000000000
--- a/typo3/sysext/core/Documentation/Changelog/12.2/Feature-99632-IntroducePHPAttributeToMarkAnEventAsRemoteEventWebhook.rst
+++ /dev/null
@@ -1,39 +0,0 @@
-.. include:: /Includes.rst.txt
-
-.. _feature-99632-1674121967:
-
-====================================================================================
-Feature: #99632 - Introduce PHP attribute to mark an event as remote event (webhook)
-====================================================================================
-
-See :issue:`99632`
-
-Description
-===========
-
-A new custom PHP attribute :php:`\TYPO3\CMS\Core\Attribute\RemoteEvent` has
-been added in order to register an event as remote event.
-
-The attribute must have a description that explains the purpose of the event.
-
-Example
--------
-
-..  code-block:: php
-
-    use TYPO3\CMS\Core\Attribute\RemoteEvent;
-
-    #[RemoteEvent(description: 'Event fired when ...')]
-    final class AnyKindOfEvent
-    {
-        // ...
-    }
-
-
-Impact
-======
-
-It is now possible to tag an event as remote event by the PHP attribute
-:php:`\TYPO3\CMS\Core\Attribute\RemoteEvent`.
-
-.. index:: Backend, Frontend, PHP-API, ext:core
diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Feature-99629-Webhooks-OutgoingWebhooksForTYPO3.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-99629-Webhooks-OutgoingWebhooksForTYPO3.rst
new file mode 100644
index 0000000000000000000000000000000000000000..b0cd93612dc915da164f54afc1da706a9adb8c15
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-99629-Webhooks-OutgoingWebhooksForTYPO3.rst
@@ -0,0 +1,223 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-99629-1674550092:
+
+========================================================
+Feature: #99629 - Webhooks - Outgoing webhooks for TYPO3
+========================================================
+
+See :issue:`99629`
+
+Description
+===========
+
+A webhook is an automated message sent from one application to another via HTTP.
+
+This feature adds the possibility to configure webhooks in TYPO3.
+
+A new backend module :guilabel:`System > Webhooks` provides the possibility to
+configure webhooks. The module is available in the TYPO3 backend for users with
+administrative rights.
+
+A webhook is defined as an authorized POST or GET request to a defined URL.
+For example, a webhook can be used to send a notification to a Slack channel
+when a new page is created in TYPO3.
+
+Any webhook record is defined by a unique uid (UUID), a speaking name, an optional
+description, a trigger, the target URL and a signing-secret.
+Both the unique identifier and the signing-secret are generated in the backend
+when a new webhook is created.
+
+Triggers provided by the TYPO3 core
+-----------------------------------
+
+The TYPO3 core currently provides the following triggers for webhooks:
+
+* Page Modification: Triggers when a page is created, updated or deleted
+* File Added: Triggers when a file is added
+* File Updated: Triggers when a file is updated
+* File Removed: Triggers when a file is removed
+* Login Error Occurred: Triggers when a login error occurred
+
+These triggers are meant as a first set of triggers that can be used to send webhooks,
+further triggers will be added in the future. In most projects however, it's likely
+that custom triggers are required.
+
+Custom triggers
+---------------
+
+Trigger by PSR-14 Events
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Custom triggers can be added by creating a `Message` for an specific PSR-14 event and
+tagging that message as a webhook message.
+
+The following example shows how to create a simple webhook message for the
+:php:`\TYPO3\CMS\Core\Resource\Event\AfterFolderAddedEvent`:
+
+.. code-block:: php
+
+    namespace TYPO3\CMS\Webhooks\Message;
+
+    use TYPO3\CMS\Core\Attribute\WebhookMessage;
+    use TYPO3\CMS\Core\Messaging\WebhookMessageInterface;
+    use TYPO3\CMS\Core\Resource\Event\AfterFolderAddedEvent;
+
+    #[WebhookMessage(
+        identifier: 'typo3/folder-added',
+        description: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.webhook_type.typo3-folder-added'
+    )]
+    final class FolderAddedMessage implements WebhookMessageInterface
+    {
+        public function __construct(
+            private readonly int $storageUid,
+            private readonly string $identifier,
+            private readonly string $publicUrl
+        ) {
+        }
+
+        public static function createFromEvent(AfterFolderAddedEvent $event): self
+        {
+            $file = $event->getFile();
+            return new self($file->getStorage()->getUid(), $file->getIdentifier(), $file->getPublicUrl());
+        }
+
+        public function jsonSerialize(): array
+        {
+            return [
+                'storage' => $this->storageUid,
+                'identifier' => $this->identifier,
+                'url' => $this->publicUrl,
+            ];
+        }
+    }
+
+#. Create a final class implementing the `WebhookMessageInterface`.
+#. Add the :php:`WebhookMessage` attribute to the class. The attribute requires the
+    following information:
+
+    *   `identifier`: The identifier of the webhook message.
+    *   `description`: The description of the webhook message. This description
+        is used to describe the trigger in the TYPO3 backend.
+#. Add a static method `createFromEvent` that creates a new instance of the message from the event you want to use as a trigger.
+#. Add a method `jsonSerialize` that returns an array with the data that should be send with the webhook.
+
+Trigger by hooks or custom code
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In case a trigger is not provided by the TYPO3 core or a PSR-14 event is not available,
+it's possible to create a custom trigger - for example by using a TYPO3 hook.
+
+The message itself should look similar to the example above, but does not need the
+:php:`createFromEvent` method.
+
+Instead, the custom code (hook implementation) will create the message
+and dispatch it.
+
+Example hook implementation for a datahandler hook (see :php:`\TYPO3\CMS\Webhooks\Listener\PageModificationListener`):
+
+.. code-example:: php
+
+    public function __construct(
+        protected readonly \Symfony\Component\Messenger\MessageBusInterface $bus
+    ) {
+    }
+
+    public function processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, DataHandler $dataHandler)
+    {
+        if ($table !== 'pages') {
+            return;
+        }
+        // ...
+        $message = new PageModificationMessage(
+                'new',
+                $id,
+                $fieldArray,
+                $site->getIdentifier(),
+                (string)$site->getRouter()->generateUri($id),
+                $dataHandler->BE_USER,
+        );
+        // ...
+        $this->bus->dispatch($message);
+    }
+
+Use :file:`services.yaml` instead of the PHP attribute
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Instead of the PHP attribute the :file:`services.yaml` can be used to define the
+webhook message. The following example shows how to define the webhook message
+from the example above in the :file:`services.yaml`:
+
+.. code-block:: yaml
+
+    TYPO3\CMS\Webhooks\Message\FolderAddedMessage:
+        tags:
+          - name: 'core.webhook_message'
+            identifier: 'typo3/folder-added'
+            description: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.webhook_type.typo3-folder-added'
+
+
+HTTP Headers of every webhook
+-----------------------------
+
+With every webhook request, the following HTTP headers are sent:
+
+* Content-Type: application/json
+* Webhook-Signature-Algo: sha256
+* Webhook-Signature: <hash>
+
+The hash is calculated with the secret of the webhook and the JSON encoded data
+of the request. The hash is created with the PHP function :php:`hash_hmac`.
+See the following section about the hash calculation.
+
+Hash calculation
+----------------
+
+The hash is calculated with the following PHP code:
+
+.. code-block:: php
+
+    $hash = hash_hmac('sha256', sprintf(
+        '%s:%s',
+        $identifier, // The identifier of the webhook (uuid)
+        $body // The JSON encoded body of the request
+    ), $secret); // The secret of the webhook
+
+The hash is sent as HTTP header `Webhook-Signature` and should be used to
+validate that the request was sent from the TYPO3 instance and has not been
+manipulated.
+To verify this on the receiving end, build the hash with the same algorithm and
+secret and compare it with the hash that was sent with the request.
+
+The hash is not meant to be used as a security mechanism, but as a way to verify
+that the request was sent from the TYPO3 instance.
+
+Technical background and advanced usage
+---------------------------------------
+
+The webhook system is based on the Symfony Messenger component. The messages
+are simple PHP objects that implement an interface that denotes
+them as Webhook messages.
+
+That message is then dispatched to the Symfony Messenger bus. The TYPO3 core
+provides a `WebhookMessageHandler` that is responsible for sending the webhook
+requests to the third-party system if configured to do so. The handler looks up
+the webhook configuration and sends the request to the configured URL.
+
+Messages are sent to the bus in any case. The handler is then responsible for checking
+whether or not an external request (webhook) should be sent.
+
+If advanced request handling is necessary or a custom implementation should be used,
+a custom handler can be created that handles `WebhookMessageInterface`
+messages. See the TYPO3 queue documentation for more information on messages and their
+handlers.
+
+Impact
+======
+
+The TYPO3 core does now provide a convenient GUI to create and send webhooks to
+third party systems.
+In combination with the system extension `reactions` TYPO3 can now be used as a
+low-code/no-code integration platform between multiple systems.
+
+.. index:: Backend, Frontend, PHP-API, ext:webhooks
diff --git a/typo3/sysext/reactions/Resources/Private/Templates/Management/Overview.html b/typo3/sysext/reactions/Resources/Private/Templates/Management/Overview.html
index 3554cb8babe49b3aec34865522bf570d51cbc470..ea120376944dcc09d40b517e67b8c3702345edb3 100644
--- a/typo3/sysext/reactions/Resources/Private/Templates/Management/Overview.html
+++ b/typo3/sysext/reactions/Resources/Private/Templates/Management/Overview.html
@@ -118,13 +118,13 @@
             <f:for each="{reactionRecords}" key="reactionId" as="reaction">
                 <f:variable name="reactionRecord" value="{reaction as array}"/>
                 <tr data-uid="{reaction.uid}" data-multi-record-selection-element="true">
+                    <td class="col-selector nowrap">
+                        <span class="form-check form-toggle">
+                            <input class="form-check-input t3js-multi-record-selection-check" type="checkbox">
+                        </span>
+                    </td>
                     <f:if condition="{reactionTypes.{reaction.type}}">
                         <f:then>
-                            <td class="col-selector nowrap">
-                                <span class="form-check form-toggle">
-                                    <input class="form-check-input t3js-multi-record-selection-check" type="checkbox">
-                                </span>
-                            </td>
                             <td class="col-icon">
                                 <a
                                     href="#"
@@ -246,24 +246,15 @@
 
 <f:section name="controls">
     <div class="btn-group">
-        <f:if condition="{reactionTypes.{reaction.type}}">
-            <f:then>
-                <be:link.editRecord
-                    returnUrl="{returnUrl}"
-                    class="btn btn-default"
-                    table="sys_reaction"
-                    uid="{reaction.uid}"
-                    title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:edit')}"
-                >
-                    <core:icon identifier="actions-open" />
-                </be:link.editRecord>
-            </f:then>
-            <f:else>
-                <span class="btn btn-default disabled">
-                    <core:icon identifier="empty-empty" />
-                </span>
-            </f:else>
-        </f:if>
+        <be:link.editRecord
+            returnUrl="{returnUrl}"
+            class="btn btn-default"
+            table="sys_reaction"
+            uid="{reaction.uid}"
+            title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:edit')}"
+        >
+            <core:icon identifier="actions-open" />
+        </be:link.editRecord>
         <f:if condition="{reactionRecord.disabled} == 1">
             <f:then>
                 <a
diff --git a/typo3/sysext/webhooks/.gitattributes b/typo3/sysext/webhooks/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..79ede152a8388ffb534b68d44b887ef6d9e759ab
--- /dev/null
+++ b/typo3/sysext/webhooks/.gitattributes
@@ -0,0 +1,2 @@
+/.gitattributes export-ignore
+/Tests/ export-ignore
diff --git a/typo3/sysext/webhooks/Classes/ConfigurationModuleProvider/WebhookTypesProvider.php b/typo3/sysext/webhooks/Classes/ConfigurationModuleProvider/WebhookTypesProvider.php
new file mode 100644
index 0000000000000000000000000000000000000000..d9f75cb286eb34ac27a5913e9ca29d6424a2db6e
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/ConfigurationModuleProvider/WebhookTypesProvider.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\ConfigurationModuleProvider;
+
+use TYPO3\CMS\Lowlevel\ConfigurationModuleProvider\AbstractProvider;
+use TYPO3\CMS\Webhooks\WebhookTypesRegistry;
+
+/**
+ * Shows configured webhook types in the EXT:lowlevel configuration module.
+ *
+ * @internal not part of TYPO3's Core API
+ */
+class WebhookTypesProvider extends AbstractProvider
+{
+    public function __construct(
+        private readonly WebhookTypesRegistry $webhookTypesRegistry
+    ) {
+    }
+
+    public function getConfiguration(): array
+    {
+        $configuration = [];
+        foreach ($this->webhookTypesRegistry->getAvailableWebhookTypes() as $identifier => $webhookType) {
+            $configuration[$identifier] = [
+                'messageName' => $webhookType->getServiceName(),
+                'description' => $webhookType->getDescription(),
+                'connectedEvent' => $webhookType->getConnectedEvent() ?? 'none',
+            ];
+        }
+        return $configuration;
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Controller/ManagementController.php b/typo3/sysext/webhooks/Classes/Controller/ManagementController.php
new file mode 100644
index 0000000000000000000000000000000000000000..db71df24e5f4210400d950b20d2fc48306bf9481
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Controller/ManagementController.php
@@ -0,0 +1,132 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Controller;
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\Attribute\Controller;
+use TYPO3\CMS\Backend\Routing\UriBuilder;
+use TYPO3\CMS\Backend\Template\Components\ButtonBar;
+use TYPO3\CMS\Backend\Template\ModuleTemplate;
+use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
+use TYPO3\CMS\Core\Imaging\Icon;
+use TYPO3\CMS\Core\Imaging\IconFactory;
+use TYPO3\CMS\Core\Localization\LanguageService;
+use TYPO3\CMS\Core\Pagination\SimplePagination;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Webhooks\Pagination\DemandedArrayPaginator;
+use TYPO3\CMS\Webhooks\Repository\WebhookDemand;
+use TYPO3\CMS\Webhooks\Repository\WebhookRepository;
+use TYPO3\CMS\Webhooks\WebhookTypesRegistry;
+
+/**
+ * The System > Webhooks module: Rendering the listing of webhooks.
+ *
+ * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API.
+ */
+#[Controller]
+class ManagementController
+{
+    public function __construct(
+        private readonly UriBuilder $uriBuilder,
+        private readonly IconFactory $iconFactory,
+        private readonly ModuleTemplateFactory $moduleTemplateFactory,
+        private readonly WebhookTypesRegistry $webhookTypesRegistry,
+        private readonly WebhookRepository $webhookRepository
+    ) {
+    }
+
+    public function overviewAction(ServerRequestInterface $request): ResponseInterface
+    {
+        $view = $this->moduleTemplateFactory->create($request);
+        $demand = WebhookDemand::fromRequest($request);
+        $requestUri = $request->getAttribute('normalizedParams')->getRequestUri();
+        $languageService = $this->getLanguageService();
+
+        $this->registerDocHeaderButtons($view, $requestUri, $demand);
+
+        $webhookRecords = $this->webhookRepository->getWebhookRecords($demand);
+        $paginator = new DemandedArrayPaginator($webhookRecords, $demand->getPage(), $demand->getLimit(), $this->webhookRepository->countAll());
+        $pagination = new SimplePagination($paginator);
+
+        return $view->assignMultiple([
+            'demand' => $demand,
+            'webhookTypes' => $this->webhookTypesRegistry->getAvailableWebhookTypes(),
+            'paginator' => $paginator,
+            'pagination' => $pagination,
+            'webhookRecords' => $webhookRecords,
+            'editActionConfiguration' => GeneralUtility::jsonEncodeForHtmlAttribute([
+                'idField' => 'uid',
+                'tableName' => 'sys_webhook',
+                'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
+            ]),
+            'deleteActionConfiguration' => GeneralUtility::jsonEncodeForHtmlAttribute([
+                'idField' => 'uid',
+                'tableName' => 'sys_webhook',
+                'title' => $languageService->sL('LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:labels.delete.title'),
+                'content' => $languageService->sL('LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:labels.delete.message'),
+                'ok' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete'),
+                'cancel' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel'),
+                'returnUrl' => $requestUri,
+            ]),
+        ])->renderResponse('Management/Overview');
+    }
+
+    protected function registerDocHeaderButtons(ModuleTemplate $view, string $requestUri, WebhookDemand $demand): void
+    {
+        $languageService = $this->getLanguageService();
+        $buttonBar = $view->getDocHeaderComponent()->getButtonBar();
+
+        // Create new
+        $newRecordButton = $buttonBar->makeLinkButton()
+            ->setHref((string)$this->uriBuilder->buildUriFromRoute(
+                'record_edit',
+                [
+                    'edit' => ['sys_webhook' => ['new']],
+                    'returnUrl' => (string)$this->uriBuilder->buildUriFromRoute('webhooks_management'),
+                ]
+            ))
+            ->setShowLabelText(true)
+            ->setTitle($languageService->sL('LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:webhook_create'))
+            ->setIcon($this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL));
+        $buttonBar->addButton($newRecordButton, ButtonBar::BUTTON_POSITION_LEFT, 10);
+
+        // Reload
+        $reloadButton = $buttonBar->makeLinkButton()
+            ->setHref($requestUri)
+            ->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
+            ->setIcon($this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL));
+        $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
+
+        // Shortcut
+        $shortcutButton = $buttonBar->makeShortcutButton()
+            ->setRouteIdentifier('webhooks_management')
+            ->setDisplayName($languageService->sL('LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:mlang_labels_tablabel'))
+            ->setArguments(array_filter([
+                'demand' => $demand->getParameters(),
+                'orderField' => $demand->getOrderField(),
+                'orderDirection' => $demand->getOrderDirection(),
+            ]));
+        $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT);
+    }
+
+    protected function getLanguageService(): LanguageService
+    {
+        return $GLOBALS['LANG'];
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/DependencyInjection/WebhookCompilerPass.php b/typo3/sysext/webhooks/Classes/DependencyInjection/WebhookCompilerPass.php
new file mode 100644
index 0000000000000000000000000000000000000000..a95d4504aafb8792859512066908f5a565b2ccd2
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/DependencyInjection/WebhookCompilerPass.php
@@ -0,0 +1,142 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\DependencyInjection;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Exception\RuntimeException;
+use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
+use TYPO3\CMS\Webhooks\Listener\MessageListener;
+use TYPO3\CMS\Webhooks\WebhookTypesRegistry;
+
+/**
+ * A compiler pass to wire Messages which can be used as "webhook" into the webhook registry when needed.
+ *
+ * Also, if the Message has an Event name as identifier, we automatically register the MessageListener
+ * to the ListenerProvider as well, and the MessageFactory can then automatically create the message for us.
+ *
+ * What does this mean? If your Message class has one object within the __construct() method,
+ * we assume this is an Event class, and we connect to it.
+ *
+ * @internal
+ */
+final class WebhookCompilerPass implements CompilerPassInterface
+{
+    private ContainerBuilder $container;
+    public function __construct(
+        private readonly string $tagName
+    ) {
+    }
+
+    public function process(ContainerBuilder $container): void
+    {
+        if (!$container->hasDefinition(WebhookTypesRegistry::class)) {
+            return;
+        }
+        $this->container = $container;
+        $webhookTypesRegistryDefinition = $container->findDefinition(WebhookTypesRegistry::class);
+
+        foreach ($container->findTaggedServiceIds($this->tagName) as $serviceName => $tags) {
+            $service = $container->findDefinition($serviceName);
+            $description = '';
+            $identifier = $serviceName;
+            $eventIdentifier = null;
+            // see if we should also auto-wire the message to the event, and register our main message listener
+            $listenerProviderDefinition = $container->findDefinition(ListenerProvider::class);
+            // we can have multiple tags on a service, so we need to loop over them and add all webhook configuration
+            // for this message type
+            foreach ($tags as $attributes) {
+                $description = $attributes['description'] ?? '';
+                if (isset($attributes['identifier'])) {
+                    $identifier = $attributes['identifier'];
+                }
+                $method = $attributes['method'] ?? 'createFromEvent';
+                $eventIdentifier = $attributes['event'] ?? $this->getParameterType($serviceName, $service, $method);
+                if ($eventIdentifier !== null && $eventIdentifier !== false) {
+                    $listenerProviderDefinition->addMethodCall(
+                        'addListener',
+                        [
+                            $eventIdentifier,
+                            MessageListener::class,
+                            '__invoke',
+                        ]
+                    );
+                }
+                $webhookTypesRegistryDefinition->addMethodCall('addWebhookType', [$identifier, $description, $serviceName, $method, $eventIdentifier]);
+            }
+        }
+    }
+
+    /**
+     * Derives the class type of the first argument of a given method.
+     */
+    protected function getParameterType(string $serviceName, Definition $definition, string $method = 'createFromEvent'): ?string
+    {
+        // A Reflection exception should never actually get thrown here, but linters want a try-catch just in case.
+        try {
+            if (!$definition->isAutowired()) {
+                throw new \InvalidArgumentException(
+                    sprintf('Service "%s" has webhooks defined but does not declare an event to listen to and is not configured to autowire it from the listener method. Set autowire: true to enable auto-detection of the listener event.', $serviceName),
+                    1679613099,
+                );
+            }
+            $params = $this->getReflectionMethod($definition, $method)?->getParameters();
+            // Only check if the method has really just one argument
+            if ($params === null || count($params) !== 1) {
+                return null;
+            }
+            $rType = $params[0]->getType();
+            if (!$rType instanceof \ReflectionNamedType) {
+                // Don't connect this webhook message to an event
+                return null;
+            }
+            return $rType->getName();
+        } catch (\ReflectionException $e) {
+            // Don't autowire this to an event
+            return null;
+        }
+    }
+
+    /**
+     * @throws RuntimeException|\ReflectionException
+     * This method borrowed very closely from Symfony's AbstractRecursivePass (and the ListenerProviderPass).
+     * @see \TYPO3\CMS\Core\DependencyInjection\ListenerProviderPass::getReflectionMethod()
+     */
+    private function getReflectionMethod(Definition $definition, string $method): ?\ReflectionFunctionAbstract
+    {
+        if (!$class = $definition->getClass()) {
+            return null;
+        }
+
+        if (!$r = $this->container->getReflectionClass($class)) {
+            return null;
+        }
+
+        if (!$r->hasMethod($method)) {
+            return null;
+        }
+
+        $r = $r->getMethod($method);
+        if (!$r->isPublic()) {
+            return null;
+        }
+
+        return $r;
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Factory/WebhookInstructionFactory.php b/typo3/sysext/webhooks/Classes/Factory/WebhookInstructionFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..6e23f5afab7a3501978541e6f3517a87ea2e262c
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Factory/WebhookInstructionFactory.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Factory;
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Webhooks\Model\WebhookInstruction;
+use TYPO3\CMS\Webhooks\Model\WebhookType;
+use TYPO3\CMS\Webhooks\WebhookTypesRegistry;
+
+/**
+ * A factory to create webhook instructions from a database row.
+ *
+ * @internal not part of TYPO3's Core API
+ */
+class WebhookInstructionFactory
+{
+    private static array $defaults = [
+        'method' => 'POST',
+        'verify_ssl' => true,
+        'additional_headers' => [],
+        'name' => null,
+        'description' => null,
+        'webhook_type' => null,
+        'identifier' => null,
+        'uid' => null,
+    ];
+
+    public static function create(
+        string $url,
+        string $secret,
+        string $method = 'POST',
+        bool $verifySSL = true,
+        array $additionalHeaders = [],
+        string $name = null,
+        string $description = null,
+        WebhookType $webhookType = null,
+        string $identifier = null,
+        int $uid = null,
+    ): WebhookInstruction {
+        return new WebhookInstruction(
+            $url,
+            $secret,
+            $method,
+            $verifySSL,
+            $additionalHeaders,
+            $name,
+            $description,
+            $webhookType,
+            $identifier,
+            $uid
+        );
+    }
+
+    public static function createFromRow(array $row): WebhookInstruction
+    {
+        $data = array_merge(self::$defaults, $row);
+
+        if ($data['webhook_type'] !== null) {
+            try {
+                $data['webhook_type'] = GeneralUtility::makeInstance(WebhookTypesRegistry::class)
+                    ->getWebhookByType($data['webhook_type']);
+            } catch (\UnexpectedValueException $e) {
+                // Webhook type not found
+                $data['webhook_type'] = null;
+            }
+        }
+
+        return new WebhookInstruction(
+            $data['url'],
+            $data['secret'],
+            $data['method'],
+            (bool)$data['verify_ssl'],
+            $data['additional_headers'],
+            $data['name'],
+            $data['description'],
+            $data['webhook_type'],
+            $data['identifier'],
+            $data['uid'],
+        );
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Listener/MessageListener.php b/typo3/sysext/webhooks/Classes/Listener/MessageListener.php
new file mode 100644
index 0000000000000000000000000000000000000000..8b7c335cb632b61ed1186a83e04f49fdb8bde261
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Listener/MessageListener.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Listener;
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Messenger\MessageBusInterface;
+use TYPO3\CMS\Webhooks\Message\WebhookMessageFactory;
+
+/**
+ * Listens to registered PSR-14 events and creates a message out of it.
+ * The MessageListener is automatically attached to the respecting Events by DI.
+ *
+ * Messages are dispatched to the message bus after creation.
+ *
+ * @internal not part of TYPO3 Core API
+ */
+class MessageListener
+{
+    public function __construct(
+        protected readonly MessageBusInterface $bus,
+        protected readonly WebhookMessageFactory $messageFactory,
+        protected readonly LoggerInterface $logger,
+    ) {
+    }
+
+    public function __invoke(mixed $object): void
+    {
+        $message = $this->messageFactory->createMessageFromEvent($object);
+        if ($message === null) {
+            return;
+        }
+        try {
+            $this->bus->dispatch($message);
+        } catch (\Throwable $e) {
+            // At the moment we ignore every exception here, but we log them.
+            // An exception here means that an error happens while sending the webhook,
+            // and we should not block the execution of other configured webhooks.
+            // This can happen if no transport is configured, and the message is handled directly.
+            $this->logger->error(get_class($message) . ': ' . $e->getMessage());
+        }
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Listener/PageModificationListener.php b/typo3/sysext/webhooks/Classes/Listener/PageModificationListener.php
new file mode 100644
index 0000000000000000000000000000000000000000..52986ecb0edd9ab04a191ba78124d579ac1c5e8a
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Listener/PageModificationListener.php
@@ -0,0 +1,94 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Listener;
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Messenger\MessageBusInterface;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\DataHandling\DataHandler;
+use TYPO3\CMS\Core\Site\SiteFinder;
+use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Webhooks\Message\PageModificationMessage;
+
+/**
+ * Creates a message everytime something changed within a page.
+ *
+ * This example does not use PSR-14 events, but creates a message manually,
+ * which is then dispatched.
+ *
+ * @internal not part of TYPO3 Core API
+ */
+class PageModificationListener
+{
+    public function __construct(
+        protected readonly MessageBusInterface $bus,
+        protected readonly LoggerInterface $logger,
+        protected readonly SiteFinder $siteFinder,
+    ) {
+    }
+
+    public function processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, DataHandler $dataHandler)
+    {
+        if ($table !== 'pages') {
+            return;
+        }
+        if (!MathUtility::canBeInterpretedAsInteger($id)) {
+            $id = $dataHandler->substNEWwithIDs[$id];
+        }
+        $site = $this->siteFinder->getSiteByPageId($id);
+        if ($status === 'new') {
+            $message = new PageModificationMessage(
+                'new',
+                $id,
+                $fieldArray,
+                (string)$site->getRouter()->generateUri($id),
+                $site->getIdentifier(),
+                $dataHandler->BE_USER,
+            );
+        } else {
+            if (isset($fieldArray['hidden'])) {
+                $action = $fieldArray['hidden'] ? 'unpublished' : 'published';
+            } else {
+                $action = 'modified';
+            }
+            $message = new PageModificationMessage(
+                $action,
+                $id,
+                BackendUtility::getRecord('pages', $id),
+                (string)$site->getRouter()->generateUri($id),
+                $site->getIdentifier(),
+                $dataHandler->BE_USER,
+                $fieldArray
+            );
+        }
+        $this->dispatchMessage($message);
+    }
+
+    protected function dispatchMessage(PageModificationMessage $message): void
+    {
+        try {
+            $this->bus->dispatch($message);
+        } catch (\Throwable $e) {
+            // At the moment we ignore every exception here, but we log them.
+            // An exception here means that an error happens while sending the webhook,
+            // and we should not block the execution of other configured webhooks.
+            // This can happen if no transport is configured, and the message is handled directly.
+            $this->logger->error(get_class($message) . ': ' . $e->getMessage());
+        }
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Message/FileAddedMessage.php b/typo3/sysext/webhooks/Classes/Message/FileAddedMessage.php
new file mode 100644
index 0000000000000000000000000000000000000000..93f68062a019664cdb2f877533124bf8e9d75ba6
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Message/FileAddedMessage.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Message;
+
+use TYPO3\CMS\Core\Attribute\WebhookMessage;
+use TYPO3\CMS\Core\Messaging\WebhookMessageInterface;
+use TYPO3\CMS\Core\Resource\Event\AfterFileAddedEvent;
+
+/**
+ * A message that is triggered after a file was added to TYPO3.
+ *
+ * @internal not part of TYPO3 Core API
+ */
+#[WebhookMessage(
+    identifier: 'typo3/file-added',
+    description: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.webhook_type.typo3-file-added'
+)]
+final class FileAddedMessage implements WebhookMessageInterface
+{
+    public function __construct(
+        private readonly int $storageUid,
+        private readonly string $identifier,
+        private readonly string $publicUrl
+    ) {
+    }
+
+    public static function createFromEvent(AfterFileAddedEvent $event): self
+    {
+        $file = $event->getFile();
+        return new self($file->getStorage()->getUid(), $file->getIdentifier(), $file->getPublicUrl());
+    }
+
+    public function jsonSerialize(): array
+    {
+        return [
+            'storage' => $this->storageUid,
+            'identifier' => $this->identifier,
+            'url' => $this->publicUrl,
+        ];
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Message/FileRemovedMessage.php b/typo3/sysext/webhooks/Classes/Message/FileRemovedMessage.php
new file mode 100644
index 0000000000000000000000000000000000000000..8ed8230d0e799f1cd89e51809982af618a213070
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Message/FileRemovedMessage.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Message;
+
+use TYPO3\CMS\Core\Attribute\WebhookMessage;
+use TYPO3\CMS\Core\Messaging\WebhookMessageInterface;
+use TYPO3\CMS\Core\Resource\Event\BeforeFileDeletedEvent;
+
+/**
+ * A message that is triggered when a file was deleted.
+ *
+ * @internal not part of TYPO3 Core API
+ */
+#[WebhookMessage(
+    identifier: 'typo3/file-removed',
+    description: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.webhook_type.typo3-file-removed'
+)]
+final class FileRemovedMessage implements WebhookMessageInterface
+{
+    public function __construct(
+        private readonly int $storageUid,
+        private readonly string $identifier,
+        private readonly string $publicUrl
+    ) {
+    }
+
+    public static function createFromEvent(BeforeFileDeletedEvent $event): self
+    {
+        $file = $event->getFile();
+        return new self($file->getStorage()->getUid(), $file->getIdentifier(), $file->getPublicUrl());
+    }
+
+    public function jsonSerialize(): array
+    {
+        return [
+            'storage' => $this->storageUid,
+            'identifier' => $this->identifier,
+            'url' => $this->publicUrl,
+        ];
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Message/FileUpdatedMessage.php b/typo3/sysext/webhooks/Classes/Message/FileUpdatedMessage.php
new file mode 100644
index 0000000000000000000000000000000000000000..8e0c8841544ebcb030f475d68f8b12b28334c5f1
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Message/FileUpdatedMessage.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Message;
+
+use TYPO3\CMS\Core\Attribute\WebhookMessage;
+use TYPO3\CMS\Core\Messaging\WebhookMessageInterface;
+use TYPO3\CMS\Core\Resource\Event\AfterFileUpdatedInIndexEvent;
+
+/**
+ * @internal not part of TYPO3 Core API
+ */
+#[WebhookMessage(
+    identifier: 'typo3/file-updated',
+    description: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.webhook_type.typo3-file-updated'
+)]
+final class FileUpdatedMessage implements WebhookMessageInterface
+{
+    public function __construct(
+        private readonly int $storageUid,
+        private readonly string $identifier,
+        private readonly string $publicUrl,
+        private readonly array $relevantProperties,
+        private readonly array $updatedFields
+    ) {
+    }
+
+    public static function createFromEvent(AfterFileUpdatedInIndexEvent $event): FileUpdatedMessage
+    {
+        $file = $event->getFile();
+        return new self($file->getStorage()->getUid(), $file->getIdentifier(), $file->getPublicUrl(), $event->getRelevantProperties(), $event->getUpdatedFields());
+    }
+
+    public function jsonSerialize(): array
+    {
+        return [
+            'storage' => $this->storageUid,
+            'identifier' => $this->identifier,
+            'url' => $this->publicUrl,
+            'relevantProperties' => $this->relevantProperties,
+            'updatedFields' => $this->updatedFields,
+        ];
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Message/LoginErrorOccurredMessage.php b/typo3/sysext/webhooks/Classes/Message/LoginErrorOccurredMessage.php
new file mode 100644
index 0000000000000000000000000000000000000000..cc56387bbde340db0427f6da1e5a0cbf78682d90
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Message/LoginErrorOccurredMessage.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Message;
+
+use Psr\Http\Message\UriInterface;
+use TYPO3\CMS\Core\Attribute\WebhookMessage;
+use TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent;
+use TYPO3\CMS\Core\Messaging\WebhookMessageInterface;
+
+/**
+ * @internal not part of TYPO3's Core API
+ */
+#[WebhookMessage(
+    identifier: 'typo3/login-error',
+    description: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.webhook_type.typo3-login-error'
+)]
+final class LoginErrorOccurredMessage implements WebhookMessageInterface
+{
+    public function __construct(
+        private readonly bool $isFrontend,
+        private readonly UriInterface $url,
+        private readonly array $loginData,
+    ) {
+    }
+
+    public function jsonSerialize(): array
+    {
+        return [
+            'context' => $this->isFrontend ? 'frontend' : 'backend',
+            'url' => (string)$this->url,
+            'loginData' => $this->loginData,
+        ];
+    }
+
+    public static function createFromEvent(LoginAttemptFailedEvent $event): self
+    {
+        return new self(
+            $event->isFrontendAttempt(),
+            $event->getRequest()->getUri(),
+            $event->getLoginData()
+        );
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Message/PageModificationMessage.php b/typo3/sysext/webhooks/Classes/Message/PageModificationMessage.php
new file mode 100644
index 0000000000000000000000000000000000000000..a496763f3e9467a9ceb0b6d8abc8aab45441f02f
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Message/PageModificationMessage.php
@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Message;
+
+use TYPO3\CMS\Core\Attribute\WebhookMessage;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Messaging\WebhookMessageInterface;
+
+/**
+ * @internal not part of TYPO3's Core API
+ */
+#[WebhookMessage(
+    identifier: 'typo3/content/page-modification',
+    description: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.webhook_type.typo3-content-page-modification'
+)]
+final class PageModificationMessage implements WebhookMessageInterface
+{
+    public function __construct(
+        private readonly string $action,
+        private readonly int $uid,
+        private readonly array $record,
+        private readonly string $url,
+        private readonly string $siteIdentifier,
+        private readonly ?BackendUserAuthentication $author = null,
+        private readonly ?array $modifiedFields = null
+    ) {
+    }
+
+    public function jsonSerialize(): array
+    {
+        $data = [
+            'action' => $this->action,
+            'identifier' => $this->uid,
+            'record' => $this->record,
+            'url' => $this->url,
+            'site' => $this->siteIdentifier,
+            'workspace' => $this->record['t3ver_wsid'] ?? 0,
+        ];
+        if ($this->author instanceof BackendUserAuthentication) {
+            $data['author'] = [
+                'uid' => $this->author->user['uid'],
+                'username' => $this->author->user['username'],
+                'isAdmin' => $this->author->isAdmin(),
+            ];
+        }
+        if (is_array($this->modifiedFields)) {
+            $data['changedFields'] = $this->modifiedFields;
+        }
+        return $data;
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Message/WebhookMessageFactory.php b/typo3/sysext/webhooks/Classes/Message/WebhookMessageFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea0480e73a66d54e4f9a02f8da92cabef08f5b6d
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Message/WebhookMessageFactory.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Message;
+
+use TYPO3\CMS\Webhooks\WebhookTypesRegistry;
+
+/**
+ * A factory to create webhook messages from events.
+ */
+class WebhookMessageFactory
+{
+    public function __construct(
+        protected readonly WebhookTypesRegistry $typesRegistry
+    ) {
+    }
+
+    public function createMessageFromEvent(object $eventObject): ?object
+    {
+        $eventName = get_class($eventObject);
+        $type = $this->typesRegistry->getWebhookByEventIdentifier($eventName);
+        if (!$type) {
+            return null;
+        }
+
+        return $type->getFactoryMethod()($eventObject);
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/MessageHandler/WebhookMessageHandler.php b/typo3/sysext/webhooks/Classes/MessageHandler/WebhookMessageHandler.php
new file mode 100644
index 0000000000000000000000000000000000000000..74e542712709af9fe0b4da58c9506eb6a503cda0
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/MessageHandler/WebhookMessageHandler.php
@@ -0,0 +1,101 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\MessageHandler;
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Log\LoggerInterface;
+use TYPO3\CMS\Core\Http\RequestFactory;
+use TYPO3\CMS\Core\Messaging\WebhookMessageInterface;
+use TYPO3\CMS\Webhooks\Model\WebhookInstruction;
+use TYPO3\CMS\Webhooks\Repository\WebhookRepository;
+
+/**
+ * A Message Handler to deal with a webhook message.
+ * It sends the message to all registered HTTP endpoints.
+ */
+class WebhookMessageHandler
+{
+    private string $algo = 'sha256';
+
+    public function __construct(
+        private readonly WebhookRepository $repository,
+        private readonly RequestFactory $requestFactory,
+        private readonly LoggerInterface $logger,
+    ) {
+    }
+
+    public function __invoke(WebhookMessageInterface $message): void
+    {
+        $configuredWebhooks = $this->repository->getConfiguredWebhooksByType(get_class($message));
+        foreach ($configuredWebhooks as $webhookInstruction) {
+            $this->logger->info('Sending webhook', [
+                'webhook-identifier' => $webhookInstruction->getIdentifier(),
+            ]);
+            try {
+                $response = $this->sendRequest($webhookInstruction, $message);
+                $this->logger->debug('Webhook sent', [
+                    'target_url' => $webhookInstruction->getTargetUrl(),
+                    'response_code' => $response->getStatusCode(),
+                ]);
+            } catch (\Exception $e) {
+                $this->logger->error('Webhook sending failed', [
+                    'failure_message' => $e->getMessage(),
+                ]);
+            }
+        }
+    }
+
+    protected function sendRequest(WebhookInstruction $webhookInstruction, WebhookMessageInterface $message): ResponseInterface
+    {
+        $body = json_encode($message, JSON_THROW_ON_ERROR);
+        $headers = $this->buildHeaders($webhookInstruction, $body);
+
+        $options = [
+            'headers' => $headers,
+            'body' => $body,
+        ];
+        if (!$webhookInstruction->verifySSL()) {
+            $options['verify'] = false;
+        }
+
+        return $this->requestFactory->request(
+            $webhookInstruction->getTargetUrl(),
+            $webhookInstruction->getHttpMethod(),
+            $options
+        );
+    }
+
+    private function buildHash(WebhookInstruction $webhookInstruction, string $body): string
+    {
+        return hash_hmac($this->algo, sprintf(
+            '%s:%s',
+            $webhookInstruction->getIdentifier(),
+            $body
+        ), $webhookInstruction->getSecret());
+    }
+
+    private function buildHeaders(WebhookInstruction $webhookInstruction, string $body): array
+    {
+        $headers = $GLOBALS['TYPO3_CONF_VARS']['HTTP']['headers'] ?? [];
+        $headers['Content-Type'] = 'application/json';
+        $headers['Webhook-Signature-Algo'] = $this->algo;
+        $headers = array_merge($headers, $webhookInstruction->getAdditionalHeaders());
+        $headers['Webhook-Signature'] = $this->buildHash($webhookInstruction, $body);
+        return $headers;
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Model/WebhookInstruction.php b/typo3/sysext/webhooks/Classes/Model/WebhookInstruction.php
new file mode 100644
index 0000000000000000000000000000000000000000..f00ccf14d3d16f4894278900db271379c0d8f193
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Model/WebhookInstruction.php
@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Model;
+
+/**
+ * DTO for an instruction - that is a representation of a configured
+ * webhook to a specific remote - contains all configuration information.
+ *
+ * What type of message should be sent (WebhookType), where should it be sent
+ * to and what additional headers etc. should be sent.
+ */
+class WebhookInstruction
+{
+    public function __construct(
+        private readonly string $url,
+        private readonly string $secret,
+        private readonly string $method = 'POST',
+        private readonly bool $verifySSL = true,
+        private readonly array $additionalHeaders = [],
+        private readonly ?string $name = null,
+        private readonly ?string $description = null,
+        private readonly ?WebhookType $webhookType = null,
+        private readonly ?string $identifier = null,
+        private readonly ?int $uid = null,
+    ) {
+    }
+
+    public function getUid(): int
+    {
+        return $this->uid ?? 0;
+    }
+
+    public function getName(): string
+    {
+        return $this->name ?? '';
+    }
+
+    public function getDescription(): string
+    {
+        return $this->description ?? '';
+    }
+
+    public function getWebhookType(): ?WebhookType
+    {
+        return $this->webhookType;
+    }
+
+    public function getIdentifier(): ?string
+    {
+        return $this->identifier;
+    }
+
+    public function getTargetUrl(): string
+    {
+        return $this->url;
+    }
+
+    public function getHttpMethod(): string
+    {
+        return strtoupper($this->method);
+    }
+
+    public function verifySSL(): bool
+    {
+        return $this->verifySSL;
+    }
+
+    public function getSecret(): string
+    {
+        return $this->secret;
+    }
+
+    public function getAdditionalHeaders(): ?array
+    {
+        return $this->additionalHeaders;
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Model/WebhookType.php b/typo3/sysext/webhooks/Classes/Model/WebhookType.php
new file mode 100644
index 0000000000000000000000000000000000000000..e5aa75975828be6393af1b57fe9be599745d64f0
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Model/WebhookType.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Model;
+
+/**
+ * @internal not part of TYPO3's Core API
+ */
+class WebhookType
+{
+    public function __construct(
+        protected readonly string $identifier,
+        protected readonly string $description,
+        protected readonly string $serviceName,
+        protected readonly string $factoryMethodName,
+        protected readonly ?string $connectedEvent = null
+    ) {
+    }
+
+    public function getIdentifier(): string
+    {
+        return $this->identifier;
+    }
+
+    public function getDescription(): string
+    {
+        return $this->description;
+    }
+
+    public function getServiceName(): string
+    {
+        return $this->serviceName;
+    }
+
+    public function getFactoryMethod(): string
+    {
+        return $this->serviceName . '::' . $this->factoryMethodName;
+    }
+
+    public function getConnectedEvent(): ?string
+    {
+        return $this->connectedEvent;
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Pagination/DemandedArrayPaginator.php b/typo3/sysext/webhooks/Classes/Pagination/DemandedArrayPaginator.php
new file mode 100644
index 0000000000000000000000000000000000000000..5274c6c41f79ff277189821883d8da67a6e802fa
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Pagination/DemandedArrayPaginator.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Pagination;
+
+use TYPO3\CMS\Core\Pagination\AbstractPaginator;
+
+/**
+ * A custom Paginator for dealing with the demand object.
+ *
+ * @internal not part of TYPO3's Core API
+ * @todo should be replaced with the regular ArrayPaginator
+ */
+final class DemandedArrayPaginator extends AbstractPaginator
+{
+    private array $items;
+    private int $allCount;
+
+    private array $paginatedItems = [];
+
+    public function __construct(
+        array $items,
+        int $currentPageNumber = 1,
+        int $itemsPerPage = 10,
+        int $allCount = 0
+    ) {
+        $this->items = $items;
+        $this->setCurrentPageNumber($currentPageNumber);
+        $this->setItemsPerPage($itemsPerPage);
+        $this->allCount = $allCount;
+
+        $this->updateInternalState();
+    }
+
+    public function getPaginatedItems(): iterable
+    {
+        return $this->paginatedItems;
+    }
+
+    protected function updatePaginatedItems(int $itemsPerPage, int $offset): void
+    {
+        $this->paginatedItems = $this->items;
+    }
+
+    protected function getTotalAmountOfItems(): int
+    {
+        return $this->allCount;
+    }
+
+    protected function getAmountOfItemsOnCurrentPage(): int
+    {
+        return count($this->paginatedItems);
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Repository/WebhookDemand.php b/typo3/sysext/webhooks/Classes/Repository/WebhookDemand.php
new file mode 100644
index 0000000000000000000000000000000000000000..0dff6089e5e8c5513becf1333b8278add87a9b8e
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Repository/WebhookDemand.php
@@ -0,0 +1,138 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Repository;
+
+use Psr\Http\Message\ServerRequestInterface;
+
+/**
+ * Demand Object for filtering webhooks in the backend module
+ *
+ * @internal
+ */
+class WebhookDemand
+{
+    protected const ORDER_DESCENDING = 'desc';
+    protected const ORDER_ASCENDING = 'asc';
+    protected const DEFAULT_ORDER_FIELD = 'name';
+    protected const ORDER_FIELDS = ['name', 'webhook_type'];
+
+    protected int $limit = 15;
+
+    public function __construct(
+        protected int $page = 1,
+        protected string $orderField = self::DEFAULT_ORDER_FIELD,
+        protected string $orderDirection = self::ORDER_ASCENDING,
+        protected string $name = '',
+        protected string $webhookType = ''
+    ) {
+        if (!in_array($orderField, self::ORDER_FIELDS, true)) {
+            $orderField = self::DEFAULT_ORDER_FIELD;
+        }
+        $this->orderField = $orderField;
+        if (!in_array($orderDirection, [self::ORDER_DESCENDING, self::ORDER_ASCENDING], true)) {
+            $orderDirection = self::ORDER_ASCENDING;
+        }
+        $this->orderDirection = $orderDirection;
+    }
+
+    public static function fromRequest(ServerRequestInterface $request): self
+    {
+        $page = (int)($request->getQueryParams()['page'] ?? $request->getParsedBody()['page'] ?? 1);
+        $orderField = (string)($request->getQueryParams()['orderField'] ?? $request->getParsedBody()['orderField'] ?? self::DEFAULT_ORDER_FIELD);
+        $orderDirection = (string)($request->getQueryParams()['orderDirection'] ?? $request->getParsedBody()['orderDirection'] ?? self::ORDER_ASCENDING);
+        $demand = $request->getQueryParams()['demand'] ?? $request->getParsedBody()['demand'] ?? [];
+        if (!is_array($demand) || $demand === []) {
+            return new self($page, $orderField, $orderDirection);
+        }
+        $name = (string)($demand['name'] ?? '');
+        $webhookTypeType = (string)($demand['webhook_type'] ?? '');
+        return new self($page, $orderField, $orderDirection, $name, $webhookTypeType);
+    }
+
+    public function getOrderField(): string
+    {
+        return $this->orderField;
+    }
+
+    public function getOrderDirection(): string
+    {
+        return $this->orderDirection;
+    }
+
+    public function getDefaultOrderDirection(): string
+    {
+        return self::ORDER_ASCENDING;
+    }
+
+    public function getReverseOrderDirection(): string
+    {
+        return $this->orderDirection === self::ORDER_ASCENDING ? self::ORDER_DESCENDING : self::ORDER_ASCENDING;
+    }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function hasName(): bool
+    {
+        return $this->name !== '';
+    }
+
+    public function getWebhookType(): string
+    {
+        return $this->webhookType;
+    }
+
+    public function hasWebhookType(): bool
+    {
+        return $this->webhookType !== '';
+    }
+
+    public function hasConstraints(): bool
+    {
+        return $this->hasName() || $this->hasWebhookType();
+    }
+
+    public function getPage(): int
+    {
+        return $this->page;
+    }
+
+    public function getLimit(): int
+    {
+        return $this->limit;
+    }
+
+    public function getOffset(): int
+    {
+        return ($this->page - 1) * $this->limit;
+    }
+
+    public function getParameters(): array
+    {
+        $parameters = [];
+        if ($this->hasName()) {
+            $parameters['name'] = $this->getName();
+        }
+        if ($this->hasWebhookType()) {
+            $parameters['webhook_type'] = $this->getWebhookType();
+        }
+        return $parameters;
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Repository/WebhookRepository.php b/typo3/sysext/webhooks/Classes/Repository/WebhookRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..6b5cd5e06a6f369781fa4c17b4aab9976075a0fb
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Repository/WebhookRepository.php
@@ -0,0 +1,177 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Repository;
+
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\QueryBuilder;
+use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Webhooks\Factory\WebhookInstructionFactory;
+use TYPO3\CMS\Webhooks\Model\WebhookInstruction;
+
+/**
+ * Accessing webhook records from the database
+ *
+ * @internal This class is not part of TYPO3's Core API.
+ */
+class WebhookRepository
+{
+    protected string $cacheIdentifierPrefix = 'webhooks_';
+
+    public function __construct(
+        protected readonly ConnectionPool $connectionPool,
+        protected readonly FrontendInterface $runtimeCache,
+    ) {
+    }
+
+    /**
+     * @return WebhookInstruction[]
+     */
+    public function findAll(): array
+    {
+        $cacheIdentifier = $this->cacheIdentifierPrefix . 'all';
+        if (!$this->runtimeCache->has($cacheIdentifier)) {
+            $data = $this->map(
+                $this->getQueryBuilder()->executeQuery()->fetchAllAssociative()
+            );
+            $this->runtimeCache->set($cacheIdentifier, $data);
+        } else {
+            $data = $this->runtimeCache->get($cacheIdentifier);
+        }
+        return $data;
+    }
+
+    public function countAll(): int
+    {
+        return (int)$this->getQueryBuilder(false)
+            ->count('*')
+            ->executeQuery()
+            ->fetchOne();
+    }
+
+    public function getWebhookRecords(?WebhookDemand $demand = null): array
+    {
+        return $demand !== null ? $this->findByDemand($demand) : $this->findAll();
+    }
+
+    /**
+     * @return WebhookInstruction[]
+     */
+    public function findByDemand(WebhookDemand $demand): array
+    {
+        return $this->map($this->getQueryBuilderForDemand($demand)
+            ->setMaxResults($demand->getLimit())
+            ->setFirstResult($demand->getOffset())
+            ->executeQuery()
+            ->fetchAllAssociative());
+    }
+
+    /**
+     * @return array<string, WebhookInstruction>
+     */
+    protected function getConfiguredWebhooks(): array
+    {
+        $webhooks = [];
+        foreach ($this->findAll() as $webhook) {
+            $webhooks[$webhook->getIdentifier()] = $webhook;
+        }
+        return $webhooks;
+    }
+
+    /**
+     * @return array<string, WebhookInstruction>
+     */
+    public function getConfiguredWebhooksByType(string $type): array
+    {
+        $webhooks = $this->getConfiguredWebhooks();
+        return array_filter($webhooks, static fn ($webhook) => $webhook->getWebhookType()?->getServiceName() === $type);
+    }
+
+    protected function getQueryBuilderForDemand(WebhookDemand $demand): QueryBuilder
+    {
+        $queryBuilder = $this->getQueryBuilder(false);
+        $queryBuilder->orderBy(
+            $demand->getOrderField(),
+            $demand->getOrderDirection()
+        );
+        // Ensure deterministic ordering.
+        if ($demand->getOrderField() !== 'uid') {
+            $queryBuilder->addOrderBy('uid', 'asc');
+        }
+
+        $constraints = [];
+        if ($demand->hasName()) {
+            $escapedLikeString = '%' . $queryBuilder->escapeLikeWildcards($demand->getName()) . '%';
+            $constraints[] = $queryBuilder->expr()->or(
+                $queryBuilder->expr()->like(
+                    'name',
+                    $queryBuilder->createNamedParameter($escapedLikeString)
+                ),
+                $queryBuilder->expr()->like(
+                    'description',
+                    $queryBuilder->createNamedParameter($escapedLikeString)
+                )
+            );
+        }
+        if ($demand->hasWebhookType()) {
+            $constraints[] = $queryBuilder->expr()->eq(
+                'webhook_type',
+                $queryBuilder->createNamedParameter($demand->getWebhookType())
+            );
+        }
+
+        if (!empty($constraints)) {
+            $queryBuilder->where(...$constraints);
+        }
+        return $queryBuilder;
+    }
+
+    protected function map(array $rows): array
+    {
+        $items = [];
+        foreach ($rows as $row) {
+            $items[] = $this->mapSingleRow($row);
+        }
+        return $items;
+    }
+
+    protected function mapSingleRow(array $row): WebhookInstruction
+    {
+        $row = BackendUtility::convertDatabaseRowValuesToPhp('sys_webhook', $row);
+        return WebhookInstructionFactory::createFromRow($row);
+    }
+
+    protected function getQueryBuilder(bool $addDefaultOrderByClause = true): QueryBuilder
+    {
+        $queryBuilder = $this->connectionPool
+            ->getQueryBuilderForTable('sys_webhook');
+        $queryBuilder->getRestrictions()
+            ->removeAll()
+            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+        $queryBuilder->select('*')->from('sys_webhook');
+        if ($addDefaultOrderByClause) {
+            $queryBuilder
+                ->orderBy('name', 'asc')
+                // Ensure deterministic ordering.
+                ->addOrderBy('uid', 'asc');
+        }
+        return $queryBuilder;
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/Tca/ItemsProcFunc/WebhookTypesItemsProcFunc.php b/typo3/sysext/webhooks/Classes/Tca/ItemsProcFunc/WebhookTypesItemsProcFunc.php
new file mode 100644
index 0000000000000000000000000000000000000000..97f170183adb334bd6e15baeadc19ffe3a4a4030
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Tca/ItemsProcFunc/WebhookTypesItemsProcFunc.php
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Tca\ItemsProcFunc;
+
+use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
+use TYPO3\CMS\Webhooks\WebhookTypesRegistry;
+
+/**
+ * Custom TCA renderings and itemsProcFunc.
+ *
+ * @internal not part of TYPO3's Core API
+ */
+class WebhookTypesItemsProcFunc
+{
+    public function __construct(
+        private readonly WebhookTypesRegistry $webhookTypesRegistry,
+        private readonly LanguageServiceFactory $languageServiceFactory
+    ) {
+    }
+
+    public function getWebhookTypes(&$fieldDefinition): void
+    {
+        $lang = $this->languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);
+        foreach ($this->webhookTypesRegistry->getAvailableWebhookTypes() as $identifier => $webhookType) {
+            $fieldDefinition['items'][] = [
+                'label' => $lang->sL($webhookType->getDescription()) ?: $webhookType->getDescription(),
+                'value' => $identifier,
+            ];
+        }
+    }
+}
diff --git a/typo3/sysext/webhooks/Classes/WebhookTypesRegistry.php b/typo3/sysext/webhooks/Classes/WebhookTypesRegistry.php
new file mode 100644
index 0000000000000000000000000000000000000000..06b12d1130dc591dba396f8287c6fae52789b55b
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/WebhookTypesRegistry.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks;
+
+use TYPO3\CMS\Webhooks\Model\WebhookType;
+
+/**
+ * Registry contains all possible webhooks types which are available to the system
+ * To register a webhook your class must be tagged with the Webhook attribute.
+ *
+ * @internal not part of TYPO3's Core API
+ */
+class WebhookTypesRegistry
+{
+    /**
+     * @var WebhookType[]
+     */
+    private array $webhookTypes = [];
+
+    /**
+     * @return WebhookType[]
+     */
+    public function getAvailableWebhookTypes(): array
+    {
+        return $this->webhookTypes;
+    }
+
+    /**
+     * Whether a registered webhook type exists
+     */
+    public function hasWebhookType(string $type): bool
+    {
+        return isset($this->webhookTypes[$type]);
+    }
+
+    public function getWebhookByType(string $type): WebhookType
+    {
+        if (!$this->hasWebhookType($type)) {
+            throw new \UnexpectedValueException('No webhook with type ' . $type . ' registered.', 1679348837);
+        }
+
+        return $this->webhookTypes[$type];
+    }
+
+    public function getWebhookByEventIdentifier(string $eventIdentifier): ?WebhookType
+    {
+        foreach ($this->webhookTypes as $type) {
+            if ($type->getConnectedEvent() === $eventIdentifier) {
+                return $type;
+            }
+        }
+        return null;
+    }
+
+    public function addWebhookType(string $identifier, string $description, string $serviceName, string $factoryMethod, ?string $connectedEvent): void
+    {
+        $this->webhookTypes[$identifier] = new WebhookType($identifier, $description, $serviceName, $factoryMethod, $connectedEvent);
+    }
+}
diff --git a/typo3/sysext/webhooks/Configuration/Backend/Modules.php b/typo3/sysext/webhooks/Configuration/Backend/Modules.php
new file mode 100644
index 0000000000000000000000000000000000000000..ed10c553087108d0c0f941c1407dda05c47c0e1a
--- /dev/null
+++ b/typo3/sysext/webhooks/Configuration/Backend/Modules.php
@@ -0,0 +1,23 @@
+<?php
+
+use TYPO3\CMS\Webhooks\Controller\ManagementController;
+
+/**
+ * Definitions for modules provided by EXT:webhooks
+ */
+return [
+    'webhooks_management' => [
+        'parent' => 'system',
+        'position' => ['after' => 'system_BeuserTxBeuser'],
+        'access' => 'admin',
+        'workspaces' => 'live',
+        'path' => '/module/webhooks',
+        'iconIdentifier' => 'module-webhooks',
+        'labels' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf',
+        'routes' => [
+            '_default' => [
+                'target' => ManagementController::class . '::overviewAction',
+            ],
+        ],
+    ],
+];
diff --git a/typo3/sysext/webhooks/Configuration/Icons.php b/typo3/sysext/webhooks/Configuration/Icons.php
new file mode 100644
index 0000000000000000000000000000000000000000..b7e26211c8124294c023d6a4eac4d970323270a3
--- /dev/null
+++ b/typo3/sysext/webhooks/Configuration/Icons.php
@@ -0,0 +1,10 @@
+<?php
+
+use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;
+
+return [
+    'module-webhooks' => [
+        'provider' => SvgIconProvider::class,
+        'source' => 'EXT:webhooks/Resources/Public/Icons/Extension.svg',
+    ],
+];
diff --git a/typo3/sysext/webhooks/Configuration/Services.php b/typo3/sysext/webhooks/Configuration/Services.php
new file mode 100644
index 0000000000000000000000000000000000000000..d8af5e7c695a803cd3e16d9ba2a3812366fea813
--- /dev/null
+++ b/typo3/sysext/webhooks/Configuration/Services.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Webhooks;
+
+use Symfony\Component\DependencyInjection\ChildDefinition;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+use TYPO3\CMS\Core\Attribute\WebhookMessage;
+
+return static function (ContainerConfigurator $container, ContainerBuilder $containerBuilder) {
+    $containerBuilder->registerAttributeForAutoconfiguration(
+        WebhookMessage::class,
+        static function (ChildDefinition $definition, WebhookMessage $attribute): void {
+            $definition->addTag(WebhookMessage::TAG_NAME, ['identifier' => $attribute->identifier, 'description' => $attribute->description, 'method' => $attribute->method]);
+        }
+    );
+    $containerBuilder->addCompilerPass(new DependencyInjection\WebhookCompilerPass(WebhookMessage::TAG_NAME));
+};
diff --git a/typo3/sysext/webhooks/Configuration/Services.yaml b/typo3/sysext/webhooks/Configuration/Services.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..15f3772aa4ecc1995e7e329e4e4a8349076aaa39
--- /dev/null
+++ b/typo3/sysext/webhooks/Configuration/Services.yaml
@@ -0,0 +1,38 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Webhooks\:
+    resource: '../Classes/*'
+
+  TYPO3\CMS\Webhooks\WebhookTypesRegistry:
+    public: true
+
+  TYPO3\CMS\Webhooks\Listener\PageModificationListener:
+    public: true
+
+  # public true is required in ajax backend calls (e.g. when adding a file)
+  TYPO3\CMS\Webhooks\Listener\MessageListener:
+    public: true
+
+  TYPO3\CMS\Webhooks\MessageHandler\WebhookMessageHandler:
+    tags:
+      - name: 'messenger.message_handler'
+
+  TYPO3\CMS\Webhooks\Repository\WebhookRepository:
+    public: true # Required in test context
+    arguments:
+      $runtimeCache: '@cache.runtime'
+
+  TYPO3\CMS\Webhooks\Tca\ItemsProcFunc\WebhookTypesItemsProcFunc:
+    public: true
+
+  lowlevel.configuration.module.provider.webhooks:
+    class: 'TYPO3\CMS\Webhooks\ConfigurationModuleProvider\WebhookTypesProvider'
+    tags:
+      - name: 'lowlevel.configuration.module.provider'
+        identifier: 'webhooks'
+        label: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:webhooks'
+        after: 'reactions'
diff --git a/typo3/sysext/webhooks/Configuration/TCA/sys_webhook.php b/typo3/sysext/webhooks/Configuration/TCA/sys_webhook.php
new file mode 100644
index 0000000000000000000000000000000000000000..cf67b4873882adcd1dd0e1810a5c3bc7356f596b
--- /dev/null
+++ b/typo3/sysext/webhooks/Configuration/TCA/sys_webhook.php
@@ -0,0 +1,193 @@
+<?php
+
+return [
+    'ctrl' => [
+        'title' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook',
+        'label' => 'name',
+        'descriptionColumn' => 'description',
+        'crdate' => 'createdon',
+        'tstamp' => 'updatedon',
+        'adminOnly' => true,
+        'rootLevel' => 1,
+        'groupName' => 'system',
+        'default_sortby' => 'name',
+        'type' => 'webhook_type',
+        'typeicon_column' => 'webhook_type',
+        'typeicon_classes' => [
+            'default' => 'content-webhook',
+        ],
+        'delete' => 'deleted',
+        'enablecolumns' => [
+            'disabled' => 'disabled',
+            'starttime' => 'starttime',
+            'endtime' => 'endtime',
+        ],
+        'searchFields' => 'name, secret',
+        'versioningWS_alwaysAllowLiveEdit' => true,
+    ],
+    'types' => [
+        '1' => [
+            'showitem' => '
+                --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
+                --palette--;;config,
+                --div--;LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:palette.http_settings,
+                --palette--;;http_settings,
+                --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
+                --palette--;;access',
+        ],
+    ],
+    'palettes' => [
+        'config' => [
+            'label' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:palette.config',
+            'description' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:palette.config.description',
+            'showitem' => 'webhook_type, identifier, --linebreak--, name, description, --linebreak--, url, secret',
+        ],
+        'http_settings' => [
+            'label' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:palette.http_settings',
+            'description' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:palette.http_settings.description',
+            'showitem' => 'method, verify_ssl, --linebreak--, additional_headers',
+        ],
+        'access' => [
+            'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.access',
+            'showitem' => 'disabled, starttime, endtime',
+        ],
+    ],
+    'columns' => [
+        'webhook_type' => [
+            'label' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.webhook_type',
+            'description' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.webhook_type.description',
+            'config' => [
+                'type' => 'select',
+                'renderType' => 'selectSingle',
+                'required' => true,
+                'items' => [
+                    [
+                        'label' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.webhook_type.select',
+                        'vaule' => '',
+                    ],
+                ],
+                'itemsProcFunc' => \TYPO3\CMS\Webhooks\Tca\ItemsProcFunc\WebhookTypesItemsProcFunc::class . '->getWebhookTypes',
+            ],
+        ],
+        'name' => [
+            'label' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.name',
+            'description' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.name.description',
+            'config' => [
+                'type' => 'input',
+                'required' => true,
+                'eval' => 'trim',
+            ],
+        ],
+        'description' => [
+            'label' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.description',
+            'description' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.description.description',
+            'config' => [
+                'type' => 'text',
+                'rows' => 5,
+                'cols' => 30,
+            ],
+        ],
+        'identifier' => [
+            'label' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.identifier',
+            'description' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.identifier.description',
+            'config' => [
+                'type' => 'uuid',
+            ],
+        ],
+        'secret' => [
+            'label' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.secret',
+            'description' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.secret.description',
+            'config' => [
+                'type' => 'password',
+                'hashed' => false, // Can't be hashed because it's used to create the signature
+                'required' => true,
+                'fieldControl' => [
+                    'passwordGenerator' => [
+                        'renderType' => 'passwordGenerator',
+                        'options' => [
+                            'title' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.secret.passwordGenerator',
+                            'allowEdit' => false,
+                            'passwordRules' => [
+                                'length' => 40,
+                                'random' => 'hex',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ],
+        'url' => [
+            'label' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.url',
+            'description' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.url.description',
+            'config' => [
+                'type' => 'link',
+                'required' => true,
+                'allowedTypes' => ['url'],
+            ],
+        ],
+        'method' => [
+            'label' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.method',
+            'config' => [
+                'type' => 'select',
+                'renderType' => 'selectSingle',
+                'required' => true,
+                'items' => [
+                    [
+                        'label' => 'POST',
+                        'value' => 'POST',
+                    ],
+                    [
+                        'label' => 'GET',
+                        'value' => 'GET',
+                    ],
+                ],
+            ],
+        ],
+        'verify_ssl' => [
+            'label' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.verify_ssl',
+            'description' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.verify_ssl.description',
+            'config' => [
+                'type' => 'check',
+                'renderType' => 'checkboxToggle',
+                'default' => 1,
+            ],
+        ],
+        'additional_headers' => [
+            'label' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.additional_headers',
+            'description' => 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.additional_headers.description',
+            'config' => [
+                'type' => 'json',
+            ],
+        ],
+        'disabled' => [
+            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.enabled',
+            'config' => [
+                'type' => 'check',
+                'renderType' => 'checkboxToggle',
+                'items' => [
+                    [
+                        'label' => '',
+                        'invertStateDisplay' => true,
+                    ],
+                ],
+            ],
+        ],
+        'starttime' => [
+            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.starttime',
+            'config' => [
+                'type' => 'datetime',
+                'default' => 0,
+            ],
+        ],
+        'endtime' => [
+            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.endtime',
+            'config' => [
+                'type' => 'datetime',
+                'default' => 0,
+                'range' => [
+                    'upper' => mktime(0, 0, 0, 1, 1, 2038),
+                ],
+            ],
+        ],
+    ],
+];
diff --git a/typo3/sysext/webhooks/LICENSE.txt b/typo3/sysext/webhooks/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d159169d1050894d3ea3b98e1c965c4058208fe1
--- /dev/null
+++ b/typo3/sysext/webhooks/LICENSE.txt
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/typo3/sysext/webhooks/README.rst b/typo3/sysext/webhooks/README.rst
new file mode 100644
index 0000000000000000000000000000000000000000..ab7cecff12d74029ff9016a19c522dd076b7b2f8
--- /dev/null
+++ b/typo3/sysext/webhooks/README.rst
@@ -0,0 +1,12 @@
+==========================
+TYPO3 extension `webhooks`
+==========================
+
+This extension handles outgoing webhooks to TYPO3. It also provides
+a corresponding backend module to manage webhook records in the TYPO3
+backend (`System > Webhooks`).
+
+:Repository:  https://github.com/typo3/typo3
+:Issues:      https://forge.typo3.org/
+:Read online: https://docs.typo3.org/c/typo3/cms-webhooks/main/en-us/
+:TER:         https://extensions.typo3.org/extension/webhooks/
diff --git a/typo3/sysext/webhooks/Resources/Private/Language/locallang_db.xlf b/typo3/sysext/webhooks/Resources/Private/Language/locallang_db.xlf
new file mode 100644
index 0000000000000000000000000000000000000000..9e2fd2c1f1a5b8d6030630371d622ecb0189e4ab
--- /dev/null
+++ b/typo3/sysext/webhooks/Resources/Private/Language/locallang_db.xlf
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+	<file source-language="en" datatype="plaintext" original="EXT:webhooks/Resources/Private/Language/locallang_db.xlf" date="2023-01-11T20:22:32Z" product-name="webhooks">
+		<header/>
+		<body>
+			<trans-unit id="webhooks" resname="webhooks">
+				<source>Webhooks</source>
+			</trans-unit>
+
+			<trans-unit id="sys_webhook" resname="sys_webhook">
+				<source>System webhooks</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.name" resname="sys_webhook.name">
+				<source>Name</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.name.description" resname="sys_webhook.name.description">
+				<source>Meaningful name of the webhook</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.description" resname="sys_webhook.description">
+				<source>Description</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.description.description" resname="sys_webhook.description.description">
+				<source>Additional information about the webhook for your internal reference.</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.url" resname="sys_webhook.url">
+				<source>URL</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.url.description" resname="sys_webhook.url.description">
+				<source>Target URL that should be called.</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.method" resname="sys_webhook.method">
+				<source>HTTP Method</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.verify_ssl" resname="sys_webhook.verify_ssl">
+				<source>Verify SSL</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.verify_ssl.description" resname="sys_webhook.verify_ssl.description">
+				<source>Verify that the connection is secure and valid, this setting should only be disabled in case a verification is not possible.</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.additional_headers" resname="sys_webhook.additional_headers">
+				<source>Additional Headers</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.additional_headers.description" resname="sys_webhook.additional_headers.description">
+				<source>Additional headers that should be added to the HTTP request. Data must be provided as valid JSON string.</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.identifier" resname="sys_webhook.identifier">
+				<source>Identifier</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.identifier.description" resname="sys_webhook.identifier.description">
+				<source>This is your unique webhook identifier</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.webhook_type" resname="sys_webhook.webhook_type">
+				<source>Webhook Trigger</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.secret" resname="sys_webhook.secret">
+				<source>Secret</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.secret.description" resname="sys_webhook.secret.description">
+				<source>The secret is mandatory to create a hash that is sent with the request. The secret can be used to verify the payload has not been modified, see the documentation for more details.</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.secret.passwordGenerator" resname="sys_webhook.secret.passwordGenerator">
+				<source>Generate secret</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.webhook_type.description" resname="sys_webhook.webhook_type.description">
+				<source>Trigger the webhook, ...</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.webhook_type.select" resname="sys_webhook.webhook_type.select">
+				<source>Select a trigger</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.webhook_type.typo3-content-page-modification" resname="sys_webhook.webhook_type.typo3-content-page-modification">
+				<source>... when a page is added or changed</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.webhook_type.typo3-login-error" resname="sys_webhook.webhook_type.typo3-login-error">
+				<source>... when an error occurs on log in</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.webhook_type.typo3-file-added" resname="sys_webhook.webhook_type.typo3-file-added">
+				<source>... when a file is added</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.webhook_type.typo3-file-removed" resname="sys_webhook.webhook_type.typo3-file-removed">
+				<source>... when a file is removed</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.webhook_type.typo3-file-updated" resname="sys_webhook.webhook_type.typo3-file-updated">
+				<source>... when a file is updated</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.storage_pid" resname="sys_webhook.storage_pid">
+				<source>Storage PID</source>
+			</trans-unit>
+			<trans-unit id="sys_webhook.storage_pid.description" resname="sys_webhook.storage_pid.description">
+				<source>Select the page on which a new record is created on.</source>
+			</trans-unit>
+
+			<trans-unit id="palette.config" resname="palette.config">
+				<source>Configuration</source>
+			</trans-unit>
+			<trans-unit id="palette.config.description" resname="palette.config.description">
+				<source>General configuration of the webhook.</source>
+			</trans-unit>
+			<trans-unit id="palette.http_settings" resname="palette.http_settings">
+				<source>HTTP Settings</source>
+			</trans-unit>
+			<trans-unit id="palette.http_settings.description" resname="palette.http_settings.description">
+				<source>Advanced HTTP request settings.</source>
+			</trans-unit>
+			<trans-unit id="palette.additional" resname="palette.additional">
+				<source>Additional configuration</source>
+			</trans-unit>
+			<trans-unit id="palette.additional.description" resname="palette.additional.description">
+				<source>Specific configuration for the selected webhook type</source>
+			</trans-unit>
+		</body>
+	</file>
+</xliff>
diff --git a/typo3/sysext/webhooks/Resources/Private/Language/locallang_module_webhooks.xlf b/typo3/sysext/webhooks/Resources/Private/Language/locallang_module_webhooks.xlf
new file mode 100644
index 0000000000000000000000000000000000000000..d8575c2f23d0bf32215e93a4ad80eae2d0b0c9eb
--- /dev/null
+++ b/typo3/sysext/webhooks/Resources/Private/Language/locallang_module_webhooks.xlf
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+    <file source-language="en" datatype="plaintext" original="EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf" date="2022-08-17T12:12:34Z" product-name="webhooks">
+        <header/>
+        <body>
+            <trans-unit id="mlang_labels_tablabel" resname="mlang_labels_tablabel">
+                <source>Webhook Administration</source>
+            </trans-unit>
+            <trans-unit id="mlang_labels_tabdescr" resname="mlang_labels_tabdescr">
+                <source>This is the administration area for Webhooks.</source>
+            </trans-unit>
+            <trans-unit id="mlang_tabs_tab" resname="mlang_tabs_tab">
+                <source>Webhooks</source>
+            </trans-unit>
+
+            <trans-unit id="heading_text" resname="heading_text">
+                <source>Webhooks — Manage Outgoing HTTP Webhooks</source>
+            </trans-unit>
+            <trans-unit id="webhook_not_found.title" resname="webhook_not_found.title">
+                <source>No webhooks found!</source>
+            </trans-unit>
+            <trans-unit id="webhook_not_found.message" resname="webhook_not_found.message">
+                <source>There are currently no webhooks records found in the database.</source>
+            </trans-unit>
+            <trans-unit id="webhook_create" resname="webhook_create">
+                <source>Create new webhook</source>
+            </trans-unit>
+            <trans-unit id="webhook_not_found_with_filter.title" resname="webhook_not_found_with_filter.title">
+                <source>No webhooks found!</source>
+            </trans-unit>
+            <trans-unit id="webhook_not_found_with_filter.message" resname="webhook_not_found_with_filter.message">
+                <source>With the current set of filters applied, no webhooks could be found.</source>
+            </trans-unit>
+            <trans-unit id="webhook_no_filter" resname="webhook_no_filter">
+                <source>Remove all filter</source>
+            </trans-unit>
+            <trans-unit id="webhook_no_implementation_class" resname="webhook_no_implementation_class">
+                <source>Implementation class for webhook type "%s" is missing.</source>
+            </trans-unit>
+            <trans-unit id="webhook_example" resname="webhook_example">
+                <source>Example</source>
+            </trans-unit>
+
+            <trans-unit id="filter.sendButton" resname="filter.sendButton">
+                <source>Filter</source>
+            </trans-unit>
+            <trans-unit id="filter.resetButton" resname="filter.resetButton">
+                <source>Reset</source>
+            </trans-unit>
+            <trans-unit id="filter.name" resname="filter.name">
+                <source>Name</source>
+            </trans-unit>
+            <trans-unit id="filter.webhook_type" resname="filter.webhook_type">
+                <source>Webhook Trigger</source>
+            </trans-unit>
+            <trans-unit id="filter.webhook_type.showAll" resname="filter.webhook_type.showAll">
+                <source>Show all</source>
+            </trans-unit>
+
+            <trans-unit id="pagination.previous" resname="pagination.previous">
+                <source>previous</source>
+            </trans-unit>
+            <trans-unit id="pagination.next" resname="pagination.next">
+                <source>next</source>
+            </trans-unit>
+            <trans-unit id="pagination.first" resname="pagination.first">
+                <source>first</source>
+            </trans-unit>
+            <trans-unit id="pagination.last" resname="pagination.last">
+                <source>last</source>
+            </trans-unit>
+            <trans-unit id="pagination.records" resname="pagination.records">
+                <source>Records</source>
+            </trans-unit>
+            <trans-unit id="pagination.page" resname="pagination.page">
+                <source>Page</source>
+            </trans-unit>
+            <trans-unit id="pagination.refresh" resname="pagination.refresh">
+                <source>Refresh</source>
+            </trans-unit>
+
+			<trans-unit id="labels.delete.title" resname="labels.delete.title">
+				<source>Delete webhooks</source>
+			</trans-unit>
+			<trans-unit id="labels.delete.message" resname="labels.delete.message">
+				<source>Are you sure you want to delete all marked webhooks?</source>
+			</trans-unit>
+        </body>
+    </file>
+</xliff>
diff --git a/typo3/sysext/webhooks/Resources/Private/Partials/Pagination.html b/typo3/sysext/webhooks/Resources/Private/Partials/Pagination.html
new file mode 100644
index 0000000000000000000000000000000000000000..b9fda8550717874befe80fd552bb31f68f01e9f0
--- /dev/null
+++ b/typo3/sysext/webhooks/Resources/Private/Partials/Pagination.html
@@ -0,0 +1,96 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
+    xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers"
+    data-namespace-typo3-fluid="true"
+>
+<f:if condition="{paginator.numberOfPages} > 1">
+    <nav class="pagination-wrap">
+        <ul class="pagination">
+            <f:if condition="{pagination.previousPageNumber} && {pagination.previousPageNumber} >= {pagination.firstPageNumber}">
+                <f:then>
+                    <li class="page-item">
+                        <a href="{f:be.uri(route:'webhooks_management', parameters: '{demand: demand.parameters, orderField: demand.orderField, orderDirection: demand.orderDirection, page: 1}')}" title="{f:translate(key:'LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:pagination.first')}" class="page-link">
+                            <core:icon identifier="actions-view-paging-first" />
+                        </a>
+                    </li>
+                    <li class="page-item">
+                        <a href="{f:be.uri(route:'webhooks_management', parameters: '{demand: demand.parameters, orderField: demand.orderField, orderDirection: demand.orderDirection, page: pagination.previousPageNumber}')}" title="{f:translate(key:'LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:pagination.previous')}" class="page-link">
+                            <core:icon identifier="actions-view-paging-previous" />
+                        </a>
+                    </li>
+                </f:then>
+                <f:else>
+                    <li class="page-item disabled">
+                        <span class="page-link">
+                            <core:icon identifier="empty-empty"/>
+                        </span>
+                    </li>
+                    <li class="page-item disabled">
+                        <span class="page-link">
+                            <core:icon identifier="empty-empty"/>
+                        </span>
+                    </li>
+                </f:else>
+            </f:if>
+            <li class="page-item">
+                <span class="page-link">
+                    <f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:pagination.records" /> {pagination.startRecordNumber} - {pagination.endRecordNumber}
+                </span>
+            </li>
+            <li class="page-item">
+                <span class="page-link">
+                    <f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:pagination.page" />
+                    <form style="display:inline;"
+                        data-global-event="submit"
+                        data-action-navigate="$form=~s/$value/"
+                        data-navigate-value="{f:be.uri(route:'webhooks_management', parameters: '{demand: demand.parameters, orderField: demand.orderField, orderDirection: demand.orderDirection, page: \'$[value]\'}')}"
+                        data-value-selector="input[name='paginator-target-page']">
+                        <input
+                            min="{pagination.firstPageNumber}"
+                            max="{pagination.lastPageNumber}"
+                            data-number-of-pages="{paginator.numberOfPages}"
+                            name="paginator-target-page"
+                            class="form-control form-control-sm paginator-input"
+                            size="5"
+                            value="{paginator.currentPageNumber}"
+                            type="number"
+                        />
+                    </form>
+                    / {pagination.lastPageNumber}
+                </span>
+            </li>
+
+            <f:if condition="{pagination.nextPageNumber} && {pagination.nextPageNumber} <= {pagination.lastPageNumber}">
+                <f:then>
+                    <li class="page-item">
+                        <a href="{f:be.uri(route:'webhooks_management', parameters: '{demand: demand.parameters, orderField: demand.orderField, orderDirection: demand.orderDirection, page: pagination.nextPageNumber}')}" title="{f:translate(key:'LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:pagination.next')}" class="page-link">
+                            <core:icon identifier="actions-view-paging-next" />
+                        </a>
+                    </li>
+                    <li class="page-item">
+                        <a href="{f:be.uri(route:'webhooks_management', parameters: '{demand: demand.parameters, orderField: demand.orderField, orderDirection: demand.orderDirection, page: pagination.lastPageNumber}')}" title="{f:translate(key:'LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:pagination.next')}" class="page-link">
+                            <core:icon identifier="actions-view-paging-last" />
+                        </a>
+                    </li>
+                </f:then>
+                <f:else>
+                    <li class="page-item disabled">
+                        <span class="page-link">
+                            <core:icon identifier="empty-empty"/>
+                        </span>
+                    </li>
+                    <li class="page-item disabled">
+                        <span class="page-link">
+                            <core:icon identifier="empty-empty"/>
+                        </span>
+                    </li>
+                </f:else>
+            </f:if>
+            <li class="page-item">
+                <a href="{f:be.uri(route:'webhooks_management', parameters: '{demand: demand.parameters, orderField: demand.orderField, orderDirection: demand.orderDirection, page: pagination.currentPageNumber}')}" title="{f:translate(key:'LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:pagination.refresh')}" class="page-link">
+                    <core:icon identifier="actions-refresh" />
+                </a>
+            </li>
+        </ul>
+    </nav>
+</f:if>
+</html>
diff --git a/typo3/sysext/webhooks/Resources/Private/Templates/Management/Overview.html b/typo3/sysext/webhooks/Resources/Private/Templates/Management/Overview.html
new file mode 100644
index 0000000000000000000000000000000000000000..8349eef75e6c14428bd1233ec8cd14a71881fbb0
--- /dev/null
+++ b/typo3/sysext/webhooks/Resources/Private/Templates/Management/Overview.html
@@ -0,0 +1,305 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
+    xmlns:be="http://typo3.org/ns/TYPO3/CMS/Backend/ViewHelpers"
+    xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers"
+    data-namespace-typo3-fluid="true"
+>
+<f:layout name="Module" />
+
+<f:section name="Content">
+    <f:be.pageRenderer
+        includeJavaScriptModules="{
+            0: '@typo3/backend/context-menu.js',
+            1: '@typo3/backend/modal.js',
+            2: '@typo3/backend/multi-record-selection.js',
+            3: '@typo3/backend/multi-record-selection-edit-action.js',
+            4: '@typo3/backend/multi-record-selection-delete-action.js'
+        }"
+    />
+    <h1><f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:heading_text"/></h1>
+    <f:variable
+        name="returnUrl"
+        value="{f:be.uri(route:'webhooks_management')}"
+    />
+    <f:if condition="{webhookRecords -> f:count()}">
+        <f:then>
+            <f:render section="filter" arguments="{_all}" />
+            <f:render section="table" arguments="{_all}" />
+        </f:then>
+        <f:else>
+            <f:if condition="{demand.constraints}">
+                <f:then>
+                    <f:render section="filter" arguments="{_all}" />
+                    <f:be.infobox state="-2" title="{f:translate(key: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:webhook_not_found_with_filter.title')}">
+                        <p><f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:webhook_not_found_with_filter.message"/></p>
+                        <a class="btn btn-default" href="{f:be.uri(route:'webhooks_management')}">
+                            <f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:webhook_no_filter"/>
+                        </a>
+                        <be:link.newRecord returnUrl="{returnUrl}" class="btn btn-primary" table="sys_webhook">
+                            <f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:webhook_create"/>
+                        </be:link.newRecord>
+                    </f:be.infobox>
+
+                    <f:variable name="gotToPageUrl"><f:be.uri route="webhooks_management" parameters="{demand: demand.parameters, page: 987654322}" /></f:variable>
+                    <form data-on-submit="processNavigate">
+                        <input type="hidden" value="1" name="paginator-target-page" data-number-of-pages="1" data-url="{gotToPageUrl}"/>
+                    </form>
+                </f:then>
+                <f:else>
+                    <f:be.infobox state="-1" title="{f:translate(key: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:webhook_not_found.title')}">
+                        <p><f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:webhook_not_found.message"/></p>
+                        <be:link.newRecord returnUrl="{returnUrl}" class="btn btn-primary" table="sys_webhook">
+                            <f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:webhook_create"/>
+                        </be:link.newRecord>
+                    </f:be.infobox>
+                </f:else>
+            </f:if>
+        </f:else>
+    </f:if>
+</f:section>
+
+<f:section name="table">
+    <f:render section="multiRecordSelectionActions" arguments="{_all}" />
+    <div class="table-fit mb-0">
+        <table class="table table-striped table-hover">
+            <thead>
+            <tr>
+                <th><f:render section="multiRecordSelectionCheckboxActions" /></th>
+                <th colspan="2"><f:render section="listHeaderSorting" arguments="{field: 'name', label: 'sys_webhook.name', demand: demand}"/></th>
+                <th><f:render section="listHeaderSorting" arguments="{field: 'webhook_type', label: 'sys_webhook.webhook_type', demand: demand}"/></th>
+                <th>URL</th>
+                <th></th>
+            </tr>
+            </thead>
+            <tbody data-multi-record-selection-row-selection="true">
+            <f:for each="{webhookRecords}" key="webhookId" as="webhook">
+                <f:variable name="webhookRecord" value="{webhook as array}"/>
+                <tr data-uid="{webhook.uid}" data-multi-record-selection-element="true">
+                    <td class="col-selector nowrap">
+                        <span class="form-check form-toggle">
+                            <input class="form-check-input t3js-multi-record-selection-check" type="checkbox"/>
+                        </span>
+                    </td>
+                    <f:if condition="{webhookTypes.{webhook.webhookType.identifier}}">
+                        <f:then>
+                            <td class="col-icon">
+                                <a
+                                    href="#"
+                                    data-contextmenu-trigger="click"
+                                    data-contextmenu-table="sys_webhook"
+                                    data-contextmenu-uid="{webhook.uid}"
+                                    title="{webhook.name}"
+                                >
+                                    <core:iconForRecord table="sys_webhook" row="{webhookRecord}" />
+                                </a>
+                            </td>
+                            <td>
+                                <be:link.editRecord
+                                    returnUrl="{returnUrl}"
+                                    table="sys_webhook"
+                                    uid="{webhook.uid}"
+                                    title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:edit')}: {webhook.name}"
+                                >
+                                    {webhook.name}
+                                </be:link.editRecord>
+                            </td>
+                            <td>
+                                <f:translate key="{webhookTypes.{webhook.webhookType.identifier}.description}" default="{webhookTypes.{webhook.webhookType.identifier}.description}"/>
+                            </td>
+                            <td>
+                                <f:render section="codesnippet" arguments="{webhook: webhook}" />
+                            </td>
+                            <td class="col-control">
+                                <f:render section="controls" arguments="{_all}" />
+                            </td>
+                        </f:then>
+                        <f:else>
+                            <td class="col-icon">
+                                <core:iconForRecord table="sys_webhook" row="{webhookRecord}" />
+                            </td>
+                            <td>
+                                {webhook.name}
+                            </td>
+                            <td colspan="2">
+                                <span class="badge badge-danger">
+                                    <f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:webhook_no_implementation_class" arguments="{0: webhook.webhookType.identifier}" />
+                                </span>
+                            </td>
+                            <td class="col-control">
+                                <f:render section="controls" arguments="{_all}" />
+                            </td>
+                        </f:else>
+                    </f:if>
+                </tr>
+            </f:for>
+            </tbody>
+        </table>
+    </div>
+    <f:render section="multiRecordSelectionActions" arguments="{_all}" />
+    <f:render partial="Pagination" arguments="{_all}" />
+</f:section>
+
+<f:section name="listHeaderSorting">
+    <f:if condition="{demand.orderField} === {field}">
+        <f:then>
+            <a href="{f:be.uri(route:'webhooks_management', parameters: '{demand: demand.parameters, orderField: field, orderDirection: demand.reverseOrderDirection}')}">
+                <f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:{label}"/>
+            </a>
+            <core:icon identifier="status-status-sorting-{demand.orderDirection}"/>
+        </f:then>
+        <f:else>
+            <a href="{f:be.uri(route:'webhooks_management', parameters: '{demand: demand.parameters, orderField: field, orderDirection: demand.defaultOrderDirection}')}">
+                <f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:{label}"/>
+            </a>
+        </f:else>
+    </f:if>
+</f:section>
+
+<f:section name="filter">
+    <form action="{f:be.uri(route:'webhooks_management')}" method="post" enctype="multipart/form-data" name="demand">
+        <input type="hidden" name="orderField" value="{demand.orderField}">
+        <input type="hidden" name="orderDirection" value="{demand.orderDirection}">
+        <div class="row row-cols-auto align-items-end g-3 mb-4">
+            <div class="col">
+                <label for="demand-name" class="form-label"><f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:filter.name"/></label>
+                <input type="text" id="demand-name" class="form-control" name="demand[name]" value="{demand.name}"/>
+            </div>
+            <div class="col">
+                <label for="demand-webhook-type" class="form-label"><f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:filter.webhook_type"/></label>
+                <select id="demand-webhook-type" class="form-select" name="demand[webhook_type]" data-on-change="submit">
+                    <option value=""><f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:filter.webhook_type.showAll"/></option>
+                    <f:for each="{webhookTypes}" key="identifier" as="webhookType">
+                        <option value="{identifier}" {f:if(condition: '{identifier} == {demand.webhookType}', then: 'selected')}>
+                            <f:translate key="{webhookType.description}" default="{webhookType.description}"/>
+                        </option>
+                    </f:for>
+                </select>
+            </div>
+            <div class="col">
+                <input type="submit" value="{f:translate(key: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:filter.sendButton')}" class="btn btn-default" />
+                <a href="{f:be.uri(route:'webhooks_management')}" class="btn btn-link"><f:translate key="LLL:EXT:webhooks/Resources/Private/Language/locallang_module_webhooks.xlf:filter.resetButton"/></a>
+            </div>
+        </div>
+    </form>
+</f:section>
+
+<f:section name="codesnippet">
+    <f:if condition="{webhook.targetUrl}">
+        <code class="my-1 p-2 me-2 bg-dark text-bg-primary">
+            {webhook.targetUrl}
+        </code>
+    </f:if>
+</f:section>
+
+<f:section name="controls">
+    <div class="btn-group">
+        <be:link.editRecord
+            returnUrl="{returnUrl}"
+            class="btn btn-default"
+            table="sys_webhook"
+            uid="{webhook.uid}"
+            title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:edit')}"
+        >
+            <core:icon identifier="actions-open" />
+        </be:link.editRecord>
+        <f:if condition="{webhookRecord.disabled} == 1">
+            <f:then>
+                <a
+                    class="btn btn-default"
+                    href="{be:moduleLink(route:'tce_db', query:'data[sys_webhook][{webhook.uid}][disabled]=0', arguments:'{redirect: returnUrl}')}"
+                    title="{f:translate(key:'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide')}"
+                >
+                    <core:icon identifier="actions-edit-unhide" />
+                </a>
+            </f:then>
+            <f:else>
+                <a
+                    class="btn btn-default"
+                    href="{be:moduleLink(route:'tce_db', query:'data[sys_webhook][{webhook.uid}][disabled]=1', arguments:'{redirect: returnUrl}')}"
+                    title="{f:translate(key:'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide')}"
+                >
+                    <core:icon identifier="actions-edit-hide" />
+                </a>
+            </f:else>
+        </f:if>
+        <a class="btn btn-default t3js-modal-trigger"
+            href="{be:moduleLink(route:'tce_db', query:'cmd[sys_webhook][{webhook.uid}][delete]=1', arguments:'{redirect: returnUrl}')}"
+            title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete')}"
+            data-severity="warning"
+            data-title="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:label.confirm.delete_record.title')}"
+            data-bs-content="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:deleteWarning')}"
+            data-button-close-text="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_record.no')}">
+            <core:icon identifier="actions-delete" />
+        </a>
+    </div>
+</f:section>
+
+<f:section name="multiRecordSelectionCheckboxActions">
+    <div class="btn-group dropdown">
+        <a href="javascript:;" class="dropdown-toggle t3js-multi-record-selection-check-actions-toggle" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false">
+            <core:icon identifier="actions-selection" size="small" />
+        </a>
+        <ul class="dropdown-menu t3js-multi-record-selection-check-actions">
+            <li>
+                <button type="button" class="dropdown-item disabled" data-multi-record-selection-check-action="check-all" title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')}">
+                    <span class="dropdown-item-columns">
+                        <span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
+                            <core:icon identifier="actions-selection-elements-all" size="small" />
+                        </span>
+                        <span class="dropdown-item-column dropdown-item-column-title">
+                            <f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll" />
+                        </span>
+                    </span>
+                </button>
+            </li>
+            <li>
+                <button type="button" class="dropdown-item disabled" data-multi-record-selection-check-action="check-none" title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')}">
+                    <span class="dropdown-item-columns">
+                        <span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
+                            <core:icon identifier="actions-selection-elements-none" size="small" />
+                        </span>
+                        <span class="dropdown-item-column dropdown-item-column-title">
+                            <f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll" />
+                        </span>
+                    </span>
+                </button>
+            </li>
+            <li>
+                <button type="button" class="dropdown-item" data-multi-record-selection-check-action="toggle" title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')}">
+                    <span class="dropdown-item-columns">
+                        <span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
+                            <core:icon identifier="actions-selection-elements-invert" size="small" />
+                        </span>
+                        <span class="dropdown-item-column dropdown-item-column-title">
+                            <f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection" />
+                        </span>
+                    </span>
+                </button>
+            </li>
+        </ul>
+    </div>
+</f:section>
+
+<f:section name="multiRecordSelectionActions">
+    <div class="multi-record-selection-actions-wrapper">
+        <div class="row row-cols-auto align-items-center g-2 t3js-multi-record-selection-actions hidden">
+            <div class="col">
+                <strong><f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selection"/></strong>
+            </div>
+            <div class="col">
+                <button class="btn btn-default btn-sm" data-multi-record-selection-action="edit" data-multi-record-selection-action-config='{editActionConfiguration -> f:format.raw()}'>
+                    <core:icon identifier="actions-open" size="small" />
+                    <span class="text">
+                        <f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit" />
+                    </span>
+                </button>
+                <button class="btn btn-default btn-sm" data-multi-record-selection-action="delete" data-multi-record-selection-action-config='{deleteActionConfiguration -> f:format.raw()}'>
+                    <core:icon identifier="actions-edit-delete" size="small" />
+                    <span class="text">
+                        <f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete" />
+                    </span>
+                </button>
+            </div>
+        </div>
+    </div>
+</f:section>
+</html>
diff --git a/typo3/sysext/webhooks/Resources/Public/Icons/Extension.svg b/typo3/sysext/webhooks/Resources/Public/Icons/Extension.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e3f075b44be417790237d6d588cf56e1b5230885
--- /dev/null
+++ b/typo3/sysext/webhooks/Resources/Public/Icons/Extension.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 64 64"><path fill="#631EE9" d="M0 0h64v64H0z"/><path fill="#FFF" d="M43.002 37a3.99 3.99 0 0 0-3.858 3h-9.969a9.202 9.202 0 0 0-1.456-4.09l-1.492 2.487c.336.801.523 1.68.523 2.603 0 3.722-3.028 6.75-6.75 6.75s-6.75-3.028-6.75-6.75c0-3.508 2.69-6.397 6.116-6.718l1.493-2.489A9.339 9.339 0 0 0 20 31.75 9.25 9.25 0 1 0 29.194 42h9.95a3.99 3.99 0 0 0 3.858 3 4 4 0 0 0 0-8zm0 6c-1.103 0-2-.897-2-2s.897-2 2-2 2 .897 2 2-.897 2-2 2z"/><path fill="#FFF" d="M41.251 20.998a9.25 9.25 0 1 0-18.5 0c0 3.008 1.443 5.673 3.667 7.362l-5.292 8.82A3.956 3.956 0 0 0 20 37a4 4 0 1 0 4 4c0-1.093-.44-2.082-1.151-2.804l5.283-8.805a9.2 9.2 0 0 0 3.87.857c.17 0 .337-.016.505-.026l-1.397-2.54c-3.301-.438-5.859-3.265-5.859-6.684a6.758 6.758 0 0 1 6.75-6.75 6.758 6.758 0 0 1 6.75 6.75c0 1.012-.23 1.968-.63 2.83l1.399 2.544a9.197 9.197 0 0 0 1.731-5.374zM20 43c-1.103 0-2-.897-2-2s.897-2 2-2 2 .897 2 2-.897 2-2 2z"/><path fill="#FFF" d="M43.002 31.75a9.16 9.16 0 0 0-3.549.727l-4.716-8.574a3.978 3.978 0 0 0 1.264-2.905 4 4 0 1 0-4 4c.341 0 .666-.056.982-.136l4.721 8.584A9.299 9.299 0 0 0 34.672 37h2.906a6.738 6.738 0 0 1 5.424-2.75c3.722 0 6.75 3.028 6.75 6.75s-3.028 6.75-6.75 6.75A6.738 6.738 0 0 1 37.578 45h-2.906c1.493 3.103 4.657 5.25 8.33 5.25a9.25 9.25 0 1 0 0-18.5zm-11-8.752c-1.104 0-2-.897-2-2s.896-2 2-2c1.102 0 2 .897 2 2s-.898 2-2 2z"/></svg>
\ No newline at end of file
diff --git a/typo3/sysext/webhooks/Tests/Functional/Fixtures/sys_webhooks.csv b/typo3/sysext/webhooks/Tests/Functional/Fixtures/sys_webhooks.csv
new file mode 100644
index 0000000000000000000000000000000000000000..0ab71c3ee099fef7f27053993481d03580f3eea7
--- /dev/null
+++ b/typo3/sysext/webhooks/Tests/Functional/Fixtures/sys_webhooks.csv
@@ -0,0 +1,5 @@
+"sys_webhook"
+,"uid","pid","updatedon","createdon","deleted","disabled","starttime","endtime","description","name","url","method","secret","webhook_type","identifier","verify_ssl","additional_headers"
+,1,0,1674072816,1674066297,0,0,0,0,"Test","Test with valid SSL","https://t3main.devbox.example.com/","POST","9699783a11b233aa07beb5c5a33fc7056d639d62","typo3/login-error","06f39184-6a7c-4756-81e1-c170f114d689",1,"[]"
+,2,0,1674072864,1674072838,0,0,0,0,"Test with invalid SSL","Test with invalid SSL","https://localhost","POST","f73e4475886379ce1926f20da4acffb13e7b6268","typo3/login-error","ee828ed4-5e81-49af-8139-5678a22e7715",0,"[]"
+,3,0,1674072864,1674072838,0,0,0,0,"Test with other settings","Test with other settings","https://localhost","POST","f73e4475886379ce1926f20da4acffb13e7b6268","typo3/content/page-modification","73cbf4cf-ed1b-497e-ae4e-345abe936783",0,"[]"
diff --git a/typo3/sysext/webhooks/Tests/Functional/WebhookExecutionTest.php b/typo3/sysext/webhooks/Tests/Functional/WebhookExecutionTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..140f11f8ad0f64be52af01721e3b8a5d5a9e2a11
--- /dev/null
+++ b/typo3/sysext/webhooks/Tests/Functional/WebhookExecutionTest.php
@@ -0,0 +1,134 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Tests\Functional;
+
+use Psr\Http\Message\RequestInterface;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Context\SecurityAspect;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Http\Response;
+use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\CMS\Core\Security\RequestToken;
+use TYPO3\CMS\Core\Tests\Functional\DataScenarios\AbstractDataHandlerActionTestCase;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Tests that check if a certain message is triggered and about to be sent
+ * out via HTTP.
+ *
+ * It simulates a full scenario to trigger a webhook message to a remote URL.
+ */
+class WebhookExecutionTest extends AbstractDataHandlerActionTestCase
+{
+    protected array $coreExtensionsToLoad = ['webhooks'];
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->importCSVDataSet(__DIR__ . '/../../../core/Tests/Functional/Fixtures/pages.csv');
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/sys_webhooks.csv');
+        $this->setUpFrontendSite(1, $this->siteLanguageConfiguration);
+    }
+
+    /**
+     * @test
+     */
+    public function requestIsSentOutForMessagesWithAGivenType(): void
+    {
+        $numberOfRequestsFired = 0;
+        $inspector = function (RequestInterface $request) use (&$numberOfRequestsFired) {
+            $payload = json_decode($request->getBody()->getContents(), true);
+            $numberOfRequestsFired++;
+            self::assertSame('modified', $payload['action']);
+            self::assertSame('Dummy Modified', $payload['changedFields']['title']);
+        };
+        $this->registerRequestInspector($inspector);
+
+        // Catch any requests, evaluate their payload
+        $this->actionService->modifyRecord('pages', 10, ['title' => 'Dummy Modified']);
+        // @todo: this is a bug in DataHandler, because it triggers the option twice.
+        self::assertEquals(2, $numberOfRequestsFired);
+    }
+
+    /**
+     * @test
+     */
+    public function oneMessageWithMultipleRequestsIsTriggeredAndDispatched(): void
+    {
+        $numberOfRequestsFired = 0;
+        $inspector = function (RequestInterface $request) use (&$numberOfRequestsFired) {
+            $payload = json_decode($request->getBody()->getContents(), true);
+            self::assertSame('backend', $payload['context']);
+            self::assertSame('han-solo', $payload['loginData']['uname']);
+            self::assertSame('********', $payload['loginData']['uident']);
+            $numberOfRequestsFired++;
+        };
+        $this->registerRequestInspector($inspector);
+        $context = GeneralUtility::makeInstance(Context::class);
+        $securityAspect = SecurityAspect::provideIn($context);
+        $nonce = $securityAspect->provideNonce();
+        $requestToken = RequestToken::create('core/user-auth/be');
+        $securityAspect->setReceivedRequestToken($requestToken);
+
+        $request = new ServerRequest('https://example.com/site1/', 'POST');
+        $request = $request->withParsedBody([
+            'login_status' => 'login',
+            'username' => 'han-solo',
+            'userident' => 'chewbaka',
+            RequestToken::PARAM_NAME => $requestToken->toHashSignedJwt($nonce),
+        ]);
+
+        $userRequest = GeneralUtility::makeInstance(BackendUserAuthentication::class);
+        $userRequest->start($request);
+        self::assertEquals(2, $numberOfRequestsFired);
+    }
+
+    /**
+     * @test
+     */
+    public function messageWithoutConfiguredTypesDoesNotSendARequest(): void
+    {
+        // Just empty the table for the request, other ways are possible to do this
+        GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_webhook')->truncate('sys_webhook');
+        $numberOfRequestsFired = 0;
+        $inspector = function () use (&$numberOfRequestsFired) {
+            $numberOfRequestsFired++;
+        };
+        $this->registerRequestInspector($inspector);
+
+        // Catch any requests, evaluate their payload
+        $this->actionService->modifyRecord('pages', 10, ['title' => 'Dummy Modified']);
+        self::assertEquals(0, $numberOfRequestsFired);
+    }
+
+    protected function assertCleanReferenceIndex(): void
+    {
+        // do not do anything here yet
+    }
+
+    protected function registerRequestInspector(callable $inspector): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler']['logger'] = function () use ($inspector) {
+            return function (RequestInterface $request) use ($inspector) {
+                $inspector($request);
+                return new Response('success', 200);
+            };
+        };
+    }
+}
diff --git a/typo3/sysext/webhooks/Tests/Unit/Factory/WebhookInstructionFactoryTest.php b/typo3/sysext/webhooks/Tests/Unit/Factory/WebhookInstructionFactoryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..3821cff7aed8cfec2a9daa658d6a14c81d38ba0a
--- /dev/null
+++ b/typo3/sysext/webhooks/Tests/Unit/Factory/WebhookInstructionFactoryTest.php
@@ -0,0 +1,136 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Tests\Unit\Factory;
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Webhooks\Factory\WebhookInstructionFactory;
+use TYPO3\CMS\Webhooks\Model\WebhookType;
+use TYPO3\CMS\Webhooks\WebhookTypesRegistry;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class WebhookInstructionFactoryTest extends UnitTestCase
+{
+    /**
+     * Simulate a tt_content record
+     */
+    protected array $mockRecord = [
+        'url' => 'https://example.com',
+        'secret' => 'a random secret',
+        'method' => 'POST',
+        'verify_ssl' => true,
+        'additional_headers' => [
+            'X-My-Header' => 'My Header Value',
+        ],
+        'name' => 'My Webhook',
+        'description' => 'My Webhook Description',
+        'webhook_type' => null,
+        'identifier' => '033c049f-7762-4755-b072-805350a8726a',
+        'uid' => 200413,
+    ];
+
+    protected WebhookType $webhookType;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->webhookType = new WebhookType('typo3/test-webhook', 'My WebhookType description', 'My\Webhook\Type', 'myFactoryMethod');
+    }
+
+    /**
+     * @test
+     */
+    public function createWebhookInstructionWithMinimalData(): void
+    {
+        $webhookInstruction = WebhookInstructionFactory::create(
+            $this->mockRecord['url'],
+            $this->mockRecord['secret'],
+        );
+        self::assertSame($this->mockRecord['url'], $webhookInstruction->getTargetUrl());
+        self::assertSame($this->mockRecord['secret'], $webhookInstruction->getSecret());
+        self::assertSame('POST', $webhookInstruction->getHttpMethod());
+        self::assertTrue($webhookInstruction->verifySSL());
+        self::assertSame([], $webhookInstruction->getAdditionalHeaders());
+        self::assertSame('', $webhookInstruction->getName());
+        self::assertSame('', $webhookInstruction->getDescription());
+        self::assertNull($webhookInstruction->getWebhookType());
+        self::assertNull($webhookInstruction->getIdentifier());
+        self::assertSame(0, $webhookInstruction->getUid());
+    }
+
+    /**
+     * @test
+     */
+    public function createWebhookInstructionWithAllData(): void
+    {
+        $this->mockRecord['webhook_type'] = $this->webhookType;
+        $webhookInstruction = WebhookInstructionFactory::create(
+            $this->mockRecord['url'],
+            $this->mockRecord['secret'],
+            $this->mockRecord['method'],
+            $this->mockRecord['verify_ssl'],
+            $this->mockRecord['additional_headers'],
+            $this->mockRecord['name'],
+            $this->mockRecord['description'],
+            $this->mockRecord['webhook_type'],
+            $this->mockRecord['identifier'],
+            $this->mockRecord['uid'],
+        );
+        self::assertSame($this->mockRecord['url'], $webhookInstruction->getTargetUrl());
+        self::assertSame($this->mockRecord['secret'], $webhookInstruction->getSecret());
+        self::assertSame('POST', $webhookInstruction->getHttpMethod());
+        self::assertTrue($webhookInstruction->verifySSL());
+        self::assertSame([ 'X-My-Header' => 'My Header Value'], $webhookInstruction->getAdditionalHeaders());
+        self::assertSame('My Webhook', $webhookInstruction->getName());
+        self::assertSame('My Webhook Description', $webhookInstruction->getDescription());
+        self::assertSame($this->webhookType, $webhookInstruction->getWebhookType());
+        self::assertSame($this->webhookType->getIdentifier(), $webhookInstruction->getWebhookType()->getIdentifier());
+        self::assertSame($this->webhookType->getDescription(), $webhookInstruction->getWebhookType()->getDescription());
+        self::assertSame('033c049f-7762-4755-b072-805350a8726a', $webhookInstruction->getIdentifier());
+        self::assertSame(200413, $webhookInstruction->getUid());
+    }
+
+    /**
+     * @test
+     */
+    public function createWebhookInstructionFromRow(): void
+    {
+        $webhookTypesRegistryMock = $this->createMock(WebhookTypesRegistry::class);
+        $webhookTypesRegistryMock
+            ->method('getWebhookByType')
+            ->with($this->webhookType->getIdentifier())
+            ->willReturn($this->webhookType);
+
+        GeneralUtility::addInstance(WebhookTypesRegistry::class, $webhookTypesRegistryMock);
+
+        $this->mockRecord['webhook_type'] = 'typo3/test-webhook';
+        $webhookInstruction = WebhookInstructionFactory::createFromRow($this->mockRecord);
+        self::assertSame($this->mockRecord['url'], $webhookInstruction->getTargetUrl());
+        self::assertSame($this->mockRecord['url'], $webhookInstruction->getTargetUrl());
+        self::assertSame($this->mockRecord['secret'], $webhookInstruction->getSecret());
+        self::assertSame('POST', $webhookInstruction->getHttpMethod());
+        self::assertTrue($webhookInstruction->verifySSL());
+        self::assertSame([ 'X-My-Header' => 'My Header Value'], $webhookInstruction->getAdditionalHeaders());
+        self::assertSame('My Webhook', $webhookInstruction->getName());
+        self::assertSame('My Webhook Description', $webhookInstruction->getDescription());
+        self::assertSame($this->webhookType, $webhookInstruction->getWebhookType());
+        self::assertSame($this->webhookType->getIdentifier(), $webhookInstruction->getWebhookType()->getIdentifier());
+        self::assertSame($this->webhookType->getDescription(), $webhookInstruction->getWebhookType()->getDescription());
+        self::assertSame('033c049f-7762-4755-b072-805350a8726a', $webhookInstruction->getIdentifier());
+        self::assertSame(200413, $webhookInstruction->getUid());
+    }
+}
diff --git a/typo3/sysext/webhooks/composer.json b/typo3/sysext/webhooks/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..89e400edd5b5cef4855e37ab0bd0bd15b45083fb
--- /dev/null
+++ b/typo3/sysext/webhooks/composer.json
@@ -0,0 +1,44 @@
+{
+	"name": "typo3/cms-webhooks",
+	"type": "typo3-cms-framework",
+	"description": "TYPO3 CMS Webhooks - Handle outgoing Webhooks for TYPO3",
+	"homepage": "https://typo3.org",
+	"license": ["GPL-2.0-or-later"],
+	"authors": [{
+		"name": "TYPO3 Core Team",
+		"email": "typo3cms@typo3.org",
+		"role": "Developer"
+	}],
+	"support": {
+		"chat": "https://typo3.org/help",
+		"docs": "https://docs.typo3.org/c/typo3/cms-webhooks/main/en-us/",
+		"issues": "https://forge.typo3.org",
+		"source": "https://github.com/typo3/typo3"
+	},
+	"config": {
+		"sort-packages": true
+	},
+	"require": {
+		"symfony/uid": "^6.2",
+		"typo3/cms-core": "12.3.*@dev"
+	},
+	"suggest": {
+		"typo3/cms-lowlevel": "To display registered webhooks in the configuration module"
+	},
+	"conflict": {
+		"typo3/cms": "*"
+	},
+	"extra": {
+		"branch-alias": {
+			"dev-main": "12.3.x-dev"
+		},
+		"typo3/cms": {
+			"extension-key": "webhooks"
+		}
+	},
+	"autoload": {
+		"psr-4": {
+			"TYPO3\\CMS\\Webhooks\\": "Classes/"
+		}
+	}
+}
diff --git a/typo3/sysext/webhooks/ext_emconf.php b/typo3/sysext/webhooks/ext_emconf.php
new file mode 100644
index 0000000000000000000000000000000000000000..e920fa2d14634e17c445f8868cbf44a532eeb419
--- /dev/null
+++ b/typo3/sysext/webhooks/ext_emconf.php
@@ -0,0 +1,19 @@
+<?php
+
+$EM_CONF[$_EXTKEY] = [
+    'title' => 'TYPO3 CMS Webhooks',
+    'description' => 'Handle outgoing Webhooks for TYPO3',
+    'category' => 'module',
+    'author' => 'TYPO3 Core Team',
+    'author_email' => 'typo3cms@typo3.org',
+    'author_company' => '',
+    'state' => 'stable',
+    'version' => '12.3.0',
+    'constraints' => [
+        'depends' => [
+            'typo3' => '12.3.0',
+        ],
+        'conflicts' => [],
+        'suggests' => [],
+    ],
+];
diff --git a/typo3/sysext/webhooks/ext_localconf.php b/typo3/sysext/webhooks/ext_localconf.php
new file mode 100644
index 0000000000000000000000000000000000000000..b915e98c2081ad35eb6e176d3e3f9177e54fe708
--- /dev/null
+++ b/typo3/sysext/webhooks/ext_localconf.php
@@ -0,0 +1,7 @@
+<?php
+
+declare(strict_types=1);
+
+defined('TYPO3') or die();
+
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = \TYPO3\CMS\Webhooks\Listener\PageModificationListener::class;
diff --git a/typo3/sysext/webhooks/ext_tables.sql b/typo3/sysext/webhooks/ext_tables.sql
new file mode 100644
index 0000000000000000000000000000000000000000..c1a92b39d50e0d440217fc7a46990b05f1c8d75f
--- /dev/null
+++ b/typo3/sysext/webhooks/ext_tables.sql
@@ -0,0 +1,14 @@
+#
+# Table structure for table 'sys_webhook'
+#
+CREATE TABLE sys_webhook (
+	name varchar(100) DEFAULT '' NOT NULL,
+	url varchar(2048) DEFAULT '' NOT NULL,
+	method varchar(10) DEFAULT '' NOT NULL,
+	secret varchar(255) DEFAULT '' NOT NULL,
+	webhook_type varchar(255) DEFAULT '' NOT NULL,
+	verify_ssl int(1) DEFAULT 1 NOT NULL,
+
+	UNIQUE identifier_key (identifier),
+	KEY index_source (webhook_type(5))
+);