> ## Documentation Index
> Fetch the complete documentation index at: https://confidence.spotify.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Configuration

> Configure webhooks to receive activity notifications at your HTTPS endpoint

Webhooks allow you to receive real-time notifications about activities in Confidence by sending HTTP POST requests to your specified endpoint. You can configure webhooks as notification channels for [activity feeds](./activity-feeds), enabling integration with external systems, monitoring tools, or custom notification workflows.

## Configure a Webhook

### Create an Activity Feed with Webhook

<Steps>
  <Step title="Navigate to Settings > Activity Feeds" />

  <Step title="Click Create to create a new activity feed" />

  <Step title="Enter a name for your feed" />

  <Step title="Select which resource types to follow">
    Choose from Flags, A/B Tests, or Rollouts
  </Step>

  <Step title="Toggle the Webhook switch" />

  <Step title="Enter the Webhook URI">
    For example: `https://example.com/webhooks/confidence`
  </Step>

  <Step title="Enter a Webhook Secret for signature verification">
    See the [Security](#security) section for details on signature verification
  </Step>

  <Step title="Select the minimum Priority level for notifications" />

  <Step title="Click Create" />
</Steps>

### Edit a Feed

<Steps>
  <Step title="Navigate to Settings > Activity Feeds" />

  <Step title="Click the Edit button on the feed you want to modify" />

  <Step title="Toggle the Webhook switch to enable webhook notifications" />

  <Step title="Configure the webhook URI and secret" />

  <Step title="Click Save" />
</Steps>

<Note>
  When editing an existing webhook, you must re-enter the secret. The secret is write-only and never returned from the server for security reasons.
</Note>

## Webhook Payload

Confidence sends activity notifications as JSON in the `ListActivitiesResponse` format. Each webhook request contains an array of activities.

### Example Payload

```json theme={null}
{
  "activities": [
    {
      "name": "activityTypes/experiment-started/activities/abc123",
      "primaryResource": "workflows/abtest/instances/my-experiment",
      "relatedResources": ["surfaces/global"],
      "priority": "MEDIUM",
      "actor": "identities/user@example.com",
      "title": "Experiment started",
      "body": "The A/B test 'Homepage Button Test' has been started.",
      "activityTime": "2024-02-26T10:30:00Z",
      "createTime": "2024-02-26T10:30:00Z",
      "updateTime": "2024-02-26T10:30:00Z",
      "creator": "identities/user@example.com",
      "updater": "identities/user@example.com"
    }
  ]
}
```

### Activity Fields

* `name`: Unique identifier for the activity
* `primaryResource`: The main resource this activity relates to
* `relatedResources`: Other resources involved in the activity
* `priority`: Activity priority level (LOW, MEDIUM, HIGH)
* `actor`: The identity that performed the activity
* `title`: Human-readable activity title
* `body`: Detailed description in Markdown format
* `activityTime`: When the activity occurred
* `createTime`: When Confidence created the activity
* `updateTime`: When the activity was last updated
* `creator`: Identity that created the activity record
* `updater`: Identity that last updated the activity record

<Warning>
  The `title` and `body` fields are designed for human-readable display and may change without notice. Do not parse or depend on these fields for automation logic. Instead, use the `primaryResource` field to fetch the authoritative resource data via the corresponding API endpoint.
</Warning>

## Security

Webhooks use HMAC-SHA256 signatures to ensure authenticity and integrity of messages. Every webhook request includes three custom headers for verification.

### Request Headers

| Header                            | Description                                               |
| --------------------------------- | --------------------------------------------------------- |
| `Confidence-Webhook-Signature`    | HMAC-SHA256 signature of the payload                      |
| `Confidence-Webhook-Id-Signature` | Webhook configuration ID                                  |
| `Confidence-Webhook-Timestamp`    | Unix timestamp (seconds) when Confidence sent the request |

### Signature Generation

Confidence computes the signature as:

```
HMAC-SHA256(secret, "{timestamp}.{payload}")
```

Where:

* `secret`: The webhook secret you provided during configuration
* `timestamp`: The value from the `Confidence-Webhook-Timestamp` header
* `payload`: The raw JSON request body

### Verify Signatures

To verify a webhook request is authentic:

<Steps>
  <Step title="Extract headers from the incoming request">
    * `Confidence-Webhook-Signature`
    * `Confidence-Webhook-Timestamp`
  </Step>

  <Step title="Validate timestamp to prevent replay attacks">
    * Check that the timestamp is recent (within ±5 minutes of current time)
    * Reject requests with timestamps too far in the past or future
  </Step>

  <Step title="Compute expected signature">
    * Concatenate timestamp and payload: `"{timestamp}.{payload}"`
    * Compute HMAC-SHA256 using your webhook secret
    * Convert result to hexadecimal string
  </Step>

  <Step title="Compare signatures">
    * Use constant-time comparison to prevent timing attacks
    * If signatures match, the request is authentic
  </Step>
</Steps>

### Example Verification

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifyWebhook(request, secret) {
    const signature = request.headers['confidence-webhook-signature'];
    const timestamp = request.headers['confidence-webhook-timestamp'];
    const payload = request.body; // Raw JSON string

    // Validate timestamp (within 5 minutes)
    const now = Math.floor(Date.now() / 1000);
    const requestTime = parseInt(timestamp);
    if (Math.abs(now - requestTime) > 300) {
      throw new Error('Timestamp too old or too far in the future');
    }

    // Compute expected signature
    const signedPayload = `${timestamp}.${payload}`;
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(signedPayload)
      .digest('hex');

    // Compare signatures (constant-time)
    if (!crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    )) {
      throw new Error('Invalid signature');
    }

    return true;
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import time

  def verify_webhook(headers, body, secret):
      signature = headers.get('confidence-webhook-signature')
      timestamp = headers.get('confidence-webhook-timestamp')

      # Validate timestamp (within 5 minutes)
      now = int(time.time())
      request_time = int(timestamp)
      if abs(now - request_time) > 300:
          raise ValueError('Timestamp too old or too far in the future')

      # Compute expected signature
      signed_payload = f"{timestamp}.{body}"
      expected_signature = hmac.new(
          secret.encode('utf-8'),
          signed_payload.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()

      # Compare signatures (constant-time)
      if not hmac.compare_digest(signature, expected_signature):
          raise ValueError('Invalid signature')

      return True
  ```

  ```java Java theme={null}
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;
  import java.nio.charset.StandardCharsets;
  import java.security.MessageDigest;
  import java.time.Instant;

  public class WebhookVerifier {

    private static final String HMAC_SHA256 = "HmacSHA256";

    public static boolean verifyWebhook(String signature, String timestamp,
                                        String payload, String secret)
        throws Exception {
      // Validate timestamp (within 5 minutes)
      long now = Instant.now().getEpochSecond();
      long requestTime = Long.parseLong(timestamp);
      if (Math.abs(now - requestTime) > 300) {
        throw new IllegalArgumentException(
            "Timestamp too old or too far in the future");
      }

      // Compute expected signature
      String signedPayload = timestamp + "." + payload;
      SecretKeySpec secretKey = new SecretKeySpec(
          secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
      Mac mac = Mac.getInstance(HMAC_SHA256);
      mac.init(secretKey);
      byte[] hash = mac.doFinal(signedPayload.getBytes(StandardCharsets.UTF_8));
      String expectedSignature = bytesToHex(hash);

      // Compare signatures (constant-time)
      if (!MessageDigest.isEqual(
          signature.getBytes(StandardCharsets.UTF_8),
          expectedSignature.getBytes(StandardCharsets.UTF_8))) {
        throw new IllegalArgumentException("Invalid signature");
      }

      return true;
    }

    private static String bytesToHex(byte[] bytes) {
      StringBuilder result = new StringBuilder();
      for (byte b : bytes) {
        result.append(String.format("%02x", b));
      }
      return result.toString();
    }
  }
  ```

  ```go Go theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "crypto/subtle"
      "encoding/hex"
      "errors"
      "fmt"
      "math"
      "strconv"
      "time"
  )

  func verifyWebhook(signature, timestamp, payload, secret string) error {
      // Validate timestamp (within 5 minutes)
      now := time.Now().Unix()
      requestTime, err := strconv.ParseInt(timestamp, 10, 64)
      if err != nil {
          return fmt.Errorf("invalid timestamp: %w", err)
      }

      if math.Abs(float64(now-requestTime)) > 300 {
          return errors.New("timestamp too old or too far in the future")
      }

      // Compute expected signature
      signedPayload := timestamp + "." + payload
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write([]byte(signedPayload))
      expectedSignature := hex.EncodeToString(mac.Sum(nil))

      // Compare signatures (constant-time)
      if subtle.ConstantTimeCompare(
          []byte(signature),
          []byte(expectedSignature)) != 1 {
          return errors.New("invalid signature")
      }
      return nil
  }
  ```

  ```rust Rust theme={null}
  use hmac::{Hmac, Mac};
  use sha2::Sha256;
  use std::time::{SystemTime, UNIX_EPOCH};

  type HmacSha256 = Hmac<Sha256>;

  fn verify_webhook(
      signature: &str,
      timestamp: &str,
      payload: &str,
      secret: &str,
  ) -> Result<bool, Box<dyn std::error::Error>> {
      // Validate timestamp (within 5 minutes)
      let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
      let request_time: u64 = timestamp.parse()?;

      if (now as i64 - request_time as i64).abs() > 300 {
          return Err("Timestamp too old or too far in the future".into());
      }

      // Compute expected signature
      let signed_payload = format!("{}.{}", timestamp, payload);
      let mut mac = HmacSha256::new_from_slice(secret.as_bytes())?;
      mac.update(signed_payload.as_bytes());
      let result = mac.finalize();
      let expected_signature = hex::encode(result.into_bytes());

      // Compare signatures (constant-time)
      if expected_signature != signature {
          return Err("Invalid signature".into());
      }

      Ok(true)
  }
  ```
</CodeGroup>

## Best Practices

<Tabs>
  <Tab title="Secret Management">
    * **Generate strong secrets**: Use cryptographically random strings (at least 32 characters)
    * **Rotate secrets regularly**: Update webhook secrets periodically
    * **Store securely**: Never commit secrets to version control or expose in logs
    * **Use environment variables**: Store secrets in environment variables or secret management systems
  </Tab>

  <Tab title="Endpoint Implementation">
    * **Return quickly**: Respond with HTTP 200 within a few seconds to avoid timeouts
    * **Process asynchronously**: Queue webhook payloads for background processing
    * **Implement retries**: Confidence does not automatically retry failed webhooks
    * **Log webhook ID**: Store the `Confidence-Webhook-Id-Signature` for debugging
  </Tab>

  <Tab title="Error Handling">
    * **Validate signatures first**: Always verify signatures before processing payload
    * **Handle malformed JSON**: Gracefully handle invalid JSON payloads
    * **Monitor failures**: Track webhook failures and set up alerts
    * **Return appropriate status codes**:
      * `200 OK`: Successfully received and validated
      * `400 Bad Request`: Invalid signature or malformed payload
      * `500 Internal Server Error`: Processing error (avoid if possible)
  </Tab>

  <Tab title="Security Considerations">
    * **Validate timestamp**: Prevent replay attacks by checking timestamp freshness
    * **Use constant-time comparison**: Prevent timing attacks when comparing signatures
    * **Rate limiting**: Implement rate limiting on your webhook endpoint
  </Tab>
</Tabs>

## Troubleshoot Common Issues

<Tabs>
  <Tab title="Webhook not receiving notifications">
    * Verify the webhook is **enabled** (toggle switched on)
    * Check that the **priority filter** allows the activity level
    * Ensure your endpoint is **publicly accessible** via HTTPS
    * Verify your endpoint **returns HTTP 200** status
  </Tab>

  <Tab title="Signature verification failing">
    * Confirm you're using the **correct secret**
    * Verify you're computing the signature over `"{timestamp}.{payload}"`
    * Check that you're using the **raw request body** (not parsed JSON)
    * Ensure timestamp format is **Unix seconds** (not milliseconds)
  </Tab>

  <Tab title="Activities missing from webhook">
    * Check the **activity priority** matches your webhook configuration
    * Verify the **resource type** is followed by the activity feed
    * Ensure the activity feed is **subscribed** to the relevant resources
  </Tab>
</Tabs>

## Related Resources

<CardGroup cols={2}>
  <Card title="Activity Feeds" href="/docs/notifications/activity-feeds">
    Configure activity feeds for different resources
  </Card>

  <Card title="Notifications Introduction" href="/docs/notifications/introduction">
    Overview of notification types and priorities
  </Card>

  <Card title="Surface Settings" href="/docs/surfaces/surface-settings">
    Configure surface-level notifications
  </Card>

  <Card title="API Reference" href="/docs/api">
    Explore the Confidence API
  </Card>
</CardGroup>
