Skip to main content

Outbound Webhooks

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:

  1. Create a public HTTPS endpoint on your server to receive POST requests
  2. Register your URL — Go to Dashboard → Workflows → Webhooks → Add Webhook (1 endpoint on the Base plan, up to 5 on Professional and Enterprise plans)
  3. Select the events you want to listen to (e.g., call.completed, call.failed)
  4. Return 200 OK immediately when your server receives the request — HuskyVoice waits up to 8 seconds for a response before treating the attempt as failed and queuing a retry
  5. Process the event asynchronously after acknowledging

Example — Webhook listener:

# Simulate a webhook delivery to test your endpoint
curl -X POST https://your-server.com/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Id: evt_test123" \
-H "X-Webhook-Timestamp: 1748000000" \
-H "X-Webhook-Signature: v1=your_expected_hmac_base64" \
-d '{
"event_id": "evt_test123",
"event_type": "call.completed",
"created_at": "2026-06-01T10:00:00Z",
"data": {
"call_id": "call_987654321",
"status": "completed",
"call_direction": "outbound",
"call_to": "+15551234567",
"call_duration_seconds": 124,
"agent_id": "agent_alpha",
"agent_name": "Priya",
"call_recording": "https://studio.huskyvoice.ai/spark/calls/call_987654321",
"call_analytics": {
"summary": "Customer confirmed appointment.",
"sentiment": "positive",
"disposition": "appointment_booked"
},
"call_transcript": [ { "speaker": "agent", "text": "Hi, this is Priya...", "timestamp": 0.5 } ],
"call_triggered_at": "2026-06-01T09:58:00Z",
"scheduled_at": null,
"completed_at": "2026-06-01T10:00:00Z",
"error": null,
"contact_name": "Jordan Lee",
"contact_email": "jordan@example.com",
"custom_data": null
}
}'
Testing Locally

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

EventDescription
call.initiatedFired immediately when the telephony system successfully triggers the call, before it's answered or completed.
call.completedFired when the call ends successfully. Includes full call analytics and transcript.
call.failedFired when the call could not be completed due to a technical failure (busy, no answer, carrier/infrastructure error).
call.disallowedFired when a call is blocked for an internal policy reason (e.g. insufficient credits) — distinct from call.failed, which is a technical failure during an attempted call.
note

call.answered is not currently a supported event — only the four events above are deliverable.

Payload Schema

Every webhook shares a consistent envelope structure. The data object contains the event-specific details and varies slightly by event type — not every field is populated for every event.

{
"event_id": "evt_c961fe0c-9cbd-4b7b-855e-0a555205f8f0",
"event_type": "call.completed",
"created_at": "2026-06-01T10:00:00.000Z",
"data": {
"call_id": "call_987654321",
"status": "completed",
"call_direction": "outbound",
"call_to": "+15551234567",
"call_duration_seconds": 124,
"agent_id": "agent_alpha",
"agent_name": "Priya",
"call_recording": "https://studio.huskyvoice.ai/spark/calls/call_987654321",
"call_analytics": {
"summary": "Customer confirmed appointment.",
"sentiment": "positive",
"next_steps": "No further action needed.",
"disposition": "appointment_booked",
"extracted_details": { "...": "..." },
"appointment_details": { "...": "..." }
},
"call_transcript": [
{ "speaker": "agent", "text": "Hi, this is Priya from...", "timestamp": 11.6 }
],
"call_triggered_at": "2026-06-01T09:58:00.000Z",
"scheduled_at": null,
"completed_at": "2026-06-01T10:00:00.000Z",
"error": null,
"contact_name": "Jordan Lee",
"contact_email": "jordan@example.com",
"custom_data": null
}
}

Envelope fields

FieldTypeDescription
event_idstringUnique ID for this event delivery, prefixed evt_. Use this for idempotency — see below.
event_typestringOne of the event types above.
created_atstring (ISO 8601)When this event was generated.
dataobjectEvent-specific payload — see below.

data fields

FieldTypeDescription
call_idstring | nullUnique identifier for the call.
statusstringcompleted, initiated, disallowed, or failed.
call_directionstringinbound or outbound.
call_tostringPhone number dialed. Present only when call_direction is outbound.
call_fromstringCaller's phone number. Present only when call_direction is inbound.
call_duration_secondsnumber | nullTotal call duration in seconds.
agent_idstring | nullID of the agent that handled the call.
agent_namestring | nullDisplay name of the agent.
call_recordingstring | nullURL to view/play the call recording.
call_analyticsobject | nullAI-generated call analysis. Commonly includes summary, sentiment, next_steps, disposition, and extracted_details — the exact set of fields is configurable per organization, so don't assume a fixed schema. Also includes appointment_details when an appointment was booked during the call. Populated on call.completed only.
call_transcriptarray | nullOrdered list of { speaker, text, timestamp } turns. Populated on call.completed only.
call_triggered_atstring (ISO 8601) | nullWhen the call was actually placed.
scheduled_atstring (ISO 8601) | nullWhen the call was scheduled for, if placed via the scheduling API.
completed_atstring (ISO 8601) | nullWhen the call ended.
errorstring | nullError/reason message, populated on call.failed and call.disallowed; null otherwise.
contact_namestring | nullName of the contact, as submitted at call setup or captured by the agent during the call.
contact_emailstring | nullEmail of the contact, as submitted at call setup or captured by the agent during the call.
custom_dataobject | nullAny custom metadata you attached when triggering the call.

Signature Verification

HuskyVoice signs every webhook delivery with an HMAC-SHA256 signature so you can verify it actually came from us. Every request includes:

HeaderAlways sent?Description
Content-TypeYesAlways application/json.
User-AgentYesAlways AppEQ-Webhooks/1.0.
X-Webhook-IdYesThe event_id of this delivery.
X-Webhook-TimestampOnly if signing is enabledUnix timestamp (seconds) the request was sent. Reject requests where this is too far in the past to guard against replay attacks.
X-Webhook-SignatureOnly if signing is enabledv1=<signature> — HMAC-SHA256 of "{timestamp}.{raw_body}", base64-encoded, using your webhook's secret.

Your webhook secret is shown once at creation time, in the format whsec_.... Signature verification is enabled by default — if it's turned off for an endpoint, X-Webhook-Timestamp and X-Webhook-Signature will not be sent at all. Disabling it is not recommended.

# Compute the expected signature to compare with X-Webhook-Signature
SECRET="whsec_your_webhook_secret"
TIMESTAMP="1748000000" # value of the X-Webhook-Timestamp header
BODY='{"event_id":"evt_123",...}' # exact raw request body, unmodified

echo -n "${TIMESTAMP}.${BODY}" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64
# Compare the output (prefixed with "v1=") against the X-Webhook-Signature header value

Implementation Best Practices

  1. Acknowledge Immediately: Your server should return a 200 OK response within 8 seconds, before doing any real work.
  2. Async Processing: Perform slow operations (like CRM sync) in a background worker after acknowledging the webhook.
  3. Idempotency: Use the event_id to ignore duplicate deliveries — retries reuse the same event_id.
  4. Security: Always verify signatures using a constant-time comparison, and use HTTPS endpoints only (enforced at registration).
  5. Retries: A failed delivery (non-2xx response, timeout, or connection error) is retried up to 5 times total with backoff: immediately, then after 30s, 2min, 10min, and 1hr.
  6. Auto-disable: A webhook endpoint is automatically disabled after 50 consecutive failed delivery attempts (including retries) with no successful delivery in between. Re-enable it from the dashboard once your endpoint is healthy again.