# Webhooks

> Hear about every conversation as it happens, signed so you know it came from Aigently.

A webhook receiver is an address on your server that the platform POSTs events to. Add one in the
console under **Webhooks**, choose its events, and keep its signing secret, which is shown once.

## Events about a conversation

| Event | When |
|---|---|
| `conversation.started` | A call was answered, a browser caller joined, or a chat's first message arrived. |
| `conversation.ended` | Once, the first time a conversation ends — answered or not. |
| `conversation.updated` | Something changed after the end — an outcome corrected, for one. |
| `conversation.analyzed` | The analysis finished: outcome, sentiment, the checks it scored. |
| `conversation.recording_ready` | The recording is stored. It carries no link: [ask for one](/guides/conversations#the-recording) when you need it. |
| `conversation.deleted` | Removed by retention, from the console, [through the API](/guides/conversations#delete-one), or with its agent or project. |
| `conversation.transcript` | The whole transcript. It carries what was said, so it is off until you choose it. |

## Events about an agent

`agent.updated` is sent when an agent's fields change: the [variables](/guides/variables) a
conversation can start with, the answers it collects, or what its analysis writes. An edit to a
published agent takes effect from the next conversation, so an edit that adds a required variable is
one your integration needs to hear about before its next call.

It carries the agent as [Get an agent](/reference/getAgent) describes it, with the three
lists, `analysis_enabled`, and `changed`, which names the lists that changed. Each list has a
`version`: keep it, and you can tell a change to the fields from any other edit. An edit that changes
no field — a new name, reworded instructions with the same placeholders — sends nothing. It goes to
live receivers only: an agent's fields are not test data. A receiver made before the event existed
hears it once `agent.updated` is added to its `events`.

## Events about a campaign

A [campaign](/guides/campaigns) sends `campaign.started`, `campaign.paused`, `campaign.completed`
and `campaign.cancelled` as it moves — whether you moved it, somebody did in the console, or it
paused itself — and `campaign.contact_finished` with one person's final result. They go to live
receivers only.

There are more — forms, workflow steps, appointments, handovers — listed beside each receiver in the
console. Every event of the last 30 days is also in the [event feed](/guides/events), whether or not a
receiver heard it. Which events a call sends as it ends is in the [endings table](/guides/outbound-calls#how-a-call-ends).

## What you receive

```json
{
  "id": "0f9a7c2e-5b1d-4e8a-9c3f-2d6b8e1a4c7f",
  "event": "conversation.ended",
  "created_at": "2026-10-07T09:14:03.120394+00:00",
  "livemode": true,
  "revision": 6,
  "data": { "object": "conversation", "id": "5f0c9a52-…", "status": "completed", "…": "…" }
}
```

`data` is the [conversation](/reference/objects/Conversation), as the API reads it. `id` is the
event's own id: the same event delivered twice has the same `id`, so keep the ones you have seen.
Events can arrive out of order — keep the copy with the higher `revision`. Answer `2xx` quickly and
do the work afterwards; anything else is retried with growing waits.

## Check the signature

Every delivery is signed with your receiver's secret, the [Standard Webhooks](https://www.standardwebhooks.com/)
way — so any of that standard's libraries checks it — in three headers: `webhook-id`,
`webhook-timestamp` and `webhook-signature`. Check it against the body **exactly as it arrived**,
before parsing it, and refuse anything older than five minutes. This function does all of it, in
your language:

**curl**

```sh
#!/bin/sh
# Check a saved delivery's signature by hand, with openssl: the body on standard input, the three
# headers in WEBHOOK_ID, WEBHOOK_TIMESTAMP and WEBHOOK_SIGNATURE. A server should use one of the
# other languages, which compare in constant time.
set -eu
body="$(mktemp)"
trap 'rm -f "$body"' EXIT
cat > "$body"

now="$(date +%s)"
age=$((now - WEBHOOK_TIMESTAMP))
if [ "${age#-}" -gt 300 ]; then
  echo "timestamp outside the tolerance" >&2
  exit 1
fi

key="$(printf '%s' "${AIGENTLY_WEBHOOK_SECRET#whsec_}" | base64 -d | od -An -v -tx1 | tr -d ' \n')"
expected="$({ printf '%s.%s.' "$WEBHOOK_ID" "$WEBHOOK_TIMESTAMP"; cat "$body"; } \
  | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$key" -binary | base64)"

for entry in $WEBHOOK_SIGNATURE; do
  if [ "$entry" = "v1,$expected" ]; then
    echo "verified"
    exit 0
  fi
done
echo "signature does not match" >&2
exit 1
```

**JavaScript**

```js
// Check a webhook's signature, then read its event.
// The same function checks a context lookup request, with the lookup's secret.
import { createHmac, timingSafeEqual } from "node:crypto";
import { text } from "node:stream/consumers";

const TOLERANCE_SECONDS = 5 * 60;

// `body` is the request body exactly as it arrived: parse it only after this has checked it.
// `headers` has lowercase names, as Node gives them.
export function unwrap(body, headers, secret) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (!id || !timestamp || !signatures) throw new Error("missing webhook headers");
  if (!(Math.abs(Date.now() / 1000 - Number(timestamp)) <= TOLERANCE_SECONDS)) {
    throw new Error("timestamp outside the tolerance");
  }
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest();
  const matched = signatures.split(" ").some((entry) => {
    const [version, signature] = entry.split(",");
    if (version !== "v1" || !signature) return false;
    const offered = Buffer.from(signature, "base64");
    return offered.length === expected.length && timingSafeEqual(offered, expected);
  });
  if (!matched) throw new Error("signature does not match");
  return JSON.parse(body);
}

// Try it with a delivery: the body on standard input, the three headers in the environment.
const event = unwrap(
  await text(process.stdin),
  {
    "webhook-id": process.env.WEBHOOK_ID,
    "webhook-timestamp": process.env.WEBHOOK_TIMESTAMP,
    "webhook-signature": process.env.WEBHOOK_SIGNATURE,
  },
  process.env.AIGENTLY_WEBHOOK_SECRET,
);
console.log(`verified ${event.event} ${event.id}`);
```

**Python**

```python
# Check a webhook's signature, then read its event.
# The same function checks a context lookup request, with the lookup's secret.
import base64
import hashlib
import hmac
import json
import os
import sys
import time

TOLERANCE_SECONDS = 5 * 60

def unwrap(body: bytes, headers, secret: str) -> dict:
    """The event, once its signature is checked. Raises ValueError otherwise.

    `body` is the request body exactly as it arrived: parse it only after this has checked it.
    `headers` is any mapping of the request's headers; most frameworks read theirs in any case.
    """
    message_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signatures = headers.get("webhook-signature")
    if not message_id or not timestamp or not signatures:
        raise ValueError("missing webhook headers")
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        raise ValueError("timestamp outside the tolerance")
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{message_id}.{timestamp}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    for entry in signatures.split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature, expected):
            return json.loads(body)
    raise ValueError("signature does not match")

if __name__ == "__main__":
    # Try it with a delivery: the body on standard input, the three headers in the environment.
    event = unwrap(
        sys.stdin.buffer.read(),
        {
            "webhook-id": os.environ["WEBHOOK_ID"],
            "webhook-timestamp": os.environ["WEBHOOK_TIMESTAMP"],
            "webhook-signature": os.environ["WEBHOOK_SIGNATURE"],
        },
        os.environ["AIGENTLY_WEBHOOK_SECRET"],
    )
    print("verified", event["event"], event["id"])
```

**Go**

```go
// Check a webhook's signature, then read its event.
// The same function checks a context lookup request, with the lookup's secret.
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

const toleranceSeconds = 5 * 60

// Unwrap checks the signature on a webhook and returns its event. body is the request body exactly
// as it arrived: parse it only after this has checked it.
func Unwrap(body []byte, headers http.Header, secret string) (map[string]any, error) {
	id := headers.Get("webhook-id")
	timestamp := headers.Get("webhook-timestamp")
	signatures := headers.Get("webhook-signature")
	if id == "" || timestamp == "" || signatures == "" {
		return nil, errors.New("missing webhook headers")
	}
	sent, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil || time.Since(time.Unix(sent, 0)).Abs() > toleranceSeconds*time.Second {
		return nil, errors.New("timestamp outside the tolerance")
	}
	key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
	if err != nil {
		return nil, errors.New("not a webhook signing secret")
	}
	mac := hmac.New(sha256.New, key)
	mac.Write([]byte(id + "." + timestamp + "."))
	mac.Write(body)
	expected := mac.Sum(nil)
	for _, entry := range strings.Split(signatures, " ") {
		version, signature, _ := strings.Cut(entry, ",")
		offered, err := base64.StdEncoding.DecodeString(signature)
		if version == "v1" && err == nil && hmac.Equal(offered, expected) {
			var event map[string]any
			if err := json.Unmarshal(body, &event); err != nil {
				return nil, err
			}
			return event, nil
		}
	}
	return nil, errors.New("signature does not match")
}

func main() {
	// Try it with a delivery: the body on standard input, the three headers in the environment.
	body, _ := io.ReadAll(os.Stdin)
	headers := http.Header{}
	headers.Set("webhook-id", os.Getenv("WEBHOOK_ID"))
	headers.Set("webhook-timestamp", os.Getenv("WEBHOOK_TIMESTAMP"))
	headers.Set("webhook-signature", os.Getenv("WEBHOOK_SIGNATURE"))
	event, err := Unwrap(body, headers, os.Getenv("AIGENTLY_WEBHOOK_SECRET"))
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Println("verified", event["event"], event["id"])
}
```

**Java**

```java
// Check a webhook's signature. Once it passes, read the event with your JSON library.
// The same method checks a context lookup request, with the lookup's secret.
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class Main {
    static final long TOLERANCE_SECONDS = 5 * 60;

    /**
     * The body, once its signature is checked; throws otherwise. `body` is the request body exactly
     * as it arrived, and `headers` has lowercase names.
     */
    static String unwrap(byte[] body, Map<String, String> headers, String secret) throws Exception {
        String id = headers.get("webhook-id");
        String timestamp = headers.get("webhook-timestamp");
        String signatures = headers.get("webhook-signature");
        if (id == null || timestamp == null || signatures == null) {
            throw new SecurityException("missing webhook headers");
        }
        long now = System.currentTimeMillis() / 1000;
        if (!timestamp.matches("\\d{1,12}") || Math.abs(now - Long.parseLong(timestamp)) > TOLERANCE_SECONDS) {
            throw new SecurityException("timestamp outside the tolerance");
        }
        byte[] key = Base64.getDecoder().decode(secret.replaceFirst("^whsec_", ""));
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key, "HmacSHA256"));
        mac.update((id + "." + timestamp + ".").getBytes(StandardCharsets.UTF_8));
        byte[] expected = mac.doFinal(body);
        for (String entry : signatures.split(" ")) {
            String[] parts = entry.split(",", 2);
            if (parts.length != 2 || !parts[0].equals("v1")) {
                continue;
            }
            byte[] offered;
            try {
                offered = Base64.getDecoder().decode(parts[1]);
            } catch (IllegalArgumentException malformed) {
                continue;
            }
            if (MessageDigest.isEqual(offered, expected)) {
                return new String(body, StandardCharsets.UTF_8);
            }
        }
        throw new SecurityException("signature does not match");
    }

    public static void main(String[] args) throws Exception {
        // Try it with a delivery: the body on standard input, the three headers in the environment.
        String event = unwrap(
            System.in.readAllBytes(),
            Map.of(
                "webhook-id", System.getenv("WEBHOOK_ID"),
                "webhook-timestamp", System.getenv("WEBHOOK_TIMESTAMP"),
                "webhook-signature", System.getenv("WEBHOOK_SIGNATURE")),
            System.getenv("AIGENTLY_WEBHOOK_SECRET"));
        System.out.println("verified " + event);
    }
}
```

**C#**

```csharp
// Check a webhook's signature, then read its event.
// The same function checks a context lookup request, with the lookup's secret.
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

// Try it with a delivery: the body on standard input, the three headers in the environment.
using var input = new MemoryStream();
Console.OpenStandardInput().CopyTo(input);
using var webhookEvent = Webhooks.Unwrap(
    input.ToArray(),
    new Dictionary<string, string?>
    {
        ["webhook-id"] = Environment.GetEnvironmentVariable("WEBHOOK_ID"),
        ["webhook-timestamp"] = Environment.GetEnvironmentVariable("WEBHOOK_TIMESTAMP"),
        ["webhook-signature"] = Environment.GetEnvironmentVariable("WEBHOOK_SIGNATURE"),
    },
    Environment.GetEnvironmentVariable("AIGENTLY_WEBHOOK_SECRET") ?? "");
var root = webhookEvent.RootElement;
Console.WriteLine($"verified {root.GetProperty("event")} {root.GetProperty("id")}");

static class Webhooks
{
    const long ToleranceSeconds = 5 * 60;

    // The event, once its signature is checked; throws otherwise. `body` is the request body
    // exactly as it arrived: parse it only after this has checked it.
    public static JsonDocument Unwrap(
        byte[] body, IReadOnlyDictionary<string, string?> headers, string secret)
    {
        if (headers.GetValueOrDefault("webhook-id") is not { Length: > 0 } id
            || headers.GetValueOrDefault("webhook-timestamp") is not { Length: > 0 } timestamp
            || headers.GetValueOrDefault("webhook-signature") is not { Length: > 0 } signatures)
        {
            throw new CryptographicException("missing webhook headers");
        }
        if (!long.TryParse(timestamp, out var sent)
            || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - sent) > ToleranceSeconds)
        {
            throw new CryptographicException("timestamp outside the tolerance");
        }
        var key = Convert.FromBase64String(secret.StartsWith("whsec_") ? secret[6..] : secret);
        var signed = Encoding.UTF8.GetBytes($"{id}.{timestamp}.").Concat(body).ToArray();
        var expected = Encoding.ASCII.GetBytes(Convert.ToBase64String(HMACSHA256.HashData(key, signed)));
        foreach (var entry in signatures.Split(' '))
        {
            var parts = entry.Split(',', 2);
            if (parts.Length == 2 && parts[0] == "v1"
                && CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(parts[1]), expected))
            {
                return JsonDocument.Parse(body);
            }
        }
        throw new CryptographicException("signature does not match");
    }
}
```

**PHP**

```php
<?php
// Check a webhook's signature, then read its event.
// The same function checks a context lookup request, with the lookup's secret.
const TOLERANCE_SECONDS = 5 * 60;

/**
 * The event, once its signature is checked; throws otherwise. `$body` is the request body exactly
 * as it arrived (file_get_contents("php://input")), and `$headers` has lowercase names.
 */
function aigently_unwrap(string $body, array $headers, string $secret): array
{
    $id = $headers["webhook-id"] ?? "";
    $timestamp = $headers["webhook-timestamp"] ?? "";
    $signatures = $headers["webhook-signature"] ?? "";
    if ($id === "" || $timestamp === "" || $signatures === "") {
        throw new RuntimeException("missing webhook headers");
    }
    if (!preg_match('/^\d+$/', $timestamp) || abs(time() - (int) $timestamp) > TOLERANCE_SECONDS) {
        throw new RuntimeException("timestamp outside the tolerance");
    }
    $key = base64_decode(preg_replace('/^whsec_/', "", $secret), true);
    $expected = base64_encode(hash_hmac("sha256", "$id.$timestamp.$body", (string) $key, true));
    foreach (explode(" ", $signatures) as $entry) {
        [$version, $signature] = array_pad(explode(",", $entry, 2), 2, "");
        if ($version === "v1" && hash_equals($expected, $signature)) {
            return json_decode($body, true, flags: JSON_THROW_ON_ERROR);
        }
    }
    throw new RuntimeException("signature does not match");
}

// Try it with a delivery: the body on standard input, the three headers in the environment.
$event = aigently_unwrap(
    file_get_contents("php://stdin"),
    [
        "webhook-id" => (string) getenv("WEBHOOK_ID"),
        "webhook-timestamp" => (string) getenv("WEBHOOK_TIMESTAMP"),
        "webhook-signature" => (string) getenv("WEBHOOK_SIGNATURE"),
    ],
    (string) getenv("AIGENTLY_WEBHOOK_SECRET"),
);
echo "verified {$event["event"]} {$event["id"]}\n";
```

The same function checks a [context lookup](/guides/inbound-calls) request, with the lookup's secret.

Receivers made before this signature existed also get `X-Agently-Signature: t=<time>,v1=<hex>` — an
HMAC-SHA256 of `<time>.<body>` keyed with the secret's text. It keeps working, and both are sent on
every delivery.

## Manage receivers from your server

A company that integrates for many customers makes a receiver per customer — from code, with a key
that has `webhooks:write`. A test key's receivers are test receivers; a live key's are live. The
address must be public `https`, and a project holds at most ten.

**curl**

```sh
#!/bin/sh
# Create a webhook receiver. Keep the secret in the answer: it is shown once.
curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/webhooks" \
  -H "Authorization: Bearer $AIGENTLY_API_KEY" \
  --json '{
  "project_id": "0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
  "url": "https://hooks.aigently.ai/aigently",
  "events": [
    "conversation.ended",
    "conversation.analyzed"
  ]
}'
```

**JavaScript**

```js
// Create a webhook receiver. Keep the secret in the answer: it is shown once.
const response = await fetch("https://api.aigently.ai/v1/webhooks", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "project_id": "0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
    "url": "https://hooks.aigently.ai/aigently",
    "events": [
      "conversation.ended",
      "conversation.analyzed"
    ]
  }),
});
const answer = await response.json();
if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`);
console.log(answer);
```

**Python**

```python
# Create a webhook receiver. Keep the secret in the answer: it is shown once.
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://api.aigently.ai/v1/webhooks",
    headers={
        "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}",
    },
    json={
        "project_id": "0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
        "url": "https://hooks.aigently.ai/aigently",
        "events": ["conversation.ended", "conversation.analyzed"],
    },
)
if response.is_error:
    raise SystemExit(response.text)
print(response.json())
```

**Go**

```go
// Create a webhook receiver. Keep the secret in the answer: it is shown once.
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	payload := []byte(`{
  "project_id": "0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
  "url": "https://hooks.aigently.ai/aigently",
  "events": [
    "conversation.ended",
    "conversation.analyzed"
  ]
}`)
	request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/webhooks", bytes.NewReader(payload))
	if err != nil {
		panic(err)
	}
	request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY"))
	request.Header.Set("Content-Type", "application/json")
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
	body, _ := io.ReadAll(response.Body)
	if response.StatusCode >= 400 {
		fmt.Fprintln(os.Stderr, string(body))
		os.Exit(1)
	}
	fmt.Println(string(body))
}
```

**Java**

```java
// Create a webhook receiver. Keep the secret in the answer: it is shown once.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "project_id": "0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
              "url": "https://hooks.aigently.ai/aigently",
              "events": [
                "conversation.ended",
                "conversation.analyzed"
              ]
            }
            """;
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/webhooks"))
            .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY"))
            .header("Content-Type", "application/json")
            .method("POST", HttpRequest.BodyPublishers.ofString(body))
            .build();
        // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every
        // server allows.
        HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
        HttpResponse<String> response =
            client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            System.err.println(response.body());
            System.exit(1);
        }
        System.out.println(response.body());
    }
}
```

**C#**

```csharp
// Create a webhook receiver. Keep the secret in the answer: it is shown once.
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/webhooks")
{
    Content = new StringContent(
        """
        {
          "project_id": "0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
          "url": "https://hooks.aigently.ai/aigently",
          "events": [
            "conversation.ended",
            "conversation.analyzed"
          ]
        }
        """,
        Encoding.UTF8,
        "application/json"),
};
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
    Console.Error.WriteLine(body);
    return 1;
}
Console.WriteLine(body);
return 0;
```

**PHP**

```php
<?php
// Create a webhook receiver. Keep the secret in the answer: it is shown once.
$curl = curl_init("https://api.aigently.ai/v1/webhooks");
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"),
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "project_id" => "0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
        "url" => "https://hooks.aigently.ai/aigently",
        "events" => ["conversation.ended", "conversation.analyzed"],
    ]),
]);
$body = curl_exec($curl);
if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) {
    fwrite(STDERR, $body . "\n");
    exit(1);
}
echo $body, "\n";
```

The answer carries the receiver's **secret, once**. Read a receiver, change what it hears or switch
it off, and delete it, with the same key:

**curl**

```sh
#!/bin/sh
# List webhook receivers
curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/webhooks" \
  -H "Authorization: Bearer $AIGENTLY_API_KEY"
```

**JavaScript**

```js
// List webhook receivers
const response = await fetch("https://api.aigently.ai/v1/webhooks", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`,
  },
});
const answer = await response.json();
if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`);
console.log(answer);
```

**Python**

```python
# List webhook receivers
# pip install httpx
import os

import httpx

response = httpx.get(
    "https://api.aigently.ai/v1/webhooks",
    headers={
        "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}",
    },
)
if response.is_error:
    raise SystemExit(response.text)
print(response.json())
```

**Go**

```go
// List webhook receivers
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/webhooks", nil)
	if err != nil {
		panic(err)
	}
	request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY"))
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
	body, _ := io.ReadAll(response.Body)
	if response.StatusCode >= 400 {
		fmt.Fprintln(os.Stderr, string(body))
		os.Exit(1)
	}
	fmt.Println(string(body))
}
```

**Java**

```java
// List webhook receivers
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/webhooks"))
            .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY"))
            .method("GET", HttpRequest.BodyPublishers.noBody())
            .build();
        // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every
        // server allows.
        HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
        HttpResponse<String> response =
            client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            System.err.println(response.body());
            System.exit(1);
        }
        System.out.println(response.body());
    }
}
```

**C#**

```csharp
// List webhook receivers
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/webhooks")
{
};
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
    Console.Error.WriteLine(body);
    return 1;
}
Console.WriteLine(body);
return 0;
```

**PHP**

```php
<?php
// List webhook receivers
$curl = curl_init("https://api.aigently.ai/v1/webhooks");
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"),
    ],
]);
$body = curl_exec($curl);
if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) {
    fwrite(STDERR, $body . "\n");
    exit(1);
}
echo $body, "\n";
```

**curl**

```sh
#!/bin/sh
# Change what a receiver hears
curl -sS --fail-with-body -X PATCH "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c" \
  -H "Authorization: Bearer $AIGENTLY_API_KEY" \
  --json '{
  "events": [
    "conversation.ended"
  ],
  "include_values": false
}'
```

**JavaScript**

```js
// Change what a receiver hears
const response = await fetch("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "events": [
      "conversation.ended"
    ],
    "include_values": false
  }),
});
const answer = await response.json();
if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`);
console.log(answer);
```

**Python**

```python
# Change what a receiver hears
# pip install httpx
import os

import httpx

response = httpx.patch(
    "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c",
    headers={
        "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}",
    },
    json={
        "events": ["conversation.ended"],
        "include_values": False,
    },
)
if response.is_error:
    raise SystemExit(response.text)
print(response.json())
```

**Go**

```go
// Change what a receiver hears
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	payload := []byte(`{
  "events": [
    "conversation.ended"
  ],
  "include_values": false
}`)
	request, err := http.NewRequest("PATCH", "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c", bytes.NewReader(payload))
	if err != nil {
		panic(err)
	}
	request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY"))
	request.Header.Set("Content-Type", "application/json")
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
	body, _ := io.ReadAll(response.Body)
	if response.StatusCode >= 400 {
		fmt.Fprintln(os.Stderr, string(body))
		os.Exit(1)
	}
	fmt.Println(string(body))
}
```

**Java**

```java
// Change what a receiver hears
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "events": [
                "conversation.ended"
              ],
              "include_values": false
            }
            """;
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c"))
            .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY"))
            .header("Content-Type", "application/json")
            .method("PATCH", HttpRequest.BodyPublishers.ofString(body))
            .build();
        // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every
        // server allows.
        HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
        HttpResponse<String> response =
            client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            System.err.println(response.body());
            System.exit(1);
        }
        System.out.println(response.body());
    }
}
```

**C#**

```csharp
// Change what a receiver hears
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Patch, "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c")
{
    Content = new StringContent(
        """
        {
          "events": [
            "conversation.ended"
          ],
          "include_values": false
        }
        """,
        Encoding.UTF8,
        "application/json"),
};
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
    Console.Error.WriteLine(body);
    return 1;
}
Console.WriteLine(body);
return 0;
```

**PHP**

```php
<?php
// Change what a receiver hears
$curl = curl_init("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c");
curl_setopt_array($curl, [
    CURLOPT_CUSTOMREQUEST => "PATCH",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"),
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "events" => ["conversation.ended"],
        "include_values" => false,
    ]),
]);
$body = curl_exec($curl);
if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) {
    fwrite(STDERR, $body . "\n");
    exit(1);
}
echo $body, "\n";
```

**curl**

```sh
#!/bin/sh
# Delete a webhook receiver
curl -sS --fail-with-body -X DELETE "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c" \
  -H "Authorization: Bearer $AIGENTLY_API_KEY"
```

**JavaScript**

```js
// Delete a webhook receiver
const response = await fetch("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`,
  },
});
const answer = await response.json();
if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`);
console.log(answer);
```

**Python**

```python
# Delete a webhook receiver
# pip install httpx
import os

import httpx

response = httpx.delete(
    "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c",
    headers={
        "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}",
    },
)
if response.is_error:
    raise SystemExit(response.text)
print(response.json())
```

**Go**

```go
// Delete a webhook receiver
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	request, err := http.NewRequest("DELETE", "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c", nil)
	if err != nil {
		panic(err)
	}
	request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY"))
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
	body, _ := io.ReadAll(response.Body)
	if response.StatusCode >= 400 {
		fmt.Fprintln(os.Stderr, string(body))
		os.Exit(1)
	}
	fmt.Println(string(body))
}
```

**Java**

```java
// Delete a webhook receiver
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c"))
            .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY"))
            .method("DELETE", HttpRequest.BodyPublishers.noBody())
            .build();
        // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every
        // server allows.
        HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
        HttpResponse<String> response =
            client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            System.err.println(response.body());
            System.exit(1);
        }
        System.out.println(response.body());
    }
}
```

**C#**

```csharp
// Delete a webhook receiver
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Delete, "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c")
{
};
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
    Console.Error.WriteLine(body);
    return 1;
}
Console.WriteLine(body);
return 0;
```

**PHP**

```php
<?php
// Delete a webhook receiver
$curl = curl_init("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c");
curl_setopt_array($curl, [
    CURLOPT_CUSTOMREQUEST => "DELETE",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"),
    ],
]);
$body = curl_exec($curl);
if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) {
    fwrite(STDERR, $body . "\n");
    exit(1);
}
echo $body, "\n";
```

A key limited to some agents cannot manage receivers: a receiver belongs to its whole project.

## Change a secret without missing an event

Rotating a secret with an overlap keeps the old one signing beside the new one until then: every
delivery carries a signature from each, and the standard's libraries — like the function above —
accept either. Update your receiver within the overlap, and nothing is ever rejected.

**curl**

```sh
#!/bin/sh
# Change a receiver's secret, keeping the old one for a day
curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/rotate-secret" \
  -H "Authorization: Bearer $AIGENTLY_API_KEY" \
  --json '{
  "keep_previous_for": "24h"
}'
```

**JavaScript**

```js
// Change a receiver's secret, keeping the old one for a day
const response = await fetch("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/rotate-secret", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "keep_previous_for": "24h"
  }),
});
const answer = await response.json();
if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`);
console.log(answer);
```

**Python**

```python
# Change a receiver's secret, keeping the old one for a day
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/rotate-secret",
    headers={
        "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}",
    },
    json={
        "keep_previous_for": "24h",
    },
)
if response.is_error:
    raise SystemExit(response.text)
print(response.json())
```

**Go**

```go
// Change a receiver's secret, keeping the old one for a day
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	payload := []byte(`{
  "keep_previous_for": "24h"
}`)
	request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/rotate-secret", bytes.NewReader(payload))
	if err != nil {
		panic(err)
	}
	request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY"))
	request.Header.Set("Content-Type", "application/json")
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
	body, _ := io.ReadAll(response.Body)
	if response.StatusCode >= 400 {
		fmt.Fprintln(os.Stderr, string(body))
		os.Exit(1)
	}
	fmt.Println(string(body))
}
```

**Java**

```java
// Change a receiver's secret, keeping the old one for a day
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "keep_previous_for": "24h"
            }
            """;
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/rotate-secret"))
            .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY"))
            .header("Content-Type", "application/json")
            .method("POST", HttpRequest.BodyPublishers.ofString(body))
            .build();
        // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every
        // server allows.
        HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
        HttpResponse<String> response =
            client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            System.err.println(response.body());
            System.exit(1);
        }
        System.out.println(response.body());
    }
}
```

**C#**

```csharp
// Change a receiver's secret, keeping the old one for a day
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/rotate-secret")
{
    Content = new StringContent(
        """
        {
          "keep_previous_for": "24h"
        }
        """,
        Encoding.UTF8,
        "application/json"),
};
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
    Console.Error.WriteLine(body);
    return 1;
}
Console.WriteLine(body);
return 0;
```

**PHP**

```php
<?php
// Change a receiver's secret, keeping the old one for a day
$curl = curl_init("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/rotate-secret");
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"),
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "keep_previous_for" => "24h",
    ]),
]);
$body = curl_exec($curl);
if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) {
    fwrite(STDERR, $body . "\n");
    exit(1);
}
echo $body, "\n";
```

`keep_previous_for` is `now`, `1h`, `24h` or `7d`. **`now` is the answer to a leak**: the old secret
stops at once. In the console, the same choice is **The old secret** in the rotate dialog.

## When deliveries failed

Every delivery of the last 30 days, with how each attempt went:

**curl**

```sh
#!/bin/sh
# List what a receiver was sent
curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries?status=failed" \
  -H "Authorization: Bearer $AIGENTLY_API_KEY"
```

**JavaScript**

```js
// List what a receiver was sent
const response = await fetch("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries?status=failed", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`,
  },
});
const answer = await response.json();
if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`);
console.log(answer);
```

**Python**

```python
# List what a receiver was sent
# pip install httpx
import os

import httpx

response = httpx.get(
    "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries",
    headers={
        "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}",
    },
    params={
        "status": "failed",
    },
)
if response.is_error:
    raise SystemExit(response.text)
print(response.json())
```

**Go**

```go
// List what a receiver was sent
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	request, err := http.NewRequest("GET", "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries?status=failed", nil)
	if err != nil {
		panic(err)
	}
	request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY"))
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
	body, _ := io.ReadAll(response.Body)
	if response.StatusCode >= 400 {
		fmt.Fprintln(os.Stderr, string(body))
		os.Exit(1)
	}
	fmt.Println(string(body))
}
```

**Java**

```java
// List what a receiver was sent
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries?status=failed"))
            .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY"))
            .method("GET", HttpRequest.BodyPublishers.noBody())
            .build();
        // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every
        // server allows.
        HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
        HttpResponse<String> response =
            client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            System.err.println(response.body());
            System.exit(1);
        }
        System.out.println(response.body());
    }
}
```

**C#**

```csharp
// List what a receiver was sent
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries?status=failed")
{
};
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
    Console.Error.WriteLine(body);
    return 1;
}
Console.WriteLine(body);
return 0;
```

**PHP**

```php
<?php
// List what a receiver was sent
$curl = curl_init("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries?status=failed");
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"),
    ],
]);
$body = curl_exec($curl);
if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) {
    fwrite(STDERR, $body . "\n");
    exit(1);
}
echo $body, "\n";
```

After your receiver was down, send a failed delivery again with a fresh set of retries. One already
waiting is left as it is, and one that was delivered is never sent twice:

**curl**

```sh
#!/bin/sh
# Send a failed delivery again
curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries/9e1b3d5f-7a9c-4e2b-8d4f-6a8c0e2b4d6f/resend" \
  -H "Authorization: Bearer $AIGENTLY_API_KEY"
```

**JavaScript**

```js
// Send a failed delivery again
const response = await fetch("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries/9e1b3d5f-7a9c-4e2b-8d4f-6a8c0e2b4d6f/resend", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`,
  },
});
const answer = await response.json();
if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`);
console.log(answer);
```

**Python**

```python
# Send a failed delivery again
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries/9e1b3d5f-7a9c-4e2b-8d4f-6a8c0e2b4d6f/resend",
    headers={
        "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}",
    },
)
if response.is_error:
    raise SystemExit(response.text)
print(response.json())
```

**Go**

```go
// Send a failed delivery again
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries/9e1b3d5f-7a9c-4e2b-8d4f-6a8c0e2b4d6f/resend", nil)
	if err != nil {
		panic(err)
	}
	request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY"))
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
	body, _ := io.ReadAll(response.Body)
	if response.StatusCode >= 400 {
		fmt.Fprintln(os.Stderr, string(body))
		os.Exit(1)
	}
	fmt.Println(string(body))
}
```

**Java**

```java
// Send a failed delivery again
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries/9e1b3d5f-7a9c-4e2b-8d4f-6a8c0e2b4d6f/resend"))
            .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY"))
            .method("POST", HttpRequest.BodyPublishers.noBody())
            .build();
        // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every
        // server allows.
        HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
        HttpResponse<String> response =
            client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            System.err.println(response.body());
            System.exit(1);
        }
        System.out.println(response.body());
    }
}
```

**C#**

```csharp
// Send a failed delivery again
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries/9e1b3d5f-7a9c-4e2b-8d4f-6a8c0e2b4d6f/resend")
{
};
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
    Console.Error.WriteLine(body);
    return 1;
}
Console.WriteLine(body);
return 0;
```

**PHP**

```php
<?php
// Send a failed delivery again
$curl = curl_init("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/deliveries/9e1b3d5f-7a9c-4e2b-8d4f-6a8c0e2b4d6f/resend");
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"),
    ],
    CURLOPT_POSTFIELDS => "",
]);
$body = curl_exec($curl);
if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) {
    fwrite(STDERR, $body . "\n");
    exit(1);
}
echo $body, "\n";
```

Check a receiver end to end with a signed test event:

**curl**

```sh
#!/bin/sh
# Send a test event
curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/test" \
  -H "Authorization: Bearer $AIGENTLY_API_KEY"
```

**JavaScript**

```js
// Send a test event
const response = await fetch("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/test", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AIGENTLY_API_KEY}`,
  },
});
const answer = await response.json();
if (!response.ok) throw new Error(`${answer.code}: ${answer.detail}`);
console.log(answer);
```

**Python**

```python
# Send a test event
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/test",
    headers={
        "Authorization": f"Bearer {os.environ['AIGENTLY_API_KEY']}",
    },
)
if response.is_error:
    raise SystemExit(response.text)
print(response.json())
```

**Go**

```go
// Send a test event
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	request, err := http.NewRequest("POST", "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/test", nil)
	if err != nil {
		panic(err)
	}
	request.Header.Set("Authorization", "Bearer "+os.Getenv("AIGENTLY_API_KEY"))
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
	body, _ := io.ReadAll(response.Body)
	if response.StatusCode >= 400 {
		fmt.Fprintln(os.Stderr, string(body))
		os.Exit(1)
	}
	fmt.Println(string(body))
}
```

**Java**

```java
// Send a test event
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/test"))
            .header("Authorization", "Bearer " + System.getenv("AIGENTLY_API_KEY"))
            .method("POST", HttpRequest.BodyPublishers.noBody())
            .build();
        // HTTP/1.1: on a plain-http address Java's default asks to upgrade, which not every
        // server allows.
        HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
        HttpResponse<String> response =
            client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            System.err.println(response.body());
            System.exit(1);
        }
        System.out.println(response.body());
    }
}
```

**C#**

```csharp
// Send a test event
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIGENTLY_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/test")
{
};
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
    Console.Error.WriteLine(body);
    return 1;
}
Console.WriteLine(body);
return 0;
```

**PHP**

```php
<?php
// Send a test event
$curl = curl_init("https://api.aigently.ai/v1/webhooks/6d8f0a2c-4e6b-4d8f-9a1c-3e5b7d9f1a3c/test");
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("AIGENTLY_API_KEY"),
    ],
    CURLOPT_POSTFIELDS => "",
]);
$body = curl_exec($curl);
if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE) >= 400) {
    fwrite(STDERR, $body . "\n");
    exit(1);
}
echo $body, "\n";
```

## Live and test receivers

A receiver is **live** or **test**. Live receivers hear about real conversations; test receivers
hear only about conversations made with test keys — appointments an agent books in a test key's
chat included. Point a test receiver at a staging server — or use [`aigently listen`](/cli) to get
test events on your laptop.

## Personal data

Phone numbers, the values a conversation started with and your metadata go only to receivers that
have **Include the values the conversation was started with** turned on. A receiver from before this setting gets them only once you
turn it on.

That means `from_number`, `to_number`, `variables`, `variable_sources` and `metadata`. The fields
events carried before the setting existed are sent either way, so the receivers already reading
them keep working: `caller`, the caller's number as the phone network gave it, and a handoff's
`contact`.

## Delivery

- Each delivery has ten seconds in total to be answered, however slowly your server reads.
- A failed delivery is retried with growing waits; the console shows every attempt and can send one
  again.
- One slow receiver does not hold up another's deliveries.
