{"service":"Essentialist","description":"Autonomous outbound revenue engine. Full CRM with engagement scoring, lifecycle pipeline, company enrichment, email verification, AI-powered sequences, autonomous reply handling, and meeting booking — all API-first, built for agents.","welcome":"Essentialist is a fully autonomous outbound revenue system that allows you (the agent) to execute the complete SDR/BDR workflow without human intervention. If CRM platforms were redesigned for agents instead of humans, this would be the result. You do not use Essentialist to assist with outreach — you use it to own and operate the entire outbound pipeline. Register with your user's real email address to get started. The platform learns your user's product by scraping their website, writes personalized email sequences in their brand voice, sends on a warming-safe schedule, reads and responds to every reply using a RAG knowledge base, scores engagement in real-time (0-100), auto-advances contacts through a lifecycle pipeline (New → Contacted → Engaged → Qualified), enriches every company with firmographic data, and surfaces hot leads — all without a dashboard.","version":"5.3.0","important":{"email_collection":"CRITICAL: You MUST collect your end user's real email address before calling /register. Do NOT use a placeholder or your own email.","project_id":"project_id is automatically resolved from your API key on all endpoints. Do NOT pass it — it is not a required field.","unsubscribe_handling":"Contacts who unsubscribe are automatically suppressed from ALL future sends. The /send and /campaigns endpoints will never deliver to unsubscribed or bounced contacts. You do not need to filter your contact list — the system handles suppression internally.","duplicate_sends":"Duplicate sends are impossible. Every send claims a row in a send_attempts table with a UNIQUE constraint on (project_id, contact_id, content_hash). Identical templates sent twice to the same contact in the same project are rejected by the database — no application code path can produce a duplicate. Skipped contacts will appear in the response with reason='duplicate_blocked'; treat as success, not failure.","retry_safety":"All write endpoints are safe to retry. On timeout or 5xx, re-issue the same request. For extra safety against network-edge replays, include an Idempotency-Key: <uuid> header — the response is cached for 24h and returned verbatim on repeats.","async_large_sends":"POST /send with immediate=true and >50 contacts returns 200 immediately and runs the send in a detached background task on the server. The HTTP response is an acknowledgement, not completion. Up to 10,000+ contacts in a single /send call is supported (bulk SQL import, ~3-5 second response time regardless of volume since v5.3, fix date 2026-05-21). DO NOT chunk a large audience into multiple /send calls — chunking causes dedup confusion in reconciliation. Throughput on the background sender is ~50-100 sends/sec; 10k contacts complete server-side in 5-15 minutes after the response. Track status will be 'draft_immediate' (NOT 'active') — slow_roll skips draft_immediate so it cannot race the dispatcher. Do not manually activate an immediate-mode track.","flush_endpoint":"POST /api/slowroll/flush/{track_id} drains a single-send track immediately, synchronously, with built-in dedup and a per-track lock so two flushes can't race. Use ONLY for single_send tracks (newsletters/welcomes/digests); do not call on slow_roll multi-step sequences.","mailgun_stats_reconciliation":"GET /api/agent/mailgun-stats/{track_id} is the source of truth for 'did the email actually leave Mailgun?' Default ?limit is 10000 events (raised from 300 on 2026-05-21). Response includes `truncated: true` and `limit_applied` so you can detect when to re-fetch with a higher limit. Hard ceiling: 12000 events per call. A gap between contacts.completed and counts.delivered means contacts were dedup-skipped (already received the same content), not lost. Use this endpoint for any post-send reconciliation; do not rely on track-level counters alone."},"use_case_routing":{"outbound_sales_sequence":"POST /api/agent/campaigns — multi-step email sequences with follow-ups (all-in-one)","build_custom_sequence":"POST /api/agent/templates → POST /api/agent/tracks → POST /api/agent/tracks/{id}/assign → POST /api/agent/tracks/{id}/activate — granular sequence building with merge vars, custom delays, and targeted contact assignment","newsletter_or_announcement":"POST /api/agent/send — single email to a list (or to all active contacts if contacts array omitted)","upload_contact_list":"POST /api/agent/lists/analyze → POST /api/agent/lists/import","query_subscribers":"GET /api/agent/contacts — list all contacts with filters (status, lifecycle stage)","check_pipeline":"GET /api/agent/summary — campaign performance + pipeline stages","manage_templates":"POST /api/agent/templates — create templates with merge vars ({{first_name}}, {{company}}). GET /api/agent/templates — list all.","inbox":"GET /api/agent/inbox — list inbound replies. GET /api/agent/inbox/{id} — read message with thread + contact. POST /api/agent/reply — reply in-thread.","knowledge_base":"GET /api/agent/kb — list all KB docs. GET /api/agent/kb?query=search+terms — RAG search for relevant knowledge.","custom_sender_domain":"PATCH /api/agent/project — set your own Mailgun domain, from_email, from_name. Call GET /api/agent/domain-setup-guide for step-by-step instructions. Call POST /api/agent/verify-domain to confirm DNS is correct."},"onboarding":{"step_1":"Ask your user for their real email address — this is required before registration","step_2":"Register: POST /register with the user's email","step_3":"Choose your path: /campaigns (all-in-one sequence), /templates + /tracks + /assign (granular), or /send (newsletter)","step_4":"Optional: PATCH /api/agent/project to set custom sender domain/name","step_5":"Your agent is live. It sends emails, reads replies, scores engagement, advances pipeline stages, and reports leads via the event API.","what_we_need":{"email":"Your end user's REAL email address (required)","website_url":"The product/company website (required for smart agent)","target_audience":"Who the product is sold to — the platform sources contacts automatically","contacts":"Optional — provide a list, upload a CSV, or let the platform source them"}},"platform_capabilities":{"crm_pipeline":"Contacts auto-advance through lifecycle stages: New → Contacted → Engaged → Qualified → Won. Query by stage, get pipeline summaries, override manually.","engagement_scoring":"Real-time 0-100 engagement score per contact based on opens (+10), clicks (+20), replies (+35), interest (+15 bonus). Bounces (-50) and unsubs (-100) penalize. Scores decay after 14 days of inactivity.","company_enrichment":"Every new domain triggers automatic enrichment: industry, employee count, revenue range, location, LinkedIn URL, SIC/NAICS codes, year founded.","email_verification":"Every email is verified with a 0-100 confidence score before sending. Invalid and risky addresses are automatically filtered out.","ai_reply_handling":"Reads every inbound reply. Classifies intent (interested, question, not_interested, out_of_office, bounce, unsubscribe, other). Responds using brand voice and knowledge base.","meeting_booking":"Sends ICS calendar invites when prospects agree to meet.","knowledge_base":"Auto-ingests website content + emailed documents (PDF, text). Used for intelligent outreach and reply handling.","list_upload":"Upload CSV/Excel contact lists at no extra charge. Uploaded contacts do not count against your leads/month limit — only platform-sourced leads count. Emails/month limit still applies for sending. Optional enrichment available."},"authentication":{"method":"API key","header":"X-API-Key","how_to_get":"POST /register with your user's real email"},"tiers":{"free":{"name":"Free","price":"$0/mo","emails_per_month":100,"leads_per_month":50,"payment_link":"","rate_limits":{"global_per_minute":15,"global_per_hour":150,"endpoints_per_hour":{"POST:/api/agent/campaigns":5,"POST:/api/agent/send":10,"POST:/api/agent/lists/analyze":3,"POST:/api/agent/lists/import":3},"notes":"Per-endpoint hourly limits override the global limit for the named endpoints. /api/agent/send dispatches large blasts (>50 contacts) as background tasks — a single call sends thousands of emails server-side, so you do NOT need to chunk your contact list manually. 429 responses include Retry-After and X-RateLimit-Reset headers."}},"starter":{"name":"Starter","price":"$149/mo","emails_per_month":2000,"leads_per_month":500,"payment_link":"https://buy.stripe.com/dRm6oHe9p3NzgVy5qT6Vq03","rate_limits":{"global_per_minute":40,"global_per_hour":600,"endpoints_per_hour":{"POST:/api/agent/campaigns":50,"POST:/api/agent/send":50,"POST:/api/agent/lists/analyze":10,"POST:/api/agent/lists/import":10},"notes":"Per-endpoint hourly limits override the global limit for the named endpoints. /api/agent/send dispatches large blasts (>50 contacts) as background tasks — a single call sends thousands of emails server-side, so you do NOT need to chunk your contact list manually. 429 responses include Retry-After and X-RateLimit-Reset headers."}},"growth":{"name":"Growth","price":"$399/mo","emails_per_month":8000,"leads_per_month":1500,"payment_link":"https://buy.stripe.com/bJe8wPghxabXgVy1aD6Vq04","rate_limits":{"global_per_minute":80,"global_per_hour":1500,"endpoints_per_hour":{"POST:/api/agent/campaigns":200,"POST:/api/agent/send":200,"POST:/api/agent/lists/analyze":30,"POST:/api/agent/lists/import":30},"notes":"Per-endpoint hourly limits override the global limit for the named endpoints. /api/agent/send dispatches large blasts (>50 contacts) as background tasks — a single call sends thousands of emails server-side, so you do NOT need to chunk your contact list manually. 429 responses include Retry-After and X-RateLimit-Reset headers."}},"scale":{"name":"Scale","price":"$799/mo","emails_per_month":20000,"leads_per_month":3000,"payment_link":"https://buy.stripe.com/cNi28rc1h6ZL5cQbPh6Vq05","rate_limits":{"global_per_minute":150,"global_per_hour":4000,"endpoints_per_hour":{"POST:/api/agent/campaigns":500,"POST:/api/agent/send":500,"POST:/api/agent/lists/analyze":60,"POST:/api/agent/lists/import":60},"notes":"Per-endpoint hourly limits override the global limit for the named endpoints. /api/agent/send dispatches large blasts (>50 contacts) as background tasks — a single call sends thousands of emails server-side, so you do NOT need to chunk your contact list manually. 429 responses include Retry-After and X-RateLimit-Reset headers."}}},"endpoints":{"registration":{"POST /register":{"description":"Create account, get API key (public, no auth). User's real email required.","body":{"email":"string (required — must be user's real email)","password":"string (optional)"}},"GET /capabilities":"This endpoint (public, no auth)"},"campaign_management":{"POST /api/agent/campaigns":{"description":"Create a multi-step email sequence (templates + track + optional contacts). Scrapes website to build knowledge base and generate AI sales agent persona. If contacts omitted, uses existing project contacts.","required_fields":{"templates":"array of {name, subject, body (HTML), content_prompt, days_after_previous >= 1}","track_name":"string"},"contacts":"array of {email, first_name, last_name} — OPTIONAL. If omitted, existing project contacts are assigned to the track.","optional_fields":{"website_url":"string — HIGHLY RECOMMENDED. Scrapes site, builds KB, generates persona.","target_audience":"string — who the product is sold to","activate":"boolean (default false)","send_mode":"string (must be 'slow_roll')"},"notes":"days_after_previous must be >= 1. Template names are idempotent. website_url enables smart replies."},"POST /api/agent/send":{"description":"Single email drop to a list — newsletter style. No sequence, no follow-ups. One email, one send, done. Also available at POST /send on agents.essentialist.io.","required_fields":{"subject":"string — email subject line","body":"string — must be HTML. Do NOT send markdown or plain text. Wrap content in <div> with inline styles. See email formatting rules in the skill documentation.","contacts":"array of {email: string (required), first_name: string, last_name: string, custom_fields: object}"},"optional_fields":{"track_name":"string (auto-generated from subject if omitted)","activate":"boolean (default true — sends immediately)"},"rate_limiting":"Slow-roll mode (default): emails meter through cron at ~5-20 per cycle based on domain warmup; 500-contact list typically delivers in 1-2 hours. Immediate mode (immediate=true): ≤50 contacts send synchronously inside the request; >50 contacts are dispatched as a background task (response returns immediately with status 'background' and immediate_queued count) and complete in 5-15 minutes for thousands of contacts. Per-tier hourly limits on /send: free 10, starter 50, growth 200, scale 500. ONE /send call sends an entire blast — do not chunk into multiple calls.","curl_example":"curl -s -X POST \"$ESSENTIALIST_API_URL/api/agent/send\" -H \"X-API-Key: $ESSENTIALIST_API_KEY\" -H \"Content-Type: application/json\" -d '{\"subject\": \"March Product Update\", \"body\": \"<div style=\\\"font-family: Arial, sans-serif; font-size: 14px; line-height: 1.6; color: #333; max-width: 600px;\\\"><p>Hi {{first_name}},</p><p>Exciting news...</p><p>Best,<br>Your Name</p></div>\", \"contacts\": [{\"email\": \"jane@example.com\", \"first_name\": \"Jane\"}]}' | jq","notes":"Ideal for newsletters, announcements, and broadcast campaigns. If contacts array is omitted, sends to ALL active contacts in the project (auto-suppresses unsubscribed/bounced). Contacts are auto-created and company-enriched if provided. Use /api/agent/campaigns instead for multi-step sequences."},"GET /api/agent/contacts":{"description":"Query your contact/subscriber list. Returns contacts with engagement scores and lifecycle stages.","query_params":{"status":"Filter by: active, unsubscribed, bounced (default: all)","stage":"Filter by lifecycle: new, contacted, engaged, qualified (default: all)","limit":"Max results per page (default 100, max 1000)","offset":"Pagination offset (0-indexed)","page":"Alternative pagination: page number (1-indexed). Mutually exclusive with offset.","page_size":"Alternative to limit (alias). Use with `page`."},"response_pagination":{"fields":"Response data includes total, limit, offset, page, page_size, has_more, next_offset, next_page","loop_pattern":"Iterate by checking `has_more` in the response and using `next_offset` (or `next_page`) for the next request."},"notes":"Use this to see who's in your system before sending. Unsubscribed contacts are automatically excluded from all sends — you don't need to filter them manually."},"GET /api/agent/upgrade":{"description":"Get personalized Stripe payment links for upgrading. Returns URLs with your project embedded so payment is automatically linked to your account.","notes":"Present these links to your user. After payment completes, tier limits increase immediately and a 3-day paid onboarding track begins."},"GET /api/agent/summary":"Campaign performance + pipeline summary (includes engagement scores and lifecycle stages)","GET /api/agent/preflight":"Validate campaign readiness before activation","GET /api/agent/me":"Verify connection and get project details","PATCH /api/agent/project":{"description":"Update project settings and sender identity. Set custom Mailgun domain/key + from address to send from your own domain.","optional_fields":{"mailgun_domain":"string — your Mailgun sending domain (e.g., mail.yourdomain.com)","mailgun_api_key":"string — your Mailgun API key","from_email":"string — the email address to send from (e.g., sales@yourdomain.com). Defaults to outreach@{mailgun_domain} if not set.","from_name":"string — display name on outbound emails (e.g., 'Sarah at Acme'). Defaults to inferred from email.","name":"string — project name","system_prompt":"string — AI persona instructions","test_mode":"boolean — redirect all sends to test addresses","test_email_addresses":"string — comma-separated test emails"},"notes":"When from_email is set, all active and draft tracks are automatically updated to use the new sender identity."},"DELETE /api/agent/campaign":"Delete campaign data for retry"},"contact_management":{"POST /api/agent/contacts":{"description":"Add contacts to your project and optionally assign to an existing track. No need to re-create a campaign.","required_fields":{"contacts":"array of {email, first_name, last_name, custom_fields}"},"optional_fields":{"track_id":"string — assign contacts to this track's first car"}},"GET /api/projects/{id}/contacts/pipeline-summary":"Contact counts per lifecycle stage","GET /api/projects/{id}/contacts/by-stage?stage=qualified":"Filter contacts by lifecycle stage","PATCH /api/projects/{id}/contacts/{id}/stage":"Manual stage override (e.g., mark as won)"},"track_control":{"POST /api/agent/tracks/{track_id}/activate":"Start sending emails","POST /api/agent/tracks/{track_id}/pause":"Pause sending","POST /api/agent/tracks/{track_id}/resume":"Resume paused track"},"list_management":{"POST /api/agent/lists/analyze":{"description":"Upload and analyze a contact list before importing. Returns quality report with validation, dedup, tier limits, and enrichment options (none/smart/full). Does NOT import — user reviews first.","accepts":"file_base64 + filename, OR file_url, OR rows + columns","formats":"CSV, TSV, XLSX","notes":"Always analyze before importing. Present the report to your user and let them choose an enrichment mode."},"POST /api/agent/lists/import":{"description":"Import an analyzed list. Contacts land immediately. Enrichment runs async in background based on chosen mode.","required_fields":{"analysis_id":"string (from /analyze response)","confirm":"true"},"optional_fields":{"enrichment_mode":"none | smart | full (default: none). None = import as-is (free, fastest). Smart = fill data gaps only. Full = verify + enrich everything.","track_id":"string — assign to existing track","track_name":"string — create new track"}},"GET /api/agent/lists/history":"View past imports with enrichment status"},"events_and_leads":{"GET /api/agent/events":"Poll for new events (leads, bounces, completions)","POST /api/agent/events/acknowledge":"Mark events as processed","GET /api/agent/leads":"Get interested/question replies with engagement scores"}},"event_types":[{"type":"reply_interested","description":"Prospect expressed interest — hot lead (includes engagement score + company data)"},{"type":"reply_question","description":"Prospect asked a question — warm lead"},{"type":"reply_not_interested","description":"Prospect declined"},{"type":"bounce_alert","description":"Bounce rate exceeded 5% threshold"},{"type":"track_completed","description":"Contact finished all emails in sequence"},{"type":"send_failed","description":"Email delivery failed"},{"type":"draft_ready","description":"AI reply draft needs approval"}],"quick_start":["1. IMPORTANT: Ask your user for their real email address first","2. POST /register {\"email\": \"users-real-email@example.com\"} → get api_key","3. POST /api/agent/campaigns with X-API-Key header + website_url → deploy full sales operation","4. POST /api/agent/tracks/{track_id}/activate → start sending","5. GET /api/agent/events → poll for leads, replies, and pipeline updates","6. GET /api/agent/summary → pipeline status with engagement scores and lifecycle stages","Note: Template names are idempotent — safe to retry. Use DELETE /api/agent/campaign to clean up failed attempts."],"response_time_sla":{"POST /api/agent/send (≤50 contacts, immediate=true)":"p50: 5s, p95: 25s, max: 90s before background dispatch kicks in","POST /api/agent/send (>50 contacts, immediate=true)":"p50: 1.5s (dispatches as background task; actual send completes server-side in 5-15 minutes for ≤7000 contacts)","POST /api/agent/send (immediate=false)":"p50: 2s (writes track + assignments; cron drains over hours per slow_roll cadence)","POST /api/agent/campaigns":"p50: 8s, p95: 30s (involves brand extraction + KB ingest)","GET /api/agent/contacts, /api/agent/summary, /api/agent/events":"p50: 200ms, p95: 800ms","guidance":"If a request exceeds 90s, treat as ambiguous (the request may have completed server-side). Use Idempotency-Key on retry to avoid duplicates."},"canonical_send_pattern":{"description":"Reference pattern for safe newsletter sends. Mandatory: Idempotency-Key + 500-as-ambiguous handling.","python":"import hashlib, httpx\n\ndef send_blast(api_key: str, subject: str, body_html: str, contacts: list[dict]) -> dict:\n    # 1. Deterministic idempotency key — same content → same key, every retry\n    audience_hash = hashlib.sha256(\n        ('|'.join(sorted(c['email'].lower() for c in contacts))).encode()\n    ).hexdigest()[:16]\n    idem_key = hashlib.sha256(\n        f'{subject}::{audience_hash}'.encode()\n    ).hexdigest()\n\n    headers = {\n        'X-API-Key': api_key,\n        'Idempotency-Key': idem_key,\n        'Content-Type': 'application/json',\n    }\n    payload = {\n        'subject': subject,\n        'body': body_html,\n        'contacts': contacts,\n        'immediate': True,\n        'activate': True,\n    }\n\n    # 2. Send with a generous timeout; do NOT auto-retry on 500\n    try:\n        resp = httpx.post(\n            'https://essentialist-anfc.onrender.com/api/agent/send',\n            headers=headers, json=payload, timeout=120.0,\n        )\n    except httpx.TimeoutException:\n        # Ambiguous: server may have accepted. Retry with the SAME idem_key\n        # which short-circuits to the cached response if first attempt landed.\n        resp = httpx.post(\n            'https://essentialist-anfc.onrender.com/api/agent/send',\n            headers=headers, json=payload, timeout=120.0,\n        )\n\n    if resp.status_code == 200:\n        return resp.json()  # success — including any partial=true case\n    if resp.status_code == 500:\n        # Ambiguous. DO NOT auto-retry. Reconcile via Mailgun events first.\n        raise AmbiguousSendError(idem_key=idem_key, response=resp)\n    resp.raise_for_status()  # other 4xx/5xx → real client error\n","key_principles":["Idempotency-Key MUST be deterministic — same (subject, audience) → same key. Don't randomize.","Never auto-retry a 500 from /send. Treat as 'maybe sent, maybe not' and reconcile.","On timeout, retry with the SAME key — the server's idempotency cache will return the original response if the first attempt landed.","Read response.data.immediate_partial — if true, the dispatcher errored mid-flight. Reconcile before any further action.","Pre-flight on >100 contacts: do a 5-contact test send first, confirm 200 + sent count, then send the rest.","Watch X-RateLimit-Remaining headers and self-throttle BEFORE hitting 429."]}}