REST API

Token-authenticated /api/v1/* surface. Personal access tokens carry scopes that gate per-endpoint access. Mirror of every customer-panel write — full automation surface for integrations + CI / CD / cron-driven workflows.

Auth

Bearer token in the Authorization header. Create tokens at Customer → API tokens → New token:

curl https://app.example.com/api/v1/me \
    -H "Authorization: Bearer kp_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
    -H "Accept: application/json"

Token scopes

ScopeWhat it allows
*Full access (wildcard).
lists:read / lists:writeList CRUD.
subscribers:read / subscribers:writeSubscriber CRUD.
campaigns:read / campaigns:writeCampaign management.
templates:read / templates:writeTemplate CRUD.
transactional:sendFire individual transactional emails.

Errors

CodeMeaningBody
401Token missing / malformed / revoked{"message":"Unauthenticated."}
403Token valid but scope missing{"message":"This action is unauthorized."}
404Resource not found or not owned by customer{"message":"Not found."}
422Validation failed{"message":"...","errors":{"field":[...]}}
429Rate-limited (60 req/min per token)Retry-After header

Endpoints

Identity

GET /api/v1/me

200 OK
{
    "id": 12,
    "uid": "abc123def456",
    "name": "Acme Inc.",
    "email": "[email protected]",
    "plan": "pro"
}

Lists

GET    /api/v1/lists              # list all
POST   /api/v1/lists              # create
GET    /api/v1/lists/{uid}        # show one
PUT    /api/v1/lists/{uid}        # update
DELETE /api/v1/lists/{uid}        # soft delete

Subscribers

GET    /api/v1/lists/{uid}/subscribers           # list (paginated)
POST   /api/v1/lists/{uid}/subscribers           # create + confirm
GET    /api/v1/lists/{uid}/subscribers/{email}   # show
PUT    /api/v1/lists/{uid}/subscribers/{email}   # update fields / status
DELETE /api/v1/lists/{uid}/subscribers/{email}   # remove

# create example
curl -X POST https://app.example.com/api/v1/lists/abc123/subscribers \
    -H "Authorization: Bearer kp_..." \
    -H "Content-Type: application/json" \
    -d '{
        "email": "[email protected]",
        "fields": { "FIRST_NAME": "Jane", "COUNTRY": "GB" },
        "status": "confirmed"
    }'

Transactional email

POST /api/v1/transactional/send

{
    "to": "[email protected]",
    "to_name": "Jane Doe",
    "subject": "Your receipt",
    "template_uid": "tpl_abc123",    // optional, uses html_body if absent
    "html_body": "<p>Hello :first_name</p>",
    "merge": { "first_name": "Jane" },
    "tags": ["receipt", "order-1234"]
}

202 Accepted
{ "message_id": "<[email protected]>", "queued_at": "2026-06-04T..." }

Campaigns

GET    /api/v1/campaigns
POST   /api/v1/campaigns               # create draft
PUT    /api/v1/campaigns/{uid}         # update
POST   /api/v1/campaigns/{uid}/send    # schedule / send now
POST   /api/v1/campaigns/{uid}/pause
POST   /api/v1/campaigns/{uid}/resume
GET    /api/v1/campaigns/{uid}/report  # engagement metrics

Outbound webhooks

KodMail POSTs JSON to your URL when events happen in the account. Configure at Customer → Webhooks → New webhook.

Event catalogue

Signature verification

Every payload arrives with:

X-Webhook-Signature: t=1717512000,v1=abc123def...
X-Webhook-Event: subscriber.created

Verify with HMAC-SHA256 using your webhook signing secret:

// PHP example
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];
preg_match('/t=(\d+),v1=(\w+)/', $sig, $m);
[$_, $timestamp, $signature] = $m;

$payload = file_get_contents('php://input');
$expected = hash_hmac('sha256', "{$timestamp}.{$payload}", $secret);

if (! hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

// Timestamp skew check — reject >5 min old
if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}

Retry policy

Non-2xx response / timeout / connection error → retry on exponential backoff up to 8 attempts (30s, 1m, 5m, 15m, 30m, 1h, 6h, 24h). After 8 failures, delivery is dropped and the webhook gets last_failure_at stamped.