Eleven tools cover transactional email — sending, your vanity sender alias, deliverability logs, and the suppression list. Comms is email-only today; SMS returns 501 Not Implemented.
All endpoints follow the pattern POST https://mcp.vlozi.app/tools/comms.<name> with Authorization: Bearer ls_xxx. All responses use this envelope:
{ "data": <payload>, "error": null } // success
{ "data": null, "error": "..." } // failure (with HTTP 4xx/5xx)Sending
comms.send_message
Send a transactional email.
Scope: comms:send
Input: { to: string, subject?: string, html?: string, text?: string, metadata?: object } (to required; provide html and/or text)
Response: { messageId: string, to: string, sent: boolean, status: string, reason?: string }
IMPORTANT
Rate-limited to 60 sends per 60 seconds per tenant (429 once exceeded). Billed at 1 credit per 5 emails. If the recipient is on your suppression list, the send is silently skipped — you get sent: false, status: "suppressed", reason: "Recipient is suppressed (bounce or complaint)" back, not an error, and it isn't billed.
curl -X POST https://mcp.vlozi.app/tools/comms.send_message \
-H "Authorization: Bearer $VLOZI_API_KEY" \
-d '{"to": "ben@example.com", "subject": "Your export is ready", "html": "<p>Download it here.</p>"}'Sender identity
comms.claim_alias
Claim a vanity sending alias — <slug>@mail.vlozi.app.
Scope: comms:sender.write
Input: { slug: string } (required, 3–30 chars, lowercase letters/digits/single hyphens)
Response (new claim): { alias: string, fromEmail: string, chargedCredits: 20 }
Response (already yours): { alias: string, fromEmail: string, alreadyOwned: true }
IMPORTANT
Costs 20 credits, charged only on a genuinely new claim (idempotent re-claiming your own alias is free). Rejects reserved/role addresses (postmaster, support, …) and common brand names with 422. Returns 409 if another workspace claims the same slug in a race — the charge is automatically refunded in that case.
comms.check_alias_availability
Check whether a slug is available before claiming it.
Scope: comms:sender.read
Input: { slug: string } (required)
Response: { available: boolean, slug: string, address: string, reason: string | null }
NOTE
reason explains an unavailable result (too_short, too_long, invalid_format, reserved, or taken) — always 200, even for malformed input. This check can't tell "you already own it" apart from "someone else owns it"; both read as unavailable. claim_alias itself does handle that distinction.
comms.get_sender_settings
Get your workspace's sender configuration.
Scope: comms:sender.read
Input: none
Response: { settings: { senderName, senderEmail, fromEmail, vloziAlias, phoneNumber, brandLogoUrl, brandPrimaryColor, orgName, orgStreet, orgCity, orgRegion, orgPostalCode, orgCountry, updatedAt } }
comms.update_sender_settings
Update sender configuration. Pass only the fields to change.
Scope: comms:sender.write
Input: { sender_name?, sender_email?, from_email?, phone_number?, brand_logo_url?, brand_primary_color?, org_name?, org_street?, org_city?, org_region?, org_postal_code?, org_country? } (all optional)
Response: { settings: object } — the full updated row
WARNING
from_email is validated against what you're actually allowed to send as: on mail.vlozi.app you can only use the shared notify@mail.vlozi.app or your own claimed alias; on any other domain, that exact domain must already be verified under Domains. An invalid choice returns 422 with a message explaining what to do — claim an alias or verify a domain first. Pass null to clear it back to the platform default.
comms.get_sender_identities
List every address you're allowed to send from right now.
Scope: comms:sender.read
Input: none
Response: { identities: { address, label, kind: "default" | "alias" | "domain", domain? }[], current: string | null }
NOTE
Always includes the shared notify@mail.vlozi.app, plus your claimed alias (if any) and one entry per verified custom domain. Check this before calling update_sender_settings so the address you pick is guaranteed to pass validation.
Deliverability
comms.list_logs
List recent send logs.
Scope: comms:logs.read
Input: { status?: string, channel?: string, limit?: number } (limit max 200, default 50)
Response: { logs: { id, channel, recipient, subject, status, provider, providerMessageId, error, metadata, sentAt, createdAt, deliveryStatus }[], count: number }
NOTE
deliveryStatus is the latest provider webhook event (delivered, bounced, opened, clicked, complained) and can differ from status — e.g. status: "sent" with deliveryStatus: "bounced" once the bounce webhook lands.
comms.get_analytics
Send/delivery stats over a trailing window.
Scope: comms:logs.read
Input: { days?: number } (1–365, default 30)
Response:
{
"window_days": 30,
"sends": { "sent": 0, "failed": 0, "suppressed": 0, "queued": 0 },
"events": { "delivered": 0, "bounced": 0, "complained": 0, "opened": 0, "clicked": 0 },
"rates": { "delivery_rate": 0, "bounce_rate": 0, "complaint_rate": 0 }
}comms.list_suppression
List your do-not-mail list.
Scope: comms:logs.read
Input: { q?: string, limit?: number, offset?: number } (limit max 200, default 50)
Response: { entries: { email, reason, createdAt }[], total: number }
NOTE
Rows land here automatically on a hard bounce or spam complaint, in addition to whatever you add manually.
comms.add_suppression
Manually add an address to the suppression list.
Scope: comms:sender.write
Input: { email: string, reason?: string } (reason defaults to "manual")
Response: { email: string } (normalized) — idempotent, adding an already-suppressed address is a no-op
CAUTION
Be careful suppressing an address tied to your own account's auth email (password reset, invites) — there's no in-product way to un-suppress your way back into account recovery. Use remove_suppression deliberately if that happens.
comms.remove_suppression
Remove an address from the suppression list.
Scope: comms:sender.write
Input: { email: string } (required)
Response: { email: string } (normalized) — a no-op, still 200, if the address wasn't suppressed
NOTE
Only use this once the recipient has explicitly re-opted in — nothing in the API enforces that, it's on you.