Skip to main content

Overview

The Public API provides unauthenticated endpoints for subscriber-facing features like subscriptions, preferences, and archives. These endpoints are designed to be used from public websites and subscription forms.
Public API endpoints do not require authentication and are accessible to anyone. Use CAPTCHA protection to prevent abuse.

Lists

Get Public Lists

Retrieve all public mailing lists.
GET /api/public/lists
cURL
curl http://localhost:9000/api/public/lists
Response:
{
  "data": [
    {
      "id": 1,
      "uuid": "5e5e7e5e-5e5e-5e5e-5e5e-5e5e7e5e7e5e",
      "name": "Newsletter",
      "type": "public",
      "optin": "double",
      "description": "Weekly newsletter with updates and tips"
    },
    {
      "id": 2,
      "uuid": "6f6f8f6f-6f6f-6f6f-6f6f-6f6f8f6f8f6f",
      "name": "Product Updates",
      "type": "public",
      "optin": "single",
      "description": "Get notified about new features and releases"
    }
  ]
}
id
integer
List ID
uuid
string
List UUID
name
string
List name
type
string
Always public for this endpoint
optin
string
Opt-in mode: single or double
description
string
List description

Subscriptions

Subscribe to Lists

Subscribe an email address to one or more public lists.
POST /api/public/subscription
email
string
required
Subscriber email address
name
string
Subscriber name
list_uuids
array
required
Array of list UUIDs to subscribe to
altcha
string
Altcha CAPTCHA challenge response (if enabled)
h-captcha-response
string
HCaptcha response token (if enabled)
Example Request:
curl -X POST http://localhost:9000/api/public/subscription \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "[email protected]",
    "name": "John Doe",
    "list_uuids": ["5e5e7e5e-5e5e-5e5e-5e5e-5e5e7e5e7e5e"]
  }'
Response:
{
  "data": {
    "message": "Subscription successful"
  }
}
For double opt-in lists, a confirmation email will be sent. For single opt-in lists, the subscriber is added immediately.

HTML Form Example

<form action="http://localhost:9000/api/public/subscription" method="POST">
  <input type="email" name="email" placeholder="Email" required>
  <input type="text" name="name" placeholder="Name">
  <input type="hidden" name="list_uuids" value="5e5e7e5e-5e5e-5e5e-5e5e-5e5e7e5e7e5e">
  <button type="submit">Subscribe</button>
</form>

JavaScript Example

async function subscribe(email, name, listUUIDs) {
  const response = await fetch('http://localhost:9000/api/public/subscription', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      email: email,
      name: name,
      list_uuids: listUUIDs
    })
  });
  
  const data = await response.json();
  if (response.ok) {
    console.log('Subscription successful:', data);
  } else {
    console.error('Subscription failed:', data);
  }
}

subscribe('[email protected]', 'User Name', ['5e5e7e5e-5e5e-5e5e-5e5e-5e5e7e5e7e5e']);

CAPTCHA

Get Altcha Challenge

Generate an Altcha CAPTCHA challenge.
GET /api/public/captcha/altcha
cURL
curl http://localhost:9000/api/public/captcha/altcha
Response:
{
  "data": {
    "challenge": "base64-encoded-challenge",
    "salt": "random-salt",
    "algorithm": "SHA-256",
    "signature": "challenge-signature"
  }
}
Altcha is a privacy-friendly proof-of-work CAPTCHA that doesn’t track users or require external services.

Using Altcha in Forms

Include the Altcha widget in your subscription form:
<!DOCTYPE html>
<html>
<head>
  <script type="module" src="https://cdn.jsdelivr.net/npm/altcha@latest/dist/altcha.min.js"></script>
</head>
<body>
  <form id="subscribeForm">
    <input type="email" name="email" placeholder="Email" required>
    <input type="text" name="name" placeholder="Name">
    
    <!-- Altcha widget -->
    <altcha-widget 
      challengeurl="http://localhost:9000/api/public/captcha/altcha"
      hidefooter="true">
    </altcha-widget>
    
    <button type="submit">Subscribe</button>
  </form>

  <script>
    document.getElementById('subscribeForm').addEventListener('submit', async (e) => {
      e.preventDefault();
      
      const formData = new FormData(e.target);
      const altchaWidget = document.querySelector('altcha-widget');
      const altchaResponse = altchaWidget.value;
      
      const response = await fetch('http://localhost:9000/api/public/subscription', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          email: formData.get('email'),
          name: formData.get('name'),
          list_uuids: ['your-list-uuid'],
          altcha: altchaResponse
        })
      });
      
      const data = await response.json();
      console.log(data);
    });
  </script>
</body>
</html>

Campaign Archives

Get Campaign Archives

Retrieve public campaign archives.
GET /api/public/archive
Query Parameters:
  • page - Page number (default: 1)
  • per_page - Results per page (default: 20)
cURL
curl http://localhost:9000/api/public/archive
Response:
{
  "data": {
    "results": [
      {
        "id": 123,
        "uuid": "abc123",
        "subject": "Monthly Newsletter - January 2024",
        "created_at": "2024-01-15T10:00:00Z",
        "sent_at": "2024-01-15T10:30:00Z",
        "url": "http://localhost:9000/archive/abc123"
      }
    ],
    "total": 50,
    "per_page": 20,
    "page": 1
  }
}
This endpoint is only available if enable_public_archive is set to true in settings.

Web Pages

The following pages are available for subscriber-facing interactions:

Subscription Form Page

GET /subscription/form
Display a public subscription form with all public lists. URL: http://localhost:9000/subscription/form

Subscription Management Page

GET /subscription/:campaignUUID/:subscriberUUID
Manage subscription preferences for a specific subscriber. URL: http://localhost:9000/subscription/{campaign-uuid}/{subscriber-uuid}

Opt-in Confirmation Page

GET /subscription/optin/:subscriberUUID
Confirm subscription for double opt-in lists. URL: http://localhost:9000/subscription/optin/{subscriber-uuid}

Campaign View Page

GET /campaign/:campaignUUID/:subscriberUUID
View a campaign message in the browser. URL: http://localhost:9000/campaign/{campaign-uuid}/{subscriber-uuid}

Campaign Archive Page

GET /archive/:id
View an archived campaign. URL: http://localhost:9000/archive/{campaign-id}

Latest Archive Page

GET /archive/latest
View the most recent archived campaign. URL: http://localhost:9000/archive/latest

Archive Feed (RSS)

GET /archive.xml
RSS feed of campaign archives. URL: http://localhost:9000/archive.xml

Subscriber Actions

Export Subscriber Data

Subscriber can request their data export.
POST /subscription/export/:subscriberUUID

Wipe Subscriber Data

Subscriber can request complete data deletion (GDPR right to be forgotten).
POST /subscription/wipe/:subscriberUUID
Data wipe is permanent and cannot be undone. The subscriber will be completely removed from the system.
Track link clicks and redirect to target URL.
GET /link/:linkUUID/:campaignUUID/:subscriberUUID
This endpoint is automatically used in campaign emails when link tracking is enabled.

Campaign View Tracking

Track campaign opens with a tracking pixel.
GET /campaign/:campaignUUID/:subscriberUUID/px.png
Returns a 1x1 transparent PNG pixel for email open tracking.

CORS and Security

CORS Headers

Public API endpoints support CORS for cross-origin requests. Configure allowed origins in settings:
[app.security]
cors_origins = ["https://example.com", "https://app.example.com"]

Rate Limiting

Public endpoints do not have built-in rate limiting. Implement rate limiting at the reverse proxy level (nginx, Caddy, etc.) to prevent abuse.
Example nginx rate limiting:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

location /api/public/ {
    limit_req zone=api burst=20;
    proxy_pass http://localhost:9000;
}

Integration Examples

React Subscription Component

import React, { useState, useEffect } from 'react';

function SubscriptionForm() {
  const [lists, setLists] = useState([]);
  const [email, setEmail] = useState('');
  const [name, setName] = useState('');
  const [selectedLists, setSelectedLists] = useState([]);

  useEffect(() => {
    // Fetch public lists
    fetch('http://localhost:9000/api/public/lists')
      .then(res => res.json())
      .then(data => setLists(data.data));
  }, []);

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    const response = await fetch('http://localhost:9000/api/public/subscription', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email,
        name,
        list_uuids: selectedLists
      })
    });

    const data = await response.json();
    if (response.ok) {
      alert('Subscription successful!');
    } else {
      alert('Subscription failed: ' + data.message);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
      />
      
      <div>
        {lists.map(list => (
          <label key={list.uuid}>
            <input
              type="checkbox"
              value={list.uuid}
              onChange={(e) => {
                if (e.target.checked) {
                  setSelectedLists([...selectedLists, list.uuid]);
                } else {
                  setSelectedLists(selectedLists.filter(id => id !== list.uuid));
                }
              }}
            />
            {list.name}
          </label>
        ))}
      </div>
      
      <button type="submit">Subscribe</button>
    </form>
  );
}

WordPress Integration

<?php
// WordPress shortcode for subscription form
function listmonk_subscription_form() {
    $api_url = 'http://localhost:9000';
    
    // Fetch public lists
    $response = wp_remote_get($api_url . '/api/public/lists');
    $lists = json_decode(wp_remote_retrieve_body($response))->data;
    
    ob_start();
    ?>
    <form id="listmonk-subscribe" method="POST">
        <input type="email" name="email" placeholder="Email" required>
        <input type="text" name="name" placeholder="Name">
        
        <?php foreach ($lists as $list): ?>
            <label>
                <input type="checkbox" name="lists[]" value="<?php echo $list->uuid; ?>">
                <?php echo esc_html($list->name); ?>
            </label>
        <?php endforeach; ?>
        
        <button type="submit">Subscribe</button>
    </form>
    
    <script>
    document.getElementById('listmonk-subscribe').addEventListener('submit', async (e) => {
        e.preventDefault();
        const formData = new FormData(e.target);
        
        const response = await fetch('<?php echo $api_url; ?>/api/public/subscription', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                email: formData.get('email'),
                name: formData.get('name'),
                list_uuids: formData.getAll('lists[]')
            })
        });
        
        const data = await response.json();
        alert(response.ok ? 'Subscription successful!' : 'Subscription failed!');
    });
    </script>
    <?php
    return ob_get_clean();
}
add_shortcode('listmonk_subscribe', 'listmonk_subscription_form');
?>

Best Practices

Use UUIDs for Lists

Always use list UUIDs instead of IDs in public forms. UUIDs are stable and won’t change if you reorganize your lists.

Enable CAPTCHA

Protect public subscription endpoints with Altcha or HCaptcha to prevent spam and bot submissions.

Validate Email Addresses

Implement client-side and server-side email validation to catch typos before submission.

Handle Errors Gracefully

Display user-friendly error messages for failed subscriptions and provide clear next steps.

Build docs developers (and LLMs) love