Outbound Webhooks Reference
Outbound webhooks allow HuskyVoice AI to push real-time event data to your server as JSON payloads via HTTPS POST requests.
How to Listen to Webhooks
HuskyVoice sends real-time event data to a URL you provide. Follow these steps to start receiving events:
- Create a public HTTPS endpoint on your server to receive POST requests
- Register your URL — Go to Dashboard → Integrations → Outbound Webhooks → Add Endpoint
- Select the events you want to listen to (e.g.,
call.completed,call.failed) - Return
200 OKimmediately when your server receives the request - Process the event asynchronously after acknowledging
Example — Webhook listener:
- cURL
- Python
- Node.js
- n8n
# Simulate a webhook delivery to test your endpoint
curl -X POST https://your-server.com/webhook \
-H "Content-Type: application/json" \
-H "X-Husky-Signature: your_expected_hmac_hex" \
-d '{
"event_id": "evt_test123",
"event_type": "call.completed",
"created_at": "2026-06-01T10:00:00Z",
"org_id": "org_abc_123",
"data": {
"call_id": "call_987654321",
"agent_id": "agent_alpha",
"status": "completed",
"duration_seconds": 124,
"summary": "Customer confirmed appointment."
}
}'
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
# Always acknowledge immediately
data = request.get_json()
event_type = data.get('event_type')
event_data = data.get('data', {})
if event_type == 'call.completed':
print(f"Call {event_data['call_id']} completed. Duration: {event_data['duration_seconds']}s")
print(f"Summary: {event_data['summary']}")
if event_type == 'call.failed':
print(f"Call {event_data['call_id']} failed.")
return jsonify({"status": "ok"}), 200
if __name__ == '__main__':
app.run(port=3000)
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
// Always acknowledge immediately
res.status(200).send('OK');
// Process the event asynchronously
const { event_type, data } = req.body;
if (event_type === 'call.completed') {
console.log(`Call ${data.call_id} completed. Duration: ${data.duration_seconds}s`);
console.log(`Summary: ${data.summary}`);
}
if (event_type === 'call.failed') {
console.log(`Call ${data.call_id} failed.`);
}
});
app.listen(3000, () => console.log('Webhook listener running on port 3000'));
Add a Webhook trigger node in n8n. Register the generated webhook URL in the HuskyVoice Dashboard under Integrations → Outbound Webhooks → Add Endpoint.
{
"name": "HuskyVoice Event Listener",
"nodes": [
{
"parameters": {
"path": "huskyvoice-events",
"options": {}
},
"id": "1",
"name": "HuskyVoice Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [250, 300]
},
{
"parameters": {
"jsCode": "const { event_type, data } = $input.first().json;\n\nif (event_type === 'call.completed') {\n console.log(`Call ${data.call_id} completed. Duration: ${data.duration_seconds}s`);\n console.log(`Summary: ${data.summary}`);\n}\n\nif (event_type === 'call.failed') {\n console.log(`Call ${data.call_id} failed.`);\n}\n\nreturn $input.all();"
},
"id": "2",
"name": "Process Event",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [450, 300]
}
],
"connections": {
"HuskyVoice Webhook": {
"main": [[{ "node": "Process Event", "type": "main", "index": 0 }]]
}
},
"settings": {},
"meta": { "instanceId": "huskyvoice-docs" }
}
Use a tool like ngrok to expose your local server to the internet while testing — run ngrok http 3000 and use the generated HTTPS URL in your dashboard.
Event Types
| Event | Description |
|---|---|
call.initiated | Fired when the telephony system begins dialing the contact. |
call.answered | Fired when the recipient picks up the call. |
call.completed | Fired when the call ends successfully (includes summary and transcripts). |
call.failed | Fired when the call could not be completed (busy, no answer, or technical error). |
call.disallowed | Fired when a call is blocked due to DND or policy restrictions. |
Payload Schema
Every webhook share a consistent envelope structure. The data object contains the event-specific details.
{
"event_id": "evt_123456789",
"event_type": "call.completed",
"created_at": "2025-04-16T12:00:00Z",
"org_id": "org_abc_123",
"data": {
"call_id": "call_987654321",
"agent_id": "agent_alpha",
"status": "completed",
"duration_seconds": 124,
"transcript": "[...]",
"summary": "Customer interested in a follow-up demo."
}
}
Signature Verification
HuskyVoice signs all webhook payloads with an HMAC-SHA256 signature to ensure authenticity. The signature is sent in the X-Husky-Signature header.
- cURL
- Python
- Node.js
- n8n
# Compute the expected HMAC to compare with X-Husky-Signature
SECRET="YOUR_WEBHOOK_SECRET"
PAYLOAD='{"event_id":"evt_123","event_type":"call.completed",...}'
echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET"
# Compare the hex output with the X-Husky-Signature header value
import hmac
import hashlib
def verify_signature(payload: str, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
const crypto = require('crypto');
function verify(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const expected = hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// n8n Code node — verify signature before processing the event
const crypto = require("crypto");
const secret = "YOUR_WEBHOOK_SECRET";
const payload = JSON.stringify($input.first().json.body);
const receivedSig = $input.first().json.headers["x-husky-signature"];
const expectedSig = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
const isValid = crypto.timingSafeEqual(
Buffer.from(receivedSig),
Buffer.from(expectedSig)
);
if (!isValid) {
throw new Error("Invalid webhook signature — request rejected");
}
return $input.all();
Implementation Best Practices
- Acknowledge Immediately: Your server should return a
200 OKresponse instantly. - Async Processing: Perform slow operations (like CRM sync) in a background worker after acknowledging the webhook.
- Idempotency: Use the
event_idto ignore duplicate deliveries. - Security: Always verify signatures and use HTTPS endpoints.