Conversation Events
Conversation Events
Conversation events notify you about the lifecycle of conversations in your campaigns.
Available Events
| Event | Description |
|---|---|
CONVERSATION_QUEUED | Conversation is queued and waiting to start |
CONVERSATION_STARTED | Conversation has started with the candidate |
CONVERSATION_COMPLETED | Conversation completed successfully |
CONVERSATION_NEEDS_REVIEW | Conversation requires manual review |
CONVERSATION_OPTED_OUT | Candidate opted out of the conversation |
CONVERSATION_MAX_NUDGES_REACHED | All configured nudges were sent without a response |
START_CONVERSATION_FAILED | Failed 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
| Field | Type | Description |
|---|---|---|
conversationStatus | string | Current status of the conversation (e.g., ONGOING) |
candidateId | string | Popp internal candidate ID |
externalId | string | Your external reference ID for the conversation |
externalCampaignId | string | Your external reference ID for the campaign |
participantFirstName | string | Candidate's first name |
participantLastName | string | Candidate's last name |
campaignConversationUrl | string | Direct 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
| Field | Type | Description |
|---|---|---|
conversationStatus | string | Final status (COMPLETED_SCREENING_PASSED, COMPLETED_SCREENING_FAILED, COMPLETED_CANDIDATE_NOT_INTERESTED) |
scorecardTotalValue | number | Total score from screening questions (0-100) |
screeningQuestionsOutcome | array | Array of screening question results |
Screening Question Outcome Object
| Field | Type | Description |
|---|---|---|
screeningQuestion | string | The screening question asked |
responseSummary | string | Summary of the candidate's response |
questionOutcome | string | Outcome 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"
}
}| Field | Type | Description |
|---|---|---|
recordId | string | The conversation ID |
organizationId | string | Your organization ID |
externalId | string | Your external ID for the conversation (if set) |
externalCampaignId | string | Your external campaign ID (if set) |
campaignConversationUrl | string | Direct link to the conversation in the Popp dashboard |
isHumanTakeover | boolean | Whether the conversation is currently in human takeover mode. When true, the AI agent is paused and a human is handling the conversation |
needsReviewReason | string | Categorises why the conversation was flagged (see values below). Set for both AI-classified and system-triggered reviews; omitted only when no specific category applies |
returnDate | string | null | Optional. 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 valuesneedsReviewReason 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:
| Value | Meaning |
|---|---|
OUT_OF_OFFICE | The participant is away or on leave. returnDate carries the date they said they'll be back, when they gave one |
ASKED_FOR_HUMAN | The participant explicitly asked to speak with a person |
UNANSWERED_QUESTION | The participant asked something the AI could not answer |
SPECIAL_NEEDS_REQUEST | The participant raised a special accommodation or access need |
OTHER | Flagged for review for a reason that doesn't fit the categories above |
System-triggered:
| Value | Meaning |
|---|---|
MORE_TIME_REQUESTED | The participant asked for different or additional interview times (also sent as the CALENDAR_MORE_TIME_REQUESTED event) |
AVAILABILITY_NOT_FOUND | No suitable interview availability could be found (also sent as the CALENDAR_AVAILABILITY_NOT_FOUND event) |
HUMAN_TAKEOVER | A message arrived while the conversation was in human-takeover mode (isHumanTakeover is true in this payload) |
CLOSED_CONVERSATION_REPLY | The 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
| Field | Type | Description |
|---|---|---|
conversationId | string | Popp conversation ID |
campaignId | string | Popp campaign ID |
campaignType | string | null | Campaign type (e.g. SCHEDULING, AVAILABILITY_OUTREACH) |
participantEmail | string | null | Participant's email; null for phone-only (SMS/WhatsApp) conversations |
nudgesSent | number | Number of nudges actually sent before giving up |
externalId | string | null | Your external reference ID for the conversation, if set |
externalCampaignId | string | null | Your 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;
}
}