# AgentWarm API Reference > Complete API reference for AI agents. Every endpoint, every field, every > error code. If you're looking for a quick overview, read /llms.txt first. Base URL: https://app.agentwarm.com Auth: `Authorization: Bearer aw_YOUR_API_KEY` Rate limit: 100 req/min per key Idempotency: `Idempotency-Key: ` header on POST/PUT/PATCH (24h TTL) --- ## Table of Contents 1. Authentication & API Keys 2. Agent Discovery (start here) 3. Accounts 4. Warmup Control 5. Warmup Pools 6. Suppression List 7. Domains 8. Domain Onboarding Pipeline 9. Analytics 10. Seed Testing 11. Companies & Shared Inbox 12. Mail Servers 13. Templates 14. Webhooks 15. Provider Push Notifications (Gmail / Outlook) 16. Workspace Admin (Google/Microsoft 365) 17. OAuth (Account Connection) 18. Google Postmaster Tools 19. Audit Log 20. Health 21. Error Codes 22. Plans & Limits 23. Workflows & Decision Trees --- ## 1. Authentication & API Keys All endpoints require `Authorization: Bearer aw_YOUR_API_KEY` unless marked PUBLIC. API keys start with `aw_` and are 35 characters. Scoped to one user. **Public endpoints (no auth):** GET /api/health, /api/healthz, /metrics, GET /api/agent/capabilities, /api/docs, /api/openapi.json, GET /llms.txt, /llms-full.txt ### GET /api/keys List your API keys. Values are masked — only prefix shown. -> 200 { "keys": [ { "id": "uuid", "name": "Production", "keyPrefix": "aw_abc12...", "lastUsedAt": "2026-03-13T10:00:00Z", "createdAt": "2026-03-10T..." } ]} ### POST /api/keys Create a new API key. The full key is returned ONLY in this response. { "name": "My Agent Key" } -> 201 { "id": "uuid", "name": "My Agent Key", "key": "aw_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "keyPrefix": "aw_xxxxx...", "createdAt": "..." } ### DELETE /api/keys/:id Revoke an API key permanently. -> 200 { "deleted": true } --- ## 2. Agent Discovery ### GET /api/agent/capabilities — PUBLIC Machine-readable manifest of all operations, organized by intent. This is the FIRST endpoint an agent should call. -> 200 { "service": "email-warmup", "version": "1.0.0", "capabilities": [ { "intent": "onboard_domain", "endpoint": "POST /api/domains/onboard", "estimatedDuration": "2-5 minutes", "lifecycle": "async" }, { "intent": "manage_accounts", "endpoints": ["GET /api/accounts", "POST /api/accounts", ...] }, { "intent": "control_warmup", "endpoints": ["POST /api/warmup/start/:accountId", ...] }, ... ], "workflows": { "new_domain_setup": { "steps": [...] }, "monitor_warmup": {...}, "investigate_issues": {...} }, "authentication": { "method": "Bearer token", "header": "Authorization: Bearer " }, "rateLimits": { "default": "100 requests/minute" }, "idempotency": { "header": "Idempotency-Key", "ttl": "24 hours" }, "errors": { "format": { "error": "string", "code": "MACHINE_CODE", "statusCode": 400, "action": "hint" } } } ### GET /api/agent/status System dashboard. Call this at the start of every session. -> 200 { "overview": { "domains": { "total": 3, "ready": 1, "warming": 2, "blocked": 0, "degraded": 0 }, "accounts": { "total": 27, "warming": 25, "active": 0, "paused": 2, "error": 0 }, "todayEmails": { "sent": 142, "received": 98, "bounced": 3, "spammed": 1 }, "systemHealth": "healthy" }, "domains": [ { "id": "uuid", "name": "example.com", "verdict": "warming", "readinessScore": 65, "accounts": [...] } ], "issues": [ { "severity": "warning", "type": "high_bounce_rate", "accountId": "uuid", "message": "Bounce rate 8% exceeds threshold", "action": "check_credentials" } ], "recommendations": [ "Account alice@example.com has been paused for 3 days — consider resuming", "Domain newcorp.com DNS is fully propagated — ready to start warmup" ], "nextActions": [ { "priority": "critical", "action": "Pause sales@trouble.com", "reason": "Bounce rate 8.2% exceeds 7% threshold", "endpoint": { "method": "POST", "href": "/api/warmup/pause/acc-uuid" }, "autoExecuteIn": "2h" }, { "priority": "recommended", "action": "Fix DNS for trouble.com", "reason": "Domain is blocked — readiness score 32/100", "endpoint": { "method": "GET", "href": "/api/domains/dom-uuid/readiness" } } ] } ### Domain Lifecycle #### POST /api/agent/domains/search Search for available domains across TLDs, scored by reputation and price. { "keyword": "acmecorp", "tlds": ["com", "net", "io"], "maxResults": 10 } -> 200 { "results": [ { "domain": "acmecorp.com", "available": true, "price": "10.73", "premium": false, "tldReputationScore": 100, "overallScore": 85 }, { "domain": "acmecorp.io", "available": true, "price": "32.00", "premium": false, "tldReputationScore": 70, "overallScore": 52 } ], "total": 2 } #### POST /api/agent/domains/recommend Get domain recommendations with diversity scoring and reasons. { "keyword": "acmecorp", "count": 5 } -> 200 { "recommendations": [ { "domain": "acmecorp.net", "available": true, "price": "9.73", "overallScore": 88, "reason": "Good TLD reputation + adds diversity to your .com-heavy portfolio" } ]} #### GET /api/agent/domains/:id/burn-status Assess whether a domain is being burned (bounce/spam/blacklist signals). -> 200 { "domainId": "uuid", "domain": "trouble.com", "status": "burning", "burnRiskScore": 72, "signals": [ { "type": "high_bounce_rate", "severity": "high", "value": 0.12 }, { "type": "reputation_collapse", "severity": "high", "value": 25 } ], "accounts": [ { "email": "sales@trouble.com", "contribution": "toxic", "bounceRate": 0.20 } ], "recommendation": "Pause all accounts, wait 14+ days, then resurrect with fresh DKIM" } Statuses: `healthy`, `warning`, `burning`, `burned`, `quarantined`, `recovering`. #### GET /api/agent/burn-report Get burn status for ALL domains at once. Includes summary counts. -> 200 { "domains": [{ "domainId": "uuid", "domain": "a.com", "status": "healthy", "burnRiskScore": 5, ... }], "summary": { "healthy": 2, "warning": 1, "burning": 0, "burned": 0, "quarantined": 0, "recovering": 0 } } #### POST /api/agent/domains/:id/resurrect Attempt to resurrect a burned domain — generates fresh DKIM, verifies DNS, resumes accounts at 2/day. -> 200 { "success": true, "steps": ["Generated fresh DKIM", "Verified DNS", "Resumed 3 accounts at 2/day"] } -> 200 { "success": false, "error": "Domain is healthy — resurrection not needed" } #### GET /api/agent/capacity Report on infrastructure capacity — server utilization, pool health, provider diversity. -> 200 { "totalDomains": 5, "totalAccounts": 42, "warmingAccounts": 35, "availableCapacity": 8, "mailServers": [{ "id": "uuid", "hostname": "mail-1", "utilization": 0.7, "maxAccounts": 50 }], "poolHealth": { "totalPools": 2, "avgPoolSize": 21 }, "recommendations": [{ "type": "add_server", "reason": "Primary server at 70% capacity" }] } #### POST /api/agent/plan Generate a provisioning plan — what to buy, create, resume, fix. { "targetAccounts": 50, "keyword": "acmecorp" } -> 200 { "domainsToCreate": [{ "domain": "acmecorp-mail.com", "estimatedCost": "$10.73" }], "accountsToCreate": [{ "email": "info@acmecorp-mail.com", "localPart": "info" }], "accountsToResume": [{ "id": "uuid", "email": "old@existing.com" }], "accountsToFix": [], "domainsToResurrect": [], "estimatedTotalCost": "$32.19", "estimatedTimeToFullCapacity": "18 days" } #### POST /api/agent/provision Execute a provisioning plan (or dry-run it). { "plan": { ... from POST /api/agent/plan ... }, "dryRun": true } -> 200 { "steps": ["[DRY RUN] Would purchase acmecorp-mail.com ($10.73)", ...], "errors": [] } --- ## 3. Accounts ### GET /api/accounts List email accounts with filtering and pagination. Query params: `status` (active|paused|warming|error|suspended), `provider` (gmail|outlook|imap_smtp), `email` (partial match), `domain` (filter by @domain), `limit` (1-200, default 50), `offset` (default 0) GET /api/accounts?status=warming&limit=10 -> 200 { "accounts": [{ "id": "550e8400-e29b-41d4-a716-446655440000", "email": "hello@example.com", "displayName": "Hello", "provider": "imap_smtp", "status": "warming", "warmupPhase": "ramp_up", "currentDailyLimit": 8, "totalSent": 42, "totalReceived": 38, "bounceRate": 0.02, "spamRate": 0.01, "reputationScore": 85.5, "timezone": "America/New_York", "warmupStartedAt": "2026-03-08T10:00:00Z", "createdAt": "2026-03-08T09:50:00Z" }], "total": 27, "limit": 10, "offset": 0 } ### GET /api/accounts/:id Get a single account with full details. -> 200 (same fields as list item, plus providerMetadata and updatedAt) ### POST /api/accounts Create an email account. Body differs by provider. **IMAP/SMTP (any mail server):** { "email": "hello@example.com", "displayName": "Hello", "provider": "imap_smtp", "timezone": "America/New_York", "credentials": { "imapHost": "mail.example.com", "imapPort": 993, "smtpHost": "mail.example.com", "smtpPort": 465, "username": "hello@example.com", "password": "app-password" }, "poolId": "optional-pool-uuid" } **Gmail (OAuth tokens):** { "email": "user@gmail.com", "displayName": "User", "provider": "gmail", "credentials": { "accessToken": "ya29.xxx", "refreshToken": "1//xxx" } } **Outlook (OAuth tokens):** { "email": "user@outlook.com", "displayName": "User", "provider": "outlook", "credentials": { "accessToken": "eyJ0...", "refreshToken": "M.C5..." } } -> 201 { "id": "new-account-uuid", "email": "hello@example.com", "displayName": "Hello", "provider": "imap_smtp", "status": "active", "warmupPhase": "ramp_up", "currentDailyLimit": 2, "totalSent": 0, "totalReceived": 0, "bounceRate": 0, "spamRate": 0, "reputationScore": 50, "timezone": "America/New_York", "warmupStartedAt": null, "createdAt": "2026-03-13T12:00:00Z" } ### PATCH /api/accounts/:id Update account fields. Only displayName, timezone, and status (active|paused) are updateable. { "displayName": "New Name", "timezone": "Europe/London" } -> 200 (full account object) ### PATCH /api/accounts/:id/credentials Update stored credentials. Provider field determines the schema. { "provider": "imap_smtp", "credentials": { "imapHost": "new-host.com", "imapPort": 993, "smtpHost": "new-host.com", "smtpPort": 465, "username": "user", "password": "new-pass" } } -> 200 { "id": "uuid", "email": "...", "provider": "imap_smtp", "credentialsUpdated": true } ### DELETE /api/accounts/:id Delete an account and remove from all pools. -> 204 (no content) ### POST /api/accounts/:id/test-connection Test IMAP and SMTP connectivity for an account. -> 200 { "accountId": "uuid", "email": "hello@example.com", "provider": "imap_smtp", "imap": { "success": true, "latencyMs": 340 }, "smtp": { "success": true, "latencyMs": 220 }, "overall": true, "testedAt": "2026-03-13T12:00:00Z" } If connection fails: { "accountId": "uuid", "email": "hello@example.com", "provider": "imap_smtp", "imap": { "success": false, "error": "Authentication failed: invalid credentials" }, "smtp": { "success": true, "latencyMs": 220 }, "overall": false, "testedAt": "2026-03-13T12:00:00Z" } ### GET /api/accounts/:id/persona Get the account persona (used for email generation context). -> 200 { "id": "uuid", "persona": { "title": "Sales Manager", "department": "Sales", "topics": ["SaaS", "B2B"] } } ### PUT /api/accounts/:id/persona Set the account persona. { "title": "Sales Manager", "department": "Sales", "topics": ["SaaS", "B2B"] } -> 200 { "id": "uuid", "persona": { "title": "Sales Manager", "department": "Sales", "topics": ["SaaS", "B2B"] } } ### POST /api/accounts/batch Create up to 50 accounts in one request. { "accounts": [ { "email": "a@example.com", "displayName": "A", "provider": "imap_smtp", "credentials": {...} }, { "email": "b@example.com", "displayName": "B", "provider": "imap_smtp", "credentials": {...} } ]} -> 201 { "results": [ { "email": "a@example.com", "success": true, "accountId": "uuid-1" }, { "email": "b@example.com", "success": false, "error": "Account already exists", "code": "ACCOUNT_ALREADY_EXISTS" } ], "successCount": 1, "failureCount": 1 } ### POST /api/accounts/batch/test-connection Batch test connectivity for up to 100 accounts. { "accountIds": ["uuid-1", "uuid-2", "uuid-3"] } -> 200 { "results": [...], "successCount": 2, "failureCount": 1 } ### POST /api/accounts/:id/send Send an email from an account. { "to": "recipient@example.com", "subject": "Hello from AgentWarm", "textBody": "This is a test email.", "htmlBody": "

This is a test email.

", "cc": ["cc@example.com"], "bcc": ["bcc@example.com"], "inReplyTo": "", "references": [""], "attachments": [ { "filename": "report.pdf", "content": "base64-encoded-data", "contentType": "application/pdf" } ] } -> 200 { "success": true, "messageId": "" } Limits: max 25 attachments, 25MB total. Subject max 998 chars. ### GET /api/accounts/:id/messages List inbox messages for an account. Query params: `folder` (default "INBOX", also "SPAM", "Sent", etc.), `since` (ISO 8601, default last 24h), `limit` (1-100, default 50) -> 200 { "messages": [{ "uid": "12345", "messageId": "", "from": "sender@example.com", "to": "hello@example.com", "subject": "Re: Your proposal", "textBody": "Thanks for reaching out...", "date": "2026-03-13T11:30:00Z", "folder": "INBOX", "hasAttachments": false, "attachments": [] }], "count": 1, "folder": "INBOX" } ### CRM Entity Links Link email accounts to external CRM person/account IDs (Salesforce, HubSpot, etc.). Many-to-many: one email can have multiple CRM persons, one person can have multiple emails. ### POST /api/accounts/:id/crm-links Create a CRM link for an email account. POST /api/accounts/acc-uuid/crm-links { "crmPersonId": "sf_003x0001", "crmSource": "salesforce" } -> 201 { "id": "link-uuid", "emailAccountId": "acc-uuid", "crmPersonId": "sf_003x0001", "crmSource": "salesforce", ... } At least one of `crmPersonId` or `crmAccountId` must be provided. ### GET /api/accounts/:id/crm-links List all CRM links for an email account. -> 200 { "links": [{ "id": "...", "crmPersonId": "...", "crmAccountId": "...", "crmSource": "...", ... }] } ### DELETE /api/accounts/:id/crm-links/:linkId Remove a CRM link. Returns 204 on success, 404 if not found. ### Filtering accounts by CRM IDs GET /api/accounts?crmPersonId=sf_003x0001 GET /api/accounts?crmAccountId=sf_001x0001&crmSource=salesforce Returns the standard paginated account list, filtered to accounts linked to the given CRM entity. ### CRM Analytics GET /api/analytics/crm/person/:personId?crmSource=salesforce GET /api/analytics/crm/account/:accountId?crmSource=hubspot Returns aggregated warmup metrics across all accounts linked to the CRM entity: -> 200 { "crmId": "sf_003x0001", "crmIdType": "person", "accounts": [{ "id": "...", "email": "...", "status": "warming", "warmupPhase": "ramp_up", "reputationScore": 68 }], "aggregate": { "totalAccounts": 3, "warmingAccounts": 2, "averageReputationScore": 68.5, "todaySent": 12, "todayReceived": 8, "todayBounced": 0 } } --- ## 4. Warmup Control ### POST /api/warmup/start/:accountId Start warmup for an account. Begins at 2 emails/day, ramps to configured max over ramp-up period. -> 200 { "message": "Warmup started", "accountId": "uuid", "phase": "ramp_up", "initialDailyLimit": 2 } Errors: ACCOUNT_NOT_FOUND, ACCOUNT_ALREADY_WARMING, ACCOUNT_INVALID_STATUS_TRANSITION ### POST /api/warmup/pause/:accountId Pause warmup. Schedule is preserved for resume. -> 200 { "message": "Warmup paused", "accountId": "uuid" } ### POST /api/warmup/resume/:accountId Resume from pause. Continues from where it left off. -> 200 { "message": "Warmup resumed", "accountId": "uuid", "phase": "ramp_up" } Error: ACCOUNT_NEVER_WARMED (use /start instead) ### GET /api/warmup/schedule/:accountId Get the warmup schedule for a specific date. Query: `date` (YYYY-MM-DD, default today) -> 200 { "id": "uuid", "accountId": "uuid", "date": "2026-03-13", "plannedSendCount": 8, "actualSendCount": 5, "plannedReceiveCount": 6, "actualReceiveCount": 4, "sendSlots": [ { "scheduledAt": "2026-03-13T09:15:00Z", "targetAccountId": "uuid", "executed": true, "executedAt": "2026-03-13T09:15:32Z" }, { "scheduledAt": "2026-03-13T10:30:00Z", "targetAccountId": "uuid", "executed": false, "executedAt": null } ] } ### POST /api/warmup/batch/start Start warmup for up to 100 accounts. { "accountIds": ["uuid-1", "uuid-2", "uuid-3"] } -> 200 { "results": [ { "accountId": "uuid-1", "success": true }, { "accountId": "uuid-2", "success": false, "error": "Already warming", "code": "ACCOUNT_ALREADY_WARMING" } ], "successCount": 1, "failureCount": 1 } ### POST /api/warmup/batch/pause Pause warmup for up to 100 accounts. { "accountIds": ["uuid-1", "uuid-2"] } -> 200 (same format as batch/start) ### GET /api/warmup/test-mode Check if test mode is enabled (limits all sends to a maximum). -> 200 { "enabled": false, "maxDailyEmails": 20, "source": "env" } ### POST /api/warmup/test-mode Toggle test mode. { "enabled": true, "maxDailyEmails": 5 } -> 200 { "enabled": true, "maxDailyEmails": 5, "source": "runtime" } --- ## Warmup Mechanics ### Phases Each account progresses through warmup phases: - **ramp_up** (Day 1–45): Volume increases linearly at +1 email/day, starting at 2/day up to the configured max (default 40). - **sustain** (Day 46–59): Maintains max volume with ±15% daily variance for naturalness. - **cooldown** (triggered by poor metrics): Volume cut to 50%. Auto-recovers to ramp_up/sustain when metrics improve. - **graduated** (Day 60+, if healthy): Account moves to `active` status with maintenance trickle sends. Graduation requires 14 consecutive days in sustain with bounceRate ≤ 3%, spamRate ≤ 2%, and reputationScore ≥ 70. ### Reputation Score Every account has a `reputationScore` (0–100) recalculated daily from that day's metrics: reputationScore = clamp(0, 100, 50 base score + inboxPlacementRate × 20 emails that weren't bounced or spammed + openRate × 15 emails opened by recipients + replyRate × 15 emails replied to by recipients - bounceRate × 30 penalty for bounces - spamRate × 40 penalty for spam reports ) Where: - `inboxPlacementRate` = (sent − bounced − spammed) / sent - `openRate` = opened / received - `replyRate` = replied / received - `bounceRate` = bounced / sent - `spamRate` = spammed / sent A new account with no sends starts at **50**. Perfect deliverability with full engagement reaches **100**. Bounces and spam are penalized heavily — a 10% bounce rate costs 3 points, a 10% spam rate costs 4 points. Reputation drives automated decisions: - **< 40**: Account flagged as at-risk. - **< 70**: Account cannot graduate from warmup. - **> 0.05 bounceRate or > 0.03 spamRate**: Triggers cooldown phase (50% volume reduction). - **> 0.08 bounceRate**: Auto-paused by the scheduler. ### Volume Protection - **Day-over-day spike cap**: Daily volume cannot increase more than 1.5× the previous day (configurable via pool config `maxDailyVolumeIncrease`). - **Moderate bounce reduction**: Accounts with 3–5% bounce rate get a 25% volume reduction before cooldown kicks in. - **Weekend sends**: Disabled by default. Enable per-pool via `sendOnWeekends: true` in pool config. --- ## 5. Warmup Pools Pools are groups of accounts that send warmup emails to each other. ### GET /api/warmup/pools List all pools. -> 200 { "pools": [{ "id": "uuid", "name": "Default Pool", "description": null, "member_count": 27, "created_at": "...", "updated_at": "..." }] } ### POST /api/warmup/pools Create a pool. { "name": "Sales Team Pool", "description": "Pool for sales team accounts" } -> 201 { "id": "uuid", "name": "Sales Team Pool", "description": "...", "created_at": "...", "updated_at": "..." } ### POST /api/warmup/pools/:poolId/accounts/:accountId Add an account to a pool. Idempotent. -> 200 { "message": "Account added to pool" } ### DELETE /api/warmup/pools/:poolId/accounts/:accountId Remove an account from a pool. -> 204 (no content) ### GET /api/warmup/pools/:poolId/config Get pool warmup configuration (merged with global defaults). -> 200 { "poolId": "uuid", "config": { "minDailyEmails": 2, "maxDailyEmails": 40, "rampUpDays": 45, "replyRate": 0.7, "openRate": 0.95, "markImportantRate": 0.3, "workingHoursStart": 8, "workingHoursEnd": 18, "sendOnWeekends": false }, "overrides": {} } ### PUT /api/warmup/pools/:poolId/config Update pool config overrides. Only supplied fields are changed. { "maxDailyEmails": 30, "replyRate": 0.8 } -> 200 (same format, overrides now populated) Config field ranges: minDailyEmails 1-100, maxDailyEmails 1-200, rampUpDays 1-180, replyRate/openRate/markImportantRate 0-1, hours 0-23, maxDailyVolumeIncrease 1.0-5.0 --- ## 6. Suppression List Emails on the suppression list are excluded from warmup sends. ### GET /api/warmup/suppression List suppressed emails. Query: `reason` (hard_bounce|complaint|manual), `includeExpired` (boolean), `limit` (1-500, default 100), `offset` -> 200 { "entries": [ { "id": "uuid", "email": "bounced@example.com", "reason": "hard_bounce", "suppressedAt": "2026-03-12T...", "expiresAt": null, "createdAt": "..." } ], "total": 5 } ### POST /api/warmup/suppression Suppress an email address. { "email": "bad@example.com", "reason": "manual", "expiresAt": "2026-06-13T00:00:00Z" } -> 201 { "id": "uuid", "email": "bad@example.com", "reason": "manual", ... } ### DELETE /api/warmup/suppression/:email Remove from suppression. Email must be URL-encoded. DELETE /api/warmup/suppression/bad%40example.com -> 204 (no content) --- ## 7. Domains ### GET /api/domains List domains. Query: `limit` (1-200, default 50), `offset` -> 200 { "domains": [{ "id": "uuid", "name": "example.com", "tld": "com", "status": "active", "mailServerId": "uuid", "dnsProvider": "cloudflare", "dnsRecords": [{ "type": "MX", "name": "example.com", "content": "mail.example.com", "priority": 10 }], "createdAt": "...", "updatedAt": "..." }], "total": 3, "limit": 50, "offset": 0 } Domain statuses: pending, registered, dns_configured, active, error ### GET /api/domains/:id Get domain with linked email accounts. -> 200 { ...domain fields..., "emailAccounts": [{ "id": "uuid", "domainId": "uuid", "accountId": "uuid", "localPart": "hello" }] } ### POST /api/domains/check Check if a domain is available to purchase. { "domain": "newcorp.com" } -> 200 { "available": true, "price": "12.99", "regularPrice": "14.99", "premium": false, "minDuration": 1 } ### POST /api/domains/purchase Purchase a domain. { "domain": "newcorp.com" } -> 201 (domain object with status "registered") ### POST /api/domains/import Import a domain you already own. { "domain": "existing.com", "dnsProvider": "cloudflare" } Supported: cloudflare, route53 -> 201 (domain object) ### POST /api/domains/:id/provision Full provisioning in one call: DNS + mail server + DKIM + optional accounts. { "mailServerId": "server-uuid", "emailAccounts": [ { "localPart": "hello", "displayName": "Hello" }, { "localPart": "support", "displayName": "Support" } ] } -> 200 (domain object with accounts created) ### POST /api/domains/:id/configure-dns Set up MX, SPF, DMARC records. -> 200 { "records": [...], "count": 3 } ### POST /api/domains/:id/generate-dkim Generate DKIM key pair and publish to DNS. -> 200 { "selector": "s1", "dnsRecord": { "type": "TXT", "name": "s1._domainkey.example.com", "content": "v=DKIM1; ..." } } ### POST /api/domains/:id/stalwart-domain Register domain on the Stalwart mail server. -> 200 { "message": "Domain created on mail server" } ### POST /api/domains/:id/verify-dns Verify DNS record propagation. -> 200 { "verified": true, "records": [ { "type": "MX", "name": "example.com", "expected": "mail.example.com", "found": true }, { "type": "TXT", "name": "example.com", "expected": "v=spf1 ...", "found": true }, { "type": "TXT", "name": "_dmarc.example.com", "expected": "v=DMARC1 ...", "found": true } ] } ### GET /api/domains/:id/auth-readiness Check domain authentication readiness (SPF, DKIM, DMARC status). -> 200 { "domainId": "uuid", "domain": "example.com", "dnsVerification": {...}, "readiness": {...}, "warmupEligible": true } ### GET /api/domains/:id/readiness Comprehensive production readiness score. -> 200 { "domainId": "uuid", "domain": "example.com", "ready": false, "readinessScore": 65, "verdict": "warming", "estimatedDaysToReady": 18, "checks": { "dns": { "pass": true, "details": "All records configured" }, "warmupProgress": { "pass": false, "details": "Day 12 of 45", "value": 27, "threshold": 80 }, "bounceRate": { "pass": true, "details": "2.1% < 5% threshold", "value": 0.021, "threshold": 0.05 }, "spamRate": { "pass": true, "details": "0.5% < 2% threshold", "value": 0.005, "threshold": 0.02 }, "reputationScore": { "pass": true, "details": "Score 82/100", "value": 82, "threshold": 70 }, "connectionHealth": { "pass": true, "details": "All accounts connected" } }, "accounts": [ { "email": "hello@example.com", "accountId": "uuid", "status": "warming", "phase": "ramp_up", "reputation": 85, "ready": false, "daysSinceStart": 12 } ], "recommendations": [ "Continue warmup — 18 estimated days remaining", "Consider adding more accounts to increase pool diversity" ] } Verdict values: not_started, warming, ready, degraded, blocked ### POST /api/domains/:id/accounts Create an email account on Stalwart for this domain. { "localPart": "sales", "displayName": "Sales Team" } -> 201 { "accountId": "uuid", "email": "sales@example.com" } ### DELETE /api/domains/:id/accounts/:accountId Remove an email account from the domain. -> 204 ### GET /api/domains/:id/accounts List accounts for a domain. -> 200 { "accounts": [...], "total": 5 } ### POST /api/domains/:id/accounts/batch Create up to 50 accounts at once. { "accounts": [{ "localPart": "a", "displayName": "A" }, { "localPart": "b", "displayName": "B" }] } -> 201 { "results": [{ "email": "a@example.com", "accountId": "uuid" }, ...] } ### POST /api/domains/:id/link-account Link an existing email account to this domain. { "accountId": "existing-account-uuid" } -> 201 { "id": "uuid", "domainId": "uuid", "accountId": "uuid", "localPart": "hello", "createdAt": "..." } ### POST /api/domains/:id/redirect Set up a 301 redirect from this warmup domain to the company's real domain. For Cloudflare domains: edge-level redirect rule (fastest). For other DNS providers: A record → our server; Caddy handles TLS and the API returns the 301. { "target": "https://algomo.com" } -> 200 { "message": "Redirect configured: joinalgomo.com → https://algomo.com", "domain": "joinalgomo.com", "redirectTarget": "https://algomo.com" } ### GET /api/domains/:id/redirect-check Verify the redirect is working by checking the HTTP response. -> 200 { "working": true, "actualTarget": "https://algomo.com/test-path", "statusCode": 301, "expectedTarget": "https://algomo.com" } --- ## 8. Domain Onboarding Pipeline Automated pipeline that provisions a domain end-to-end (DNS, mail server, DKIM, accounts, warmup). ### POST /api/domains/onboard Start the pipeline. Returns a job ID. { "domain": "newcorp.com", "mailServerId": "server-uuid", "accounts": [{ "localPart": "hello", "displayName": "Hello" }] } -> 202 { "jobId": "job-uuid", "status": "pending" } ### GET /api/domains/onboard/:jobId/stream Stream progress via Server-Sent Events (SSE). -> 200 (text/event-stream) data: { "step": "configure_dns", "status": "running", "progress": 25 } data: { "step": "generate_dkim", "status": "running", "progress": 50 } data: { "step": "verify_dns", "status": "running", "progress": 75 } data: { "step": "complete", "status": "done", "progress": 100, "domainId": "uuid" } If your client doesn't support SSE, poll `/api/agent/status` and check for the new domain. ### POST /api/domains/onboard/account-connection Connect an email account during onboarding. { "accountId": "uuid" } -> 200 { "connected": true } ### POST /api/domains/onboard/domain-validation Validate domain readiness during onboarding. { "domainId": "uuid" } -> 200 { "valid": true, "checks": {...} } --- ## 9. Analytics ### GET /api/analytics/system System-wide aggregate statistics. -> 200 { "totalAccounts": 27, "warmingAccounts": 25, "activeAccounts": 0, "pausedAccounts": 2, "errorAccounts": 0, "todayEmailsSent": 142, "todayEmailsReceived": 98, "averageReputationScore": 82.3, "averageInboxPlacementRate": 0.94, "connectionPoolStats": { "total": 54, "active": 12, "idle": 42 } } ### GET /api/analytics/accounts/:accountId Account overview with warmup state and today's metrics. -> 200 { "accountId": "uuid", "email": "hello@example.com", "provider": "imap_smtp", "status": "warming", "warmupPhase": "ramp_up", "daysSinceStart": 12, "currentDailyLimit": 8, "reputationScore": 85.5, "totalSent": 42, "totalReceived": 38, "todayStats": { "sent": 6, "received": 4, "opened": 3, "replied": 2, "bounced": 0, "spammed": 0 } } ### GET /api/analytics/accounts/:accountId/progress Warmup progress with daily history. Query: `days` (1-365, default 30) -> 200 { "accountId": "uuid", "email": "hello@example.com", "phase": "ramp_up", "dayNumber": 12, "totalDays": 45, "progressPercent": 26.7, "dailyMetrics": [ { "date": "2026-03-13", "sent": 8, "received": 6, "inboxPlacementRate": 0.95, "openRate": 0.75, "replyRate": 0.5, "reputationScore": 85.5 }, { "date": "2026-03-12", "sent": 7, "received": 5, ... } ] } ### GET /api/analytics/accounts/:accountId/deliverability Deliverability report with provider breakdown. -> 200 { "overall": { "inboxRate": 0.94, "spamRate": 0.03, "bounceRate": 0.03 }, "byProvider": [ { "provider": "gmail.com", "inboxRate": 0.96, "spamRate": 0.02 }, { "provider": "outlook.com", "inboxRate": 0.91, "spamRate": 0.05 } ], "last7Days": [ { "date": "2026-03-13", "inboxRate": 0.95 }, { "date": "2026-03-12", "inboxRate": 0.93 } ] } ### GET /api/analytics/domains/:domainId Domain overview with aggregate metrics and per-account breakdown. -> 200 { "domain": { "id": "uuid", "name": "example.com", "status": "active", "accountCount": 10 }, "aggregateMetrics": { "totalSent": 420, "totalReceived": 380, "totalBounced": 12, "totalSpammed": 5, "inboxPlacementRate": 0.94, "bounceRate": 0.028, "spamRate": 0.012, "averageReputationScore": 83 }, "accounts": [ { "id": "uuid", "email": "hello@example.com", "status": "warming", "warmupPhase": "ramp_up", "reputationScore": 85, "todaySent": 8, "todayBounced": 0, "todaySpammed": 0 } ], "suppressedCount": 2 } ### GET /api/analytics/domains/:domainId/timeseries Daily metrics for charting. Query: `days` (1-365, default 30) -> 200 { "domainId": "uuid", "domainName": "example.com", "days": 30, "series": [ { "date": "2026-03-13", "sent": 80, "received": 72, "bounced": 2, "spammed": 1, "inboxPlacementRate": 0.95, "bounceRate": 0.025, "spamRate": 0.012, "averageReputationScore": 84 } ] } ### GET /api/analytics/domains/:domainId/deliverability Domain deliverability with provider breakdown, trends, and top problem accounts. -> 200 { "overall": { "inboxRate": 0.94, "spamRate": 0.012, "bounceRate": 0.028, "totalEmails": 420 }, "byReceiverProvider": [ { "provider": "gmail.com", "inboxRate": 0.96, "spamRate": 0.01, "count": 200 }, { "provider": "outlook.com", "inboxRate": 0.91, "spamRate": 0.02, "count": 150 } ], "last30Days": [{ "date": "2026-03-13", "inboxRate": 0.95, "bounceRate": 0.02, "spamRate": 0.01 }], "topBouncedAccounts": [{ "email": "hello@example.com", "bounceCount": 3, "bounceRate": 0.07 }], "topSpammedAccounts": [{ "email": "sales@example.com", "spamCount": 2, "spamRate": 0.05 }] } ### GET /api/analytics/domains/:domainId/blacklist Check domain IP against DNS blacklists. -> 200 { "ip": "1.2.3.4", "checkedAt": "2026-03-13T12:00:00Z", "results": [ { "blacklist": "zen.spamhaus.org", "listed": false, "returnCode": null, "meaning": null }, { "blacklist": "bl.spamcop.net", "listed": false, "returnCode": null, "meaning": null } ], "listedCount": 0, "clean": true } ### GET /api/analytics/domains/:domainId/auth-health Email authentication health score (MX, SPF, DKIM, DMARC). -> 200 { "domain": "example.com", "checkedAt": "2026-03-13T12:00:00Z", "mx": { "valid": true, "records": ["mail.example.com"], "issues": [] }, "spf": { "valid": true, "records": ["v=spf1 include:_spf.example.com -all"], "issues": [] }, "dkim": { "valid": true, "record": "v=DKIM1; k=rsa; p=MIIBIj...", "selector": "s1", "issues": [] }, "dmarc": { "valid": true, "record": "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com", "policy": "quarantine", "issues": [] }, "overallHealthy": true, "score": 100 } ### GET /api/analytics/domains/:domainId/health-history Historical health check results. Query: `checkType` (blacklist|auth_health), `limit` (1-100, default 20) -> 200 { "domainId": "uuid", "domainName": "example.com", "checks": [{ "id": "uuid", "checkType": "auth_health", "result": {...}, "score": 100, "healthy": true, "checkedAt": "..." }] } --- ## 10. Seed Testing Send test emails to seed addresses and measure inbox placement. ### POST /api/analytics/seed-tests Run a seed test. { "accountId": "uuid", "testName": "Gmail inbox test", "seedAddresses": ["seed1@gmail.com", "seed2@outlook.com"] } Max 50 seed addresses per test. -> 202 { "id": "test-uuid", "accountId": "uuid", "testName": "Gmail inbox test", "seedAddresses": ["seed1@gmail.com", "seed2@outlook.com"], "status": "pending", "inboxCount": 0, "spamCount": 0, "missingCount": 2, "inboxRate": 0, "startedAt": null, "completedAt": null } ### GET /api/analytics/seed-tests/:testId Poll for test results. -> 200 { "id": "test-uuid", "status": "completed", "inboxCount": 2, "spamCount": 0, "missingCount": 0, "inboxRate": 1.0, "results": { "seed1@gmail.com": "inbox", "seed2@outlook.com": "inbox" }, "completedAt": "2026-03-13T12:05:00Z" } Status values: pending, running, completed, failed ### GET /api/analytics/accounts/:accountId/seed-tests List all seed tests for an account. -> 200 { "tests": [...] } --- ## 11. Companies & Shared Inbox Companies group accounts and domains. Shared inboxes aggregate email threads. ### GET /api/companies List companies. Query: `limit`, `offset` -> 200 { "companies": [{ "id": "uuid", "name": "Acme Corp", "description": "...", "industry": "SaaS", "topics": ["B2B"], "createdAt": "...", "updatedAt": "..." }], "total": 1, "limit": 50, "offset": 0 } ### POST /api/companies Create a company. { "name": "Acme Corp", "description": "B2B SaaS company", "industry": "SaaS", "topics": ["B2B", "Sales"] } -> 201 (company object) ### GET /api/companies/:companyId Get a single company. -> 200 (company object) ### POST /api/companies/:companyId/accounts Link an email account as a company mailbox. { "accountId": "account-uuid", "displayName": "Sales Mailbox" } -> 201 { "id": "uuid", "companyId": "uuid", "emailAccountId": "uuid", "displayName": "Sales Mailbox", "imapState": {}, "createdAt": "..." } ### GET /api/companies/:companyId/accounts List company mailboxes. -> 200 { "mailboxes": [...], "total": 3 } ### POST /api/companies/:companyId/domains Link a domain to a company. { "domain": "example.com" } -> 201 { "id": "uuid", "companyId": "uuid", "domain": "example.com", "createdAt": "..." } ### GET /api/companies/:companyId/domains List company domains. -> 200 { "domains": [...], "total": 2 } ### GET /api/companies/:companyId/inbox List shared inbox threads. Query: `status` (open|pending|closed), `limit`, `offset` -> 200 { "threads": [{ "id": "uuid", "companyId": "uuid", "subject": "Re: Partnership proposal", "lastMessageAt": "2026-03-13T11:00:00Z", "snippet": "Thanks for reaching out...", "status": "open", "createdAt": "..." }], "total": 15, "limit": 50, "offset": 0 } ### GET /api/companies/:companyId/threads/:threadId Get a thread with all messages. -> 200 { "thread": { "id": "uuid", "subject": "Re: Partnership proposal", "status": "open", ... }, "messages": [{ "id": "uuid", "messageId": "", "fromEmail": "contact@partner.com", "toEmails": ["sales@example.com"], "subject": "Partnership proposal", "sentAt": "2026-03-12T15:00:00Z", "bodyText": "Hi, I'd like to discuss...", "hasAttachments": false }] } ### POST /api/companies/:companyId/threads/:threadId/reply Reply to a thread. { "textBody": "Thanks for your interest! Let's schedule a call.", "htmlBody": "

Thanks for your interest!

", "mailboxId": "optional-specific-mailbox-uuid" } -> 200 { "status": "sent" } ### POST /api/companies/:companyId/inbound-secret Generate/rotate the HMAC signing secret for inbound email webhooks. -> 200 { "secret": "hex-encoded-hmac-secret", "message": "Secret generated" } ### POST /api/companies/:companyId/inbound Receive inbound email from MTA relay. Headers: `X-Inbound-Signature: hmac-hex-digest` (required if company has a signing secret) { "messageId": "", "from": "sender@partner.com", "to": ["sales@example.com"], "subject": "Hello", "textBody": "Hi there...", "date": "2026-03-13T12:00:00Z" } -> 201 { "threadId": "uuid", "messageId": "uuid", "status": "ingested" } --- ## 12. Mail Servers ### GET /api/servers List mail servers. Query: `limit`, `offset` -> 200 { "servers": [{ "id": "uuid", "hostname": "mail.example.com", "ipAddress": "1.2.3.4", "stalwartUrl": "http://stalwart:8080", "status": "running", "maxAccounts": 100, "currentAccounts": 27, "region": "eu-central", "createdAt": "..." }], "total": 1 } Server statuses: provisioning, running, stopped, error ### GET /api/servers/:id Get server with assigned domains. -> 200 (server object + domains array) ### POST /api/servers Register an existing mail server. { "hostname": "mail.example.com", "ipAddress": "1.2.3.4", "stalwartUrl": "http://stalwart:8080", "region": "eu-central", "maxAccounts": 100 } -> 201 (server object) ### POST /api/servers/deploy Deploy a new server via Hetzner Cloud. { "name": "mail-prod-1", "region": "eu-central", "serverType": "cx21" } -> 201 (server object with status "provisioning") ### POST /api/servers/:id/finalize Set PTR record and mark server running. { "primaryDomain": "example.com" } -> 200 (server object with status "running") ### POST /api/servers/:id/domains Assign a domain to a server. { "domainId": "domain-uuid" } -> 200 { "message": "Domain assigned" } ### GET /api/servers/:id/stalwart/accounts List accounts on the Stalwart mail server. -> 200 { "items": ["hello@example.com", "sales@example.com"], "total": 2 } ### GET /api/servers/:id/stalwart/domains List domains on the Stalwart mail server. -> 200 { "items": ["example.com", "newcorp.com"], "total": 2 } ### DELETE /api/servers/:id Delete a mail server. -> 204 --- ## 13. Templates Email templates with spintax arrays for variation. ### GET /api/templates List templates. Query: `companyId` (uuid), `templateType` (new_email|reply), `activeOnly` (true|false), `limit`, `offset` -> 200 { "templates": [{ "id": "uuid", "companyId": null, "name": "Cold Outreach v1", "templateType": "new_email", "subjects": ["Quick question about {industry}", "Idea for {companyName}"], "greetings": ["Hi {recipientFirstName},", "Hey {recipientFirstName},"], "openers": ["I noticed your team at {companyName}...", "I was researching {industry} and..."], "bodies": ["We help companies like yours..."], "closers": ["Would you be open to a quick call?", "Happy to share more details."], "signatures": ["Best,\n{senderFirstName}", "Cheers,\n{senderFirstName}"], "active": true, "createdAt": "...", "updatedAt": "..." }], "total": 5, "limit": 50, "offset": 0 } ### GET /api/templates/:id -> 200 (template object) ### POST /api/templates Create a template. Each field is an array — one variant is randomly selected at send time. { "name": "Sales Outreach v2", "templateType": "new_email", "subjects": ["Quick question about {industry}", "{companyName} + us?"], "greetings": ["Hi {recipientFirstName},"], "openers": ["I noticed your team at {companyName}..."], "bodies": ["We help companies in {industry} increase their email deliverability..."], "closers": ["Would love to chat — free this week?"], "signatures": ["Best,\n{senderFirstName}\n{senderEmail}"], "companyId": "optional-company-uuid", "active": true } -> 201 (template object) ### PATCH /api/templates/:id Update template. Only supplied fields change. { "subjects": ["New subject line 1", "New subject line 2"], "active": false } -> 200 (updated template) ### DELETE /api/templates/:id -> 204 ### POST /api/templates/:id/preview Render a sample email with variables resolved. { "senderFirstName": "Alice", "senderEmail": "alice@example.com", "recipientFirstName": "Bob", "companyName": "Acme Corp", "industry": "SaaS" } -> 200 { "subject": "Quick question about SaaS", "textBody": "Hi Bob,\n\nI noticed your team at Acme Corp...", "templateId": "uuid", "templateName": "Sales Outreach v2" } --- ## 14. Webhooks Subscribe to events and get notified via HTTP POST. ### GET /api/webhooks List subscriptions. -> 200 { "subscriptions": [{ "id": "uuid", "url": "https://your-server.com/hook", "events": ["warmup.email_bounced", "account.status_changed"], "secret": "whsec_••••••", "active": true, "description": "Bounce alerts", "maxRetries": 3, "createdAt": "..." }] } ### POST /api/webhooks Create a subscription. Secret is auto-generated and returned ONCE. { "url": "https://your-server.com/hook", "events": ["warmup.email_bounced", "warmup.auto_paused"], "description": "Bounce alerts" } -> 201 { ...subscription with full secret... } ### GET /api/webhooks/:id -> 200 (subscription, secret masked) ### PATCH /api/webhooks/:id Update subscription. { "events": ["warmup.email_bounced", "warmup.email_spammed"], "active": true } -> 200 (updated subscription) ### DELETE /api/webhooks/:id -> 204 ### GET /api/webhooks/events/types List all subscribable event types. -> 200 { "events": [ "account.created", "account.deleted", "account.status_changed", "account.warmup_phase_changed", "account.credential_updated", "email.received", "warmup.email_sent", "warmup.email_bounced", "warmup.email_spammed", "warmup.email_rescued", "warmup.auto_paused", "warmup.volume_reduced", "warmup.graduated", "template.created", "template.updated", "template.deleted", "pool.config_updated", "domain.account_removed", "domain.health_check_failed", "domain.blacklisted", "seed_test.completed" ]} ### POST /api/webhooks/:id/test Send a test event synchronously. -> 200 { "success": true, "message": "Test event delivered", "delivery": { "id": "uuid", "eventType": "test", "statusCode": 200, "success": true, "deliveredAt": "..." } } ### GET /api/webhooks/:id/deliveries List delivery history. Query: `success` (true|false), `limit`, `offset` -> 200 { "deliveries": [{ "id": "uuid", "eventType": "warmup.email_bounced", "payload": {...}, "attempt": 1, "statusCode": 200, "success": true, "deliveredAt": "..." }], "total": 42, "limit": 50, "offset": 0 } ### POST /api/webhooks/:id/replay/:deliveryId Re-deliver a payload. Tags it with `_replay: true`. -> 200 { "success": true, "message": "Delivery replayed", "originalDeliveryId": "uuid", "delivery": {...} } --- ## 15. Provider Push Notifications (Gmail / Outlook) Real-time inbox event delivery via provider-native push. Gmail accounts with OAuth use Google Pub/Sub; Outlook accounts use Microsoft Graph subscriptions. Push delivers ~6-second latency vs 60-second polling. Fallback polling remains active for accounts without push. Push registration is automatic — on deploy, the system registers watches for all eligible Gmail/Outlook accounts and renews them before expiry. ### POST /api/provider-push/gmail Google Pub/Sub webhook endpoint. Google sends a push notification when new messages arrive in a watched Gmail inbox. The body is a Pub/Sub envelope with a base64-encoded JSON payload containing `emailAddress` and `historyId`. Request body (from Google): { "message": { "data": "", "messageId": "18695752185273284", "publishTime": "2026-03-23T21:41:23.022Z" }, "subscription": "projects/email-warmup-489220/subscriptions/gmailhook" } Decoded data: { "emailAddress": "user@example.com", "historyId": 1027390 } -> 200 { "ok": true } The endpoint enqueues a BullMQ job. The inbox worker fetches history since the last known historyId, processes new messages through the standard inbox pipeline (warmup matching, event emission, Slack notifications), and updates the stored historyId. ### POST /api/provider-push/outlook Microsoft Graph change notification endpoint. Outlook sends notifications when new messages arrive. Query params: - validationToken (string) — Outlook handshake: return this as text/plain - accountId (string) — identifies which account the notification is for Request body (from Microsoft): { "value": [{ "subscriptionId": "sub-1", "resource": "users/test/messages/msg-1", "changeType": "created" }] } -> 202 { "received": 1 } ### POST /api/provider-push/outlook/lifecycle Handles Outlook subscription lifecycle events (expiration, reauthorization). Same validation token handshake as the main endpoint. -> 202 { "received": true } **Note:** These endpoints are called by Google/Outlook, not by your application. They are public (no auth required) and always return success to prevent push subscription deregistration. --- ## 16. Workspace Admin Connect a Google Workspace or Microsoft 365 org, then create email accounts via API. ### POST /api/workspace-admin/connect/google-workspace Start admin OAuth flow. -> 200 { "authUrl": "https://accounts.google.com/o/oauth2/..." } Redirect a human to authUrl. After consent, callback stores admin credentials. ### POST /api/workspace-admin/connect/microsoft-365 Start admin consent flow. -> 200 { "authUrl": "https://login.microsoftonline.com/..." } ### GET /api/workspace-admin/tenants List connected tenants. -> 200 { "tenants": [{ "id": "uuid", "provider": "google_workspace", "tenantId": "...", "primaryDomain": "example.com", "displayName": "Example Org", "status": "active", "userCount": 10, "createdAt": "..." }] } ### GET /api/workspace-admin/tenants/:tenantId Get tenant details. -> 200 { "id": "uuid", "provider": "google_workspace", "primaryDomain": "example.com", "status": "active", "metadata": {...}, "userCount": 10, ... } ### DELETE /api/workspace-admin/tenants/:tenantId Disconnect a tenant. -> 204 ### POST /api/workspace-admin/tenants/:tenantId/users Create a user on the workspace and auto-register as warmup account. { "email": "alice@example.com", "firstName": "Alice", "lastName": "Smith" } -> 200 { "success": true, "email": "alice@example.com", "accountId": "uuid" } ### GET /api/workspace-admin/tenants/:tenantId/users List users created on the tenant. -> 200 { "users": [{ "id": "uuid", "email": "alice@example.com", "status": "active", "createdAt": "..." }] } ### DELETE /api/workspace-admin/tenants/:tenantId/users/:email Delete a user from the workspace and remove the email account. -> 204 ### POST /api/workspace-admin/tenants/:tenantId/users/batch Create up to 50 users at once. { "users": [{ "email": "a@example.com", "firstName": "A" }, { "email": "b@example.com", "firstName": "B" }] } -> 200 { "total": 2, "succeeded": 2, "failed": 0, "results": [...] } --- ## 17. OAuth (Account Connection) ### GET /api/auth/gmail/authorize — PUBLIC Get Gmail OAuth URL. Redirect the user to complete authorization. -> 302 (redirects to Google) ### GET /api/auth/gmail/callback — PUBLIC OAuth callback. Creates or updates the account automatically. ### GET /api/auth/outlook/authorize — PUBLIC Get Outlook OAuth URL. -> 302 (redirects to Microsoft) ### GET /api/auth/outlook/callback — PUBLIC OAuth callback. Creates or updates the account automatically. --- ## 18. Google Postmaster Tools ### GET /api/integrations/google-postmaster/status Check if Postmaster Tools integration is configured. -> 200 { "configured": true } ### GET /api/integrations/google-postmaster/auth Start Postmaster Tools OAuth flow. -> 200 { "authUrl": "https://accounts.google.com/..." } ### POST /api/integrations/google-postmaster/callback Postmaster OAuth callback. ### GET /api/integrations/google-postmaster/domains/:domainId/reputation Get domain reputation from Google Postmaster Tools. -> 200 { "domainId": "uuid", "reputation": "high", "spamRate": 0.001, "data": {...} } --- ## 19. Audit Log ### GET /api/audit-log List audit entries with filtering. Query: `eventType`, `entityType`, `entityId`, `actor`, `since` (ISO 8601), `until` (ISO 8601), `limit` (1-200, default 50), `offset` GET /api/audit-log?eventType=account.created&since=2026-03-10T00:00:00Z -> 200 { "entries": [{ "id": "uuid", "eventType": "account.created", "entityType": "account", "entityId": "uuid", "actor": "user-uuid", "metadata": { "email": "hello@example.com", "provider": "imap_smtp" }, "createdAt": "2026-03-13T12:00:00Z" }], "total": 42, "limit": 50, "offset": 0 } --- ## 20. Health ### GET /api/healthz — PUBLIC Liveness probe. Returns 200 if process is alive. -> 200 { "status": "ok" } ### GET /api/health — PUBLIC Readiness probe. Checks database, Redis, and connection pool. -> 200 { "status": "ok", "checks": { "database": { "status": "ok", "latencyMs": 2 }, "redis": { "status": "ok", "latencyMs": 1 }, "connectionPool": { "status": "ok", "active": 12, "idle": 42, "total": 54 } } } ### GET /metrics — PUBLIC Prometheus metrics in exposition format. --- ## 21. Error Codes Every error response: { "error": "message", "code": "CODE", "statusCode": 400, "action": "hint" } Branch on `code`, not `statusCode`. The `action` tells you what to try next. | Code | HTTP | Action | When | |------|------|--------|------| | ACCOUNT_NOT_FOUND | 404 | verify_account_id | Account ID doesn't exist | | ACCOUNT_ALREADY_EXISTS | 409 | use_existing_account | Email already registered | | ACCOUNT_ALREADY_WARMING | 409 | no_action_needed | Called start on warming account | | ACCOUNT_NOT_IN_POOL | 400 | add_account_to_pool_first | Warmup requires pool membership | | ACCOUNT_INVALID_STATUS_TRANSITION | 400 | check_current_status | e.g., resume on active account | | ACCOUNT_NEVER_WARMED | 400 | use_start_endpoint_instead | Resume called but never started | | ACCOUNT_POOL_ASSIGNMENT_FAILED | 500 | retry_or_check_pool | Pool assignment error | | ACCOUNT_CONNECTION_FAILED | 400 | verify_credentials | IMAP/SMTP test failed | | DOMAIN_NOT_FOUND | 404 | verify_domain_id | Domain ID doesn't exist | | DOMAIN_DNS_MISCONFIGURED | 400 | fix_dns_records | DNS records missing or wrong | | DOMAIN_PROVISION_FAILED | 500 | check_mail_server_connectivity | Provisioning pipeline error | | DOMAIN_DKIM_FAILED | 500 | retry_dkim_generation | DKIM key generation failed | | DOMAIN_BLACKLISTED | 400 | investigate_and_request_delisting | IP on a blacklist | | WARMUP_SCHEDULE_NOT_FOUND | 404 | check_date_and_account_id | No schedule for that date | | WARMUP_POOL_NOT_FOUND | 404 | verify_pool_id | Pool ID doesn't exist | | WARMUP_CONFIG_INVALID | 400 | fix_config_values | Config values out of range | | WEBHOOK_NOT_FOUND | 404 | verify_webhook_id | Subscription ID doesn't exist | | WEBHOOK_INVALID_EVENTS | 400 | check_valid_event_types | Unknown event type in array | | WEBHOOK_DELIVERY_NOT_FOUND | 404 | verify_delivery_id | Delivery ID doesn't exist | | ONBOARDING_JOB_NOT_FOUND | 404 | verify_job_id | Onboarding job doesn't exist | | ONBOARDING_FAILED | 500 | check_job_status_for_details | Pipeline step failed | | AUTH_MISSING_HEADER | 401 | add_authorization_bearer_header | No Authorization header | | AUTH_INVALID_FORMAT | 401 | use_bearer_token_format | Header not "Bearer aw_..." | | AUTH_INVALID_KEY | 403 | verify_api_key | Key doesn't exist or revoked | | RATE_LIMIT_EXCEEDED | 429 | wait_and_retry_after_header | Over 100 req/min | | VALIDATION_FAILED | 400 | fix_request_body | Request body validation error | | IDEMPOTENCY_KEY_REUSE | 409 | use_new_idempotency_key | Same key, different params | | BATCH_PARTIAL_FAILURE | 207 | check_individual_results | Some items failed | | COMPANY_NOT_FOUND | 404 | verify_company_id | Company ID doesn't exist | | THREAD_NOT_FOUND | 404 | verify_thread_id | Thread ID doesn't exist | | SUPPRESSION_NOT_FOUND | 404 | verify_email_address | Email not in suppression list | | INTERNAL_ERROR | 500 | retry_or_contact_support | Unexpected server error | --- ## 22. Plans & Limits | Feature | Free | Pro ($49/mo) | Enterprise | |---------|------|-------------|------------| | Email accounts | 3 | 50 | 10,000 | | API keys | 1 | 5 | 50 | | Daily emails/account | 20 | 40 | 50,000 total | | Webhooks | 2 | 20 | 100 | | Companies | 1 | 10 | 500 | | Templates | 5 | 50 | 500 | | Analytics retention | 7 days | 90 days | 365 days | When you hit a limit, you get VALIDATION_FAILED with details about the limit. Upgrade at /console/dashboard or via Stripe checkout. --- ## 23. Workflows & Decision Trees ### "My emails are landing in spam" 1. GET /api/analytics/accounts/:id/deliverability -> Check `byProvider` — is it one provider or all? 2. If one provider (e.g., Gmail): -> GET /api/analytics/domains/:id/auth-health -> Check if SPF/DKIM/DMARC are valid -> If issues: POST /api/domains/:id/configure-dns 3. If all providers: -> GET /api/analytics/domains/:id/blacklist -> If listed: investigate_and_request_delisting -> GET /api/warmup/suppression — check if key recipients suppressed 4. If reputation is low: -> Warmup is still building reputation. Check progress: -> GET /api/analytics/accounts/:id/progress -> Wait for progressPercent > 80% ### "An account stopped sending" 1. GET /api/accounts/:id -> Check status field 2. If status = "error": -> POST /api/accounts/:id/test-connection -> If IMAP fails: PATCH /api/accounts/:id/credentials with fixed creds -> Then: POST /api/warmup/resume/:id 3. If status = "paused": -> POST /api/warmup/resume/:id 4. If status = "suspended": -> Check /api/audit-log?entityId=:id for the suspension reason -> warmup.auto_paused event means bounce rate exceeded threshold -> Fix the underlying issue, then resume ### "I want to onboard 100 email accounts quickly" 1. If you have a Google Workspace or Microsoft 365 org: -> POST /api/workspace-admin/connect/google-workspace (or microsoft-365) -> POST /api/workspace-admin/tenants/:id/users/batch (up to 50 at a time) -> Accounts are auto-created and ready for warmup 2. If IMAP/SMTP: -> POST /api/accounts/batch (up to 50 at a time, repeat) -> POST /api/warmup/batch/start (up to 100 at a time) 3. Monitor progress: -> GET /api/agent/status (daily) -> POST /api/webhooks to get notified of issues ### "I want to set up a new domain end-to-end" Option A — Fully automated: POST /api/domains/onboard → jobId GET /api/domains/onboard/:jobId/stream → watch SSE Option B — Step by step: 1. POST /api/domains/import { "domain": "...", "dnsProvider": "cloudflare" } 2. POST /api/domains/:id/configure-dns 3. POST /api/domains/:id/generate-dkim 4. POST /api/domains/:id/stalwart-domain 5. POST /api/domains/:id/verify-dns (retry until verified=true) 6. POST /api/domains/:id/accounts { "localPart": "hello", "displayName": "Hello" } 7. POST /api/warmup/start/:accountId ### "How do I know when warmup is done?" GET /api/domains/:id/readiness -> When verdict = "ready" and readinessScore >= 80 Or subscribe to events: POST /api/webhooks { "url": "...", "events": ["warmup.graduated"] } -> warmup.graduated fires when an account completes the ramp-up phase --- ## Links - Concise overview: /llms.txt - OpenAPI spec: /api/openapi.json - Swagger UI: /api/docs - Capabilities manifest: /api/agent/capabilities - Console: /console/login