> ## Documentation Index
> Fetch the complete documentation index at: https://confidence-auth-testing.mintlify.site/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](/docs/platform/notifications/activity-feeds.md), enabling integration with external systems, monitoring tools, or custom notification workflows.

## Configure a Webhook

### Create an Activity Feed with Webhook

**1. Navigate to Settings > Activity Feeds**

**2. Click Create to create a new activity feed**

**3. Enter a name for your feed**

**4. Select which resource types to follow**

Choose from Flags, A/B Tests, or Rollouts

**5. Toggle the Webhook switch**

**6. Enter the Webhook URI**

For example: `https://example.com/webhooks/confidence`

**7. Enter a Webhook Secret for signature verification**

See the [Security](#security) section for details on signature verification

**8. Select the minimum Priority level for notifications**

**9. Click Create**

### Edit a Feed

**1. Navigate to Settings > Activity Feeds**

**2. Click the Edit button on the feed you want to modify**

**3. Toggle the Webhook switch to enable webhook notifications**

**4. Configure the webhook URI and secret**

**5. Click Save**

> [!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.

## 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.

## 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:

```text theme={null}
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:

**1. Extract headers from the incoming request**

* `Confidence-Webhook-Signature`
* `Confidence-Webhook-Timestamp`

**2. 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

**3. Compute expected signature**

* Concatenate timestamp and payload: `"{timestamp}.{payload}"`
* Compute HMAC-SHA256 using your webhook secret
* Convert result to hexadecimal string

**4. Compare signatures**

* Use constant-time comparison to prevent timing attacks
* If signatures match, the request is authentic

### Example Verification

```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)
}
```

## Best Practices

**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

**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

**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)

**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

## Troubleshoot Common Issues

**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

**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)

**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

## Related Resources

> **[Activity Feeds](/docs/platform/notifications/activity-feeds.md)**
>
> Configure activity feeds for different resources

> **[Notifications Introduction](/docs/platform/notifications/overview)**
>
> Overview of notification types and priorities

> **[Surface Settings](/docs/capabilities/surfaces/surface-settings.md)**
>
> Configure surface-level notifications

> **[API Reference](/docs/reference/api/quickstart.md)**
>
> Explore the Confidence API
