Campaign Guide
Step-by-step guide to creating an audience, adding contacts, and sending your first email campaign.
This guide walks through the full campaign flow - from creating an audience to tracking results after sending.
Create an Audience
An audience is the contact list your campaign will be sent to. Create one in the dashboard under Audiences → New Audience, or via the API:
curl -X POST "https://api.keplars.com/api/v1/public/audiences/add-audience" \
-H "Authorization: Bearer $KEPLARS_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Newsletter"}'const res = await fetch(
'https://api.keplars.com/api/v1/public/audiences/add-audience',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.KEPLARS_ADMIN_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Newsletter',
}),
},
);
const { data } = await res.json();import requests, os
res = requests.post(
'https://api.keplars.com/api/v1/public/audiences/add-audience',
json={
'name': 'Newsletter',
},
headers={'Authorization': f'Bearer {os.environ["KEPLARS_ADMIN_KEY"]}'},
)
data = res.json()['data']package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body := bytes.NewBufferString(`{"name":"Newsletter"}`)
req, _ := http.NewRequest("POST",
"https://api.keplars.com/api/v1/public/audiences/add-audience", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KEPLARS_ADMIN_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := (&http.Client{}).Do(req)
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
fmt.Println(string(b))
}<?php
$ch = curl_init('https://api.keplars.com/api/v1/public/audiences/add-audience');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Newsletter',
]),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('KEPLARS_ADMIN_KEY'),
'Content-Type: application/json',
],
]);
$data = json_decode(curl_exec($ch), true)['data'];
curl_close($ch);using System.Net.Http;
using System.Net.Http.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add(
"Authorization", $"Bearer {Environment.GetEnvironmentVariable("KEPLARS_ADMIN_KEY")}"
);
var response = await client.PostAsJsonAsync(
"https://api.keplars.com/api/v1/public/audiences/add-audience",
new
{
name = "Newsletter"
}
);
var body = await response.Content.ReadAsStringAsync();use reqwest::Client;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let res = client
.post("https://api.keplars.com/api/v1/public/audiences/add-audience")
.header("Authorization", format!("Bearer {}", std::env::var("KEPLARS_ADMIN_KEY")?))
.json(&json!({
"name": "Newsletter"
}))
.send()
.await?;
let data: serde_json::Value = res.json().await?;
println!("{}", data);
Ok(())
}#include <curl/curl.h>
#include <cstdlib>
#include <string>
int main() {
CURL* curl = curl_easy_init();
std::string auth = std::string("Authorization: Bearer ") + std::getenv("KEPLARS_ADMIN_KEY");
const char* payload = "{\"name\":\"Newsletter\"}";
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, auth.c_str());
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, "https://api.keplars.com/api/v1/public/audiences/add-audience");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
}#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
CURL* curl = curl_easy_init();
char auth[256];
snprintf(auth, sizeof(auth), "Authorization: Bearer %s", getenv("KEPLARS_ADMIN_KEY"));
const char* payload = "{\"name\":\"Newsletter\"}";
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, auth);
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, "https://api.keplars.com/api/v1/public/audiences/add-audience");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return 0;
}{
"data": {
"id": "019dd568-1ac3-7320-ad48-ea6189b937d0",
"name": "Newsletter",
"contactCount": 0,
"subscribeUrl": "https://keplars.com/s/a3f9bc4d..."
}
}The subscribeUrl is your public signup form - share it on your website so visitors can subscribe themselves.
Add Contacts
You have three ways to populate your audience:
Option 1 - Dashboard form
Go to the audience page, click Add Contact, and fill in the form.
Option 2 - CSV import
Click the ··· menu → Import CSV on the audience page. Your CSV should have an email column. Supported columns: email, first_name, last_name, company_name, job_title, phone_number, linkedin, timezone, tags (pipe-separated).
Option 3 - API
curl -X POST "https://api.keplars.com/api/v1/public/contacts/add-contact" \
-H "Authorization: Bearer $KEPLARS_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","audience_id":"019dd568-1ac3-7320-ad48-ea6189b937d0","first_name":"John","last_name":"Doe","tags":"newsletter"}'const res = await fetch(
'https://api.keplars.com/api/v1/public/contacts/add-contact',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.KEPLARS_ADMIN_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: '[email protected]',
audience_id: '019dd568-1ac3-7320-ad48-ea6189b937d0',
first_name: 'John',
last_name: 'Doe',
tags: 'newsletter',
}),
},
);
const { data } = await res.json();import requests, os
res = requests.post(
'https://api.keplars.com/api/v1/public/contacts/add-contact',
json={
'email': '[email protected]',
'audience_id': '019dd568-1ac3-7320-ad48-ea6189b937d0',
'first_name': 'John',
'last_name': 'Doe',
'tags': 'newsletter',
},
headers={'Authorization': f'Bearer {os.environ["KEPLARS_ADMIN_KEY"]}'},
)
data = res.json()['data']package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body := bytes.NewBufferString(`{"email":"[email protected]","audience_id":"019dd568-1ac3-7320-ad48-ea6189b937d0","first_name":"John","last_name":"Doe","tags":"newsletter"}`)
req, _ := http.NewRequest("POST",
"https://api.keplars.com/api/v1/public/contacts/add-contact", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KEPLARS_ADMIN_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := (&http.Client{}).Do(req)
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
fmt.Println(string(b))
}<?php
$ch = curl_init('https://api.keplars.com/api/v1/public/contacts/add-contact');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'email' => '[email protected]',
'audience_id' => '019dd568-1ac3-7320-ad48-ea6189b937d0',
'first_name' => 'John',
'last_name' => 'Doe',
'tags' => 'newsletter',
]),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('KEPLARS_ADMIN_KEY'),
'Content-Type: application/json',
],
]);
$data = json_decode(curl_exec($ch), true)['data'];
curl_close($ch);using System.Net.Http;
using System.Net.Http.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add(
"Authorization", $"Bearer {Environment.GetEnvironmentVariable("KEPLARS_ADMIN_KEY")}"
);
var response = await client.PostAsJsonAsync(
"https://api.keplars.com/api/v1/public/contacts/add-contact",
new
{
email = "[email protected]",
audience_id = "019dd568-1ac3-7320-ad48-ea6189b937d0",
first_name = "John",
last_name = "Doe",
tags = "newsletter"
}
);
var body = await response.Content.ReadAsStringAsync();use reqwest::Client;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let res = client
.post("https://api.keplars.com/api/v1/public/contacts/add-contact")
.header("Authorization", format!("Bearer {}", std::env::var("KEPLARS_ADMIN_KEY")?))
.json(&json!({
"email": "[email protected]",
"audience_id": "019dd568-1ac3-7320-ad48-ea6189b937d0",
"first_name": "John",
"last_name": "Doe",
"tags": "newsletter"
}))
.send()
.await?;
let data: serde_json::Value = res.json().await?;
println!("{}", data);
Ok(())
}#include <curl/curl.h>
#include <cstdlib>
#include <string>
int main() {
CURL* curl = curl_easy_init();
std::string auth = std::string("Authorization: Bearer ") + std::getenv("KEPLARS_ADMIN_KEY");
const char* payload = "{\"email\":\"[email protected]\",\"audience_id\":\"019dd568-1ac3-7320-ad48-ea6189b937d0\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"tags\":\"newsletter\"}";
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, auth.c_str());
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, "https://api.keplars.com/api/v1/public/contacts/add-contact");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
}#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
CURL* curl = curl_easy_init();
char auth[256];
snprintf(auth, sizeof(auth), "Authorization: Bearer %s", getenv("KEPLARS_ADMIN_KEY"));
const char* payload = "{\"email\":\"[email protected]\",\"audience_id\":\"019dd568-1ac3-7320-ad48-ea6189b937d0\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"tags\":\"newsletter\"}";
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, auth);
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, "https://api.keplars.com/api/v1/public/contacts/add-contact");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return 0;
}Create a Template
Campaigns use email templates. Create one in the dashboard under Templates → New Template.
Required placeholder
Your template must include {"{{"}unsubscribeUrl{"}}"} somewhere in the body. Keplars injects the correct per-audience unsubscribe link at send time. Campaigns will be blocked from sending if this placeholder is missing.
You can use any Handlebars variables in your template:
<p>Hi {{firstName}},</p>
<p>Here's your weekly update...</p>
<p><a href="{{unsubscribeUrl}}">Unsubscribe</a></p>Then pass values when setting up the campaign:
{
"variables": {
"firstName": "will be replaced per-contact automatically"
}
}For help building templates, see Email Templates and Template Libraries.
Create a Campaign
In the dashboard, go to Campaigns → New Campaign and fill in:
| Field | Description |
|---|---|
| Name | Internal name for this campaign |
| Audience | The audience to send to |
| Template | The email template to use |
| From Email | Sender address (must be a verified email or custom domain) |
| From Name | Display name shown in the inbox |
| Subject | Email subject line (if not defined in the template) |
Only contacts with subscribed status in the selected audience will receive the campaign. Unsubscribed and globally blocked contacts are automatically excluded.
Send or Schedule
Send now - click Send Campaign. Keplars fans out the emails immediately through the priority queue.
Schedule - click Schedule, pick a date/time and timezone. The campaign status changes to SCHEDULED and sends automatically at that time.
Scheduling requires LAUNCH plan
Scheduled campaigns are available on the LAUNCH plan and above. On the FREE plan, use Send now instead.
You can cancel a scheduled campaign any time before it starts sending.
Track Results
Once sending starts, the campaign dashboard shows live metrics:
| Metric | Description |
|---|---|
| Dispatched | Emails handed to the mail provider |
| Delivered | Confirmed delivery to inbox |
| Opened | Recipient opened the email |
| Clicked | Recipient clicked a tracked link |
| Bounced | Email bounced (hard or soft) |
| Failed | Delivery failed after retries |
Click any campaign to drill into per-contact delivery status - see exactly who received, opened, clicked, or bounced.
Handle Unsubscribes
When a contact clicks the unsubscribe link in a campaign email, their status in that audience is automatically set to unsubscribed. They are:
- Excluded from all future campaigns to that audience
- Not removed from the audience - they still appear in the contacts list
- Not affected in other audiences - their other subscriptions remain active
They can re-subscribe at any time by submitting the audience's subscribeUrl form.
You can also check and filter by subscription status via the API using subscribedOnly=true on the List Contacts endpoint.
Next Steps
- Audiences API - manage audiences programmatically
- Contacts API - add and manage contacts via API
- Email Templates - create reusable templates
- Template Libraries - React Email, MJML, Handlebars