Docsapi
Webhooks
Receive real-time events — new conversation, lead captured, handoff requested — with HMAC verification.
Webhooks push Simple Agent events to your server in real time. Use them to sync leads with your CRM, trigger automations, or log conversations in your system.
Set up a webhook
- Dashboard → Settings → Webhooks → Add endpoint
- Enter your server URL
- Select the events you want to receive
- Save — Simple Agent sends a verification event immediately
Available events
| Event | When it fires |
|---|---|
conversation.created |
User's first message |
conversation.ended |
Conversation ended (timeout or user closed) |
message.received |
Each user message |
message.sent |
Each agent response |
lead.captured |
User's email collected via an Action |
handoff.requested |
User requested human support |
source.indexed |
Training source indexed successfully |
source.failed |
Source indexing failed |
Payload format
{
"event": "lead.captured",
"id": "evt_01hwxyz...",
"created_at": "2026-05-14T10:30:00Z",
"agent_id": "ag_xxx",
"data": {
"lead": {
"email": "user@example.com",
"name": "Jane Smith",
"phone": "+15551234567"
},
"conversation_id": "conv_xxx",
"source_channel": "widget",
"session_locale": "en"
}
}
HMAC verification
Simple Agent signs each request with HMAC-SHA256 using the Webhook Secret generated in the dashboard. You must verify the signature to reject unauthorized requests.
The header sent:
X-Simple Agent-Signature: sha256=a4b7c3d2e1f0...
X-Simple Agent-Timestamp: 1715686800
Verify in Node.js
import crypto from "crypto";
function verifyWebhook(
payload: string, // raw body string (not parsed)
signature: string, // X-Simple Agent-Signature header
timestamp: string, // X-Simple Agent-Timestamp header
secret: string // your Webhook Secret
): boolean {
// Reject requests older than 5 minutes
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) return false;
const signedPayload = `${timestamp}.${payload}`;
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// In Express:
app.post("/webhook/simple-agent", express.raw({ type: "application/json" }), (req, res) => {
const isValid = verifyWebhook(
req.body.toString(),
req.headers["x-simpleagent-signature"] as string,
req.headers["x-simpleagent-timestamp"] as string,
process.env.SIMPLE AGENT_WEBHOOK_SECRET!
);
if (!isValid) return res.status(401).json({ error: "Invalid signature" });
const event = JSON.parse(req.body.toString());
// process event...
res.status(200).json({ received: true });
});
Verify in Python
import hmac
import hashlib
import time
def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
# Reject old requests (>5 min)
if abs(time.time() - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.{payload.decode()}"
expected = "sha256=" + hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Expected response
Your endpoint must return HTTP 200 within 10 seconds. If it doesn't return 2xx, Simple Agent considers it a failure and retries.
Retry policy:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 8 hours |
After 5 failed attempts, the event is discarded and appears as failed in the webhook log.
Delivery log
In the dashboard → Settings → Webhooks → View deliveries, you can see:
- Status of each delivery (success / failure)
- Sent payload
- Your server's response
- Resend button for failures
Resend manually via API
POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/retry
curl -X POST https://simple-agent.me/api/v1/webhooks/wh_xxx/deliveries/del_xxx/retry \
-H "Authorization: Bearer af_live_xxx"
Security tips
- Never rely on IP alone — always validate the HMAC signature
- Use
timingSafeEqualto compare signatures — prevents timing attacks - Verify the timestamp — reject events older than 5 minutes to prevent replay attacks
- Save
event.id— ignore events with an already-processed ID (idempotency)