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).