osmTalk Docs
Campaigns

Outcomes & Webhooks

How lead outcomes are determined and pushed to your CRM.

After every campaign call ends, two things happen in this order:

  1. Outcome decided — combining call status + post-call analysis
  2. Webhook fired — to the campaign-specific URL, or the org default

How outcomes are decided

osmTalk picks one of these statuses for each lead:

StatusTrigger
completedCall connected and finished normally. See disposition for finer detail.
voicemailVoicemail detection fired during the call.
no_answerCall ended in under 3 seconds or never picked up.
busySIP signaled busy.
failedBot crashed / SIP error / network failure. Retried if policy allows.
dnc_blockedPhone matched the DNC list — call was never placed.

Within completed, the disposition field captures the business outcome — derived from post-call analysis when enabled:

DispositionSource
qualifiedanalysis.lead_qualified === true OR analysis.success === true
not_interestedanalysis.next_action === "not_interested"
callbackanalysis.next_action === "callback_later"
completedFallback 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:

  1. campaign.webhookUrl if set (per-campaign), OR
  2. org_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

PropertyValue
MethodPOST
Content-Typeapplication/json
Timeout15 seconds
RetriesCurrently no automatic retries — return 2xx to confirm receipt; we log non-2xx but don't redrive
OrderNot 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}`,
  });
}