# Configure and use webhooks (Beta)

This topic describes how to configure and use webhooks for Event Notifications.

Webhooks allow you to receive notifications as HTTP POST requests to an endpoint you control. This enables integration with services like Slack, PagerDuty, custom monitoring systems, and other tools that support webhook integrations.

## About adding custom HTTP headers {#custom-headers}

You can add custom HTTP headers to webhook subscriptions to authenticate with your receiving endpoint. This is useful when your endpoint requires an API key, bearer token, or other credentials in the request headers.

Header values are encrypted at rest and masked in the UI after you save the subscription. To update a saved header value, you must re-enter it. Custom headers are included in both real webhook deliveries and test webhook requests.

Custom headers have the following limitations:

- Maximum of five headers per subscription
- Header values can be up to 2048 characters
- Duplicate header names are not allowed
- Certain reserved headers (such as `Content-Type` and `Host`) cannot be overridden

## About the webhook payload format {#payload-preview}

When you create or edit a webhook notification and select event types, you can click the **View example** button to open the payload preview modal and see the exact JSON structure your endpoint receives. This is useful when building your endpoint's request parser.

The preview shows:
- The top-level payload envelope (`event`, `timestamp`, `data`, `text`)
- All event-specific fields in the `data` object, with type annotations (string, number, boolean, ISO-8601-timestamp, array, or object)

{/* Add screenshot of expanded webhook payload format preview */}

### Webhook payload structure

Event Notifications webhooks deliver a JSON payload with the following structure:

```json
{
  "event": "customer.created",
  "timestamp": "2026-01-25T22:48:32Z",
  "text": "A new customer has been created.\n\nCustomer: Testy McTestface\nCustomer ID: 38ljzNKNZZSIp3bUQYSPzJUUBpd\nApplication: Demo\nChannel: Stable\nLicense Type: trial\nExpiration: 2026-02-24\nCreated at: 2026-01-25 22:48:32 UTC\n\nView customer: https://vendor.replicated.com/apps/demo-jaybird/customer/38ljzNKNZZSIp3bUQYSPzJUUBpd",
  "data": {
    "app_id": "34LgWqPkIlmhPDhvQVrbWcRwvLW",
    "team_id": "CKUTNRX16FghU69v_RjZ1Q1EFXBcQBMZ",
    "app_name": "Demo",
    "app_slug": "demo-jaybird",
    "eventType": "customer.created",
    "channel_id": "34LgWuB1oCNbdLV6BbeepUSAEA6",
    "created_at": "2026-01-25T22:48:32.391894468Z",
    "event_type": "customer.created",
    "expires_at": "2026-02-24T22:47:37Z",
    "customer_id": "38ljzNKNZZSIp3bUQYSPzJUUBpd",
    "channel_name": "Stable",
    "license_type": "trial",
    "customer_name": "Testy McTestface",
    "subscription_name": "Trial Customer Alerts"
  }
}
```

:::note
The `subscription_name` field is included in the `data` object only when a custom name is set on the subscription. For email notifications, the custom name is prepended to the email subject line.
:::

### Payload fields

The following describes the fields in the webhook payload:

<table>
  <tr>
    <th>Field</th>
    <th>Type</th>
    <th>Description</th>
   </tr>
  <tr>
    <td>`event`</td>
    <td>string</td>
    <td>Event type identifier (e.g., "customer.created", "instance.upgrade_started")</td>
   </tr>
  <tr>
    <td>`timestamp`</td>
    <td>string</td>
    <td>ISO 8601 timestamp when the event occurred</td>
   </tr>
  <tr>
    <td>`text`</td>
    <td>string</td>
    <td>Human-readable text description of the event, formatted for readability in Slack and other chat tools</td>
    </tr>
  <tr>
    <td>`data`</td>
    <td>object</td>
    <td>
      <p>Event-specific data containing detailed information about the event. The `data` object contains fields specific to each event type. Common fields include:</p>
      <ul>
        <li>`app_id` - Application identifier</li>
        <li>`team_id` - Team identifier</li>
        <li>`app_name` - Application name</li>
        <li>`app_slug` - Application slug (URL-safe identifier)</li>
        <li>`event_type` - Event type identifier</li>
        <li>`subscription_name` - Custom subscription name, if set</li>
        <li>Additional fields specific to the event type (customer_id, instance_id, channel_id, etc.)</li>
      </ul>
    </td>
   </tr>
</table>

## Verify webhook signatures

Event Notifications webhooks include an HMAC-SHA256 signature for verification. This allows you to verify that webhook requests are genuinely from Replicated and have not been tampered with.

When you configure a webhook with a signing secret, Replicated generates an HMAC-SHA256 signature of the webhook payload using your secret. The signature is sent in the `X-Replicated-Signature` HTTP header. The `X-Replicated-Signature` header contains the signature in the following format: `X-Replicated-Signature: sha256=<hex-encoded-signature>`. For example:
`X-Replicated-Signature: sha256=5d7f8e9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e`.

To verify webhook signatures in your endpoint:

1. Extract the signature from the `X-Replicated-Signature` header:

   ```javascript
   const signatureHeader = req.headers['x-replicated-signature'];
   const signature = signatureHeader.replace('sha256=', '');
   ```

1. Compute the HMAC-SHA256 signature of the raw request body using your signing secret:

   ```javascript
   const crypto = require('crypto');

   const hmac = crypto.createHmac('sha256', signingSecret);
   hmac.update(requestBody); // Use the raw request body string
   const expectedSignature = hmac.digest('hex');
   ```

1. Use a constant-time comparison to prevent timing attacks:

   ```javascript
   const crypto = require('crypto');

   function verifySignature(signature, expectedSignature) {
   return crypto.timingSafeEqual(
      Buffer.from(signature, 'hex'),
      Buffer.from(expectedSignature, 'hex')
   );
   }

   if (!verifySignature(signature, expectedSignature)) {
   // Signature verification failed - reject the request
   return res.status(401).send('Invalid signature');
   }

   // Signature verified - process the webhook
   ```

## Integrate with Slack using webhooks

For more information about configuring incoming webhooks with Slack, see the [Sending messages using incoming webhooks](https://api.slack.com/messaging/webhooks) in the Slack documentation.

To send notifications to a Slack channel using webhooks:

1. Go to your Slack workspace settings.
1. Go to **Apps** > **Incoming Webhooks** and click **Add to Slack**.
1. Select the channel where you want to receive notifications.
1. Copy the webhook URL.
1. Create an Event Notification subscription in the Vendor Portal using your Slack webhook URL. See [Create an Event Notification](/vendor/event-notifications-create#create-an-event-notification).
1. Trigger a test event to verify that the webhook is configured properly.

## Test webhooks locally {#test-webhook}

You can test a webhook locally using a development endpoint that is either publicly accessible, or reachable through a secure tunnel.

Note the following:
- If you configured a signing secret, the test payload is signed with HMAC-SHA256 using the same `X-Replicated-Signature` header as real deliveries
- If you configured custom HTTP headers, they are included in the test request so you can verify your endpoint's authentication logic
- Private and internal IP addresses (localhost, RFC 1918 ranges, link-local, cloud metadata endpoints) are blocked for security

You can also test webhooks from the Vendor Portal when you create a webhook notification subscription. For more information, see [Create an Event Notification](/vendor/event-notifications-create#create-an-event-notification).

To test webhooks against a local development endpoint:

1. Use a tunnel service like ngrok to expose your local endpoint:
   
   ```bash
   ngrok http 3000
   ```

2. Enter the ngrok URL as your webhook URL in the notification form:
   
   ```bash
   https://abc123.ngrok.io/webhook
   ```

3. Click **Send test webhook** to send a sample payload to your local endpoint through the tunnel.

4. Check the ngrok dashboard or your application logs to verify the request was received and processed correctly.

## About delivery retries and timeouts for webhooks

The Vendor Portal automatically retries failed webhook deliveries to ensure reliability. When a webhook delivery attempt fails, the Vendor Portal makes up to eight retry attempts. Each delivery attempt times out after five seconds. The retry schedule is approximately 1m, 2m, 4m, 8m, 16m, 32m, 64m, 128m after the first failed attempt.

A webhook delivery is considered failed when:
- The endpoint responds with a non-2xx status code
- The request times out (exceeds 5 seconds)
- A network error occurs (endpoint unreachable, DNS failure, etc.)

After eight retry attempts, the Vendor Portal marks the notification as permanently failed. The Vendor Portal also sends an email to both the subscription creator and team Admins with details about the failure, including the event type, the last error message, and a link to the delivery history in the Vendor Portal.

If a webhook subscription has 10 consecutive permanent failures, the Vendor Portal automatically disables it. To resume deliveries, verify that your endpoint is reachable and then re-enable the subscription from the **Notifications** page.