Skill: concaption/foxreach-io (version 1.0.0)

--- name: foxreach description: Manage FoxReach cold email outreach — leads, campaigns, sequences, templates, email accounts, inbox, and analytics. Use when the user asks to create leads, manage campaigns, check analytics, send outreach, manage email sequences, or anything related to the FoxReach API. argument-hint: "[resource] [action] [options]" allowed-tools: Bash(python *), Bash(cd *), Bash(FOXREACH_API_KEY=* python *), Read, Grep, Glob --- # FoxReach API Management Skill You are managing the FoxReach cold email outreach platform through its Python SDK and CLI. This skill covers all API operations for leads, campaigns, sequences, templates, email accounts, inbox, and analytics. ## Setup & Authentication The Python SDK is at `integrations/sdk-python/` and the CLI is at `integrations/cli/`. Both use API key authentication with keys prefixed `otr_`. **Check if the SDK is available:** ```bash python -c "from foxreach import FoxReach; print('SDK ready')" ``` **If not installed, install it:** ```bash cd integrations/sdk-python && pip install -e . ``` **Authentication** — Always get the API key from the user or environment before making calls. Never hardcode keys. Use environment variable injection: ```bash FOXREACH_API_KEY=otr_... python script.py ``` Or use the CLI config: ```bash cd integrations/cli && PYTHONPATH=. python -m foxreach_cli.main config set-key --key otr_... ``` ## How to Execute Operations Write inline Python scripts using the SDK. Always follow this pattern: ```python import json from foxreach import FoxReach client = FoxReach([REDACTED_SECRET]") # ... perform operation ... client.close() ``` For quick operations, use one-liners: ```bash python -c " from foxreach import FoxReach client = FoxReach([REDACTED_SECRET]') result = client.leads.list(page_size=10) for lead in result: print(f'{lead.id} {lead.email} {lead.status}') print(f'Total: {result.meta.total}') client.close() " ``` --- ## Resource Reference For complete API details, see [api-reference.md](api-reference.md). For usage examples of every operation, see [examples.md](examples.md). --- ## Quick Reference — Available Operations ### Leads | Action | Method | Notes | |--------|--------|-------| | List | `client.leads.list(page=1, page_size=50, search=..., status=..., tags=...)` | Paginated, filterable | | Get | `client.leads.get(lead_id)` | Returns single Lead | | Create | `client.leads.create(LeadCreate(email=..., first_name=..., ...))` | Deduplicates by email | | Update | `client.leads.update(lead_id, LeadUpdate(company=..., ...))` | Partial update | | Delete | `client.leads.delete(lead_id)` | Soft-delete | ### Campaigns | Action | Method | Notes | |--------|--------|-------| | List | `client.campaigns.list(status=...)` | Filter by draft/active/paused/completed | | Get | `client.campaigns.get(campaign_id)` | Includes stats | | Create | `client.campaigns.create(CampaignCreate(name=..., ...))` | Creates in draft | | Update | `client.campaigns.update(campaign_id, CampaignUpdate(...))` | Can't edit if active | | Delete | `client.campaigns.delete(campaign_id)` | Must be draft | | Start | `client.campaigns.start(campaign_id)` | Transitions to active | | Pause | `client.campaigns.pause(campaign_id)` | Pauses sending | | Add Leads | `client.campaigns.add_leads(campaign_id, [lead_ids])` | Bulk add | | Add Accounts | `client.campaigns.add_accounts(campaign_id, [account_ids])` | Assign senders | ### Sequences (nested under campaigns) | Action | Method | Notes | |--------|--------|-------| | List | `client.campaigns.sequences.list(campaign_id)` | All steps | | Create | `client.campaigns.sequences.create(campaign_id, SequenceCreate(body=..., ...))` | Add step | | Update | `client.campaigns.sequences.update(campaign_id, seq_id, SequenceUpdate(...))` | Edit step | | Delete | `client.campaigns.sequences.delete(campaign_id, seq_id)` | Remove step | ### Templates | Action | Method | Notes | |--------|--------|-------| | List | `client.templates.list()` | Paginated | | Get | `client.templates.get(template_id)` | Single template | | Create | `client.templates.create(TemplateCreate(name=..., body=...))` | New template | | Update | `client.templates.update(template_id, TemplateUpdate(...))` | Partial update | | Delete | `client.templates.delete(template_id)` | Remove | ### Email Accounts | Action | Method | Notes | |--------|--------|-------| | List | `client.email_accounts.list()` | Paginated | | Get | `client.email_accounts.get(account_id)` | With health metrics | | Delete | `client.email_accounts.delete(account_id)` | Remove | ### Inbox | Action | Method | Notes | |--------|--------|-------| | List Threads | `client.inbox.list_threads(category=..., is_read=..., ...)` | Filterable | | Get | `client.inbox.get(reply_id)` | Full thread | | Update | `client.inbox.update(reply_id, ThreadUpdate(is_read=..., ...))` | Mark read/starred | ### Analytics | Action | Method | Notes | |--------|--------|-------| | Overview | `client.analytics.overview()` | Dashboard KPIs | | Campaign | `client.analytics.campaign(campaign_id)` | Metrics + daily stats | --- ## Pagination List endpoints return `PaginatedResponse` objects: ```python result = client.leads.list(page=1, page_size=50, search="acme") # Access data for lead in result: print(lead.email) # Check pagination info print(f"Page {result.meta.page}/{result.meta.total_pages}, {result.meta.total} total") # Get next page if result.has_next_page(): next_result = result.next_page() # Auto-paginate through ALL results for lead in client.leads.list().auto_paging_iter(): print(lead.email) ``` --- ## Error Handling Always wrap API calls in try/except: ```python from foxreach import FoxReach, NotFoundError, RateLimitError, AuthenticationError, FoxReachError try: lead = client.leads.get("cld_nonexistent") except NotFoundError: print("Lead not found") except AuthenticationError: print("Invalid API key") except RateLimitError as e: print(f"Rate limited. Retry after {e.retry_after}s") except FoxReachError as e: print(f"API error: {e}") ``` --- ## Template Variables & Personalization Email bodies support variable substitution using `{{variable}}` syntax: - `{{firstName}}`, `{{lastName}}`, `{{email}}` - `{{company}}`, `{{title}}`, `{{phone}}` - `{{website}}`, `{{linkedinUrl}}` - Custom fields: `{{customFieldName}}` Spintax is also supported: `{Hi|Hey|Hello} {{firstName}}` --- ## Common Workflows ### 1. Full Campaign Setup When the user wants to set up a complete campaign, follow these steps in order: 1. Create the campaign with `campaigns.create()` 2. Add sequence steps with `campaigns.sequences.create()` for each email in the chain 3. Add leads with `campaigns.add_leads()` 4. Assign email accounts with `campaigns.add_accounts()` 5. Start the campaign with `campaigns.start()` ### 2. Check Campaign Performance 1. Get campaign analytics with `analytics.campaign(id)` 2. Show sent, delivered, bounced, replied, opened stats 3. Show reply rate and bounce rate 4. If daily_stats are available, summarize trends ### 3. Manage Inbox 1. List unread threads with `inbox.list_threads(is_read=False)` 2. Categorize replies by updating with `inbox.update(id, ThreadUpdate(category="interested"))` 3. Common categories: interested, not_interested, out_of_office, wrong_person, unsubscribe ### 4. Bulk Lead Import For adding multiple leads, create them one by one (the API deduplicates by email): ```python leads_data = [ {"email": "[REDACTED_SECRET]", "first_name": "Alice", "company": "Acme"}, {"email": "[REDACTED_SECRET]", "first_name": "Bob", "company": "Beta"}, ] created = [] for data in leads_data: lead = client.leads.create(LeadCreate(**data)) created.append(lead) print(f"Created: {lead.id} - {lead.email}") ``` --- ## Important Notes - **Base URL**: `https://api.foxreach.io/api/v1` - **Rate limit**: 100 requests per minute. The SDK auto-retries on 429. - **ID prefixes**: Leads `cld_`, Campaigns `cmp_`, Replies `rpl_`, Templates `tpl_` - **Timezone**: All datetimes in UTC ISO 8601 format. - **Sending days**: Array of integers, 1=Monday through 7=Sunday. - **Sending hours**: 0-23 range, in the campaign's timezone. - **Campaign status flow**: draft → active → paused → active → completed - **Soft deletes**: Leads are soft-deleted and can reappear on re-import. - Always confirm with the user before destructive operations (delete, start campaign). - When listing data, default to showing a formatted summary, not raw JSON. - When creating resources, confirm the details with the user before executing.

path: examples.md
content: # FoxReach SDK Examples

Complete working examples for every operation. Replace `otr_YOUR_KEY` with a real API key.

---

## Leads

### List leads with filters

```python
from foxreach import FoxReach

client = FoxReach([REDACTED_SECRET]")

# Basic list
page = client.leads.list(page_size=10)
for lead in page:
    print(f"{lead.id}  {lead.email}  {lead.first_name}  {lead.status}")
print(f"\nPage {page.meta.page}/{page.meta.total_pages} — {page.meta.total} total leads")

# Search by email or name
results = client.leads.list(search="acme", status="active")
for lead in results:
    print(f"{lead.email} at {lead.company}")

# Auto-paginate through ALL leads
count = 0
for lead in client.leads.list(page_size=100).auto_paging_iter():
    count += 1
print(f"Total leads iterated: {count}")

client.close()
```

### Get a single lead

```python
from foxreach import FoxReach, NotFoundError

client = FoxReach([REDACTED_SECRET]")

try:
    lead = client.leads.get("cld_abc123def456")
    print(f"Name: {lead.first_name} {lead.last_name}")
    print(f"Email: {lead.email}")
    print(f"Company: {lead.company}")
    print(f"Title: {lead.title}")
    print(f"Status: {lead.status}")
    print(f"Tags: {', '.join(lead.tags) if lead.tags else 'none'}")
    print(f"Created: {lead.created_at}")
except NotFoundError:
    print("Lead not found")

client.close()
```

### Create a lead

```python
from foxreach import FoxReach, LeadCreate

client = FoxReach([REDACTED_SECRET]")

lead = client.leads.create(LeadCreate(
    email="[REDACTED_SECRET]",
    first_name="Jane",
    last_name="Smith",
    company="Acme Corp",
    title="VP of Sales",
    phone="+1-555-0123",
    linkedin_url="https://linkedin.com/in/janesmith",
    website="https://acme.com",
    notes="Met at SaaStr conference. Interested in our enterprise plan.",
    tags=["conference", "enterprise"],
))

print(f"Created lead: {lead.id} — {lead.email}")
client.close()
```

### Update a lead

```python
from foxreach import FoxReach, LeadUpdate

client = FoxReach([REDACTED_SECRET]")

lead = client.leads.update("cld_abc123def456", LeadUpdate(
    company="Acme International",
    title="SVP of Sales",
    notes="Promoted in Q1. Follow up on enterprise plan.",
))

print(f"Updated lead: {lead.id} — {lead.company}")
client.close()
```

### Delete a lead

```python
from foxreach import FoxReach

client = FoxReach([REDACTED_SECRET]")
client.leads.delete("cld_abc123def456")
print("Lead deleted")
client.close()
```

### Bulk create leads

```python
from foxreach import FoxReach, LeadCreate

client = FoxReach([REDACTED_SECRET]")

contacts = [
    {"email": "[REDACTED_SECRET]", "first_name": "Alice", "company": "Startup Inc"},
    {"email": "[REDACTED_SECRET]", "first_name": "Bob", "company": "BigCorp"},
    {"email": "[REDACTED_SECRET]", "first_name": "Carol", "company": "TechCo", "title": "CTO"},
]

created = []
for contact in contacts:
    lead = client.leads.create(LeadCreate(**contact))
    created.append(lead)
    print(f"  Created: {lead.id} — {lead.email}")

print(f"\n{len(created)} leads created")
client.close()
```

---

## Campaigns

### List campaigns

```python
from foxreach import FoxReach

client = FoxReach([REDACTED_SECRET]")

# All campaigns
page = client.campaigns.list()
for c in page:
    print(f"{c.id}  {c.name:30s}  {c.status:10s}  sent:{c.total_sent}  replied:{c.total_replied}")

# Only active campaigns
active = client.campaigns.list(status="active")
for c in active:
    print(f"{c.name} — {c.total_sent} sent, {c.total_replied} replied")

client.close()
```

### Create a campaign

```python
from foxreach import FoxReach, CampaignCreate

client = FoxReach([REDACTED_SECRET]")

campaign = client.campaigns.create(CampaignCreate(
    name="Q1 2025 Outreach — Enterprise",
    timezone="America/New_York",
    sending_days=[1, 2, 3, 4, 5],      # Mon-Fri
    sending_start_hour=9,
    sending_end_hour=17,
    daily_limit=100,
))

print(f"Campaign created: {campaign.id} — {campaign.name} (status: {campaign.status})")
client.close()
```

### Full campaign setup (create → sequences → leads → accounts → start)

```python
from foxreach import FoxReach, CampaignCreate, SequenceCreate

client = FoxReach([REDACTED_SECRET]")

# 1. Create campaign
campaign = client.campaigns.create(CampaignCreate(
    name="Product Launch Outreach",
    timezone="America/New_York",
    daily_limit=50,
))
print(f"1. Campaign created: {campaign.id}")

# 2. Add sequence steps
step1 = client.campaigns.sequences.create(campaign.id, SequenceCreate(
    subject="Quick question about {{company}}",
    body="Hi {{firstName}},\n\nI noticed {{company}} is growing fast. We help companies like yours with cold email outreach.\n\nWould you be open to a quick chat this week?\n\nBest,\n[Your Name]",
    delay_days=0,
))
print(f"2. Step 1 added: {step1.id}")

step2 = client.campaigns.sequences.create(campaign.id, SequenceCreate(
    subject="Re: Quick question about {{company}}",
    body="Hi {{firstName}},\n\nJust following up on my last email. I know you're busy — would a 15-minute call work better?\n\nHere's a link to book time: [calendar link]\n\nCheers,\n[Your Name]",
    delay_days=3,
))
print(f"   Step 2 added: {step2.id}")

step3 = client.campaigns.sequences.create(campaign.id, SequenceCreate(
    subject="Last try — {{firstName}}",
    body="Hi {{firstName}},\n\n{I understand if this isn't a priority right now|No worries if the timing isn't right}. I'll leave the door open — feel free to reach out anytime.\n\nBest,\n[Your Name]",
    delay_days=5,
))
print(f"   Step 3 added: {step3.id}")

# 3. Add leads
lead_ids = ["cld_lead1", "cld_lead2", "cld_lead3"]
result = client.campaigns.add_leads(campaign.id, lead_ids)
print(f"3. Added {result.get('added', len(lead_ids))} leads")

# 4. Assign email accounts
account_ids = ["acc_sender1"]
result = client.campaigns.add_accounts(campaign.id, account_ids)
print(f"4. Assigned {result.get('added', len(account_ids))} email accounts")

# 5. Start campaign
campaign = client.campaigns.start(campaign.id)
print(f"5. Campaign started! Status: {campaign.status}")

client.close()
```

### Pause / update a campaign

```python
from foxreach import FoxReach, CampaignUpdate

client = FoxReach([REDACTED_SECRET]")

# Pause
campaign = client.campaigns.pause("cmp_xyz789")
print(f"Paused: {campaign.name}")

# Update settings while paused
campaign = client.campaigns.update("cmp_xyz789", CampaignUpdate(
    daily_limit=200,
    sending_end_hour=19,
))
print(f"Updated daily limit to {campaign.daily_limit}")

client.close()
```

---

## Sequences

### List sequences for a campaign

```python
from foxreach import FoxReach

client = FoxReach([REDACTED_SECRET]")

steps = client.campaigns.sequences.list("cmp_xyz789")
for step in steps:
    print(f"Step {step.step_number}: {step.subject or '(no subject)'}")
    print(f"  Delay: {step.delay_days} days")
    print(f"  Body preview: {step.body[:80]}...")
    print()

client.close()
```

### Update a sequence step

```python
from foxreach import FoxReach, SequenceUpdate

client = FoxReach([REDACTED_SECRET]")

updated = client.campaigns.sequences.update(
    "cmp_xyz789",
    "seq_step2",
    SequenceUpdate(
        delay_days=5,
        body="Updated follow-up body with {{firstName}} personalization.",
    ),
)
print(f"Updated step {updated.step_number}, delay now {updated.delay_days} days")

client.close()
```

---

## Templates

### Create and list templates

```python
from foxreach import FoxReach, TemplateCreate

client = FoxReach([REDACTED_SECRET]")

# Create
template = client.templates.create(TemplateCreate(
    name="Cold Intro — SaaS",
    subject="Quick question about {{company}}",
    body="Hi {{firstName}},\n\nI saw that {{company}} is {{title ? 'hiring for ' + title : 'growing'}}. We help teams like yours scale outreach.\n\nWorth a quick chat?\n\nBest,\n[Your Name]",
))
print(f"Template created: {template.id}")

# List all
page = client.templates.list()
for t in page:
    print(f"  {t.id}  {t.name:30s}  {t.template_type}")

client.close()
```

---

## Email Accounts

### List accounts with health

```python
from foxreach import FoxReach

client = FoxReach([REDACTED_SECRET]")

page = client.email_accounts.list()
for acct in page:
    health = f"{acct.health_score:.0f}" if acct.health_score is not None else "N/A"
    active = "active" if acct.is_active else "inactive"
    print(f"  {acct.email:35s}  {active:10s}  health:{health}  sent:{acct.sent_today}/{acct.daily_limit}")

client.close()
```

---

## Inbox

### List unread replies

```python
from foxreach import FoxReach

client = FoxReach([REDACTED_SECRET]")

unread = client.inbox.list_threads(is_read=False, page_size=20)
for thread in unread:
    star = "*" if thread.is_starred else " "
    cat = thread.category or "uncategorized"
    print(f"  {star} {thread.from_email:30s}  {cat:15s}  {thread.subject}")

print(f"\n{unread.meta.total} unread threads")
client.close()
```

### Categorize a reply

```python
from foxreach import FoxReach, ThreadUpdate

client = FoxReach([REDACTED_SECRET]")

# Mark as read and categorize
thread = client.inbox.update("rpl_abc123", ThreadUpdate(
    is_read=True,
    category="interested",
    is_starred=True,
))
print(f"Thread {thread.id} marked as {thread.category}")

client.close()
```

---

## Analytics

### Dashboard overview

```python
from foxreach import FoxReach

client = FoxReach([REDACTED_SECRET]")

stats = client.analytics.overview()
print("=== Dashboard Overview ===")
print(f"Accounts:  {stats.active_accounts}/{stats.total_accounts} active")
print(f"Campaigns: {stats.active_campaigns}/{stats.total_campaigns} active")
print(f"Leads:     {stats.total_leads:,}")
print(f"Sent:      {stats.total_sent:,}")
print(f"Replies:   {stats.total_replies:,}")
print(f"Reply Rate: {stats.reply_rate:.1f}%")
print(f"Avg Health: {stats.account_health_avg:.0f}/100")

client.close()
```

### Campaign analytics with daily breakdown

```python
from foxreach import FoxReach

client = FoxReach([REDACTED_SECRET]")

stats = client.analytics.campaign("cmp_xyz789")
print("=== Campaign Analytics ===")
print(f"Sent: {stats.sent}  Delivered: {stats.delivered}  Bounced: {stats.bounced}")
print(f"Replied: {stats.replied}  Opened: {stats.opened}")
print(f"Reply Rate: {stats.reply_rate:.1f}%  Bounce Rate: {stats.bounce_rate:.1f}%")

if stats.daily_stats:
    print("\nDaily breakdown:")
    print(f"{'Date':12s} {'Sent':>6s} {'Delivered':>10s} {'Bounced':>8s} {'Replied':>8s} {'Opened':>7s}")
    print("-" * 55)
    for day in stats.daily_stats:
        print(f"{day.date:12s} {day.sent:6d} {day.delivered:10d} {day.bounced:8d} {day.replied:8d} {day.opened:7d}")

client.close()
```

---

## Error Handling

### Comprehensive error handling example

```python
from foxreach import (
    FoxReach,
    FoxReachError,
    AuthenticationError,
    NotFoundError,
    RateLimitError,
    ValidationError,
    BadRequestError,
    ServerError,
    ConnectionError,
    LeadCreate,
)

client = FoxReach([REDACTED_SECRET]")

try:
    lead = client.leads.create(LeadCreate(email="invalid"))
except ValidationError as e:
    print(f"Validation failed: {e}")
    print(f"Details: {e.response_body}")
except BadRequestError as e:
    print(f"Bad request: {e}")
except AuthenticationError:
    print("Invalid API key. Check your configuration.")
except NotFoundError:
    print("Resource not found.")
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after} seconds.")
except ServerError as e:
    print(f"Server error ({e.status_code}): {e}")
except ConnectionError as e:
    print(f"Connection failed: {e}")
except FoxReachError as e:
    print(f"Unexpected API error: {e}")

client.close()
```

---

## Async Usage

All operations are available asynchronously:

```python
import asyncio
from foxreach import AsyncFoxReach, LeadCreate

async def main():
    async with AsyncFoxReach([REDACTED_SECRET]") as client:
        # Create a lead
        lead = await client.leads.create(LeadCreate(
            email="[REDACTED_SECRET]",
            first_name="Async",
        ))
        print(f"Created: {lead.id}")

        # List with auto-pagination
        async for lead in (await client.leads.list()).auto_paging_iter():
            print(lead.email)

        # Get analytics
        overview = await client.analytics.overview()
        print(f"Total leads: {overview.total_leads}")

asyncio.run(main())
```


path: api-reference.md
content: # FoxReach API Reference

## Base Configuration

| Setting | Value |
|---------|-------|
| Base URL | `https://api.foxreach.io/api/v1` |
| Auth Header | `X-API-Key` |
| Key Prefix | `otr_` |
| Rate Limit | 100 requests/minute |
| Response Format | `{ data: T }` or `{ data: T[], meta: { page, pageSize, total, totalPages } }` |

---

## Data Types

### Lead

```python
@dataclass(frozen=True)
class Lead:
    id: str                                    # e.g. "cld_abc123def456"
    email: str                                 # required, unique per workspace
    first_name: Optional[str]
    last_name: Optional[str]
    company: Optional[str]
    title: Optional[str]                       # job title
    phone: Optional[str]
    linkedin_url: Optional[str]
    website: Optional[str]
    custom_fields: Optional[Dict[str, Any]]    # arbitrary key-value pairs
    notes: Optional[str]
    tags: List[str]                            # tag names
    status: str                                # "active" | "bounced" | "unsubscribed" | "replied"
    source: Optional[str]                      # "api" | "csv_import" | "manual"
    created_at: Optional[str]                  # ISO 8601
    updated_at: Optional[str]
```

### LeadCreate

```python
@dataclass
class LeadCreate:
    email: str                                 # required
    first_name: Optional[str] = None
    last_name: Optional[str] = None
    company: Optional[str] = None
    title: Optional[str] = None
    phone: Optional[str] = None
    linkedin_url: Optional[str] = None
    website: Optional[str] = None
    custom_fields: Optional[Dict[str, Any]] = None
    tags: Optional[List[str]] = None
    notes: Optional[str] = None
```

### LeadUpdate

Same fields as LeadCreate, all optional. Only non-None fields are sent.

---

### Campaign

```python
@dataclass(frozen=True)
class Campaign:
    id: str                       # e.g. "cmp_xyz789ghi012"
    name: str
    status: str                   # "draft" | "active" | "paused" | "completed"
    timezone: str                 # IANA timezone e.g. "America/New_York"
    sending_days: List[int]       # 1=Mon, 2=Tue, ..., 7=Sun
    sending_start_hour: int       # 0-23
    sending_end_hour: int         # 0-23
    daily_limit: int              # max emails per day
    sent_today: int
    total_leads: int
    total_sent: int
    total_delivered: int
    total_bounced: int
    total_replied: int
    total_opened: int
    created_at: Optional[str]
    updated_at: Optional[str]
    started_at: Optional[str]
    completed_at: Optional[str]
```

### CampaignCreate

```python
@dataclass
class CampaignCreate:
    name: str                                       # required
    timezone: str = "UTC"
    sending_days: Optional[List[int]] = None        # default: [1,2,3,4,5]
    sending_start_hour: Optional[int] = None        # default: 9
    sending_end_hour: Optional[int] = None          # default: 17
    daily_limit: Optional[int] = None               # default: 50
```

### CampaignUpdate

Same fields as CampaignCreate but all optional including `name`.

---

### Sequence

```python
@dataclass(frozen=True)
class Sequence:
    id: str
    campaign_id: str
    step_number: int              # order in the sequence chain
    subject: Optional[str]        # email subject (supports {{variables}})
    body: str                     # email body (supports {{variables}} and spintax)
    delay_days: int               # days to wait after previous step
    template_type: str            # "email"
    created_at: Optional[str]
    updated_at: Optional[str]
```

### SequenceCreate

```python
@dataclass
class SequenceCreate:
    body: str                                  # required
    subject: Optional[str] = None
    delay_days: int = 1
    step_number: Optional[int] = None          # auto-assigned if omitted
    template_type: str = "email"
```

### SequenceUpdate

```python
@dataclass
class SequenceUpdate:
    subject: Optional[str] = None
    body: Optional[str] = None
    delay_days: Optional[int] = None
    step_number: Optional[int] = None
```

---

### Template

```python
@dataclass(frozen=True)
class Template:
    id: str
    name: str
    subject: Optional[str]
    body: str
    template_type: str            # "email"
    send_as_html: bool
    created_at: Optional[str]
    updated_at: Optional[str]
```

### TemplateCreate

```python
@dataclass
class TemplateCreate:
    name: str                     # required
    body: str                     # required
    subject: Optional[str] = None
    template_type: str = "email"
    send_as_html: bool = False
```

---

### EmailAccount

```python
@dataclass(frozen=True)
class EmailAccount:
    id: str
    email: str
    display_name: Optional[str]
    provider: Optional[str]
    daily_limit: int
    sent_today: int
    is_active: bool
    warmup_enabled: bool
    health_score: Optional[float]     # 0-100
    bounce_rate: Optional[float]      # 0-100 percentage
    reply_rate: Optional[float]       # 0-100 percentage
    connection_status: Optional[str]
    created_at: Optional[str]
    updated_at: Optional[str]
```

---

### Thread (Inbox Reply)

```python
@dataclass(frozen=True)
class Thread:
    id: str                           # e.g. "rpl_abc123"
    from_email: Optional[str]
    to_email: Optional[str]
    subject: Optional[str]
    body: Optional[str]               # HTML body
    body_plain: Optional[str]         # plain text body
    category: Optional[str]           # "interested" | "not_interested" | "out_of_office" | etc.
    is_read: bool
    is_starred: bool
    campaign_id: Optional[str]
    lead_id: Optional[str]
    account_id: Optional[str]
    received_at: Optional[str]
```

### ThreadUpdate

```python
@dataclass
class ThreadUpdate:
    category: Optional[str] = None
    is_read: Optional[bool] = None
    is_starred: Optional[bool] = None
```

---

### OverviewStats

```python
@dataclass(frozen=True)
class OverviewStats:
    total_accounts: int
    active_accounts: int
    total_campaigns: int
    active_campaigns: int
    total_leads: int
    total_sent: int
    total_replies: int
    reply_rate: float                 # percentage
    account_health_avg: float         # 0-100
```

### CampaignAnalytics

```python
@dataclass(frozen=True)
class CampaignAnalytics:
    sent: int
    delivered: int
    bounced: int
    replied: int
    opened: int
    reply_rate: float
    bounce_rate: float
    daily_stats: List[DailyStat]      # per-day breakdown
```

### DailyStat

```python
@dataclass(frozen=True)
class DailyStat:
    date: str                         # "2025-01-15"
    sent: int
    delivered: int
    bounced: int
    replied: int
    opened: int
```

---

### WebhookEvent

```python
@dataclass(frozen=True)
class WebhookEvent:
    id: str                           # e.g. "evt_sample001"
    type: str                         # see Webhook Event Types below
    payload: Dict[str, Any]
    timestamp: Optional[str]
```

### Webhook Event Types

| Event | Description |
|-------|-------------|
| `reply.received` | A reply is received and matched to a lead/campaign |
| `reply.categorized` | A reply is categorized by AI or manually |
| `email.bounced` | A sent email bounces back |
| `email.sent` | An email is successfully sent from a campaign |
| `email.failed` | An email fails to send |
| `email.opened` | A recipient opens a tracked email |
| `campaign.started` | A campaign transitions to active status |
| `campaign.paused` | A campaign is paused |
| `campaign.completed` | All leads in a campaign have been processed |
| `lead.created` | A new lead is created |
| `lead.updated` | Lead information is updated |

---

## API Endpoints

### Leads — `/leads`

| Method | Path | Description |
|--------|------|-------------|
| GET | `/leads` | List leads. Query: `page`, `pageSize`, `search`, `status`, `tags` |
| GET | `/leads/{id}` | Get lead by ID |
| POST | `/leads` | Create lead. Deduplicates by email |
| PATCH | `/leads/{id}` | Update lead |
| DELETE | `/leads/{id}` | Soft-delete lead |

### Campaigns — `/campaigns`

| Method | Path | Description |
|--------|------|-------------|
| GET | `/campaigns` | List campaigns. Query: `page`, `pageSize`, `status` |
| GET | `/campaigns/{id}` | Get campaign with stats |
| POST | `/campaigns` | Create campaign (starts as draft) |
| PATCH | `/campaigns/{id}` | Update campaign |
| DELETE | `/campaigns/{id}` | Delete (must be draft) |
| POST | `/campaigns/{id}/start` | Start campaign |
| POST | `/campaigns/{id}/pause` | Pause campaign |
| GET | `/campaigns/{id}/sequences` | List sequence steps |
| POST | `/campaigns/{id}/sequences` | Add sequence step |
| PATCH | `/campaigns/{id}/sequences/{seq_id}` | Update step |
| DELETE | `/campaigns/{id}/sequences/{seq_id}` | Delete step |
| POST | `/campaigns/{id}/leads` | Add leads. Body: `{ leadIds: [...] }` |
| POST | `/campaigns/{id}/accounts` | Assign accounts. Body: `{ accountIds: [...] }` |

### Templates — `/templates`

| Method | Path | Description |
|--------|------|-------------|
| GET | `/templates` | List templates |
| GET | `/templates/{id}` | Get template |
| POST | `/templates` | Create template |
| PATCH | `/templates/{id}` | Update template |
| DELETE | `/templates/{id}` | Delete template |

### Email Accounts — `/email-accounts`

| Method | Path | Description |
|--------|------|-------------|
| GET | `/email-accounts` | List accounts |
| GET | `/email-accounts/{id}` | Get account with health |
| DELETE | `/email-accounts/{id}` | Delete account |

### Inbox — `/inbox`

| Method | Path | Description |
|--------|------|-------------|
| GET | `/inbox/threads` | List threads. Query: `page`, `pageSize`, `category`, `isRead`, `isStarred`, `accountId`, `campaignId`, `search` |
| GET | `/inbox/{id}` | Get thread |
| PATCH | `/inbox/{id}` | Update thread (category, read, starred) |

### Analytics — `/analytics`

| Method | Path | Description |
|--------|------|-------------|
| GET | `/analytics/overview` | Dashboard KPIs |
| GET | `/analytics/campaigns/{id}` | Campaign metrics + daily stats |

### Webhooks — `/webhooks`

| Method | Path | Description |
|--------|------|-------------|
| POST | `/webhooks/inbound` | Incoming webhook receiver |


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)