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
| Type | Use Case |
|---|---|
APPLICANT_OUTREACH | Reach out to candidates who have applied for a job |
NEW_CANDIDATE_OUTREACH | Proactively source and engage new candidates |
ENGAGEMENT_OUTREACH | Re-engage candidates for non-job related purposes (e.g., surveys, events) |
SCHEDULING | Invite candidates to book a meeting via Popp's built-in scheduling |
Communication Channels
Campaigns can communicate through different channels:
| Channel | Description |
|---|---|
SMS | Text messages to mobile phones |
WHATSAPP | WhatsApp messaging |
EMAIL | Email messaging |
Closing Methods
When a conversation completes successfully, the campaign determines what happens next:
| Method | Description |
|---|---|
CUSTOM | Custom closing message (requires customMessage) |
CALENDAR_MEETING_INTEGRATION | Use Popp's built-in scheduling (requires calendarMeetingTemplateId) |
Campaign Statuses
| Status | Description |
|---|---|
DRAFT | Campaign is being configured |
LIVE | Campaign is active and accepting conversations |
ARCHIVED | Campaign 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 with400 Unknown placeholder found. - The message must contain the word
STOP, uppercase, for opt-out compliance. - SMS and Email only.
customTemplatedMessageis 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.
| Placeholder | Applicant / New candidate | Engagement | Scheduling | Fills in |
|---|---|---|---|---|
{{CANDIDATE_FIRST_NAME}} | yes | yes | yes | The candidate's first name |
{{CANDIDATE_LAST_NAME}} | yes | yes | yes | The candidate's last name |
{{AGENT_NAME}} | yes | yes | yes | The agentName on the campaign |
{{ORGANIZATION_NAME}} | yes | yes | yes | Your organisation's candidate-facing name in Popp — not the company field |
{{JOB_TITLE}} | yes | no | no | The campaignTitle you sent, or a title read from campaignDescription |
{{EMPLOYER_NAME}} | yes | no | no | The company you sent, or an employer read from campaignDescription |
{{LOCATION}} | yes | no | no | The location you sent, or a location read from campaignDescription |
{{MEETING_URL}} | no | no | yes | The candidate's booking link |
{{MEETING_TITLE}} | no | no | yes | The 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 acampaignDescription, 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_INTEGRATIONwith acalendarMeetingTemplateId: 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 ascampaignTitle; only if you omit that does Popp fall back to a title read out ofcampaignDescription.{{EMPLOYER_NAME}}works the same way fromcompany, and{{LOCATION}}fromlocation. So if you send acampaignTitlelike"SMS test - 2026-08-20", that is what the candidate reads as the job title. To control the three independently, sendcampaignTitle,companyandlocationexplicitly. {{ORGANIZATION_NAME}}needs thecompanyfield, but does not print it. The request is rejected withoutcompany, yet what the candidate sees is your organisation's candidate-facing name as configured in Popp. Use{{EMPLOYER_NAME}}if you want thecompanyvalue itself.companyis 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'scustomTemplatedMessage— 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:
- Use the API to create conversations within that campaign
- List and filter campaigns via the API
- 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
- List Campaigns - Query and filter campaigns
- Get Campaign - Get details for a specific campaign
Next Steps
- Understanding Conversations - Learn how to create and manage conversations within campaigns
- Webhooks - Get notified when campaign conversations complete
Updated about 1 month ago
