Conversation Events

Conversation Events

Conversation events notify you about the lifecycle of conversations in your campaigns.

Available Events

EventDescription
CONVERSATION_QUEUEDConversation is queued and waiting to start
CONVERSATION_STARTEDConversation has started with the candidate
CONVERSATION_COMPLETEDConversation completed successfully
CONVERSATION_NEEDS_REVIEWConversation requires manual review
CONVERSATION_OPTED_OUTCandidate opted out of the conversation
CONVERSATION_MAX_NUDGES_REACHEDAll configured nudges were sent without a response
START_CONVERSATION_FAILEDFailed to start the conversation

CONVERSATION_STARTED

Triggered when the first message is successfully delivered to the candidate.

Payload

{
  "event": "CONVERSATION_STARTED",
  "eventId": "evt_abc123",
  "eventTimestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "recordId": "conv_xyz789",
    "organizationId": "org_123456",
    "conversationStatus": "ONGOING",
    "candidateId": "cand_abc123",
    "externalId": "your_external_id",
    "externalCampaignId": "your_campaign_external_id",
    "participantFirstName": "John",
    "participantLastName": "Doe",
    "campaignConversationUrl": "https://ai.joinpopp.com/campaign/outbound/{campaignId}?conversationId={conversationId}"
  }
}

Data Fields

FieldTypeDescription
conversationStatusstringCurrent status of the conversation (e.g., ONGOING)
candidateIdstringPopp internal candidate ID
externalIdstringYour external reference ID for the conversation
externalCampaignIdstringYour external reference ID for the campaign
participantFirstNamestringCandidate's first name
participantLastNamestringCandidate's last name
campaignConversationUrlstringDirect link to view the conversation in Popp

CONVERSATION_COMPLETED

Triggered when a conversation reaches a successful conclusion.

Payload

{
  "event": "CONVERSATION_COMPLETED",
  "eventId": "evt_abc123",
  "eventTimestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "recordId": "conv_xyz789",
    "organizationId": "org_123456",
    "conversationStatus": "COMPLETED_SCREENING_PASSED",
    "scorecardTotalValue": 85,
    "screeningQuestionsOutcome": [
      {
        "screeningQuestion": "Do you have 5+ years of experience?",
        "responseSummary": "Yes, I have 7 years of experience",
        "questionOutcome": "PASSED"
      },
      {
        "screeningQuestion": "Are you authorized to work in the US?",
        "responseSummary": "No, I would need visa sponsorship",
        "questionOutcome": "FAILED"
      },
      {
        "screeningQuestion": "Are you willing to relocate?",
        "responseSummary": null,
        "questionOutcome": "NOT_COMPLETED"
      }
    ],
    "candidateId": "cand_abc123",
    "externalId": "your_external_id",
    "externalCampaignId": "your_campaign_external_id",
    "participantFirstName": "John",
    "participantLastName": "Doe",
    "campaignConversationUrl": "https://ai.joinpopp.com/campaign/outbound/{campaignId}?conversationId={conversationId}"
  }
}

Additional Data Fields

FieldTypeDescription
conversationStatusstringFinal status (COMPLETED_SCREENING_PASSED, COMPLETED_SCREENING_FAILED, COMPLETED_CANDIDATE_NOT_INTERESTED)
scorecardTotalValuenumberTotal score from screening questions (0-100)
screeningQuestionsOutcomearrayArray of screening question results

Screening Question Outcome Object

FieldTypeDescription
screeningQuestionstringThe screening question asked
responseSummarystringSummary of the candidate's response
questionOutcomestringOutcome of the question (PASSED, FAILED, NOT_COMPLETED)

CONVERSATION_NEEDS_REVIEW

Triggered when a conversation is flagged for human review. This can be the AI classifying a reason from the participant's reply, or a system event (e.g. more-time-requested, human takeover). See the needsReviewReason values below.

Payload

{
  "event": "CONVERSATION_NEEDS_REVIEW",
  "eventId": "evt_abc123",
  "eventTimestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "recordId": "conv_xyz789",
    "organizationId": "org_123456",
    "externalId": "your_external_id",
    "externalCampaignId": "your_campaign_external_id",
    "campaignConversationUrl": "https://ai.joinpopp.com/campaign/outbound/{campaignId}?conversationId={conversationId}",
    "isHumanTakeover": false,
    "needsReviewReason": "OUT_OF_OFFICE",
    "returnDate": "2024-01-22"
  }
}
FieldTypeDescription
recordIdstringThe conversation ID
organizationIdstringYour organization ID
externalIdstringYour external ID for the conversation (if set)
externalCampaignIdstringYour external campaign ID (if set)
campaignConversationUrlstringDirect link to the conversation in the Popp dashboard
isHumanTakeoverbooleanWhether the conversation is currently in human takeover mode. When true, the AI agent is paused and a human is handling the conversation
needsReviewReasonstringCategorises why the conversation was flagged (see values below). Set for both AI-classified and system-triggered reviews; omitted only when no specific category applies
returnDatestring | nullOptional. A return date in YYYY-MM-DD format, included only for OUT_OF_OFFICE replies. Otherwise it is null or omitted entirely — treat both as "no return date"

needsReviewReason values

needsReviewReason is one of the following. The first group is classified by the AI from the participant's reply; the second is set by system events (and some of these also have their own dedicated webhook event).

AI-classified:

ValueMeaning
OUT_OF_OFFICEThe participant is away or on leave. returnDate carries the date they said they'll be back, when they gave one
ASKED_FOR_HUMANThe participant explicitly asked to speak with a person
UNANSWERED_QUESTIONThe participant asked something the AI could not answer
SPECIAL_NEEDS_REQUESTThe participant raised a special accommodation or access need
OTHERFlagged for review for a reason that doesn't fit the categories above

System-triggered:

ValueMeaning
MORE_TIME_REQUESTEDThe participant asked for different or additional interview times (also sent as the CALENDAR_MORE_TIME_REQUESTED event)
AVAILABILITY_NOT_FOUNDNo suitable interview availability could be found (also sent as the CALENDAR_AVAILABILITY_NOT_FOUND event)
HUMAN_TAKEOVERA message arrived while the conversation was in human-takeover mode (isHumanTakeover is true in this payload)
CLOSED_CONVERSATION_REPLYThe participant replied after the conversation had already closed

Not every review has a specific reason — sometimes it's just a sensitive topic or an unclear reply. When there's no clear category, needsReviewReason is left out of the payload, so always check whether it's present before using it.


CONVERSATION_OPTED_OUT

Triggered when a candidate opts out of receiving messages.

Payload

{
  "event": "CONVERSATION_OPTED_OUT",
  "eventId": "evt_abc123",
  "eventTimestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "recordId": "conv_xyz789",
    "organizationId": "org_123456",
    "campaignConversationUrl": "https://ai.joinpopp.com/campaign/outbound/{campaignId}?conversationId={conversationId}"
  }
}

CONVERSATION_MAX_NUDGES_REACHED

Triggered when all configured follow-up nudges have been sent for a conversation and the participant has not responded. Fires once per nudge sequence, for any campaign type. participantEmail is the email of the person who was being nudged (the one who didn't reply) — null for phone-only SMS/WhatsApp conversations. campaignType tells you which flow it was: for AVAILABILITY_OUTREACH that's the interviewer/hiring-manager, otherwise the candidate.

Payload

{
  "event": "CONVERSATION_MAX_NUDGES_REACHED",
  "eventId": "evt_abc123",
  "eventTimestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "recordId": "conv_xyz789",
    "organizationId": "org_123456",
    "conversationId": "conv_xyz789",
    "campaignId": "camp_123",
    "campaignType": "SCHEDULING",
    "participantEmail": "[email protected]",
    "nudgesSent": 3,
    "externalId": "your_conversation_id",
    "externalCampaignId": "your_campaign_id"
  }
}

Data Fields

FieldTypeDescription
conversationIdstringPopp conversation ID
campaignIdstringPopp campaign ID
campaignTypestring | nullCampaign type (e.g. SCHEDULING, AVAILABILITY_OUTREACH)
participantEmailstring | nullParticipant's email; null for phone-only (SMS/WhatsApp) conversations
nudgesSentnumberNumber of nudges actually sent before giving up
externalIdstring | nullYour external reference ID for the conversation, if set
externalCampaignIdstring | nullYour external reference ID for the campaign, if set

CONVERSATION_QUEUED

Triggered when a conversation is created but waiting to start (e.g., there's already an ongoing conversation with the same participant).

Payload

{
  "event": "CONVERSATION_QUEUED",
  "eventId": "evt_abc123",
  "eventTimestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "recordId": "conv_xyz789",
    "organizationId": "org_123456",
    "campaignConversationUrl": "https://ai.joinpopp.com/campaign/outbound/{campaignId}?conversationId={conversationId}"
  }
}

START_CONVERSATION_FAILED

Triggered when a conversation fails to start.

Payload

{
  "event": "START_CONVERSATION_FAILED",
  "eventId": "evt_abc123",
  "eventTimestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "recordId": "conv_xyz789",
    "organizationId": "org_123456",
    "campaignConversationUrl": "https://ai.joinpopp.com/campaign/outbound/{campaignId}?conversationId={conversationId}"
  }
}

Common Failure Reasons

  • Invalid phone number format
  • Phone number not reachable
  • Message delivery failed

Example: Handling Conversation Events

interface ConversationWebhookData {
  recordId: string;
  organizationId: string;
  conversationStatus?: string;
  scorecardTotalValue?: number;
  screeningQuestionsOutcome?: {
    screeningQuestion: string;
    responseSummary: string;
    questionOutcome: 'PASSED' | 'FAILED' | 'NOT_COMPLETED';
  }[];
  candidateId?: string;
  externalId?: string;
  externalCampaignId?: string;
  participantFirstName?: string;
  participantLastName?: string;
  campaignConversationUrl?: string;
  isHumanTakeover?: boolean;
  needsReviewReason?:
    | 'OUT_OF_OFFICE'
    | 'ASKED_FOR_HUMAN'
    | 'UNANSWERED_QUESTION'
    | 'SPECIAL_NEEDS_REQUEST'
    | 'OTHER'
    | 'MORE_TIME_REQUESTED'
    | 'AVAILABILITY_NOT_FOUND'
    | 'HUMAN_TAKEOVER'
    | 'CLOSED_CONVERSATION_REPLY';
  returnDate?: string | null;
}

function handleConversationWebhook(payload: {
  event: string;
  eventId: string;
  eventTimestamp: string;
  data: ConversationWebhookData;
}) {
  const { event, data } = payload;

  switch (event) {
    case 'CONVERSATION_STARTED':
      // Update your CRM to show conversation is active
      updateCandidateStatus(data.recordId, 'in_progress');
      break;

    case 'CONVERSATION_COMPLETED':
      // Process screening results
      if (data.scorecardTotalValue && data.scorecardTotalValue >= 70) {
        moveToNextStage(data.externalId);
      }
      break;

    case 'CONVERSATION_NEEDS_REVIEW':
      // Always alert your team for manual review
      notifyRecruiters(data.campaignConversationUrl);
      // needsReviewReason is optional — check before using it
      if (data.needsReviewReason === 'OUT_OF_OFFICE') {
        // returnDate may be null or omitted
        scheduleFollowUp(data.recordId, data.returnDate ?? null);
      }
      break;

    case 'CONVERSATION_OPTED_OUT':
      // Update opt-out status in your system
      markAsOptedOut(data.recordId);
      break;
  }
}