Thirteen tools cover subscribers, templates, segments, and campaigns.
All endpoints follow the pattern POST https://mcp.vlozi.app/tools/newsletter.<name> with Authorization: Bearer ls_xxx. All responses use this envelope:
{ "data": <payload>, "error": null } // success
{ "data": null, "error": "..." } // failure (with HTTP 4xx/5xx)Subscribers
newsletter.add_subscriber
Add a subscriber (or resubscribe one who had unsubscribed). Idempotent per email.
Scope: newsletter:subscribers.create
Input:
| Field | Type | Notes |
|---|---|---|
email |
string (required) |
|
name |
string |
|
tags |
string[] |
Merged with the workspace's configured default tags |
source |
"form" | "import" | "api" |
Default api |
Response: { subscriberId, email, created: boolean, resubscribed: boolean, pending?: boolean }
IMPORTANT
Double opt-in is on by default. A brand-new subscriber is created pending: true and sent a confirmation email — pending subscribers are never mailed by any campaign (enforced at send time regardless of segment filters). Re-adding someone who previously unsubscribed always comes back pending: true too — their earlier consent does not carry over. An already-active, already-confirmed subscriber is a pure no-op (created: false, resubscribed: false).
curl -X POST https://mcp.vlozi.app/tools/newsletter.add_subscriber \
-H "Authorization: Bearer $VLOZI_API_KEY" \
-d '{"email": "ben@example.com", "tags": ["from-form"]}'newsletter.list_subscribers
List subscribers with paging.
Scope: newsletter:subscribers.read
Input:
| Field | Type | Notes |
|---|---|---|
status |
"all" | "active" | "pending" | "unsubscribed" |
Default all |
tag |
string |
Only subscribers carrying this tag |
limit |
number |
Max 200, default 50 |
offset |
number |
Default 0 |
Response: { subscribers: { id, email, name, status, tags, createdAt }[], total, limit, offset }
NOTE
status: "pending" means active-but-unconfirmed (double opt-in awaiting click); status: "active" means active AND confirmed.
newsletter.tag_subscriber
Add one or more tags to a subscriber — a union, not a replace.
Scope: newsletter:subscribers.create
Input: { id: string, tags: string[] } (both required, tags non-empty)
Response: { subscriberId, tags } — the full tag set after the merge
newsletter.unsubscribe_subscriber
Unsubscribe an active subscriber by id, stopping all future campaign sends.
Scope: newsletter:subscribers.create
Input: { id: string } (required)
Response: { unsubscribed: true }
NOTE
Returned unconditionally — the response doesn't distinguish "unsubscribed now" from "wasn't active to begin with."
newsletter.remove_subscriber
Permanently delete a subscriber (GDPR erase).
Scope: newsletter:subscribers.delete
Input: { id: string } (required)
Response: { deleted: true }
CAUTION
Hard delete, irreversible — the record and its history are gone. Confirm with the user first.
Templates & segments
newsletter.list_templates
List reusable email templates.
Scope: newsletter:templates.read
Input: none
Response: { templates: { id, name, subject, updatedAt }[] } — capped at 100, newest-updated first
newsletter.create_template
Create a reusable email template.
Scope: newsletter:templates.create
Input:
| Field | Type | Notes |
|---|---|---|
name |
string (required) |
|
subject |
string (required) |
May contain merge variables |
htmlBody |
string (required) |
May contain merge variables |
textBody |
string |
Auto-derived from htmlBody if omitted |
previewText |
string |
Inbox preview text |
category |
string |
Response: { template: { id, name, variables: string[] } } — variables are merge tags like subscriber.name, auto-detected from the subject/body text
newsletter.create_segment
Create a saved, named, filtered subscriber list to target campaigns at.
Scope: newsletter:subscribers.create
Input:
| Field | Type | Notes |
|---|---|---|
name |
string (required) |
|
description |
string |
|
filterRules |
{ tags?: string[], status?: "active" | "all", subscribed_after?: string, subscribed_before?: string } |
Omit entirely to match all active, confirmed subscribers |
Response: { segment: { id, name, subscriberCount } } — subscriberCount is a snapshot at creation time, not live-updated
IMPORTANT
Confirmed opt-in is enforced unconditionally — even status: "all" never includes unconfirmed (pending), unsubscribed, bounced, or complained subscribers. status: "all" only widens beyond "active" within the confirmed set. tags matches on any overlap. Invalid dates in subscribed_after/subscribed_before are silently ignored, not rejected.
curl -X POST https://mcp.vlozi.app/tools/newsletter.create_segment \
-H "Authorization: Bearer $VLOZI_API_KEY" \
-d '{"name": "Engaged last 30 days", "filterRules": {"subscribed_after": "2026-07-01T00:00:00Z"}}'Campaigns
newsletter.create_campaign
Create a draft email campaign. Always created as a draft — call send_campaign to actually send it.
Scope: newsletter:campaigns.create
Input:
| Field | Type | Notes |
|---|---|---|
name |
string (required) |
Internal name |
subject |
string (required) |
|
htmlBody |
string (required) |
|
textBody |
string |
|
templateId |
string |
Must exist in the workspace |
segmentId |
string |
Must exist in the workspace; default targets all active subscribers |
Response: { campaign: { id, name, status: "draft" } }
NOTE
Returns 400 up front if segmentId or templateId doesn't exist in this workspace — validated before the campaign row is created.
newsletter.send_campaign
Send an existing draft or scheduled campaign immediately.
Scope: newsletter:campaigns.send
Input: { id: string } (required)
Response: { campaignId, status: "sending", queued: number, creditsCharged: number }
IMPORTANT
Charges credits up front — 1 credit per 5 emails (Math.ceil(recipientCount / 5)) — refunded on failure to enqueue. Only fires campaigns currently draft or scheduled (an atomic guard prevents double-firing on a race); anything else fails closed. Recipients always exclude anyone on the suppression list, unsubscribed, bounced, or complained, and always require confirmed opt-in — regardless of the target segment's own filter. Consent is re-checked again at actual dispatch time, so someone who unsubscribes between enqueue and send is skipped, not sent. Runs a CAN-SPAM pre-flight (workspace must have a configured postal address) before doing anything.
Failure reasons map to: 404 not found, 402 insufficient credits, 500 billing misconfigured, 400 for everything else (wrong_status, no_subscribers, too_many_recipients [cap 100,000], segment_not_found, message_too_large [110KB combined body], missing_postal_address, sender_settings_unavailable, queue_unavailable).
curl -X POST https://mcp.vlozi.app/tools/newsletter.send_campaign \
-H "Authorization: Bearer $VLOZI_API_KEY" \
-d '{"id": "camp_xxx"}'newsletter.cancel_campaign
Cancel a scheduled campaign before it fires.
Scope: newsletter:campaigns.create
Input: { id: string } (required)
Response: { cancelled: true }
NOTE
Only acts on campaigns currently in the scheduled state — a draft campaign was never scheduled and can't be "cancelled" this way, and an already sending/sent/cancelled campaign is untouched. Any of those returns 404 with "campaign not found or not scheduled".
AI
newsletter.generate_subject
AI-write catchy subject-line candidates from a short brief.
Scope: newsletter:campaigns.create
Input: { brief: string, tone?: string, count?: number } (brief required; count 1–10, default 5, clamped server-side)
Response: { subjects: string[], creditsCharged: number, model: string }
IMPORTANT
Charges a flat 5-credit fee, refunded automatically if generation fails. Returns 503 if no AI model key is configured, 402 if you're out of credits, 403 if the plan is inactive.
curl -X POST https://mcp.vlozi.app/tools/newsletter.generate_subject \
-H "Authorization: Bearer $VLOZI_API_KEY" \
-d '{"brief": "We just shipped OAuth login for MCP", "tone": "playful", "count": 3}'