Campaigns

Understanding Campaigns

A Campaign is the foundation of all outreach in Popp. It defines how your AI agent communicates with candidates, what questions to ask, and what happens when a conversation completes.

How Campaigns and Conversations Relate

┌─────────────────────────────────────────────────────────┐
│                       CAMPAIGN                          │
│  • Defines the outreach strategy                        │
│  • Contains AI agent configuration                      │
│  • Sets screening questions                             │
│  • Configures closing method (scheduling, call, etc.)   │
│                                                         │
│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│   │Conversation │  │Conversation │  │Conversation │     │
│   │  (John)     │  │  (Sarah)    │  │  (Mike)     │     │
│   └─────────────┘  └─────────────┘  └─────────────┘     │
└─────────────────────────────────────────────────────────┘
  • A Campaign is created once and can have many conversations
  • Each Conversation belongs to exactly one campaign
  • The campaign's settings (agent, questions, closing method) apply to all its conversations

Campaign Types

TypeUse Case
APPLICANT_OUTREACHReach out to candidates who have applied for a job
NEW_CANDIDATE_OUTREACHProactively source and engage new candidates
ENGAGEMENT_OUTREACHRe-engage candidates for non-job related purposes (e.g., surveys, events)
SCHEDULINGInvite candidates to book a meeting via Popp's built-in scheduling

Communication Channels

Campaigns can communicate through different channels:

ChannelDescription
SMSText messages to mobile phones
WHATSAPPWhatsApp messaging
EMAILEmail messaging

Closing Methods

When a conversation completes successfully, the campaign determines what happens next:

MethodDescription
CUSTOMCustom closing message (requires customMessage)
CALENDAR_MEETING_INTEGRATIONUse Popp's built-in scheduling (requires calendarMeetingTemplateId)

Campaign Statuses

StatusDescription
DRAFTCampaign is being configured
LIVECampaign is active and accepting conversations
ARCHIVEDCampaign is no longer active

Personalising the Opening Message

customTemplatedMessage is the first message Popp sends. Wrap a placeholder in double braces and Popp fills it in for each candidate:

{
  "customTemplatedMessage": "Hi {{CANDIDATE_FIRST_NAME}}, we have a {{JOB_TITLE}} role at {{EMPLOYER_NAME}} in {{LOCATION}}. Interested? Reply STOP to opt out."
}

Three rules apply to every campaign:

  • Placeholder names are SCREAMING_SNAKE_CASE. {{firstName}} and {{first_name}} are both rejected with 400 Unknown placeholder found.
  • The message must contain the word STOP, uppercase, for opt-out compliance.
  • SMS and Email only. customTemplatedMessage is not accepted on WhatsApp campaigns.

Which placeholders can I use?

It depends on the campaign type. Popp only fills in what that campaign type has already worked out by the time the opening message is built.

PlaceholderApplicant / New candidateEngagementSchedulingFills in
{{CANDIDATE_FIRST_NAME}}yesyesyesThe candidate's first name
{{CANDIDATE_LAST_NAME}}yesyesyesThe candidate's last name
{{AGENT_NAME}}yesyesyesThe agentName on the campaign
{{ORGANIZATION_NAME}}yesyesyesYour organisation's candidate-facing name in Popp — not the company field
{{JOB_TITLE}}yesnonoThe campaignTitle you sent, or a title read from campaignDescription
{{EMPLOYER_NAME}}yesnonoThe company you sent, or an employer read from campaignDescription
{{LOCATION}}yesnonoThe location you sent, or a location read from campaignDescription
{{MEETING_URL}}nonoyesThe candidate's booking link
{{MEETING_TITLE}}nonoyesThe meeting title from the meeting template

The three columns cover every campaign type this endpoint accepts. Any campaignType other than the applicant, new-candidate and scheduling ones behaves exactly like the Engagement column — the four placeholders that need no job or meeting data.

Notes on the table:

  • The job placeholders come from a structured job description. Popp builds one only for applicant and new-candidate campaigns, by reading your campaignDescription. Engagement and scheduling campaigns still take a campaignDescription, but it is not broken down into fields, so {{JOB_TITLE}}, {{EMPLOYER_NAME}} and {{LOCATION}} have nothing to read.
  • The meeting placeholders belong to the scheduling campaign's opening message. A scheduling campaign leads with the booking link, so the link exists when that message is built. Every other campaign type screens the candidate first and sends the link later in the conversation, once it closes — so the link does not exist yet at opening-message time. This holds even when the campaign has closingMethod: CALENDAR_MEETING_INTEGRATION with a calendarMeetingTemplateId: that configures the closing message, not the opening one. To open with a link on a non-scheduling campaign, put a plain URL in the text rather than a placeholder.
  • The three job placeholders read your explicit fields first. {{JOB_TITLE}} fills in whatever you sent as campaignTitle; only if you omit that does Popp fall back to a title read out of campaignDescription. {{EMPLOYER_NAME}} works the same way from company, and {{LOCATION}} from location. So if you send a campaignTitle like "SMS test - 2026-08-20", that is what the candidate reads as the job title. To control the three independently, send campaignTitle, company and location explicitly.
  • {{ORGANIZATION_NAME}} needs the company field, but does not print it. The request is rejected without company, yet what the candidate sees is your organisation's candidate-facing name as configured in Popp. Use {{EMPLOYER_NAME}} if you want the company value itself. company is already required on applicant and new-candidate campaigns; add it explicitly on engagement and scheduling campaigns to use this placeholder.
  • {{CANDIDATE_LAST_NAME}} is safe to use even when you don't have one. If the candidate has no last name the placeholder is removed from the message, along with the space before it.
  • {{MEETING_URL}} is mandatory in a scheduling campaign's customTemplatedMessage — without it the candidate has no way to book.

Using a placeholder outside its column returns 400 Missing value for template variable: <NAME>, naming the placeholder that could not be filled.

Examples

These snippets show only the message-related fields to keep the focus on placeholders — they are not complete request bodies. Send them alongside the other required campaign fields; see Creating a Campaign for a full POST /v1/campaigns payload.

An applicant or new-candidate campaign, where job details are available:

{
  "campaignType": "NEW_CANDIDATE_OUTREACH",
  "channel": "SMS",
  "company": "Acme Corp",
  "customTemplatedMessage": "Hi {{CANDIDATE_FIRST_NAME}}, {{AGENT_NAME}} here from {{ORGANIZATION_NAME}}. We have a {{JOB_TITLE}} role in {{LOCATION}} that could suit you. Interested? Reply STOP to opt out."
}

An engagement campaign, which has no job — so no job placeholders:

{
  "campaignType": "ENGAGEMENT_OUTREACH",
  "channel": "SMS",
  "company": "Acme Corp",
  "customTemplatedMessage": "Hi {{CANDIDATE_FIRST_NAME}}, {{AGENT_NAME}} here from {{ORGANIZATION_NAME}}. Do you have two minutes for a quick survey? Reply STOP to opt out."
}

A scheduling campaign, which must include the booking link:

{
  "campaignType": "SCHEDULING",
  "channel": "EMAIL",
  "customTemplatedSubjectLine": "Book your interview, {{CANDIDATE_FIRST_NAME}}",
  "customTemplatedMessage": "Hi {{CANDIDATE_FIRST_NAME}}, please pick a time for your {{MEETING_TITLE}}: {{MEETING_URL}}\n\nReply STOP to opt out."
}

When your message is rewritten

There is one case where Popp changes your opening message after you have authored it. If you create a conversation with closePreviousConversations: true and the candidate already has an open SMS conversation on that channel, Popp prepends a short transition acknowledging that the previous conversation is ending — for example, "Sorry to change topics, but I'm ending our previous discussion as..." — before your text. This transition is generated per message, so the exact wording varies and your text is not delivered word for word.

This applies to SMS only. Email conversations arrive as separate threads, so there is nothing to transition from. To have your message delivered exactly as written, create the conversation without closePreviousConversations.

Email subject lines

Email campaigns using customTemplatedMessage must also send customTemplatedSubjectLine. The subject line accepts the same placeholders as the message body, so use the ones from the table above. An unrecognised name is rejected with 400 Invalid placeholder in subject line: {{name}}.

Creating a Campaign

Campaigns can be created via the API with POST /v1/campaigns, or through the Popp dashboard. A minimal API example:

curl -X POST "https://api.joinpopp.com/v1/campaigns" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-organization-id: YOUR_ORGANIZATION_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "campaignType": "APPLICANT_OUTREACH",
    "channel": "WHATSAPP",
    "closingMethod": "CUSTOM",
    "campaignTitle": "Senior Software Engineer",
    "campaignDescription": "Senior Software Engineer with 5+ years experience in TypeScript, React, and AWS. Hybrid working (3 days a week in office). Salary 80-100k.",
    "company": "Popp",
    "location": "London, UK",
    "customMessage": "Thanks for your interest! Our recruiting team will review your responses and reach out within 2 business days to schedule a chat.",
    "openingMessageTemplateId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "agentName": "Alex",
    "agentTone": "FRIENDLY",
    "campaignStatus": "LIVE",
    "externalId": "ats-job-12345",
    "questions": [
      {
        "questionType": "TEXT",
        "isMandatory": true,
        "content": "Do you have at least 5 years of experience with TypeScript and React?"
      },
      {
        "questionType": "DOCUMENT",
        "isMandatory": false,
        "documentTypes": ["CV"]
      }
    ]
  }'

The response returns the campaign id — use it as campaignId when creating conversations. For the field-by-field walkthrough, required fields by campaign type, and how to launch conversations against the campaign, see the Job Application Flow.

Once you have a campaign, you can:

  1. Use the API to create conversations within that campaign
  2. List and filter campaigns via the API
  3. Get the Campaign ID of a dashboard-created campaign from its URL: https://ai.joinpopp.com/campaign/outbound/{CAMPAIGN_ID}

List Campaigns

curl -X GET "https://api.joinpopp.com/v1/campaigns" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-organization-id: YOUR_ORGANIZATION_ID"

Filter Campaigns

You can filter campaigns by various criteria:

# Filter by status
curl -X GET "https://api.joinpopp.com/v1/campaigns?campaignStatus=LIVE" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-organization-id: YOUR_ORGANIZATION_ID"

# Filter by type
curl -X GET "https://api.joinpopp.com/v1/campaigns?campaignType=APPLICANT_OUTREACH" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-organization-id: YOUR_ORGANIZATION_ID"

# Filter by channel
curl -X GET "https://api.joinpopp.com/v1/campaigns?channel=SMS" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-organization-id: YOUR_ORGANIZATION_ID"

Get a Specific Campaign

curl -X GET "https://api.joinpopp.com/v1/campaigns/{campaignId}" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-organization-id: YOUR_ORGANIZATION_ID"

Campaign Response

{
  "id": "10fe477c-5a4a-451d-9f18-b8340e2154e8",
  "organizationId": "org_123456",
  "campaignTitle": "Software Engineer Outreach",
  "campaignStatus": "LIVE",
  "type": "APPLICANT_OUTREACH",
  "channel": "SMS",
  "closingMethod": "CALENDAR_MEETING_INTEGRATION",
  "calendarMeetingTemplateId": "mtg_tmpl_abc123",
  "needsReview": false,
  "agent": {
    "id": "agent_abc123",
    "agentName": "Alex",
    "agentTone": "PROFESSIONAL"
  },
  "questions": [
    {
      "questionType": "TEXT",
      "content": "Do you have 5+ years of experience with React?",
      "isMandatory": true
    }
  ]
}

API Reference

Next Steps


Did this page help you?