You can test your endpoint three ways: the Test button, the test endpoint in the API, and a real call. All three send signed requests and appear in the delivery log.
Test from the dashboard#
Open Settings, Webhooks#
Enter the URL you want to test in URL. It does not need to be saved.
Press Test#
Tried sends a signed test event to the URL.
Read the result#
"Test event delivered" means your endpoint replied 2xx. "Test event failed" means every attempt failed. The Recent deliveries table refreshes and shows each attempt with its status code or error.
The test uses the normal retry rules, so a failing URL takes a few seconds to report: up to 4 attempts with 1, 2 and 4 second waits, plus the timeout of each attempt.
The Test button in an agent's Webhook Settings sends an unsigned sample body directly from your browser, with no delivery log entry. Use it only to check that the URL is reachable from your network.
Test from the API#
POST /workspace/test-webhook sends the signed test event.
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | No | Target URL. Defaults to the workspace webhook URL |
curl -X POST https://api.tried.com.au/workspace/test-webhook \
-H "Authorization: Bearer $TRIED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/front-desk"}'const res = await fetch("https://api.tried.com.au/workspace/test-webhook", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TRIED_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://example.com/hooks/front-desk" }),
});
console.log(await res.json()); // { success: true }import os, requests
r = requests.post(
"https://api.tried.com.au/workspace/test-webhook",
headers={"Authorization": f"Bearer {os.environ['TRIED_API_KEY']}"},
json={"url": "https://example.com/hooks/front-desk"},
)
print(r.json()){ "success": true }success is true when an attempt got a 2xx. With no url and no workspace URL the endpoint returns 400 with "No webhook URL configured".
Pass an agent's URL as url to send a signed test to that agent's endpoint.
Receive webhooks on your laptop#
Tried does not send webhooks to localhost or private addresses. Expose your local server through a public tunnel and register the tunnel URL.
# terminal 1: your receiver on port 8099
node server.js
# terminal 2
ngrok http 8099
# copy the https://<random>.ngrok-free.app URLcloudflared tunnel --url http://localhost:8099
# copy the https://<random>.trycloudflare.com URLThen set https://<tunnel-host>/tried/webhook as the workspace URL or as an agent URL and press Test.
A minimal receiver#
This receiver verifies the signature, prints the event and replies 204. It is enough to watch events arrive while you build.
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
const KEY = process.env.TRIED_WEBHOOK_KEY!;
function verify(raw: string, sig: string) {
const m = /^v=(\d+),d=([0-9a-f]+)$/.exec(sig);
if (!m || Math.abs(Date.now() - Number(m[1])) > 300_000) return false;
const a = Buffer.from(createHmac("sha256", KEY).update(raw + Number(m[1])).digest("hex"), "hex");
const b = Buffer.from(m[2]!, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
createServer((req, res) => {
let raw = "";
req.on("data", (c) => (raw += c));
req.on("end", () => {
const ok = verify(raw, String(req.headers["x-tried-signature"] ?? ""));
const body = JSON.parse(raw || "{}");
console.log(ok ? "verified" : "BAD SIGNATURE", body.event, body.call?.call_id ?? body.chat?.chat_id ?? "");
res.writeHead(ok ? 204 : 401).end();
});
}).listen(8099);import hashlib, hmac, json, os, re, time
from http.server import BaseHTTPRequestHandler, HTTPServer
KEY = os.environ["TRIED_WEBHOOK_KEY"].encode()
def verify(raw: bytes, sig: str) -> bool:
m = re.fullmatch(r"v=(\d+),d=([0-9a-f]+)", sig or "")
if not m or abs(int(time.time() * 1000) - int(m.group(1))) > 300_000:
return False
expected = hmac.new(KEY, raw + str(int(m.group(1))).encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, m.group(2))
class Hook(BaseHTTPRequestHandler):
def do_POST(self):
raw = self.rfile.read(int(self.headers.get("content-length", 0)))
ok = verify(raw, self.headers.get("x-tried-signature", ""))
body = json.loads(raw or b"{}")
print("verified" if ok else "BAD SIGNATURE", body.get("event"))
self.send_response(204 if ok else 401)
self.end_headers()
HTTPServer(("", 8099), Hook).serve_forever()Trigger real events#
To see real payloads, start a web call on the agent from the dashboard (Test Audio) or with create-web-call. Talk for a few turns and hang up. You receive, in order:
call_startedwhen the call connects.transcript_updatedwhile you talk, if the agent opted in.call_endedwhen you hang up.call_analyzedonce post-call analysis is stored.
Transfer events need a phone call because transfers are not supported in web calls.
For chat events, start a chat from Chat Agents with Test chat, send a message and end it.
For alert events, press Test on a rule in Alerting. That sends alert_test.
Local development without keys#
When you run Tried locally with mock providers, webhooks are real HTTP requests. A receiver on your own machine works without a tunnel when the API runs with EGRESS_ALLOW_PRIVATE=1, which turns off the private address check. Leave it unset in production.