Outcomes & Webhooks
How lead outcomes are determined and pushed to your CRM.
After every campaign call ends, two things happen in this order:
- Outcome decided — combining call status + post-call analysis
- Webhook fired — to the campaign-specific URL, or the org default
How outcomes are decided
osmTalk picks one of these statuses for each lead:
| Status | Trigger |
|---|---|
completed | Call connected and finished normally. See disposition for finer detail. |
voicemail | Voicemail detection fired during the call. |
no_answer | Call ended in under 3 seconds or never picked up. |
busy | SIP signaled busy. |
failed | Bot crashed / SIP error / network failure. Retried if policy allows. |
dnc_blocked | Phone matched the DNC list — call was never placed. |
Within completed, the disposition field captures the business outcome — derived from post-call analysis when enabled:
| Disposition | Source |
|---|---|
qualified | analysis.lead_qualified === true OR analysis.success === true |
not_interested | analysis.next_action === "not_interested" |
callback | analysis.next_action === "callback_later" |
completed | Fallback when analysis doesn't include a clear next_action |
Without post-call analysis enabled, every completed call gets disposition: "completed". You can do your own classification by reading the transcript via webhook.
Webhook event
Every lead transition into a terminal status fires a webhook to:
campaign.webhookUrlif set (per-campaign), ORorg_settings.defaultWebhookUrl(set under Settings → Webhooks)
Event payload
{
"event": "campaign.lead_completed",
"timestamp": "2026-05-06T11:23:45.123Z",
"campaign": {
"id": "cmp_xxx",
"name": "Q2 Renewals — Mumbai"
},
"lead": {
"id": "ld_xxx",
"phoneNumber": "+919876543210",
"variables": {
"first_name": "Arjun",
"company": "Acme",
"policy_type": "Health"
},
"status": "completed",
"disposition": "qualified",
"attempts": 1,
"callId": "call_xxx",
"outcome": {
"lead_qualified": true,
"next_action": "book_demo",
"budget_inr": 250000,
"summary": "Caller interested in Pro tier. Confirmed demo Friday 3pm.",
"sentiment": "positive",
"success": true
}
}
}outcome is the full post-call analysis object — same shape your agent declared.
Signing (HMAC-SHA256)
If webhookSecret is set on the campaign or org, every request includes:
X-OsmTalk-Signature: sha256=<hex_digest>Verify with:
// Node.js example
import crypto from "node:crypto";
function verify(rawBody, secret, headerValue) {
const [algo, sig] = headerValue.split("=");
if (algo !== "sha256") return false;
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
}Delivery semantics
| Property | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Timeout | 15 seconds |
| Retries | Currently no automatic retries — return 2xx to confirm receipt; we log non-2xx but don't redrive |
| Order | Not strictly ordered — events for the same lead can arrive late if a retry was scheduled |
Idempotency: every webhook includes a unique lead.callId. Use that as the dedupe key on your end if you accept the same event from multiple replicas.
Other webhook events
If enableSentiment / enableSuccessEvaluation / a postCallAnalysis.schema are configured on the agent, a separate call.analysis_completed event fires for every call (not just campaign leads) to the org-level defaultWebhookUrl. See Post-call Analysis.
The shape is similar but rooted on the call, not the campaign lead:
{
"event": "call.analysis_completed",
"timestamp": "...",
"call": { /* call fields including dynamicVariables, metadata */ },
"analysis": { /* extracted fields + summary + sentiment */ }
}Testing webhooks
Use a tool like webhook.site to capture requests in real time. Set its URL as campaign.webhookUrl and start a campaign with 1 lead — you'll see the full payload land.
For local development, use ngrok or cloudflared tunnel to route public webhooks to your localhost.
CRM integration recipes
Push qualified leads to HubSpot
Subscribe your webhook handler to event === "campaign.lead_completed" and:
if (event.lead.disposition === "qualified") {
await hubspot.contacts.create({
properties: {
phone: event.lead.phoneNumber,
firstname: event.lead.variables.first_name,
company: event.lead.variables.company,
osmtalk_call_id: event.lead.callId,
lead_status: "qualified_by_voice_ai",
voice_ai_summary: event.lead.outcome?.summary,
},
});
}Slack ping for hot leads
if (event.lead.outcome?.next_action === "book_demo") {
await slack.chat.postMessage({
channel: "#hot-leads",
text: `🔥 ${event.lead.variables.first_name} from ${event.lead.variables.company} wants a demo. Call: ${event.lead.callId}`,
});
}