Keplars

Campaigns

Understand campaign behavior, statuses, and how they interact with your contacts and audiences.

Campaigns are created and managed in the Keplars dashboard. This page documents campaign behavior that affects your contacts and API integrations. For a step-by-step walkthrough, see the Campaign Guide.

Campaign Statuses

StatusDescription
DRAFTCampaign created but not yet sent or scheduled
SCHEDULEDSet to send at a future date and time
SENDINGCurrently dispatching emails to contacts
PAUSEDSending paused mid-dispatch
SENTAll emails dispatched
COMPLETEDDispatch finished, all delivery statuses resolved
FAILEDCampaign failed before any emails were sent
CANCELLEDManually cancelled before completion

Who Receives a Campaign

When a campaign is sent, Keplars filters the audience automatically. A contact receives the email only if both conditions are true:

  1. Their per-audience status is subscribed - they have not unsubscribed from this audience
  2. Their global status is subscribed - they have not been globally blocked (hard bounce, spam complaint)

Contacts that fail either check are silently skipped - they are never removed from the audience.


Unsubscribe Behavior

Keplars uses per-audience unsubscribes. When a contact clicks an unsubscribe link in a campaign email:

  • Only their membership in that campaign's audience is set to unsubscribed
  • Their memberships in other audiences are not affected
  • The contact record is not deleted
  • They can re-subscribe via the audience's subscribe form (subscribeUrl)

This means a contact can be unsubscribed from your Newsletter audience while remaining subscribed to your Product Updates audience.

Template requirement

Every campaign template must contain {"{{"}unsubscribeUrl{"}}"} somewhere in the body. Keplars will block sending if this placeholder is missing. This ensures every recipient always has a way to unsubscribe.


Subscribe Form

Every audience has a public signup form at its subscribeUrl. No authentication is required - share it anywhere.

GET  /s/:subscribeToken   → Shows HTML signup form
POST /s/:subscribeToken   → Processes subscription

The subscribeUrl is returned in every audience API response:

{
  "id": "019dd568-...",
  "name": "Newsletter",
  "subscribeUrl": "https://keplars.com/s/a3f9bc4d...",
  ...
}

Programmatic Subscribe

You can POST to the subscribe URL directly from your own signup form or backend - no API key required.

curl -X POST "https://keplars.com/s/a3f9bc4d..." \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","first_name":"Jane","last_name":"Smith"}'
const res = await fetch(
  'https://keplars.com/s/a3f9bc4d...',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
    email: '[email protected]',
    first_name: 'Jane',
    last_name: 'Smith',
  }),
  },
);
const { data } = await res.json();
import requests, os
res = requests.post(
    'https://keplars.com/s/a3f9bc4d...',
    json={
        'email': '[email protected]',
        'first_name': 'Jane',
        'last_name': 'Smith',
    },
)
data = res.json()['data']
package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    body := bytes.NewBufferString(`{"email":"[email protected]","first_name":"Jane","last_name":"Smith"}`)
    req, _ := http.NewRequest("POST",
        "https://keplars.com/s/a3f9bc4d...", body)
    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://keplars.com/s/a3f9bc4d...');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode([
        'email' => '[email protected]',
        'first_name' => 'Jane',
        'last_name' => 'Smith',
    ]),
    CURLOPT_HTTPHEADER     => [
        '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();
var response = await client.PostAsJsonAsync(
    "https://keplars.com/s/a3f9bc4d...",
    new
{
    email = "[email protected]",
    first_name = "Jane",
    last_name = "Smith"
}
);
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://keplars.com/s/a3f9bc4d...")
        .json(&json!({
    "email": "[email protected]",
    "first_name": "Jane",
    "last_name": "Smith"
}))
        .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();
    const char* payload = "{\"email\":\"[email protected]\",\"first_name\":\"Jane\",\"last_name\":\"Smith\"}";
    struct curl_slist* headers = nullptr;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    curl_easy_setopt(curl, CURLOPT_URL, "https://keplars.com/s/a3f9bc4d...");
    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();
    const char* payload = "{\"email\":\"[email protected]\",\"first_name\":\"Jane\",\"last_name\":\"Smith\"}";
    struct curl_slist* headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    curl_easy_setopt(curl, CURLOPT_URL, "https://keplars.com/s/a3f9bc4d...");
    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;
}

The endpoint also accepts application/x-www-form-urlencoded for plain HTML forms:

<form action="https://keplars.com/s/a3f9bc4d..." method="POST">
  <input type="email" name="email" required />
  <input type="text" name="first_name" />
  <input type="text" name="last_name" />
  <button type="submit">Subscribe</button>
</form>

How it works

  • A visitor submits their email (and optionally first/last name)
  • If they are new, a contact is created and added to the audience as subscribed
  • If they were previously unsubscribed, their status is restored to subscribed
  • If they are already subscribed, the request is a no-op - no duplicate entry

Contact Status in Responses

When you fetch contacts via the API, the status field reflects the contact's state in the queried audience, not a global state:

{
  "id": "019dd571-...",
  "email": "[email protected]",
  "status": "unsubscribed",
  ...
}

The same contact may show subscribed when fetched against a different audience.


Campaign Contact Tracking

The dashboard shows per-contact delivery data for every campaign:

MetricDescription
dispatchedEmail was handed to the mail provider
deliveredProvider confirmed delivery to inbox
openedRecipient opened the email (pixel tracked)
clickedRecipient clicked a tracked link
bouncedEmail bounced (hard or soft)
failedDelivery failed after retries

Bounced contacts are not automatically unsubscribed - manage global blocks via the suppression list in the dashboard.

On this page