mailing-list-cli is a local CLI — it doesn't expose HTTP endpoints for signup forms. That's by design. The CLI manages your list; your website/backend calls the CLI to add subscribers.
This doc shows how to wire a signup form to the CLI from common web stacks.
mailing-list-cli contact add <email> --list <list-id> \
--first-name "Alice" --last-name "Smith"--list takes the numeric list id, not the list name. Find it once with mailing-list-cli list ls; the examples below use 1.
That's it. The CLI handles:
- Deduplication (same email = no-op)
- Suppression checking (won't re-add bounced/complained/erased contacts)
- Syncing to Resend via email-cli
// app/api/subscribe/route.ts
import { NextResponse } from "next/server";
import { execSync } from "child_process";
export async function POST(req: Request) {
const { email, firstName, lastName } = await req.json();
if (!email || !email.includes("@")) {
return NextResponse.json({ error: "Invalid email" }, { status: 400 });
}
try {
const args = [
"contact", "add", email,
"--list", "1", // your numeric list id (find it: mailing-list-cli list ls)
];
if (firstName) args.push("--first-name", firstName);
if (lastName) args.push("--last-name", lastName);
const result = execSync(
`mailing-list-cli ${args.map(a => `"${a}"`).join(" ")}`,
{ encoding: "utf-8", timeout: 10000 }
);
return NextResponse.json({ ok: true });
} catch (err: any) {
const exitCode = err.status;
if (exitCode === 3) {
// Bad input (e.g., invalid email format)
return NextResponse.json({ error: "Invalid input" }, { status: 400 });
}
return NextResponse.json({ error: "Server error" }, { status: 500 });
}
}const { execSync } = require("child_process");
app.post("/subscribe", (req, res) => {
const { email, firstName } = req.body;
try {
execSync(
`mailing-list-cli contact add "${email}" --list 1` +
(firstName ? ` --first-name "${firstName}"` : ""),
{ timeout: 10000 }
);
res.json({ ok: true });
} catch (err) {
res.status(err.status === 3 ? 400 : 500).json({ error: "Failed" });
}
});import subprocess
@app.post("/subscribe")
def subscribe(email: str, first_name: str = ""):
cmd = ["mailing-list-cli", "contact", "add", email, "--list", "1"]
if first_name:
cmd += ["--first-name", first_name]
result = subprocess.run(cmd, capture_output=True, timeout=10)
if result.returncode == 0:
return {"ok": True}
elif result.returncode == 3:
return {"error": "Invalid input"}, 400
else:
return {"error": "Server error"}, 500For migrating from another platform:
mailing-list-cli contact import subscribers.csv --list 1The CSV needs email and consent_source columns. Imports without a populated consent_source on every row are rejected (csv_missing_consent_source / csv_row_missing_consent) unless you pass --unsafe-no-consent, which tags every imported row imported_without_consent. Optional columns: first_name, last_name, tags (comma-separated), and any custom fields you've created with field create.
Your integration code should handle these:
| Code | Meaning | Action |
|---|---|---|
| 0 | Success | Subscriber added |
| 1 | Transient error | Retry after a delay |
| 2 | Config error | Check server setup (is mailing-list-cli installed? Is email-cli configured?) |
| 3 | Bad input | Invalid email or missing required fields — surface to user |
| 4 | Rate limited | Back off and retry |
mailing-list-cli doesn't send confirmation emails itself (that requires a hosted endpoint to handle the click). Implement double opt-in in your backend:
- User submits signup form
- Your backend sends a confirmation email (via email-cli or any transactional sender)
- User clicks the confirmation link → hits your backend
- Backend calls
mailing-list-cli contact add <email> --list 1
Only step 4 touches the CLI. Steps 1-3 are your standard web flow.
If you're using an AI agent to manage the list, the agent drives the CLI directly:
# Add a subscriber
mailing-list-cli contact add alice@example.com --list 1 --first-name Alice
# Tag them
mailing-list-cli contact tag alice@example.com vip
# Set a custom field
mailing-list-cli contact set alice@example.com company "Acme Corp"
# Check their profile
mailing-list-cli contact show alice@example.comThe agent uses agent-info to discover all available commands at runtime — no hardcoded docs needed.