diff --git a/README.md b/README.md
index 65f623c..ee3470c 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ Those extensions are in the current official extension repository, with automati
* [Show Feed ID](xExtension-showFeedID/README.md): Show the ID of feed and category
* [Title-Wrap](xExtension-TitleWrap/README.md): Applies a line-wrap to long article titles instead of truncating them
* [Unsafe Autologin](xExtension-UnsafeAutologin/README.md): Brings back removed unsafe autologin feature from FreshRSS
+* [Webhook](xExtension-Webhook/README.md): Automatically sends webhook notifications when RSS entries match specified keywords
* [Word Highlighter](xExtension-WordHighlighter/README.md): Highlight specific words
* [YouTube Video Feed](xExtension-YouTube/README.md): Embed YouTube feeds inside article content
diff --git a/xExtension-Webhook/README.md b/xExtension-Webhook/README.md
new file mode 100644
index 0000000..53bc7ed
--- /dev/null
+++ b/xExtension-Webhook/README.md
@@ -0,0 +1,228 @@
+# FreshRSS Webhook Extension
+
+[](https://www.gnu.org/licenses/agpl-3.0)
+[](https://freshrss.org/)
+
+A powerful FreshRSS extension that automatically sends webhook notifications when RSS entries match configured search filters. Perfect for integrating with Discord, Slack, Telegram, or any service that supports webhooks.
+
+## 🚀 Features
+
+- **Automated Notifications**: Automatically sends webhooks when new RSS entries match your search filters
+- **Search Filters**: FreshRSS native search filter syntax for precise matching
+- **Multiple HTTP Methods**: Supports GET, POST, PUT
+- **Configurable Formats**: Send data as JSON or form-encoded
+- **Template System**: Customizable webhook payloads with placeholders
+- **Error Handling**: Robust error handling with graceful fallbacks
+- **Test Functionality**: Built-in test feature to verify webhook configuration
+
+## 📋 Requirements
+
+- FreshRSS 1.28.2+
+
+## 🔧 Installation
+
+1. Download the extension files
+2. Upload the `xExtension-Webhook` folder to your FreshRSS `extensions` directory
+3. Enable the extension in FreshRSS admin panel under Extensions
+
+## ⚙️ Configuration
+
+### Basic Setup
+
+1. Go to **Administration** → **Extensions** → **Webhook**
+2. Configure the following settings:
+
+#### Search Filter
+
+Use [FreshRSS search filter syntax](https://freshrss.github.io/FreshRSS/en/users/10_filter.html) to match entries. Each line is an OR condition — if any line matches, the webhook fires. Leave empty to match all entries.
+
+```text
+intitle:breaking news
+intitle:security alert
+#your-project-name
+```
+
+#### Webhook Settings
+
+- **Webhook URL**: Your webhook endpoint URL
+- **HTTP Method**: Choose from GET, POST, PUT, DELETE, etc.
+- **Body Type**: JSON or Form-encoded
+- **Headers**: Custom HTTP headers (one per line)
+
+### Webhook Body Template
+
+Customize the webhook payload using placeholders:
+
+```json
+{
+ "title": "{title}",
+ "feed": "{feed_name}",
+ "url": "{url}",
+ "content": "{content}",
+ "date": "{date}",
+ "timestamp": "{date_timestamp}",
+ "author": "{author}",
+ "tags": "{tags}"
+}
+```
+
+#### Available Placeholders
+
+| Placeholder | Description |
+| ----------- | ----------- |
+| `{title}` | Article title |
+| `{url}` | Article URL |
+| `{content}` | Article content (HTML) |
+| `{date}` | Publication date (string) |
+| `{date_timestamp}` | Publication date as Unix timestamp |
+| `{author}` | Article authors |
+| `{feed_name}` | Feed name |
+| `{feed_url}` | Feed URL |
+| `{thumbnail_url}` | Thumbnail (image) URL |
+| `{tags}` | Article tags (separated by " #") |
+
+## 🎯 Use Cases
+
+### Discord Webhook
+
+```json
+{
+ "content": "New article: **{title}**",
+ "embeds": [{
+ "title": "{title}",
+ "url": "{url}",
+ "description": "{content}",
+ "color": 3447003,
+ "footer": {
+ "text": "{feed_name}"
+ }
+ }]
+}
+```
+
+### Slack Webhook
+
+```json
+{
+ "text": "New article from {feed_name}",
+ "attachments": [{
+ "title": "{title}",
+ "title_link": "{url}",
+ "text": "{content}",
+ "color": "good"
+ }]
+}
+```
+
+### Custom API Integration
+
+```json
+{
+ "event": "new_article",
+ "data": {
+ "title": "{title}",
+ "url": "{url}",
+ "feed": "{feed_name}",
+ "timestamp": "{date_timestamp}"
+ }
+}
+```
+
+## 🔍 Search Filters
+
+The extension uses [FreshRSS search filter syntax](https://freshrss.github.io/FreshRSS/en/users/10_filter.html) to match entries.
+Examples:
+
+```text
+intitle:security
+inurl:example.com
+author:John
+#breaking-news
+intitle:urgent OR intitle:critical
+intitle:release -intitle:beta
+```
+
+## 🛠️ Advanced Configuration
+
+### Custom Headers
+
+Add authentication or custom headers:
+
+```text
+Authorization: Bearer your-token-here
+X-Custom-Header: custom-value
+User-Agent: FreshRSS-Webhook/1.0
+```
+
+### Error Handling
+
+- Failed webhooks are logged for debugging
+- Network timeouts are handled gracefully
+- Invalid configurations are validated
+
+### Performance
+
+- Only sends webhooks when filters match
+- Efficient filter evaluation via FreshRSS core
+- Minimal impact on RSS processing
+
+## 🐛 Troubleshooting
+
+### Common Issues
+
+**Webhooks not sending:**
+- Check that a search filter is configured (or leave empty to match all)
+- Verify webhook URL is accessible
+- Check the FreshRSS logs for `[Webhook]` entries
+
+**Filter not matching:**
+- Try simple filters first (e.g., `intitle:test`)
+- Refer to the [FreshRSS filter documentation](https://freshrss.github.io/FreshRSS/en/users/10_filter.html) for syntax
+
+**Authentication errors:**
+- Check custom headers configuration
+- Verify webhook endpoint accepts your format
+
+### Debugging
+
+The extension logs to the FreshRSS log file with the `[Webhook]` prefix:
+- **Debug level**: filter matches, HTTP requests sent (visible in `DEVELOPMENT` environment)
+- **Warning level**: errors during article processing or failed requests (always visible)
+
+## 📝 Changelog
+
+### Version 0.1.1
+
+- Initial release
+- Automated webhook notifications
+- Pattern matching in multiple fields
+- Configurable HTTP methods and formats
+- Comprehensive error handling
+- Template-based webhook payloads
+
+## 🤝 Contributing
+
+This extension was developed to address [FreshRSS Issue #1513](https://github.com/FreshRSS/FreshRSS/issues/1513).
+
+Contributions are welcome! Please:
+1. Fork the repository
+2. Create a feature branch
+3. Follow FreshRSS coding standards
+4. Add tests for new functionality
+5. Submit a pull request
+
+## 📄 License
+
+This extension is licensed under the [GNU Affero General Public License v3.0](LICENSE).
+
+## 🙏 Acknowledgments
+
+- FreshRSS development team for the excellent extension system
+- Community members who requested and tested this feature
+- Contributors to the original feature request
+
+## 📞 Support
+
+- [FreshRSS Documentation](https://freshrss.github.io/FreshRSS/)
+- [GitHub Issues](https://github.com/FreshRSS/Extensions/issues)
+- [FreshRSS Community](https://github.com/FreshRSS/FreshRSS/discussions)
diff --git a/xExtension-Webhook/configure.phtml b/xExtension-Webhook/configure.phtml
new file mode 100644
index 0000000..e58f1a4
--- /dev/null
+++ b/xExtension-Webhook/configure.phtml
@@ -0,0 +1,151 @@
+
+= _t('ext.webhook.description') ?>
+
diff --git a/xExtension-Webhook/extension.php b/xExtension-Webhook/extension.php
new file mode 100644
index 0000000..96d175a
--- /dev/null
+++ b/xExtension-Webhook/extension.php
@@ -0,0 +1,340 @@
+
+ */
+ public array $webhook_headers = [];
+
+ /**
+ * Default webhook request body template
+ *
+ * Supports placeholders like {title}, {url}, {feed_name}, etc.
+ *
+ * @var array
+ */
+ public array $webhook_body = [
+ 'title' => '{title}',
+ 'feed' => '{feed_name}',
+ 'url' => '{url}',
+ 'created' => '{date_published}',
+ ];
+
+ /**
+ * Initialize the extension
+ *
+ * Registers translation files and hooks into FreshRSS entry processing.
+ *
+ * @return void
+ */
+ #[\Override]
+ public function init(): void {
+ $this->registerTranslates();
+ $this->registerHook('entry_before_insert', [$this, 'processArticle']);
+ }
+
+ /**
+ * Handle configuration form submission
+ *
+ * Processes configuration form data, saves settings, and optionally
+ * sends a test webhook request.
+ *
+ * @return void
+ * @throws Minz_ConfigurationException
+ * @throws Minz_PermissionDeniedException
+ */
+ #[\Override]
+ public function handleConfigureAction(): void {
+ $this->registerTranslates();
+
+ if (Minz_Request::isPost()) {
+ $this->setUserConfigurationValue('search_filter', trim(Minz_Request::paramString('search_filter', plaintext: true)));
+ $this->setUserConfigurationValue('ignore_updated', Minz_Request::paramBoolean('ignore_updated'));
+ $this->setUserConfigurationValue('webhook_url', trim(Minz_Request::paramString('webhook_url', plaintext: true)));
+ $this->setUserConfigurationValue('webhook_method', trim(Minz_Request::paramString('webhook_method', plaintext: true)));
+ $this->setUserConfigurationValue('webhook_headers',
+ array_filter(Minz_Request::paramTextToArray('webhook_headers'), static fn(string $v): bool => $v !== ''));
+ $webhookBodyJson = trim(Minz_Request::paramString('webhook_body', plaintext: true));
+ $webhookBodyArray = $webhookBodyJson === '' ? [] : json_decode($webhookBodyJson, true, 256, JSON_INVALID_UTF8_SUBSTITUTE);
+ $this->setUserConfigurationValue('webhook_body', is_array($webhookBodyArray) ? $webhookBodyArray : []);
+ $this->setUserConfigurationValue('webhook_body_type', trim(Minz_Request::paramString('webhook_body_type', plaintext: true)));
+ $this->setUserConfigurationValue('webhook_content_type', trim(Minz_Request::paramString('webhook_content_type', plaintext: true)));
+
+ if (Minz_Request::paramString('test_request', plaintext: true) !== '') {
+ try {
+ $this->sendRequest(
+ $this->getUserConfigurationString('webhook_url') ?? '',
+ $this->getUserConfigurationString('webhook_method') ?? '',
+ $this->getUserConfigurationString('webhook_content_type') ?? 'json',
+ $this->getUserConfigurationArray('webhook_body') ?? [],
+ array_values(array_filter($this->getUserConfigurationArray('webhook_headers') ?? [], 'is_string')),
+ );
+ } catch (Throwable $err) {
+ Minz_Log::warning('[Webhook] Test request failed: ' . $err->getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * Process article and send webhook if search filter matches
+ *
+ * Evaluates RSS entries against configured search filters and sends
+ * webhook notifications for matching entries.
+ *
+ * @param FreshRSS_Entry $entry The RSS entry to process
+ * @throws Minz_PermissionDeniedException
+ * @return FreshRSS_Entry The processed entry (potentially marked as read)
+ */
+ public function processArticle(FreshRSS_Entry $entry): FreshRSS_Entry {
+ if ($this->getUserConfigurationBool('ignore_updated') && $entry->isUpdated()) {
+ return $entry;
+ }
+
+ try {
+ if (!$this->entryMatchesSearchFilter($entry)) {
+ return $entry;
+ }
+ $this->sendArticle($entry);
+ } catch (Throwable $err) {
+ Minz_Log::warning('[Webhook] Error processing article: ' . $err->getMessage());
+ }
+
+ return $entry;
+ }
+
+ /**
+ * Check if entry matches the configured search filter
+ *
+ * Evaluates the entry against each line of the search filter.
+ * Lines act as OR conditions — the first match returns true.
+ * An empty filter matches all entries.
+ *
+ * @param FreshRSS_Entry $entry The RSS entry to check
+ * @return bool True if the entry matches any filter line, or if no filter is configured
+ */
+ private function entryMatchesSearchFilter(FreshRSS_Entry $entry): bool {
+ $searchFilter = $this->getUserConfigurationString('search_filter') ?? '';
+ if ($searchFilter === '') {
+ return true;
+ }
+
+ $lines = array_filter(array_map('trim', explode("\n", $searchFilter)), static fn(string $line): bool => $line !== '');
+ foreach ($lines as $line) {
+ $booleanSearch = new FreshRSS_BooleanSearch($line);
+ if ($entry->matches($booleanSearch)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Recursively replace placeholders in an array structure
+ *
+ * Walks the array and applies placeholder replacement to string leaf values.
+ * If a replacement value is null and the placeholder is the entire string value,
+ * the value becomes null (preserving null semantics in JSON output).
+ *
+ * @param array $data The array to process
+ * @param array $replacements Placeholder => replacement value map
+ * @return array The array with placeholders replaced
+ */
+ private function replacePlaceholdersRecursive(array $data, array $replacements): array {
+ foreach ($data as $key => $value) {
+ if (is_array($value)) {
+ $data[$key] = $this->replacePlaceholdersRecursive($value, $replacements);
+ } elseif (is_string($value)) {
+ // If the entire value is a single placeholder that maps to null, keep null
+ if (isset($replacements[$value]) || (array_key_exists($value, $replacements) && $replacements[$value] === null)) {
+ $data[$key] = $replacements[$value];
+ } else {
+ $data[$key] = strtr($value, array_filter($replacements, static fn($v): bool => $v !== null));
+ }
+ }
+ }
+ return $data;
+ }
+
+ /**
+ * Send article data via webhook
+ *
+ * Prepares and sends webhook notification with article data.
+ * Supports custom body templates, GReader API JSON, and RSS XML formats.
+ *
+ * @param FreshRSS_Entry $entry The RSS entry to send
+ * @throws \RuntimeException
+ * @throws Minz_ConfigurationException
+ */
+ private function sendArticle(FreshRSS_Entry $entry): void {
+ $bodyType = $this->getUserConfigurationString('webhook_body_type') ?? '';
+
+ switch ($bodyType) {
+ case 'greader':
+ $body = $entry->toGReader();
+ $contentType = 'json';
+ break;
+ case 'rss':
+ $body = $this->renderEntryAsRss($entry);
+ $contentType = 'rss';
+ break;
+ default:
+ $contentType = $this->getUserConfigurationString('webhook_content_type') ?? 'json';
+ $body = $this->getUserConfigurationArray('webhook_body') ?? $this->webhook_body;
+
+ // Replace placeholders with actual values
+ $replacements = [
+ '{title}' => htmlspecialchars_decode($entry->title(), ENT_QUOTES),
+ '{feed_name}' => htmlspecialchars_decode($entry->feed()?->name() ?? '', ENT_QUOTES),
+ '{feed_url}' => htmlspecialchars_decode($entry->feed()?->url() ?? '', ENT_QUOTES),
+ '{url}' => htmlspecialchars_decode($entry->link(), ENT_QUOTES),
+ '{content}' => htmlspecialchars_decode($entry->content(), ENT_QUOTES),
+ '{date_published}' => timestampToMachineDate($entry->date(raw: true)),
+ '{date_received}' => timestampToMachineDate($entry->dateAdded(raw: true)),
+ '{date_modified}' => $entry->lastModified() === null ? null : timestampToMachineDate($entry->lastModified()),
+ '{date_user_modified}' => $entry->lastUserModified() === null ? null : timestampToMachineDate($entry->lastUserModified()),
+ '{author}' => htmlspecialchars_decode($entry->authors(true), ENT_QUOTES),
+ '{tags}' => htmlspecialchars_decode($entry->tags(true), ENT_QUOTES),
+ ];
+
+ $body = $this->replacePlaceholdersRecursive($body, $replacements);
+ break;
+ }
+
+ $this->sendRequest(
+ $this->getUserConfigurationString('webhook_url') ?? '',
+ $this->getUserConfigurationString('webhook_method') ?? '',
+ $contentType,
+ $body,
+ array_values(array_filter($this->getUserConfigurationArray('webhook_headers') ?? [], 'is_string')),
+ );
+ }
+
+ /**
+ * Render an entry as RSS XML using the FreshRSS RSS view template
+ *
+ * @param FreshRSS_Entry $entry The RSS entry to render
+ * @return string The rendered RSS XML string
+ * @throws Minz_ConfigurationException
+ */
+ private function renderEntryAsRss(FreshRSS_Entry $entry): string {
+ $view = new FreshRSS_View();
+ $view->entries = [$entry];
+ $view->internal_rendering = true;
+ $view->publishLabelsInsteadOfTags = false;
+ $view->entryIdsTagNames = [];
+ $view->rss_base = '';
+ $view->image_url = '';
+
+ $feed = $entry->feed();
+ $view->rss_title = $feed !== null ? htmlspecialchars_decode($feed->name(), ENT_QUOTES) : '';
+ $view->html_url = $feed !== null ? htmlspecialchars_decode($feed->website()) : '';
+ $view->rss_url = $feed !== null ? htmlspecialchars_decode($feed->url()) : '';
+ $view->description = '';
+
+ $view->_layout(null);
+ $view->_path('index/rss.phtml');
+ return $view->renderToString();
+ }
+
+ /**
+ * Send an HTTP request via the FreshRSS HTTP utility
+ *
+ * @param string $url Target URL
+ * @param string $method HTTP method (GET, POST, PUT, etc.)
+ * @param string $contentType Content type ('json', 'form', or 'rss')
+ * @param array|string $body Request body as an array or string
+ * @param list $headers HTTP headers
+ * @throws \RuntimeException
+ */
+ private function sendRequest(string $url, string $method, string $contentType, array|string $body, array $headers = []): void {
+ if ($url === '') {
+ throw new RuntimeException('Webhook URL is empty');
+ }
+
+ $processedBody = null;
+ if ($body !== '' && $body !== [] && $method !== 'GET') {
+ if (is_string($body)) {
+ $processedBody = $body;
+ } else {
+ $processedBody = match ($contentType) {
+ 'form' => http_build_query($body),
+ default => json_encode($body, JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
+ };
+ }
+ }
+
+ if (empty($headers)) {
+ $headers = match ($contentType) {
+ 'form' => ['Content-Type: application/x-www-form-urlencoded'],
+ 'rss' => ['Content-Type: application/rss+xml; charset=utf-8'],
+ default => ['Content-Type: application/json'],
+ };
+ }
+
+ $curlOptions = [
+ CURLOPT_HTTPHEADER => array_values($headers),
+ CURLOPT_TIMEOUT => 10,
+ ];
+
+ if ($method === 'POST') {
+ $curlOptions[CURLOPT_POST] = true;
+ } elseif ($method !== 'GET') {
+ $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
+ }
+
+ if ($processedBody !== null && $method !== 'GET') {
+ $curlOptions[CURLOPT_POSTFIELDS] = $processedBody;
+ }
+
+ $response = FreshRSS_http_Util::httpGet($url, cachePath: null, type: 'json', curl_options: $curlOptions);
+
+ if ($response['fail'] ?? false) {
+ throw new RuntimeException('HTTP request failed for URL: ' . $url);
+ }
+ }
+
+ /**
+ * Get webhook headers configuration as formatted string
+ *
+ * Returns the configured HTTP headers as a newline-separated string
+ * for display in the configuration form.
+ *
+ * @return string HTTP headers separated by newlines
+ */
+ public function getWebhookHeaders(): string {
+ $headers = array_values(array_filter($this->getUserConfigurationArray('webhook_headers') ?? $this->webhook_headers, 'is_string'));
+ return implode(PHP_EOL, $headers);
+ }
+}
diff --git a/xExtension-Webhook/i18n/en/ext.php b/xExtension-Webhook/i18n/en/ext.php
new file mode 100644
index 0000000..29eabee
--- /dev/null
+++ b/xExtension-Webhook/i18n/en/ext.php
@@ -0,0 +1,36 @@
+ array(
+ 'event_settings' => 'Event settings',
+ 'show_hide' => 'Show/hide',
+ 'webhook_settings' => 'Webhook settings',
+ 'save_and_send_test_req' => 'Save and send test request',
+ 'description' => 'Webhooks allow external services to be notified when certain events happen.
When the specified events happen, we’ll send a HTTP request (usually POST) to the URL you provide.',
+ 'search_filter' => 'Search filter',
+ 'search_filter_description' => 'Uses FreshRSS search filter syntax. Each line is an OR condition. Leave empty to match all entries.',
+ 'http_body' => 'HTTP Body',
+ 'http_body_description' => 'Must be valid JSON or form data (x-www-form-urlencoded)',
+ 'http_body_placeholder_summary' => 'You can use special placeholders that will be replaced by the actual values:',
+ 'http_body_placeholder_title' => 'Placeholder',
+ 'http_body_placeholder_description' => 'Description',
+ 'http_body_placeholder_title_description' => 'Article title',
+ 'http_body_placeholder_url_description' => 'HTML-encoded link of the article',
+ 'http_body_placeholder_content_description' => 'Content of the article (HTML format)',
+ 'http_body_placeholder_authors_description' => 'Authors of the article',
+ 'http_body_placeholder_feed_description' => 'Name of the feed',
+ 'http_body_placeholder_feed_url_description' => 'URL of the feed',
+ 'http_body_placeholder_tags_description' => 'Article tags (string, separated by " #")',
+ 'http_body_placeholder_date_published_description' => 'Publication date (ISO 8601)',
+ 'http_body_placeholder_date_received_description' => 'Date received by FreshRSS (ISO 8601)',
+ 'http_body_placeholder_date_modified_description' => 'Last modified date (ISO 8601, null if never modified)',
+ 'http_body_placeholder_date_user_modified_description' => 'Last user-modified date (ISO 8601, null if never modified)',
+ 'webhook_headers' => 'HTTP Headers
(one per line)',
+ 'http_body_type' => 'HTTP Body type',
+ 'http_content_type' => 'Content encoding',
+ 'custom_body_type' => 'Custom',
+ 'greader_body_type' => 'GReader API (JSON)',
+ 'greader_body_type_description' => 'Sends the full article as a GReader API-compatible JSON object.',
+ 'rss_body_type' => 'RSS (XML)',
+ ),
+);
diff --git a/xExtension-Webhook/i18n/fr/ext.php b/xExtension-Webhook/i18n/fr/ext.php
new file mode 100644
index 0000000..1c35892
--- /dev/null
+++ b/xExtension-Webhook/i18n/fr/ext.php
@@ -0,0 +1,36 @@
+ array(
+ 'event_settings' => 'Paramètres des événements',
+ 'show_hide' => 'Afficher/masquer',
+ 'webhook_settings' => 'Paramètres du webhook',
+ 'save_and_send_test_req' => 'Enregistrer et envoyer une requête de test',
+ 'description' => 'Les webhooks permettent de notifier des services externes lorsque certains événements se produisent.
Lorsque les événements spécifiés se produisent, nous envoyons une requête HTTP (généralement POST) à l’URL que vous fournissez.',
+ 'search_filter' => 'Filtre de recherche',
+ 'search_filter_description' => 'Utilise la syntaxe de filtre de recherche FreshRSS. Chaque ligne est une condition OU. Laisser vide pour correspondre à tous les articles.',
+ 'http_body' => 'Corps HTTP',
+ 'http_body_description' => 'Doit être du JSON valide ou des données de formulaire (x-www-form-urlencoded)',
+ 'http_body_placeholder_summary' => 'Vous pouvez utiliser des espaces réservés spéciaux qui seront remplacés par les valeurs réelles :',
+ 'http_body_placeholder_title' => 'Espace réservé',
+ 'http_body_placeholder_description' => 'Description',
+ 'http_body_placeholder_title_description' => 'Titre de l’article',
+ 'http_body_placeholder_url_description' => 'Lien de l’article encodé en HTML',
+ 'http_body_placeholder_content_description' => 'Contenu de l’article (format HTML)',
+ 'http_body_placeholder_authors_description' => 'Auteurs de l’article',
+ 'http_body_placeholder_feed_description' => 'Nom du flux',
+ 'http_body_placeholder_feed_url_description' => 'URL du flux',
+ 'http_body_placeholder_tags_description' => 'Étiquettes de l’article (chaîne, séparées par « # »)',
+ 'http_body_placeholder_date_published_description' => 'Date de publication (ISO 8601)',
+ 'http_body_placeholder_date_received_description' => 'Date de réception par FreshRSS (ISO 8601)',
+ 'http_body_placeholder_date_modified_description' => 'Date de dernière modification (ISO 8601, null si jamais modifié)',
+ 'http_body_placeholder_date_user_modified_description' => 'Date de dernière modification par l’utilisateur (ISO 8601, null si jamais modifié)',
+ 'webhook_headers' => 'En-têtes HTTP
(un par ligne)',
+ 'http_body_type' => 'Type de corps HTTP',
+ 'http_content_type' => 'Encodage du contenu',
+ 'custom_body_type' => 'Personnalisé',
+ 'greader_body_type' => 'GReader API (JSON)',
+ 'greader_body_type_description' => 'Envoie l’article complet sous forme d’objet JSON compatible avec l’API GReader.',
+ 'rss_body_type' => 'RSS (XML)',
+ ),
+);
diff --git a/xExtension-Webhook/metadata.json b/xExtension-Webhook/metadata.json
new file mode 100644
index 0000000..71498f4
--- /dev/null
+++ b/xExtension-Webhook/metadata.json
@@ -0,0 +1,9 @@
+{
+ "name": "Webhook",
+ "author": "Lukas Melega, Ryahn",
+ "description": "Send custom webhook when new article appears (and matches custom criteria)",
+ "version": "0.2.0",
+ "entrypoint": "Webhook",
+ "type": "user",
+ "compatibility": "1.28.2"
+}