Guides
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 when you need it. |
conversation.deleted |
Removed by retention, from the console, through the API, 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 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 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 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, whether or not a receiver heard it. Which events a call sends as it ends is in the endings table.
What you receive
{
"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, 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
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:
#!/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
The same function checks a context lookup 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.
#!/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"
]
}'
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:
#!/bin/sh
# List webhook receivers
curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/webhooks" \
-H "Authorization: Bearer $AIGENTLY_API_KEY"
#!/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
}'
#!/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"
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.
#!/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"
}'
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:
#!/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"
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:
#!/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"
Check a receiver end to end with a signed test event:
#!/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"
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 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.