---
title: "Webhooks"
description: "Get a signed HTTPS request whenever a config, its targeting rules, a segment, an environment, or a project changes, or an alert is raised or resolved. What a webhook event holds, how to verify it, every event type with a sample, and how delivery and retries work."
canonical_url: "https://docs.configdirector.com/integrations/webhooks"
---
# Webhooks

> Get a signed HTTPS request whenever a config, its targeting rules, a segment, an environment, or a project changes, or an alert is raised or resolved. What a webhook event holds, how to verify it, every event type with a sample, and how delivery and retries work.

## Introduction

A webhook is a URL you give ConfigDirector. Whenever someone changes a config, its targeting rules, a segment, an environment, or a project, or an alert is raised or resolved, ConfigDirector sends that URL an HTTPS `POST` describing what happened: a webhook event. Use webhooks to post flag changes to a chat channel, start a pipeline when production changes, or open an incident when an alert fires.

A webhook event carries the entity that changed, the way the [Admin API](https://docs.configdirector.com/integrations/admin-api) returns it, together with the values it had before the change, so your receiver never has to call back to find out what happened. If you have handled Stripe webhooks, the shape will look familiar.

Webhooks belong to the organization, and only owners and admins can see and manage them. They work the same on Free and Pay as you go.

Webhook events come from changes that people make, in the dashboard or through an API token. Changes ConfigDirector makes on its own, such as a config's lifecycle state moving to settled, send none. To hear about those, subscribe to the matching alert, such as **Config settled**.

## What a webhook event looks like

This is a `targeting_rules.updated` webhook event, sent when a member turned a flag on in Production for everyone except two users:

```json
{
  "id": "01a18500…d66c",
  "data": {
    "object": {
      "id": "01a18500…b84e",
      "rules": [
        {
          "id": "f3538fdb…b8b9",
          "type": "conditional",
          "order": 0,
          "value": false,
          "target": "value",
          "valueId": "3TUVuO…",
          "conditions": [
            {
              "id": "8d6b6e0b…1ccb",
              "kind": "attribute",
              "operator": "is one of",
              "attribute": "identifier",
              "targetType": "text",
              "targetValues": [
                "user-101",
                "user-202"
              ]
            }
          ],
          "percentages": []
        }
      ],
      "environment": {
        "id": "01a184ff…ee77",
        "live": true,
        "name": "Production",
        "slug": "production",
        "color": "red"
      },
      "defaultValue": "true",
      "defaultValueId": "0Br7gC…"
    },
    "previousAttributes": {
      "rules": [],
      "defaultValue": "false",
      "defaultValueId": "3TUVuO…"
    }
  },
  "type": "targeting_rules.updated",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "config": {
    "id": "01a18500…0339",
    "key": "checkout.one-page"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:12:50.165Z",
  "environment": {
    "id": "01a184ff…ee77",
    "live": true,
    "name": "Production",
    "slug": "production"
  },
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      What it holds
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        id
      </code>
    </td>
    
    <td>
      The webhook event's id. It is the same on every webhook that receives the webhook event and on every resend, and it is also sent as the <code>
        webhook-id
      </code>
      
       header. Use it to drop duplicates.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        type
      </code>
    </td>
    
    <td>
      What happened, such as <code>
        targeting_rules.updated
      </code>
      
      . See <a href="#webhook-event-types">
        Webhook event types
      </a>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        createdAt
      </code>
    </td>
    
    <td>
      When the change was made, or when the alert was raised or resolved.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        organization
      </code>
    </td>
    
    <td>
      The organization's <code>
        id
      </code>
      
       and <code>
        name
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        project
      </code>
    </td>
    
    <td>
      The project's <code>
        id
      </code>
      
      , <code>
        slug
      </code>
      
      , and <code>
        name
      </code>
      
      . Every change has one.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        environment
      </code>
    </td>
    
    <td>
      The environment's <code>
        id
      </code>
      
      , <code>
        slug
      </code>
      
      , <code>
        name
      </code>
      
      , and whether it is <code>
        live
      </code>
      
      . Present on targeting rules changes, environment changes, and a segment change to one environment's override.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        config
      </code>
    </td>
    
    <td>
      The config's <code>
        id
      </code>
      
       and <code>
        key
      </code>
      
      . Present on config and targeting rules changes.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        actor
      </code>
    </td>
    
    <td>
      Who made the change. <code>
        member
      </code>
      
       holds the member's <code>
        id
      </code>
      
      , <code>
        name
      </code>
      
      , and <code>
        email
      </code>
      
      ; <code>
        name
      </code>
      
       and <code>
        email
      </code>
      
       are <code>
        null
      </code>
      
       if the member's account has been deleted. <code>
        sourceClient
      </code>
      
       is where the change came from: <code>
        dashboard
      </code>
      
      , <code>
        api
      </code>
      
      , <code>
        terraform
      </code>
      
      , <code>
        opentofu
      </code>
      
      , <code>
        mcp
      </code>
      
      , or <code>
        cli
      </code>
      
      . <code>
        apiToken
      </code>
      
       holds the token's <code>
        id
      </code>
      
       and <code>
        name
      </code>
      
       when the change was made with an API token. Alert and test webhook events have no actor.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        alertKind
      </code>
    </td>
    
    <td>
      The alert's kind, on alert webhook events only.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data.object
      </code>
    </td>
    
    <td>
      The entity after the change, as the Admin API returns it. On a deletion it is the entity as it was just before, and some fields, such as <code>
        createdAt
      </code>
      
      , <code>
        updatedAt
      </code>
      
      , and a segment's <code>
        overrides
      </code>
      
      , can be missing.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data.previousAttributes
      </code>
    </td>
    
    <td>
      On <code>
        *.updated
      </code>
      
       webhook events only: each property that changed, with its value before the change.
    </td>
  </tr>
</tbody>
</table>

Build your receiver to ignore fields it does not know. New fields can appear in any webhook event without notice, while existing fields keep their names and meaning. The order of the keys means nothing either: as the samples on this page show, it is not the order of the table above.

## Creating a webhook

1. Open **Webhooks** at the bottom of the dashboard's navigation and click **New webhook**.
2. Give it a name and the `https://` URL that should receive the webhook events.
3. Add any **Custom headers** your receiver expects, such as an `Authorization` header. See [URL rules and custom headers](#url-rules-and-custom-headers).
4. Narrow it down with the **Projects**, **Environments**, and **Tags** filters, or leave them empty to receive everything.
5. Choose the **Change events**, which are all on by default, and tick the alert kinds whose **Alert events** you want, which are all off by default.
6. Save. The webhook's page shows its **Signing secret**: copy it into your receiver's configuration.
7. Click **Send test event**. A `webhook.test` webhook event is sent right away and shows under **Deliveries**, with your receiver's response.

A webhook event reaches a webhook only when every filter lets it through. An empty filter lets everything through.

<table>
<thead>
  <tr>
    <th>
      Filter
    </th>
    
    <th>
      Lets a webhook event through when
    </th>
    
    <th>
      Also lets through
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Projects
    </td>
    
    <td>
      it belongs to one of the projects
    </td>
    
    <td>
      nothing else: alert webhook events belong to no project, so a webhook filtered to projects receives no alert webhook events
    </td>
  </tr>
  
  <tr>
    <td>
      Environments
    </td>
    
    <td>
      it belongs to one of the environments
    </td>
    
    <td>
      every webhook event that belongs to no environment: config settings, segments (except a change to one environment's override), projects, and alerts
    </td>
  </tr>
  
  <tr>
    <td>
      Tags
    </td>
    
    <td>
      it is about a config that has one of the tags
    </td>
    
    <td>
      every webhook event that is not about a config: segments, environments, projects, and alerts
    </td>
  </tr>
</tbody>
</table>

## Verifying requests

Every request is signed with the webhook's signing secret, following the [Standard Webhooks](https://www.standardwebhooks.com/) specification. Check the signature before you trust a request: anyone who finds your URL can send it a `POST`.

Each request carries these headers:

<table>
<thead>
  <tr>
    <th>
      Header
    </th>
    
    <th>
      Value
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        webhook-id
      </code>
    </td>
    
    <td>
      The webhook event's <code>
        id
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        webhook-timestamp
      </code>
    </td>
    
    <td>
      When this attempt was sent, in seconds since the Unix epoch. Every attempt, including retries and resends, gets a fresh one.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        webhook-signature
      </code>
    </td>
    
    <td>
      <code>
        v1,
      </code>
      
       followed by the base64 HMAC-SHA256 of <code>
        {webhook-id}.{webhook-timestamp}.{body}
      </code>
      
      . The HMAC key is the signing secret's text after <code>
        whsec_
      </code>
      
      , decoded from base64. While a rotated secret still signs, the header holds two signatures separated by a space.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        content-type
      </code>
    </td>
    
    <td>
      <code>
        application/json
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        user-agent
      </code>
    </td>
    
    <td>
      <code>
        ConfigDirector-Webhooks/1
      </code>
    </td>
  </tr>
</tbody>
</table>

Your custom headers come with every request too.

Standard Webhooks publishes a verification library for each of these languages. Each language below links to its library; install the one for your receiver:

<table>
<thead>
  <tr>
    <th>
      Language
    </th>
    
    <th>
      Install
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <a href="https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries/javascript" rel="noopener" target="_blank">
        Node.js and TypeScript
      </a>
    </td>
    
    <td>
      <code>
        npm install standardwebhooks
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <a href="https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries/python" rel="noopener" target="_blank">
        Python
      </a>
    </td>
    
    <td>
      <code>
        pip install standardwebhooks
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <a href="https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries/go" rel="noopener" target="_blank">
        Go
      </a>
    </td>
    
    <td>
      <code>
        go get github.com/standard-webhooks/standard-webhooks/libraries/go
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <a href="https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries/java" rel="noopener" target="_blank">
        Java and Kotlin
      </a>
    </td>
    
    <td>
      <code>
        implementation("com.standardwebhooks:standardwebhooks:1.2.0")
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <a href="https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries/csharp" rel="noopener" target="_blank">
        C#
      </a>
    </td>
    
    <td>
      <code>
        dotnet add package StandardWebhooks.StandardWebhooks
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <a href="https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries/ruby" rel="noopener" target="_blank">
        Ruby
      </a>
    </td>
    
    <td>
      <code>
        bundle add standardwebhooks
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <a href="https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries/php" rel="noopener" target="_blank">
        PHP
      </a>
    </td>
    
    <td>
      <code>
        composer require standard-webhooks/standard-webhooks
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <a href="https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries/rust" rel="noopener" target="_blank">
        Rust
      </a>
    </td>
    
    <td>
      <code>
        cargo add standardwebhooks
      </code>
    </td>
  </tr>
</tbody>
</table>

On NuGet, the package named plain `StandardWebhooks` is not the Standard Webhooks library; install `StandardWebhooks.StandardWebhooks`.

Each receiver below reads the signing secret from the `CONFIGDIRECTOR_WEBHOOK_SECRET` environment variable, answers `400` to a request that fails verification, and `204` once it has the webhook event. Pass the secret exactly as the dashboard shows it, `whsec_` included.

<code-group>

```js [Node.js]
import express from "express";
import { Webhook } from "standardwebhooks";

const webhook = new Webhook(process.env.CONFIGDIRECTOR_WEBHOOK_SECRET);
const app = express();

app.post("/configdirector-webhooks", express.raw({ type: "application/json" }), (req, res) => {
  let webhookEvent;
  try {
    webhookEvent = webhook.verify(req.body, req.headers);
  } catch {
    return res.sendStatus(400);
  }

  console.log(`Received ${webhookEvent.type} ${webhookEvent.id}`);
  res.sendStatus(204);
});

app.listen(8080);
```

```python [Python]
import os

from flask import Flask, request
from standardwebhooks import Webhook, WebhookVerificationError

webhook = Webhook(os.environ["CONFIGDIRECTOR_WEBHOOK_SECRET"])
app = Flask(__name__)


@app.post("/configdirector-webhooks")
def receive_webhook_event():
    try:
        webhook_event = webhook.verify(request.get_data(), request.headers)
    except WebhookVerificationError:
        return "", 400

    print(f"Received {webhook_event['type']} {webhook_event['id']}")
    return "", 204
```

```go [Go]
package main

import (
    "encoding/json"
    "io"
    "log"
    "net/http"
    "os"

    standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go"
)

type WebhookEvent struct {
    ID   string `json:"id"`
    Type string `json:"type"`
}

func main() {
    webhook, err := standardwebhooks.NewWebhook(os.Getenv("CONFIGDIRECTOR_WEBHOOK_SECRET"))
    if err != nil {
        log.Fatal(err)
    }

    http.HandleFunc("POST /configdirector-webhooks", func(w http.ResponseWriter, r *http.Request) {
        body, err := io.ReadAll(r.Body)
        if err != nil || webhook.Verify(body, r.Header) != nil {
            w.WriteHeader(http.StatusBadRequest)
            return
        }

        var webhookEvent WebhookEvent
        if err := json.Unmarshal(body, &webhookEvent); err != nil {
            w.WriteHeader(http.StatusBadRequest)
            return
        }
        log.Printf("Received %s %s", webhookEvent.Type, webhookEvent.ID)
        w.WriteHeader(http.StatusNoContent)
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}
```

```java [Java]
import java.util.Map;

import com.standardwebhooks.Webhook;
import com.standardwebhooks.exceptions.WebhookVerificationException;
import org.springframework.boot.json.JsonParserFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ConfigDirectorWebhookController {
    private final Webhook webhook;

    public ConfigDirectorWebhookController() throws Exception {
        webhook = new Webhook(System.getenv("CONFIGDIRECTOR_WEBHOOK_SECRET"));
    }

    @PostMapping("/configdirector-webhooks")
    public ResponseEntity<Void> receive(@RequestBody String body, @RequestHeader MultiValueMap<String, String> headers) {
        try {
            webhook.verify(body, headers);
        } catch (WebhookVerificationException e) {
            return ResponseEntity.badRequest().build();
        }

        Map<String, Object> webhookEvent = JsonParserFactory.getJsonParser().parseMap(body);
        System.out.println("Received " + webhookEvent.get("type") + " " + webhookEvent.get("id"));
        return ResponseEntity.noContent().build();
    }
}
```

```csharp [C#]
using System.Net;
using System.Text.Json;
using StandardWebhooks;
using StandardWebhooks.Exceptions;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var webhook = new Webhook(builder.Configuration["CONFIGDIRECTOR_WEBHOOK_SECRET"]!);

app.MapPost("/configdirector-webhooks", async (HttpRequest request) =>
{
    using var reader = new StreamReader(request.Body);
    var body = await reader.ReadToEndAsync();
    var headers = new WebHeaderCollection();
    foreach (var name in new[] { "webhook-id", "webhook-timestamp", "webhook-signature" })
    {
        headers.Add(name, request.Headers[name]);
    }

    try
    {
        webhook.Verify(body, headers);
    }
    catch (WebhookVerificationException)
    {
        return Results.BadRequest();
    }

    var webhookEvent = JsonDocument.Parse(body).RootElement;
    app.Logger.LogInformation("Received {Type} {Id}", webhookEvent.GetProperty("type"), webhookEvent.GetProperty("id"));
    return Results.NoContent();
});

app.Run("http://0.0.0.0:8080");
```

```ruby [Ruby]
# config/routes.rb: post "/configdirector-webhooks", to: "config_director_webhooks#create"
class ConfigDirectorWebhooksController < ActionController::API
  WEBHOOK = StandardWebhooks::Webhook.new(ENV.fetch("CONFIGDIRECTOR_WEBHOOK_SECRET"))

  def create
    webhook_event = WEBHOOK.verify(request.raw_post, {
      "webhook-id" => request.headers["webhook-id"],
      "webhook-timestamp" => request.headers["webhook-timestamp"],
      "webhook-signature" => request.headers["webhook-signature"],
    })

    Rails.logger.info("Received #{webhook_event["type"]} #{webhook_event["id"]}")
    head :no_content
  rescue StandardWebhooks::WebhookVerificationError
    head :bad_request
  end
end
```

```php [PHP]
<?php

require __DIR__ . '/vendor/autoload.php';

use StandardWebhooks\Exception\WebhookVerificationException;
use StandardWebhooks\Webhook;

$webhook = new Webhook(getenv('CONFIGDIRECTOR_WEBHOOK_SECRET'));
$headers = array_change_key_case(getallheaders(), CASE_LOWER);

try {
    $webhookEvent = $webhook->verify(file_get_contents('php://input'), $headers);
} catch (WebhookVerificationException $e) {
    http_response_code(400);
    exit;
}

error_log("Received {$webhookEvent['type']} {$webhookEvent['id']}");
http_response_code(204);
```

```rust [Rust]
use std::sync::Arc;

use axum::{body::Bytes, http::{HeaderMap, StatusCode}, routing::post, Router};
use serde::Deserialize;
use standardwebhooks::Webhook;

#[derive(Deserialize)]
struct WebhookEvent {
    id: String,
    r#type: String,
}

#[tokio::main]
async fn main() {
    let secret = std::env::var("CONFIGDIRECTOR_WEBHOOK_SECRET").expect("CONFIGDIRECTOR_WEBHOOK_SECRET is not set");
    let webhook = Arc::new(Webhook::new(&secret).expect("the webhook secret is not valid"));

    let app = Router::new().route(
        "/configdirector-webhooks",
        post(move |headers: HeaderMap, body: Bytes| async move {
            if webhook.verify(&body, &headers).is_err() {
                return StatusCode::BAD_REQUEST;
            }

            let Ok(webhook_event) = serde_json::from_slice::<WebhookEvent>(&body) else {
                return StatusCode::BAD_REQUEST;
            };
            println!("Received {} {}", webhook_event.r#type, webhook_event.id);
            StatusCode::NO_CONTENT
        }),
    );

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
```

</code-group>

Kotlin uses the Java library the same way.

A few rules keep verification working:

- **Verify the body exactly as it arrived.** The signature covers the raw bytes, so a body your framework has parsed and serialized again no longer matches. Each receiver above reads the raw body for that reason.
- **Keep your server's clock right.** The libraries refuse a request whose `webhook-timestamp` is more than five minutes away from your clock, so a request someone recorded cannot be replayed later.
- **Drop duplicates by webhook-id.** The same webhook event can arrive more than once, for example when your receiver handled a request but answered too late, or when someone clicks **Resend**. Keep the ids you have handled for 30 days, the time a delivery stays resendable, and skip any you have seen.
- **Answer fast.** Any `2xx` within 10 seconds counts as delivered. Do slow work after answering, for example from a queue.

### Rotating the signing secret

Click **Rotate secret** on the webhook's page if the secret may have leaked. The new secret starts signing right away, and the previous one keeps signing every request too for the next 24 hours, so update your receiver at any point in that window without a failed verification. The page shows when the previous secret stops. Rotating again within those 24 hours ends the oldest secret at once.

## Webhook event types

<table>
<thead>
  <tr>
    <th>
      Type
    </th>
    
    <th>
      Sent when
    </th>
    
    <th>
      <code>
        data.object
      </code>
    </th>
    
    <th>
      <code>
        data.previousAttributes
      </code>
      
       can hold
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        config.created
      </code>
    </td>
    
    <td>
      A config is created. Its first targeting rules send nothing of their own.
    </td>
    
    <td>
      The config as the Admin API lists it, without targeting rules
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        config.updated
      </code>
    </td>
    
    <td>
      A config's settings change, or it gets a new key
    </td>
    
    <td>
      Same
    </td>
    
    <td>
      <code>
        description
      </code>
      
      , <code>
        role
      </code>
      
      , <code>
        lifetime
      </code>
      
      , <code>
        type
      </code>
      
      , <code>
        typeOptions
      </code>
      
      , <code>
        variations
      </code>
      
      , <code>
        client
      </code>
      
      , <code>
        server
      </code>
      
      , <code>
        tags
      </code>
      
      , <code>
        key
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        config.archived
      </code>
    </td>
    
    <td>
      A config is archived
    </td>
    
    <td>
      The config as it was archived
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        config.restored
      </code>
    </td>
    
    <td>
      An archived config is restored
    </td>
    
    <td>
      The config as restored
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        targeting_rules.updated
      </code>
    </td>
    
    <td>
      The targeting rules of one environment change
    </td>
    
    <td>
      That environment's targeting rules: <code>
        id
      </code>
      
      , <code>
        environment
      </code>
      
      , <code>
        rules
      </code>
      
      , <code>
        defaultValue
      </code>
      
      , <code>
        defaultValueId
      </code>
    </td>
    
    <td>
      <code>
        rules
      </code>
      
      , <code>
        defaultValue
      </code>
      
      , <code>
        defaultValueId
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        segment.created
      </code>
      
      , <code>
        segment.updated
      </code>
      
      , <code>
        segment.deleted
      </code>
    </td>
    
    <td>
      A segment is created, changed, or deleted
    </td>
    
    <td>
      The segment as the Admin API returns it, without <code>
        usages
      </code>
    </td>
    
    <td>
      <code>
        key
      </code>
      
      , <code>
        name
      </code>
      
      , <code>
        kind
      </code>
      
      , <code>
        groups
      </code>
      
      , or <code>
        overrides
      </code>
      
       when one environment's override changed
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        environment.created
      </code>
      
      , <code>
        environment.updated
      </code>
      
      , <code>
        environment.deleted
      </code>
    </td>
    
    <td>
      An environment is created, changed, or deleted
    </td>
    
    <td>
      The environment as the Admin API returns it
    </td>
    
    <td>
      <code>
        name
      </code>
      
      , <code>
        slug
      </code>
      
      , <code>
        color
      </code>
      
      , <code>
        live
      </code>
      
      , <code>
        deletionProtection
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        project.created
      </code>
      
      , <code>
        project.updated
      </code>
      
      , <code>
        project.deleted
      </code>
    </td>
    
    <td>
      A project is created, changed, or deleted
    </td>
    
    <td>
      The project as the Admin API returns it, without its environments
    </td>
    
    <td>
      <code>
        name
      </code>
      
      , <code>
        slug
      </code>
      
      , <code>
        storeCodeSnippets
      </code>
      
      , <code>
        deletionProtection
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        alert.created
      </code>
      
      , <code>
        alert.resolved
      </code>
    </td>
    
    <td>
      An alert of a kind the webhook subscribes to is raised or resolved
    </td>
    
    <td>
      The alert: <code>
        id
      </code>
      
      , <code>
        kind
      </code>
      
      , <code>
        priority
      </code>
      
      , <code>
        facts
      </code>
      
      , <code>
        detectedAt
      </code>
      
      , <code>
        createdAt
      </code>
      
      , <code>
        resolvedAt
      </code>
      
      , and <code>
        url
      </code>
      
      , its page in the dashboard
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        webhook.test
      </code>
    </td>
    
    <td>
      Someone sends a test webhook event
    </td>
    
    <td>
      None: <code>
        data.sample
      </code>
      
       holds a sample <code>
        targeting_rules.updated
      </code>
      
       webhook event
    </td>
    
    <td>
      
    </td>
  </tr>
</tbody>
</table>

A change sends a webhook event for what the person did, not one for every entity it touched. Deleting a project sends `project.deleted` only, with nothing for its environments, configs, or segments, and deleting an environment sends `environment.deleted` only. Creating a project sends `project.created` and an `environment.created` for each of the two environments it starts with.

The samples below were sent by ConfigDirector, with ids shortened and names changed.

### Configs

A change of key is a `config.updated` whose `previousAttributes` holds the old `key`, while the envelope's `config.key` and `data.object.key` are the new one. Changing the key and other settings in one request sends two `config.updated`: one for the key and one for the other settings. Deleting a tag sends a `config.updated` for every config that had it, with the previous `tags`.

<code-group>

```json [config.created]
{
  "id": "01a18500…2b93",
  "data": {
    "object": {
      "id": "01a18500…0339",
      "key": "checkout.new-flow",
      "role": "flag",
      "tags": [
        {
          "id": "01a18500…436d",
          "name": "checkout"
        }
      ],
      "type": "boolean",
      "state": "new",
      "client": true,
      "server": true,
      "lifetime": "temporary",
      "createdAt": "2026-10-29T00:12:27.344Z",
      "projectId": "01a184ff…5f07",
      "updatedAt": "2026-10-29T00:12:27.344Z",
      "variations": [],
      "description": "The redesigned checkout flow",
      "deprecatedKeys": []
    }
  },
  "type": "config.created",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "config": {
    "id": "01a18500…0339",
    "key": "checkout.new-flow"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:12:27.344Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

```json [config.updated]
{
  "id": "01a18500…00a8",
  "data": {
    "object": {
      "id": "01a18500…0339",
      "key": "checkout.new-flow",
      "role": "flag",
      "tags": [
        {
          "id": "01a18500…436d",
          "name": "checkout"
        }
      ],
      "type": "boolean",
      "state": "new",
      "client": false,
      "server": true,
      "lifetime": "temporary",
      "createdAt": "2026-10-29T00:12:27.344Z",
      "projectId": "01a184ff…5f07",
      "updatedAt": "2026-10-29T00:12:35.960Z",
      "variations": [],
      "description": "The redesigned one-page checkout",
      "deprecatedKeys": []
    },
    "previousAttributes": {
      "client": true,
      "description": "The redesigned checkout flow"
    }
  },
  "type": "config.updated",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "config": {
    "id": "01a18500…0339",
    "key": "checkout.new-flow"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:12:35.970Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

```json [config.updated (new key)]
{
  "id": "01a18500…1547",
  "data": {
    "object": {
      "id": "01a18500…0339",
      "key": "checkout.one-page",
      "role": "flag",
      "tags": [
        {
          "id": "01a18500…436d",
          "name": "checkout"
        }
      ],
      "type": "boolean",
      "state": "new",
      "client": false,
      "server": true,
      "lifetime": "temporary",
      "createdAt": "2026-10-29T00:12:27.344Z",
      "projectId": "01a184ff…5f07",
      "updatedAt": "2026-10-29T00:12:35.960Z",
      "variations": [],
      "description": "The redesigned one-page checkout",
      "deprecatedKeys": []
    },
    "previousAttributes": {
      "key": "checkout.new-flow"
    }
  },
  "type": "config.updated",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "config": {
    "id": "01a18500…0339",
    "key": "checkout.one-page"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:12:39.270Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

```json [config.archived]
{
  "id": "01a18500…4233",
  "data": {
    "object": {
      "id": "01a18500…0339",
      "key": "checkout.one-page",
      "role": "flag",
      "tags": [
        {
          "id": "01a18500…436d",
          "name": "checkout"
        }
      ],
      "type": "boolean",
      "state": "new",
      "client": false,
      "server": true,
      "lifetime": "temporary",
      "createdAt": "2026-10-29T00:12:27.344Z",
      "projectId": "01a184ff…5f07",
      "updatedAt": "2026-10-29T00:12:35.960Z",
      "variations": [],
      "description": "The redesigned one-page checkout",
      "deprecatedKeys": []
    }
  },
  "type": "config.archived",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "config": {
    "id": "01a18500…0339",
    "key": "checkout.one-page"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:13:09.320Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

```json [config.restored]
{
  "id": "01a1850c…f9b6",
  "data": {
    "object": {
      "id": "01a18509…3b15",
      "key": "checkout.express-pay",
      "role": "flag",
      "tags": [
        {
          "id": "01a18500…436d",
          "name": "checkout"
        }
      ],
      "type": "boolean",
      "state": "new",
      "client": true,
      "server": true,
      "lifetime": "temporary",
      "createdAt": "2026-10-29T00:22:08.880Z",
      "projectId": "01a184ff…5f07",
      "updatedAt": "2026-10-29T00:25:57.036Z",
      "variations": [],
      "description": "One-tap payment in checkout",
      "deprecatedKeys": []
    }
  },
  "type": "config.restored",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "sourceClient": "dashboard"
  },
  "config": {
    "id": "01a18509…3b15",
    "key": "checkout.express-pay"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:25:57.036Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

</code-group>

### Targeting rules

The sample at the [top of this page](#what-a-webhook-event-looks-like) is a `targeting_rules.updated`. Its `data.object` is shaped like one entry of `targets` in `GET /projects/{projectId}/configs/{configKey}`: each rule's `value` has the config's type, while `defaultValue`, the default targeting rule value, is a string.

### Segments

A change to one environment's override of a segment is a `segment.updated` that names that `environment` and holds the previous `overrides`.

<code-group>

```json [segment.created]
{
  "id": "01a18500…8fd5",
  "data": {
    "object": {
      "id": "01a18500…aca7",
      "key": "beta-testers",
      "kind": "rule-based",
      "name": "Beta testers",
      "groups": [
        [
          {
            "id": "93f47530…8c54",
            "kind": "attribute",
            "trait": "/plan",
            "operator": "equals",
            "attribute": "traits",
            "targetType": "text",
            "targetValues": [
              "beta"
            ]
          }
        ]
      ],
      "createdAt": "2026-10-29T00:13:16.529Z",
      "overrides": {},
      "projectId": "01a184ff…5f07",
      "updatedAt": "2026-10-29T00:13:16.529Z"
    }
  },
  "type": "segment.created",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:13:16.529Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

```json [segment.updated]
{
  "id": "01a18501…92f3",
  "data": {
    "object": {
      "id": "01a18500…aca7",
      "key": "beta-testers",
      "kind": "rule-based",
      "name": "Beta testers",
      "groups": [
        [
          {
            "id": "93f47530…8c54",
            "kind": "attribute",
            "trait": "/plan",
            "operator": "is one of",
            "attribute": "traits",
            "targetType": "text",
            "targetValues": [
              "beta",
              "early-access"
            ]
          }
        ]
      ],
      "createdAt": "2026-10-29T00:13:16.529Z",
      "overrides": {},
      "projectId": "01a184ff…5f07",
      "updatedAt": "2026-10-29T00:13:23.392Z"
    },
    "previousAttributes": {
      "groups": [
        [
          {
            "id": "93f47530…8c54",
            "kind": "attribute",
            "trait": "/plan",
            "operator": "equals",
            "attribute": "traits",
            "targetType": "text",
            "targetValues": [
              "beta"
            ]
          }
        ]
      ]
    }
  },
  "type": "segment.updated",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:13:23.375Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

```json [segment.deleted]
{
  "id": "01a18501…a894",
  "data": {
    "object": {
      "id": "01a18500…aca7",
      "key": "beta-testers",
      "kind": "rule-based",
      "name": "Beta testers",
      "groups": [
        [
          {
            "id": "93f47530…8c54",
            "kind": "attribute",
            "trait": "/plan",
            "operator": "is one of",
            "attribute": "traits",
            "targetType": "text",
            "targetValues": [
              "beta",
              "early-access"
            ]
          }
        ]
      ],
      "createdAt": "2026-10-29T00:13:16.529Z",
      "projectId": "01a184ff…5f07",
      "updatedAt": "2026-10-29T00:13:23.392Z"
    }
  },
  "type": "segment.deleted",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:13:25.632Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

</code-group>

### Environments

<code-group>

```json [environment.created]
{
  "id": "01a18500…b284",
  "data": {
    "object": {
      "id": "01a18500…4e5e",
      "live": false,
      "name": "Staging",
      "slug": "staging",
      "color": "yellow",
      "deletionProtection": false,
      "createdAt": "2026-10-29T00:12:13.940Z",
      "projectId": "01a184ff…5f07",
      "updatedAt": "2026-10-29T00:12:13.940Z"
    }
  },
  "type": "environment.created",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:12:13.940Z",
  "environment": {
    "id": "01a18500…4e5e",
    "live": false,
    "name": "Staging",
    "slug": "staging"
  },
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

```json [environment.updated]
{
  "id": "01a18500…1b7f",
  "data": {
    "object": {
      "id": "01a18500…4e5e",
      "live": true,
      "name": "Staging",
      "slug": "staging",
      "color": "teal",
      "deletionProtection": false,
      "createdAt": "2026-10-29T00:12:13.940Z",
      "projectId": "01a184ff…5f07",
      "updatedAt": "2026-10-29T00:12:19.979Z"
    },
    "previousAttributes": {
      "live": false,
      "color": "yellow"
    }
  },
  "type": "environment.updated",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:12:19.994Z",
  "environment": {
    "id": "01a18500…4e5e",
    "live": true,
    "name": "Staging",
    "slug": "staging"
  },
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

```json [environment.deleted]
{
  "id": "01a18501…3eee",
  "data": {
    "object": {
      "id": "01a18500…4e5e",
      "live": true,
      "name": "Staging",
      "slug": "staging",
      "color": "teal",
      "deletionProtection": false,
      "projectId": "01a184ff…5f07"
    }
  },
  "type": "environment.deleted",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:13:37.099Z",
  "environment": {
    "id": "01a18500…4e5e",
    "live": true,
    "name": "Staging",
    "slug": "staging"
  },
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

</code-group>

### Projects

<code-group>

```json [project.created]
{
  "id": "01a184ff…258b",
  "data": {
    "object": {
      "id": "01a184ff…5f07",
      "name": "Storefront",
      "slug": "storefront",
      "createdAt": "2026-10-29T00:11:51.108Z",
      "updatedAt": "2026-10-29T00:11:51.108Z",
      "organizationId": "01a01632…6040",
      "storeCodeSnippets": true,
      "deletionProtection": false
    }
  },
  "type": "project.created",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:11:51.108Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

```json [project.updated]
{
  "id": "01a184ff…0e23",
  "data": {
    "object": {
      "id": "01a184ff…5f07",
      "name": "Storefront Web",
      "slug": "storefront",
      "createdAt": "2026-10-29T00:11:51.108Z",
      "updatedAt": "2026-10-29T00:12:08.449Z",
      "organizationId": "01a01632…6040",
      "storeCodeSnippets": true,
      "deletionProtection": false
    },
    "previousAttributes": {
      "name": "Storefront"
    }
  },
  "type": "project.updated",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:12:08.445Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

```json [project.deleted]
{
  "id": "01a1850c…22a5",
  "data": {
    "object": {
      "id": "01a184ff…5f07",
      "name": "Storefront Web",
      "slug": "storefront",
      "organizationId": "01a01632…6040",
      "storeCodeSnippets": true,
      "deletionProtection": false
    }
  },
  "type": "project.deleted",
  "actor": {
    "member": {
      "id": "019da45e…ec57",
      "name": "Alice Example",
      "email": "alice@example.com"
    },
    "apiToken": {
      "id": "01a184fc…d78b",
      "name": "ci"
    },
    "sourceClient": "api"
  },
  "project": {
    "id": "01a184ff…5f07",
    "name": "Storefront Web",
    "slug": "storefront"
  },
  "createdAt": "2026-10-29T00:26:16.261Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

</code-group>

### Alerts

An alert webhook event carries no sentence about the alert. Write your own from `kind` and `facts`, which holds the alert's details and differs by kind; [Alerts](https://docs.configdirector.com/monitoring/alerts#alert-kinds) describes every kind. Alerts are raised by checks that run every 30 minutes or every hour, depending on the kind, so `alert.created` can arrive that long after the problem starts. Many alerts resolve the moment you fix their cause, for example when you create a config that was not found, and `alert.resolved` follows within seconds.

`alert.created` and `alert.resolved` for the same alert have different `id`s, because they are different webhook events. Match them by `data.object.id`, the alert's id.

<code-group>

```json [alert.created]
{
  "id": "01a18512…dac1",
  "data": {
    "object": {
      "id": "01a18512…dac1",
      "url": "https://app.configdirector.com/alerts/01a18512…dac1?organization=01a01632…6040",
      "kind": "config-not-found",
      "facts": {
        "sdks": [
          {
            "name": "js-server-sdk",
            "count": 5,
            "version": "1.12.0"
          }
        ],
        "sdkType": "server",
        "sdkKeyId": "01a01632…372d",
        "configKey": "webhook-docs.missing-flag",
        "projectId": "01a01632…179b",
        "eventCount": 5,
        "lastSeenAt": "2026-10-29T00:11:23.322Z",
        "sdkKeyName": "sk_test_4e66••••66eb",
        "projectName": "Acme App",
        "quietWindow": {
          "hours": 24
        },
        "environmentId": "01a01632…7ba2",
        "countingWindow": {
          "hours": 24
        },
        "environmentName": "Test"
      },
      "priority": "high",
      "createdAt": "2026-10-29T00:30:46.735Z",
      "detectedAt": "2026-10-29T00:30:46.735Z",
      "resolvedAt": null
    }
  },
  "type": "alert.created",
  "alertKind": "config-not-found",
  "createdAt": "2026-10-29T00:30:46.735Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

```json [alert.resolved]
{
  "id": "d88613ba…a0a4",
  "data": {
    "object": {
      "id": "01a18512…dac1",
      "url": "https://app.configdirector.com/alerts/01a18512…dac1?organization=01a01632…6040",
      "kind": "config-not-found",
      "facts": {
        "sdks": [
          {
            "name": "js-server-sdk",
            "count": 5,
            "version": "1.12.0"
          }
        ],
        "sdkType": "server",
        "sdkKeyId": "01a01632…372d",
        "configKey": "webhook-docs.missing-flag",
        "projectId": "01a01632…179b",
        "eventCount": 5,
        "lastSeenAt": "2026-10-29T00:11:23.322Z",
        "sdkKeyName": "sk_test_4e66••••66eb",
        "projectName": "Acme App",
        "quietWindow": {
          "hours": 24
        },
        "environmentId": "01a01632…7ba2",
        "countingWindow": {
          "hours": 24
        },
        "environmentName": "Test"
      },
      "priority": "high",
      "createdAt": "2026-10-29T00:30:46.735Z",
      "detectedAt": "2026-10-29T00:30:46.735Z",
      "resolvedAt": "2026-10-29T00:32:45.281Z"
    }
  },
  "type": "alert.resolved",
  "alertKind": "config-not-found",
  "createdAt": "2026-10-29T00:32:45.281Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

</code-group>

### Test

```json
{
  "id": "0e168f29…66c3",
  "data": {
    "sample": {
      "id": "019a2d5e…6a7b",
      "data": {
        "object": {
          "id": "019a2d5e…9e06",
          "rules": [
            {
              "id": "019a2d5e…9e07",
              "type": "conditional",
              "order": 0,
              "value": "false",
              "target": "value",
              "valueId": "0c5nBf…",
              "conditions": [
                {
                  "id": "019a2d5e…9e08",
                  "kind": "attribute",
                  "operator": "is one of",
                  "attribute": "identifier",
                  "targetType": "text",
                  "targetValues": [
                    "user-101",
                    "user-202"
                  ]
                }
              ],
              "percentages": []
            }
          ],
          "environment": {
            "id": "019a2d5e…9e02",
            "live": true,
            "name": "Production",
            "slug": "production",
            "color": "red"
          },
          "defaultValue": "true",
          "defaultValueId": "4hR8sK…"
        },
        "previousAttributes": {
          "defaultValue": "false"
        }
      },
      "type": "targeting_rules.updated",
      "actor": {
        "member": {
          "id": "019a2d5e…9e04",
          "name": "Alice Example",
          "email": "alice@example.com"
        },
        "apiToken": {
          "id": "019a2d5e…9e05",
          "name": "ci"
        },
        "sourceClient": "terraform"
      },
      "config": {
        "id": "019a2d5e…9e03",
        "key": "checkout.new-flow"
      },
      "project": {
        "id": "019a2d5e…9e01",
        "name": "Storefront",
        "slug": "storefront"
      },
      "createdAt": "2026-10-27T14:03:12.481Z",
      "environment": {
        "id": "019a2d5e…9e02",
        "live": true,
        "name": "Production",
        "slug": "production"
      },
      "organization": {
        "id": "01a01632…6040",
        "name": "Acme"
      }
    }
  },
  "type": "webhook.test",
  "createdAt": "2026-10-29T00:11:34.020Z",
  "organization": {
    "id": "01a01632…6040",
    "name": "Acme"
  }
}
```

## Delivery

A webhook event is normally delivered within a few seconds of the change. During maintenance on our side it can take longer, up to 30 minutes, while webhook events wait to be sent.

Each delivery is one webhook event sent to one webhook. It succeeds when your receiver answers with any `2xx` status within 10 seconds. Anything else is a failed attempt: another status, a redirect (redirects are not followed), no answer within 10 seconds, a connection error, or a URL whose host now resolves to a private address. After a failed attempt the delivery is tried again after about 1 minute, 5 minutes, 30 minutes, 2 hours, and 8 hours, each up to 10% later, so six attempts over about ten and a half hours. If the last one fails too, the delivery is **failed**.

- **At least once.** A webhook event can arrive more than once. [Drop duplicates by `webhook-id`](#verifying-requests).
- **In no particular order.** A retried delivery can arrive after newer ones, and a failing delivery never holds back the ones after it. Order webhook events by `createdAt` when order matters.

The webhook's page lists its **Deliveries** from the last 30 days, newest first, with **Failures only** to show just the failed ones. Each delivery shows its webhook event type, its status, the number of attempts, and the latest attempt's result, and expands to the webhook event that was sent and the first 1 KB of your receiver's response or the connection error. A delivery is **pending** while it waits for an attempt, **succeeded**, **failed**, or **abandoned** when the webhook was disabled before it could succeed.

**Resend** sends a delivery's webhook event again right away, with the same `webhook-id` and body, and a fresh timestamp and signature. If that attempt fails, the delivery is marked failed and is not tried again on its own, even if it was still waiting for a retry. A disabled webhook cannot resend or send test webhook events until it is enabled again.

## When a webhook is disabled

An owner or admin can disable a webhook from its page. ConfigDirector also disables a webhook on its own when deliveries to it were attempted on each of the last three days and none of them succeeded. Then it:

- abandons the deliveries still waiting for an attempt,
- marks the webhook **Disabled** in the list, saying it was disabled after repeated failures,
- raises the **Webhook disabled after repeated failures** alert, which is Critical, goes to owners and admins, and is emailed right away unless they turned that off in their email preferences.

While a webhook is disabled, nothing is queued for it, and enabling it again does not send what happened in between. Fix the receiver, then click **Enable** on the webhook's page; the alert resolves when the webhook is enabled again or deleted.

## URL rules and custom headers

- The URL must start with `https://` and be at most 2,048 characters long.
- Its host must resolve to a public address. A host that cannot be found, or that resolves to a private, loopback, or link-local address, is refused when you save, and the address is checked again before every attempt. To try webhooks against a receiver on your own machine, expose it through a tunnel that gives it a public `https://` URL.
- Requests do not come from a fixed set of IP addresses, so rely on the signature, not the sender's address.

A webhook can send up to 5 custom headers with every request, for example an `Authorization` header your gateway checks. Each name can be used once and follows the HTTP rules for header names. A value is at most 1,024 characters of printable Latin-1 text with no line breaks. The headers ConfigDirector sets itself (`content-type`, `content-length`, `host`, `user-agent`, `webhook-id`, `webhook-signature`, and `webhook-timestamp`) cannot be custom headers. Header values are stored encrypted and never shown again; when you edit a webhook, a value you leave blank keeps the saved one.

## Limits

<table>
<thead>
  <tr>
    <th>
      Limit
    </th>
    
    <th>
      Value
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Webhooks per organization
    </td>
    
    <td>
      10
    </td>
  </tr>
  
  <tr>
    <td>
      Custom headers per webhook
    </td>
    
    <td>
      5, each value up to 1,024 characters
    </td>
  </tr>
  
  <tr>
    <td>
      Response time
    </td>
    
    <td>
      10 seconds
    </td>
  </tr>
  
  <tr>
    <td>
      Delivery history
    </td>
    
    <td>
      30 days, after which deliveries are deleted and can no longer be resent
    </td>
  </tr>
  
  <tr>
    <td>
      Webhook event size
    </td>
    
    <td>
      256 KB
    </td>
  </tr>
</tbody>
</table>

A change webhook event larger than 256 KB is sent with `data` replaced by `{ "truncated": true, "url": "..." }`, where `url` opens the config, segment, or project in the dashboard; read the full entity from the Admin API. Alert webhook events are always sent whole.

## Managing webhooks with the Admin API

Everything on the Webhooks page is also available through the [Admin API](https://docs.configdirector.com/integrations/admin-api#webhooks): listing, creating, updating, and deleting webhooks, rotating the secret, sending a test webhook event, listing deliveries, and resending one. The API token needs the `webhooks:manage` permission, which only owners and admins can grant.

To declare webhooks as code, use the `configdirector_webhook` resource of the [Terraform provider](https://docs.configdirector.com/integrations/terraform#webhooks), which also works with OpenTofu.


## Sitemap

See the full [sitemap](https://docs.configdirector.com/sitemap.md) for all pages.
