Documentation
DocsLLM / Runtime ReferenceLLM Context
LLM / Runtime Context
This section contains a complete, structured API reference designed to be copied into an agent runtime prompt, tool definition, or LLM context window. It includes every route, data type, SDK method, and common usage pattern in a dense, machine-readable format.
Plain text
# OutreachAgent API Reference (LLM Context)
## Overview
OutreachAgent is an API-first email infrastructure platform for teams building AI agents.
Bring your own agent runtime. OutreachAgent handles inboxes, delivery, workflows, scheduling, webhooks, and observability.
Optional intelligence helpers can add semantic search and structured extraction when configured, but they are not required.
Base URL: https://api.outreachagent.dev/v1
Auth: Bearer token in Authorization header. API keys are prefixed with "rm_".
Rate limit: 100 requests/minute per organization.
Pagination: All list endpoints accept ?limit= (max 100, default 20) and ?offset= (default 0).
Response shape for lists: { items: T[], total: number, limit: number, offset: number }
Idempotency: POST /v1/messages/send and POST /v1/enrollments accept Idempotency-Key header.
Errors: { error: { code: string, message: string } } with appropriate HTTP status.
Retryable status codes: 408, 429, 500, 502, 503, 504.
## Resource Hierarchy
Organization > Pod > Inbox > Thread > Message > Attachment
Organizations are the tenant boundary. Pods provide regional/logical isolation. Inboxes are email identities. Threads group related messages. Messages are individual emails.
## Data Model
Organization { id, name, slug, plan: "free"|"pro", createdAt }
ApiKey { id, name, maskedKey, lastUsedAt, createdAt }
Pod { id, name, region, createdAt }
Inbox { id, address, displayName, status: "active"|"warming"|"disabled", podId, createdAt, updatedAt }
Domain { id, name, status: "pending"|"verifying"|"verified"|"failed", dkimStatus: "missing"|"pending"|"verified", spfStatus: "missing"|"pending"|"verified", createdAt, dnsRecords?: [{ type, host, value, priority? }] }
Message { id, inboxId, threadId, direction: "inbound"|"outbound", subject, preview, from, to: string[], status: "queued"|"sent"|"delivered"|"received"|"bounced", createdAt, attachments: Attachment[] }
Thread { id, inboxId, subject, participants: string[], lastMessageAt, messageCount }
Attachment { id, fileName, contentType, size, url }
Contact { id, email, fullName, attributes: Record<string, string | number | boolean | null>, segmentIds: string[], lastSeenAt, createdAt }
Template { id, name, subject, status: "active"|"draft"|"paused"|"failed"|"archived", version, updatedAt }
TemplatePreview { subject, html, text?, missingVariables: string[] }
WorkflowDefinition { id, name, version, status: "active"|"draft"|"paused"|"failed"|"archived", trigger: "manual"|"api"|"event"|"scheduled", nodes: WorkflowNode[], exitCriteria?: ExitCriterion[], optOutMode?: "link"|"reply"|"header_only", schedule?: { cronExpression: string, contactFilter?: { segmentId?: string } }, pausedNodeIds?: string[], createdAt, updatedAt }
WorkflowNode { id, type: "delay"|"send_email"|"send_webhook"|"wait_for_event"|"branch"|"exit"|"goto"|"ab_test", label, nextNodeId, delayAmount?, delayUnit?: "minutes"|"hours"|"days", delayMinutes?, templateId?, inboxId?, webhookUrl?, webhookBody?, webhookAuthHeader?, waitForEvent?, timeoutMinutes?, timeoutNextNodeId?, branches?: [{ condition: { field, operator: "equals"|"contains"|"exists"|"not_equals"|"is_true"|"is_false", value? }, nextNodeId }], targetNodeId?: string, maxIterations?: number, variants?: AbTestVariant[], webhookResponseBranches?: [{ condition: { field, operator, value? }, nextNodeId }] }
AbTestVariant { id, templateId, weight }
ExitCriterion { trigger: "reply"|"bounce"|"unsubscribe"|"manual"|"custom_event", eventName?, replyClassifications?: string[] }
Enrollment { id, workflowId, contactId, status: "active"|"completed"|"paused"|"failed"|"exited", currentNodeId, exitReason?, variantId?, createdAt }
WorkflowAnalytics { workflowId, totalEnrollments, activeEnrollments, completedEnrollments, exitedEnrollments, failedEnrollments, emailsSent, emailsBounced, completionRate, exitRate }
BulkEnrollmentResult { contactId, enrollmentId?, status: "enrolled"|"skipped"|"failed", reason? }
SendLimit { id, organizationId, inboxId?, dailyLimit, createdAt }
WorkflowTemplate { id, name, description, category, trigger, nodes, exitCriteria? }
ExecutionLog { id, enrollmentId, nodeId, status: "scheduled"|"running"|"completed"|"failed"|"skipped", message, createdAt }
WebhookEndpoint { id, url, status: "healthy"|"degraded"|"failing", subscribedEvents: string[], errorRate, createdAt }
WebhookDelivery { id, endpointId, eventName, status: "delivered"|"retrying"|"failed", attemptCount, lastAttemptAt }
WebhookEvent { id, name, createdAt, resourceId, resourceType }
UsageSummary { totalSent, totalDelivered, totalComplained, deliveryRate, bounceRate, complaintRate, rejectionRate }
Subscription { id, organizationId, plan, status, stripeCustomerId?, stripeSubscriptionId?, currentPeriodEnd, createdAt, updatedAt }
UsageCounter { id, metric, count, windowStart }
Invitation { id, organizationId, email, role: "owner"|"admin"|"member", accepted, createdAt }
## Complete Route Catalog (63 routes)
GET /v1/me
GET /v1/api-keys
POST /v1/api-keys body: { name: string }
DELETE /v1/api-keys/:apiKeyId
GET /v1/pods
POST /v1/pods body: { name: string, region: string }
GET /v1/pods/:podId
DELETE /v1/pods/:podId
GET /v1/inboxes
POST /v1/inboxes body: { address: string, displayName: string, podId: string }
GET /v1/inboxes/:inboxId
PATCH /v1/inboxes/:inboxId body: { displayName?: string, status?: string }
DELETE /v1/inboxes/:inboxId
GET /v1/messages
POST /v1/messages/send body: { inboxId: string, subject: string, body: string, html?: string, from: string, to: string[] }
GET /v1/messages/:messageId
GET /v1/threads
GET /v1/threads/:threadId
GET /v1/domains
POST /v1/domains body: { name: string }
GET /v1/domains/:domainId
DELETE /v1/domains/:domainId
GET /v1/contacts
POST /v1/contacts body: { email: string, fullName: string, attributes?: Record<string, string | number | boolean | null>, segmentIds?: string[] }
GET /v1/contacts/:contactId
PATCH /v1/contacts/:contactId body: { fullName?: string, email?: string, attributes?: Record<string, string | number | boolean | null>, segmentIds?: string[] }
DELETE /v1/contacts/:contactId
GET /v1/templates
POST /v1/templates body: { name: string, subject: string, html?: string, text?: string }
GET /v1/templates/:templateId
PATCH /v1/templates/:templateId body: { name?: string, subject?: string, html?: string, text?: string }
DELETE /v1/templates/:templateId
POST /v1/templates/:templateId/preview body: { variables: Record<string, unknown> }
GET /v1/workflows
POST /v1/workflows body: { name: string, trigger: "manual"|"api"|"event", nodes: WorkflowNode[], exitCriteria?: ExitCriterion[], optOutMode?: "link"|"reply"|"header_only" }
GET /v1/workflows/:workflowId
PATCH /v1/workflows/:workflowId body: { name?: string, trigger?: string, nodes?: WorkflowNode[], exitCriteria?: ExitCriterion[], optOutMode?: "link"|"reply"|"header_only" }
DELETE /v1/workflows/:workflowId
POST /v1/workflows/:workflowId/publish
POST /v1/workflows/:workflowId/pause
POST /v1/workflows/:workflowId/resume
GET /v1/workflows/:workflowId/analytics
POST /v1/workflows/:workflowId/simulate body: { contactId?: string, event?: object, webhookResponsesByNodeId?: object, forcedVariantId?: string, seed?: number }
POST /v1/workflows/:workflowId/test-send body: { nodeId: string, to: string, contactId?: string, variables?: Record<string, unknown> }
POST /v1/workflows/:workflowId/preview body: { contactId: string, variables?: Record<string, unknown> }
POST /v1/workflows/:workflowId/nodes/:nodeId/pause
POST /v1/workflows/:workflowId/nodes/:nodeId/resume
GET /v1/workflow-templates
POST /v1/workflows/from-template body: { templateId: string, name?: string }
GET /v1/send-limits
PUT /v1/send-limits body: { inboxId?: string, dailyLimit: number }
GET /v1/enrollments
POST /v1/enrollments body: { workflowId: string, contactId: string, variables?: Record<string, unknown> }
POST /v1/enrollments/bulk body: { workflowId: string, contactIds: string[] }
GET /v1/enrollments/:enrollmentId
GET /v1/enrollments/:enrollmentId/logs
GET /v1/webhooks/endpoints
POST /v1/webhooks/endpoints body: { url: string, subscribedEvents: string[] }
GET /v1/webhooks/endpoints/:endpointId
PATCH /v1/webhooks/endpoints/:endpointId body: { url?: string, subscribedEvents?: string[] }
DELETE /v1/webhooks/endpoints/:endpointId
GET /v1/webhooks/deliveries
GET /v1/events
GET /v1/metrics/summary
GET /v1/metrics/timeseries
GET /v1/organizations
GET /v1/organizations/:organizationId/invitations
POST /v1/organizations/:organizationId/invitations body: { email: string, role: "owner"|"admin"|"member" }
GET /v1/organizations/:organizationId/memberships
GET /v1/billing/subscription
GET /v1/billing/usage-counters
POST /v1/billing/checkout-session body: { tier: "pro" }
POST /v1/billing/portal-session
## TypeScript SDK Quick Reference
Install: npm install @outreachagent/sdk-ts
Import: import { OutreachAgentClient } from "@outreachagent/sdk-ts";
Init: const client = new OutreachAgentClient("https://api.outreachagent.dev", "rm_live_...");
SDK methods (all return Promises):
client.listOrganizations() → Organization[]
client.listApiKeys(params?) → PaginatedResponse<ApiKey>
client.createApiKey(name) → ApiKey
client.revokeApiKey(apiKeyId) → { ok: true }
client.listPods(params?) → PaginatedResponse<Pod>
client.getPod(podId) → Pod
client.createPod({ name, region }) → Pod
client.deletePod(podId) → { ok: true }
client.listInboxes(params?) → PaginatedResponse<Inbox>
client.getInbox(inboxId) → Inbox
client.createInbox({ address, displayName, podId }) → Inbox
client.updateInbox(inboxId, { displayName?, status? }) → Inbox
client.deleteInbox(inboxId) → { ok: true }
client.listMessages(params?) → PaginatedResponse<Message>
client.getMessage(messageId) → Message
client.sendMessage({ inboxId, subject, body, html?, from, to }) → Message
client.listThreads(params?) → PaginatedResponse<Thread>
client.getThread(threadId) → Thread
client.listDomains(params?) → PaginatedResponse<Domain>
client.getDomain(domainId) → Domain
client.createDomain(name) → Domain
client.deleteDomain(domainId) → { ok: true }
client.listContacts(params?) → PaginatedResponse<Contact>
client.getContact(contactId) → Contact
client.createContact({ email, fullName, attributes?, segmentIds? }) → Contact
client.updateContact(contactId, { fullName?, email?, attributes?, segmentIds? }) → Contact
client.deleteContact(contactId) → { ok: true }
client.listTemplates(params?) → PaginatedResponse<Template>
client.getTemplate(templateId) → Template
client.createTemplate({ name, subject, html?, text? }) → Template
client.updateTemplate(templateId, { name?, subject?, html?, text? }) → Template
client.deleteTemplate(templateId) → { ok: true }
client.previewTemplate(templateId, variables) → TemplatePreview
client.listWorkflows(params?) → PaginatedResponse<WorkflowDefinition>
client.getWorkflow(workflowId) → WorkflowDefinition
client.createWorkflow({ name, trigger, nodes, exitCriteria?, optOutMode? }) → WorkflowDefinition
client.updateWorkflow(workflowId, { name?, trigger?, nodes?, exitCriteria?, optOutMode? }) → WorkflowDefinition
client.deleteWorkflow(workflowId) → { ok: true }
client.publishWorkflow(workflowId) → WorkflowDefinition
client.pauseWorkflow(workflowId) → WorkflowDefinition
client.resumeWorkflow(workflowId) → WorkflowDefinition
client.getWorkflowAnalytics(workflowId) → WorkflowAnalytics
client.pauseNode(workflowId, nodeId) → WorkflowDefinition
client.resumeNode(workflowId, nodeId) → WorkflowDefinition
client.simulateWorkflow(workflowId, payload) → WorkflowSimulationResponse
client.testSendWorkflow(workflowId, { nodeId, to, contactId?, variables? }) → { subject, html, text?, to }
client.previewWorkflow(workflowId, { contactId, variables? }) → { workflowId, workflowName, previews: [{ nodeId, label, subject, html, text, missingVariables }] }
client.listWorkflowTemplates() → WorkflowTemplate[]
client.cloneFromTemplate(templateId, name?) → WorkflowDefinition
client.listSendLimits() → SendLimit[]
client.upsertSendLimit({ inboxId?, dailyLimit }) → SendLimit
client.listEnrollments(params?) → PaginatedResponse<Enrollment>
client.getEnrollment(enrollmentId) → Enrollment
client.createEnrollment({ workflowId, contactId, variables? }) → Enrollment
client.bulkEnroll(workflowId, contactIds) → { results: BulkEnrollmentResult[] }
client.listEnrollmentLogs(enrollmentId) → ExecutionLog[]
client.listWebhookEndpoints(params?) → PaginatedResponse<WebhookEndpoint>
client.getWebhookEndpoint(endpointId) → WebhookEndpoint
client.createWebhookEndpoint({ url, subscribedEvents }) → WebhookEndpoint
client.updateWebhookEndpoint(endpointId, { url?, subscribedEvents? }) → WebhookEndpoint
client.deleteWebhookEndpoint(endpointId) → { ok: true }
client.listWebhookDeliveries(params?) → PaginatedResponse<WebhookDelivery>
client.listEvents(params?) → PaginatedResponse<WebhookEvent>
client.getMetricsSummary() → UsageSummary
client.createCheckoutSession("pro") → CheckoutSession
## Common Recipes
### 1. Send an email (sandbox domain — no DNS setup required)
const client = new OutreachAgentClient("https://api.outreachagent.dev", "rm_live_...");
const message = await client.sendMessage({
inboxId: "inb_abc123",
subject: "Hello from my agent",
body: "Hi there,\n\nI noticed you recently signed up for a demo. I wanted to follow up and see if you had any questions about our platform.\n\nWould you have 15 minutes this week for a quick call?\n\nBest,\nAgent",
from: "agent@mail.outreachagent.dev",
to: ["recipient@example.com"]
});
// Note: mail.outreachagent.dev is a shared sandbox domain that works without DNS verification.
// For production, add and verify your own domain via POST /v1/domains, then use that domain for better deliverability.
### 2. Create inbox and send (custom domain — requires DNS verification)
const pod = await client.createPod({ name: "production", region: "us-east-1" });
// First add and verify your domain: POST /v1/domains { name: "yourdomain.com" }
// Then configure the DNS records returned in the response and call POST /v1/domains/:id/verify
const inbox = await client.createInbox({ address: "outreach@yourdomain.com", displayName: "Sales Agent", podId: pod.id });
const msg = await client.sendMessage({ inboxId: inbox.id, subject: "Hi", body: "Just wanted to follow up on our earlier conversation. Let me know if you have any questions.", from: inbox.address, to: ["lead@company.com"] });
### 3. Set up webhook for inbound emails
const endpoint = await client.createWebhookEndpoint({
url: "https://your-server.com/webhooks/outreachagent",
subscribedEvents: ["message.received", "message.delivered", "message.bounced"]
});
### 4. Create and run a drip campaign workflow
// Templates use Liquid syntax. Variables available at render time:
// {{ contact.fullName }} — from Contact.fullName
// {{ contact.email }} — from Contact.email
// {{ contact.attributes.firstName }} — from Contact.attributes
// {{ contact.attributes.company }} — from Contact.attributes
// {{ organization.name }} — your organization name
const template = await client.createTemplate({
name: "Welcome",
subject: "Welcome to {{ organization.name }}, {{ contact.attributes.firstName }}",
text: "Hi {{ contact.attributes.firstName }},\n\nThanks for signing up! I'm reaching out because I noticed {{ contact.attributes.company }} could benefit from automated email workflows.\n\nWould you have 15 minutes this week for a quick walkthrough? I'd love to show you how teams like yours use OutreachAgent to save hours on outreach.\n\nBest,\nThe {{ organization.name }} Team",
html: "<p>Hi {{ contact.attributes.firstName }},</p><p>Thanks for signing up! I'm reaching out because I noticed {{ contact.attributes.company }} could benefit from automated email workflows.</p><p>Would you have 15 minutes this week for a quick walkthrough? I'd love to show you how teams like yours use OutreachAgent to save hours on outreach.</p><p>Best,<br>The {{ organization.name }} Team</p>"
});
const workflow = await client.createWorkflow({
name: "Onboarding Drip",
trigger: "api",
nodes: [
{ id: "n1", type: "send_email", label: "Send welcome", templateId: template.id, nextNodeId: "n2" },
{ id: "n2", type: "delay", label: "Wait 2 days", delayAmount: 2, delayUnit: "days", nextNodeId: "n3" },
{ id: "n3", type: "send_email", label: "Follow up", templateId: template.id, nextNodeId: "n4" },
{ id: "n4", type: "exit", label: "Done", nextNodeId: null }
],
exitCriteria: [{ trigger: "reply" }]
});
await client.publishWorkflow(workflow.id);
const contact = await client.createContact({
email: "jane@acmecorp.com",
fullName: "Jane Doe",
attributes: { firstName: "Jane", company: "Acme Corp", title: "VP of Sales" }
});
await client.createEnrollment({ workflowId: workflow.id, contactId: contact.id });
### 5. Monitor deliverability
const metrics = await client.getMetricsSummary();
// metrics = { totalSent, totalDelivered, deliveryRate, bounceRate, complaintRate, rejectionRate }
if (metrics.bounceRate > 0.02) console.warn("Bounce rate above 2% threshold");
if (metrics.complaintRate > 0.001) console.warn("Complaint rate above 0.1% threshold");
### 6. A/B test email variants
const workflow = await client.createWorkflow({
name: "A/B Welcome",
trigger: "api",
nodes: [
{ id: "n1", type: "ab_test", label: "Test subject lines", variants: [
{ id: "a", templateId: "tmpl_a", weight: 50 },
{ id: "b", templateId: "tmpl_b", weight: 50 }
], nextNodeId: "n2" },
{ id: "n2", type: "exit", label: "Done", nextNodeId: null }
]
});
### 7. Bulk enroll contacts
const result = await client.bulkEnroll("wf_abc", ["con_1", "con_2", "con_3"]);
// result.results = [{ contactId: "con_1", enrollmentId: "enr_1", status: "enrolled" }, ...]
### 8. Get workflow analytics
const analytics = await client.getWorkflowAnalytics("wf_abc");
// analytics = { totalEnrollments, activeEnrollments, completedEnrollments, completionRate, ... }
### 9. Set send limits
await client.upsertSendLimit({ dailyLimit: 500 }); // org-wide
await client.upsertSendLimit({ inboxId: "inb_abc", dailyLimit: 100 }); // per inbox
### 10. Create workflow from template
const templates = await client.listWorkflowTemplates();
const wf = await client.cloneFromTemplate(templates[0].id, "My Campaign");
## Template Variables (Liquid)
Templates use Liquid syntax. The following variables are available when a template is rendered (during workflow sends, direct sends, and previews):
| Variable | Source | Example |
|----------|--------|---------|
| {{ contact.fullName }} | Contact.fullName | "Jane Doe" |
| {{ contact.email }} | Contact.email | "jane@acme.com" |
| {{ contact.attributes.firstName }} | Contact.attributes | "Jane" |
| {{ contact.attributes.company }} | Contact.attributes | "Acme Corp" |
| {{ contact.attributes.title }} | Contact.attributes | "VP of Sales" |
| {{ contact.attributes.<key> }} | Any key in Contact.attributes | (custom) |
| {{ organization.name }} | Organization.name | "My Company" |
| {{ unsubscribe_text }} | Auto-generated opt-out text | "If this isn't relevant, just let me know and I won't reach out again." |
Store personalization data (firstName, company, title, etc.) in the Contact `attributes` field, then reference it in templates as `{{ contact.attributes.<key> }}`.
The `{{ unsubscribe_text }}` variable is populated when the workflow's `optOutMode` is set to `"reply"` (the default). Place it naturally in your email copy, e.g. as a PS line. When `optOutMode` is `"link"` or `"header_only"`, it renders as an empty string.
## MCP Server (Model Context Protocol)
The @outreachagent/mcp package exposes all 75 SDK methods as MCP tools for AI agents in Cursor, Claude Desktop, or any MCP-compatible client.
Install: npx @outreachagent/mcp
Config: OUTREACHAGENT_API_KEY (required), OUTREACHAGENT_BASE_URL (optional, defaults to https://api.outreachagent.dev)
Cursor setup (.cursor/mcp.json):
{
"mcpServers": {
"outreachagent": {
"command": "npx",
"args": ["@outreachagent/mcp"],
"env": { "OUTREACHAGENT_API_KEY": "rm_live_..." }
}
}
}
Tool groups (75 total): Organizations (1), API Keys (3), Pods (4), Inboxes (5), Messages (4), Policies (4), Approvals (4), Threads (2), Domains (4), Webhooks (8), Contacts (5), Segments (5), Templates (6), Workflows (11), Enrollments (4), Metrics (1), Extractions (2), Realtime (1), Billing (1).
Docs: https://outreachagent.dev/docs/mcp-server
## Best Practices
Follow these guidelines before sending real emails. Skipping domain setup, warmup, or contact verification can permanently damage your sender reputation.
### Domain Setup
- Do NOT use the sandbox domain (mail.outreachagent.dev) for real outreach. Add a custom domain via POST /v1/domains.
- Use a subdomain for outreach (e.g. outreach.yourdomain.com) to isolate reputation from your primary domain.
- Configure SPF and DKIM using the DNS records returned from domain creation.
- Complete the warmup schedule: week 1 at 5/day, week 2 at 15/day, week 3 at 30/day, week 4+ unrestricted.
- Set daily send limits with PUT /v1/send-limits to match your warmup stage.
### Contact Verification
- Always verify email addresses before enrolling contacts. Hard bounces permanently damage sender reputation.
- Verify individual contacts: POST /v1/contacts/:contactId/verify
- Bulk verification: POST /v1/contacts/verify-bulk
- Contacts marked "invalid" are automatically rejected from enrollment.
- Treat "catch_all" and "unknown" results with caution.
### Writing Cold Email
- Subject lines: plain, lowercase-friendly, no fake urgency. "Quick question" and "Following up" are red flags.
- Body: short sentences, conversational tone. No walls of text.
- Every follow-up needs a fresh angle — never send "just bumping this."
- Use real research about the recipient. Don't fake familiarity.
### Testing Workflows (Do This Before Every Publish)
- Step 1 — Simulate: client.simulateWorkflow(workflowId, { contactId }) → dry-run, no emails sent, returns step-by-step trace.
- Step 2 — Preview: client.previewWorkflow(workflowId, { contactId }) → renders all templates, shows missing variables.
- Step 3 — Test send: client.testSendWorkflow(workflowId, { nodeId, to: "you@yourteam.com", contactId }) → delivers real email to your inbox.
### Sending Configuration
- Use send windows: restrict to business hours in recipient's timezone (e.g. 8am–11am).
- Add jitterMinutes to delay nodes to avoid machine-like sending patterns.
- For lists over 50 contacts, use inbox rotation (inboxIds on send nodes, senderStrategy: round_robin or random).
- Always set exitCriteria: [{ trigger: "reply" }] at minimum. Stop sending when someone responds.
- Ramp volume gradually. Match send limits to your warmup stage.
### Compliance
- Set optOutMode on every workflow ("reply" is the default — adds List-Unsubscribe headers).
- Include {{ unsubscribe_text }} in templates for a soft opt-out line.
- Unsubscribed contacts are automatically suppressed and cannot be re-enrolled.
- Use a real sender identity.
### Monitoring
- Check GET /v1/metrics/summary regularly. Targets: delivery rate > 95%, bounce rate < 2%, complaint rate < 0.1%.
- If bounce > 2% or complaint > 0.1% for 24 hours, sending auto-pauses for the inbox.
- Check metrics after every campaign — don't wait for auto-pause.
### Common Mistakes
- Using sandbox domain for real outreach (shared, not authenticated for your brand).
- Skipping warmup (sending 500 emails on day one from a new domain lands in spam).
- Sending to unverified lists (one bad batch can tank your reputation permanently).
- Enrolling contacts without testing the workflow first.
- Writing generic, template-sounding copy ("I hope this email finds you well" is a spam signal).
- No exit criteria (sending follow-ups after a reply causes spam complaints).