MOLTED EMAIL

Email Lists

Create subscriber lists for newsletters, announcements, and marketing broadcasts.

Lists are named groups of subscribers for sending newsletters, announcements, and marketing broadcasts. Unlike segments (dynamic, filter-based), lists use explicit subscribe/unsubscribe membership.

Create a list

POST https://api.molted.email/v1/agent/lists
CLI
molted lists create --name "Weekly Digest" --type newsletter --double-opt-in
curl
curl -X POST https://api.molted.email/v1/agent/lists \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Weekly Digest",
    "type": "newsletter",
    "doubleOptIn": true,
    "description": "Weekly product updates"
  }'

List types

TypeUse case
newsletterRecurring content (weekly digest, blog roundup)
announcementProduct updates, release notes
marketingPromotional campaigns, offers

List all lists

GET https://api.molted.email/v1/agent/lists
CLI
molted lists list
molted lists list --limit 10 --offset 20

Get a list

GET https://api.molted.email/v1/agent/lists/:id
CLI
molted lists get <list-id>

Update a list

PATCH https://api.molted.email/v1/agent/lists/:id
CLI
molted lists update <list-id> --name "New Name" --description "Updated description"
molted lists update <list-id> --status paused

Updatable fields: name, description, type, status, doubleOptIn.

Archive a list

DELETE https://api.molted.email/v1/agent/lists/:id
CLI
molted lists archive <list-id>

Archiving soft-deletes the list. Subscribers are preserved but no new sends can be initiated.

Subscribe a contact

POST https://api.molted.email/v1/agent/lists/:id/subscribers
CLI
molted lists subscribe <list-id> --email alice@example.com --name "Alice"

If double opt-in is enabled on the list, the subscriber status will be pending until they confirm via the confirmation email.

Double opt-in

When doubleOptIn: true is set on a list, subscribing a contact triggers a confirmation flow:

  1. The contact is added with status: pending.
  2. A list.subscribe.pending journey event is emitted automatically. If you have a journey configured to trigger on this event, it will send the confirmation email containing a link to GET /v1/lists/confirm?token=<token>.
  3. When the contact clicks the link, their status is updated to active and the list subscriberCount is incremented.

Confirmation tokens expire after 48 hours. Pending subscriptions that are never confirmed are automatically deleted after 7 days.

Setting up the confirmation journey

Create a journey in the portal with trigger event list.subscribe.pending. Use the confirmationUrl field from the event payload in your confirmation email template:

Event payload
{
  "listId": "...",
  "listName": "Weekly Digest",
  "contactId": "...",
  "confirmationUrl": "https://api.molted.email/v1/lists/confirm?token=..."
}

The journey step that sends the confirmation email should include {{ payload.confirmationUrl }} as the confirm link.

Subscription statuses

StatusMeaning
pendingAwaiting confirmation (double opt-in only)
activeConfirmed subscriber
unsubscribedOpted out

Unsubscribe a contact

POST https://api.molted.email/v1/agent/lists/:id/unsubscribe
CLI
molted lists unsubscribe <list-id> --email alice@example.com

List subscribers

GET https://api.molted.email/v1/agent/lists/:id/subscribers
CLI
molted lists subscribers <list-id>
molted lists subscribers <list-id> --limit 100 --offset 0

members is an alias for subscribers - both commands do exactly the same thing:

CLI
molted lists members <list-id>   # equivalent to 'subscribers'

Note: The CLI help screen shows subscribers|members using pipe notation to indicate the alias. This is not a special syntax you need to type - use either subscribers or members as the command name.

Bulk import subscribers

CLI
molted lists import <list-id> --file subscribers.csv
molted lists import <list-id> --file subscribers.json

The file format is determined by extension: .csv for CSV, anything else is parsed as JSON.

CSV format

The first row must be a header row. The email column is required. name is optional. Any additional columns are collected into a metadata object on the subscriber.

subscribers.csv
email,name,plan,source
alice@example.com,Alice,pro,website
bob@example.com,Bob,starter,referral
charlie@example.com,,free,import

JSON format

Must be a top-level JSON array. Each element is a subscriber object. Fields beyond email and name should go inside a metadata object.

subscribers.json
[
  { "email": "alice@example.com", "name": "Alice", "metadata": { "plan": "pro" } },
  { "email": "bob@example.com", "name": "Bob", "metadata": { "plan": "starter" } }
]

Subscriber fields

FieldTypeRequiredNotes
emailstringYesRows/objects with a missing or empty email are silently skipped.
namestringNoDisplay name.
Additional CSV columnsstringNoCollected into a metadata object automatically.
metadata (JSON)objectNoArbitrary key-value pairs. Passed through as-is.

Validation errors

ErrorCause
Cannot read file: <path>File does not exist or cannot be read.
File must contain a valid JSON array of subscriber objectsJSON is malformed or is not an array.
No subscribers found in file (all rows missing email)Every row/object is missing the email field.

Import response

Files are processed in chunks of 500. If a chunk fails, the import stops - earlier chunks may already be committed.

Success
{
  "status": "success",
  "message": "Bulk subscribe completed: 150 subscribed, 3 already subscribed, 0 failed.",
  "summary": {
    "total": 153,
    "subscribed": 150,
    "alreadySubscribed": 3,
    "failed": 0
  },
  "results": [
    { "email": "alice@example.com", "outcome": "subscribed" },
    { "email": "bob@example.com", "outcome": "already_subscribed" }
  ]
}

Send to a list

POST https://api.molted.email/v1/agent/lists/:id/send
CLI
molted lists send <list-id> --template <template-id> --yes
molted lists send <list-id> --template <template-id> --mailbox <mailbox-id> --payload '{"promo_code": "SPRING25"}' --yes

Always pass --yes when sending from an agent or script. Without it, the CLI shows an interactive confirmation prompt that will hang any non-interactive process. The CLI auto-skips the prompt in non-TTY environments and when CI or MOLTED_NONINTERACTIVE is set, but some agent runtimes (including Claude Code and other tool-use shells) allocate a pseudo-TTY -- so --yes is the most reliable way to ensure your send completes without blocking.

Broadcasts are sent sequentially to each active subscriber. For each recipient, the system:

  • Renders the template with subscriberEmail, subscriberName, listName, and unsubscribeUrl injected into the payload
  • Generates a unique dedupe key (list-{listId}-{sendId}-{contactEmail}) to prevent duplicate sends
  • Attaches RFC 8058 List-Unsubscribe and List-Unsubscribe-Post headers for one-click unsubscribe
  • Runs every send through the full policy engine (rate limits, suppressions, cooldowns)

The response includes sent, blocked, failed, and blockedReasons counts:

  • sent - send requests accepted by the policy engine (queued, scheduled, or pending approval)
  • blocked - sends the policy engine refused for this recipient (e.g. suppressed, trial_not_activated, rate_limited)
  • failed - sends that threw an upstream error (non-policy)
  • blockedReasons - map of block reason to count, e.g. { "suppressed": 2 }

Partial failures do not abort the broadcast - the system continues sending to remaining subscribers. When every subscriber was blocked or errored (sent === 0), the list-send record is marked failed so agents can distinguish "everyone was blocked" from "everyone was delivered".

Limits: Sequential broadcasts support up to 500 active subscribers per list. Lists exceeding this limit will return an error. For larger lists, contact support.

View send history

GET https://api.molted.email/v1/agent/lists/:id/sends
CLI
molted lists sends <list-id>

CLI output format

All list commands use a structured JSON envelope for agent-friendly parsing:

Success
{
  "status": "success",
  "message": "List 'Weekly Digest' created successfully.",
  "data": { "id": "...", "name": "Weekly Digest", "subscriberCount": 0 }
}
Error
{
  "status": "error",
  "message": "Failed to create list",
  "details": { "error": "..." }
}

For bulk import response structure, see Bulk import subscribers.