Skip to content

API Reference

The CloviReach API lets you integrate CloviReach into your own applications and automations. All endpoints are served over HTTPS from https://clovireach.com.

Authentication

Authenticate every request with a Bearer token in the Authorization header. Generate a token from your account settings on the CloviReach dashboard.

curl https://clovireach.com/api/me \
  -H "Authorization: Bearer $CLOVI_TOKEN"
import requests

resp = requests.get(
    "https://clovireach.com/api/me",
    headers={"Authorization": f"Bearer {token}"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://clovireach.com/api/me", {
  headers: { Authorization: `Bearer ${token}` },
});
const data = await resp.json();
console.log(data);
import axios from "axios";

const { data } = await axios.get(
  "https://clovireach.com/api/me",
  { headers: { Authorization: `Bearer ${token}` } }
);
console.log(data);

Keep your token secret

Treat your API token like a password. Send it only over HTTPS and never commit it to source control — load it from an environment variable instead.

Responses & errors

All responses are JSON. Successful calls return 2xx; client errors return 4xx with a JSON body describing the problem. Common status codes:

Status Meaning
200 Success
400 Bad request — check your parameters
401 Missing or invalid token
404 Resource not found
429 Rate limit exceeded — slow down and retry
500 Server error — retry or contact support

CloviReach API Reference

Service: clovireach — multi-channel lead engagement platform
Base URL: https://clovireach.com (port 8979, bound to 127.0.0.1, reverse-proxied via nginx)
Source: /root/clovireach/server.js + /root/clovireach/marketing_routes.js
Generated: 2026-06-16 — 50 routes documented


Auth Model

All application routes use session cookie auth. The requireAuth middleware checks for a valid JWT in any of: 1. cl_session cookie (CloviTek AI SSO / platform-wide) 2. reach_token cookie (CloviReach local session) 3. Authorization: Bearer <token> header

Token is a JWT signed with JWT_SECRET. A 401 is returned if no valid token is present.

No public developer API exists yet. All routes below are internal application routes serving the CloviReach web UI. A public REST API with API key auth is planned.


Rate Limits

Limiter Applied to Window Max
authLimiter /api/auth/* 15 min 10 req
campaignLimiter POST /api/campaigns 1 min 20 req
importLimiter POST /api/contacts/import 1 min 10 req
graderLimiter /api/grader/* 1 min 30 req
leadLimiter POST /api/lead 1 min 5 req
checkoutLimiter POST /api/v1/billing/checkout 5 min 10 req
webhookLimiter POST /api/v1/billing/webhook 15 min 120 req
apiLimiter All other /api/* 1 min 200 req

All rate limit responses: 429 Too Many Requests with Retry-After header.


Plan Limits (Early Access)

Plan Contacts Sends/Month Campaigns
free 500 1,000 3
starter 5,000 25,000 25
pro 50,000 250,000 250
business 500,000 2,000,000 unlimited

When a cap is reached, POST endpoints that enforce a quota return 402 with early_access: true (billing not yet live).


HTML Page Routes (Web App)

These routes serve static HTML files. Protected routes redirect unauthenticated users to /login.html.

Method Path Auth Page
GET / Public landing.html
GET /login, /login.html Public login.html
GET /dashboard, /dashboard.html Session cookie dashboard.html
GET /admin, /admin.html Session cookie admin.html
GET /demo, /demo.html Public demo.html
GET /crm-integration, /crm-integration.html Public crm-integration.html
GET /cold-email-grader, /cold-email-grader.html, /grader Public cold-email-grader.html
GET /features, /features.html Public features.html
GET /how-it-works, /how-it-works.html Public how-it-works.html
GET /compare, /compare.html, /vs Public compare.html
GET /pricing, /pricing.html Public pricing.html

Application Routes (JSON API)

Health


GET /api/health

GET /health

Returns service health. Returns 503 if the database is unreachable (used by PM2 + UptimeRobot).

Auth: None (public)
Parameters: None
Example:

curl https://clovireach.com/api/health
Response:
{ "status": "ok", "ok": true, "service": "clovireach-api", "port": 8979 }
Errors: 503 — database unavailable


Authentication


GET /api/auth/me

Returns the currently authenticated user's identity from the JWT.

Auth: Session cookie or Bearer token
Parameters: None
Example:

curl https://clovireach.com/api/auth/me \
  -H "Cookie: reach_token=<jwt>"
Response:
{
  "success": true,
  "data": { "id": 1, "email": "user@example.com", "name": "Jane Doe", "plan": "free", "role": "user" }
}
Errors: 401 Unauthorized


POST /api/auth/register

Creates a new local user account and issues a reach_token session cookie.

Auth: None (public)
Parameters (JSON body):

Field Type Required Notes
email string Yes Must be valid email format
password string Yes Minimum 8 characters
name string No Display name

Example:

curl -X POST https://clovireach.com/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"s3cureP4ss","name":"Jane Doe"}'
Response (201):
{ "success": true, "data": { "email": "user@example.com", "name": "Jane Doe" } }
Sets reach_token HttpOnly cookie (7-day, SameSite=Strict).

Errors: 400 bad email / password too short, 409 account already exists


POST /api/auth/login

Authenticates an existing user with email + password and issues a session cookie.

Auth: None (public)
Parameters (JSON body):

Field Type Required
email string Yes
password string Yes

Example:

curl -X POST https://clovireach.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"s3cureP4ss"}'
Response:
{ "success": true, "data": { "email": "user@example.com", "name": "Jane Doe" } }
Sets reach_token HttpOnly cookie (7-day, SameSite=Strict).

Errors: 401 invalid credentials


POST /api/auth/logout

Clears the reach_token session cookie.

Auth: None (public)
Parameters: None
Response:

{ "success": true }


POST /api/auth/forgot

Sends a password reset email. Always returns success to prevent account enumeration. Reset link is valid for 15 minutes and single-use.

Auth: None (public, rate-limited: 10/15min)
Parameters (JSON body):

Field Type Required
email string Yes

Response:

{ "success": true, "message": "If an account exists for that email, a reset link has been sent." }


POST /api/auth/reset

Completes a password reset using a token from the reset email.

Auth: None (public, rate-limited: 10/15min)
Parameters (JSON body):

Field Type Required Notes
token string Yes From reset email URL
password string Yes Minimum 8 characters

Response:

{ "success": true, "message": "Password updated. Please sign in." }
Errors: 400 invalid/expired token, 400 password too short


Contacts


GET /api/contacts

Returns all non-deleted contacts for the tenant, up to 1,000 records, ordered by most recent.

Auth: Session cookie or Bearer token
Parameters: None
Example:

curl https://clovireach.com/api/contacts \
  -H "Cookie: reach_token=<jwt>"
Response:
{
  "success": true,
  "data": [
    {
      "id": 1,
      "first_name": "Jane",
      "last_name": "Doe",
      "name": "Jane Doe",
      "email": "jane@example.com",
      "linkedin_url": "",
      "phone": "",
      "sms_opt_in": false,
      "company": "Acme Corp",
      "title": "VP Sales",
      "source": "manual",
      "enriched": false,
      "created_at": "2026-06-01T12:00:00Z"
    }
  ]
}
Errors: 401 Unauthorized


POST /api/contacts

Creates or upserts a single contact. Upserts on (tenant_id, lower(email)) when email is present.

Auth: Session cookie or Bearer token
Quota: Enforced — returns 402 when the plan's contact limit is reached.
Parameters (JSON body):

Field Type Required Notes
email string One of these three Stored lowercase
linkedin_url string One of these three
first_name string One of these three
last_name string No
phone string No Normalized via channels.normalizePhone
sms_opt_in boolean No TCPA opt-in flag
company string No
title string No
source string No Defaults to 'manual'

Example:

curl -X POST https://clovireach.com/api/contacts \
  -H "Content-Type: application/json" \
  -H "Cookie: reach_token=<jwt>" \
  -d '{"email":"jane@example.com","first_name":"Jane","company":"Acme"}'
Response (201):
{ "success": true, "data": { "id": 1, "email": "jane@example.com", "first_name": "Jane", ... } }
Errors: 400 missing identifier, 401, 402 plan limit


POST /api/contacts/import

Bulk-imports contacts from a CSV string. Triggers async Clodura enrichment for LinkedIn-only rows (fire-and-forget, does not block response).

Auth: Session cookie or Bearer token (rate-limited: 10/min)
Content-Type: text/csv or text/plain (body is raw CSV), OR application/json with { "csv": "..." }
CSV columns recognized (case-insensitive): name, first_name, last_name, email, company, title, linkedin_url or linkedin
Example:

curl -X POST https://clovireach.com/api/contacts/import \
  -H "Content-Type: text/csv" \
  -H "Cookie: reach_token=<jwt>" \
  --data-binary $'first_name,last_name,email,company\nJane,Doe,jane@example.com,Acme'
Response:
{
  "success": true,
  "data": { "created": 1, "updated": 0, "skipped": 0, "total": 1, "errors": [] }
}
Errors: 400 no CSV / insufficient rows, 401


Lists


GET /api/lists

Returns all contact lists with their contact counts.

Auth: Session cookie or Bearer token
Response:

{
  "success": true,
  "data": [{ "id": 1, "name": "Q2 Prospects", "contact_count": 42, "created_at": "..." }]
}
Errors: 401


POST /api/lists

Creates a contact list, optionally pre-populated with contacts.

Auth: Session cookie or Bearer token
Parameters (JSON body):

Field Type Required Notes
name string Yes
contact_ids array of ints No Specific contact IDs to add
all_contacts boolean No Add all current tenant contacts

Response (201):

{ "success": true, "data": { "id": 1, "name": "Q2 Prospects" } }
Errors: 400 name missing, 401


POST /api/lists/:id/contacts

Adds contacts to an existing list by contact ID array.

Auth: Session cookie or Bearer token
Path params: id — list ID (integer)
Parameters (JSON body):

Field Type Required
contact_ids array of ints Yes

Response:

{ "success": true, "data": { "added": 3 } }
Errors: 401


Campaigns & Sequences


GET /api/campaigns

Returns all campaigns for the tenant, ordered newest first.

Auth: Session cookie or Bearer token
Response:

{
  "success": true,
  "data": [{ "id": 1, "name": "June Outreach", "list_id": 2, "status": "draft", "aimfox_campaign_id": null, "created_at": "..." }]
}
Errors: 401


POST /api/campaigns

Creates a campaign, optionally with sequence steps.

Auth: Session cookie or Bearer token (rate-limited: 20/min)
Quota: Enforced — returns 402 when the plan's campaign limit is reached.
Parameters (JSON body):

Field Type Required Notes
name string Yes
list_id int No Contact list to target
steps array No Sequence steps (see step schema below)

Step schema:

Field Type Notes
day int Day offset for this step (0 = launch day)
channel string email, sms, linkedin
subject string Email subject line
body string Message body; supports {{first_name}}, {{company}} tokens
action string Optional action label

Response (201):

{ "success": true, "data": { "id": 1, "name": "June Outreach", "status": "draft", ... } }
Errors: 400 name missing, 401, 402 plan limit


GET /api/campaigns/:id

Returns a campaign with its full sequence steps array.

Auth: Session cookie or Bearer token
Path params: id — campaign ID (integer)
Response:

{
  "success": true,
  "data": {
    "id": 1,
    "name": "June Outreach",
    "status": "draft",
    "steps": [
      { "id": 10, "day": 0, "channel": "email", "subject": "Quick question", "body": "Hi {{first_name}}...", "action": null, "order_idx": 0 }
    ]
  }
}
Errors: 401, 404


POST /api/campaigns/:id/steps

Replaces all sequence steps for a campaign. Deletes existing steps then inserts new ones.

Auth: Session cookie or Bearer token
Path params: id — campaign ID (integer)
Parameters (JSON body):

Field Type Required
steps array Yes — same step schema as POST /api/campaigns

Response:

{ "success": true, "data": { "saved": 3 } }
Errors: 400 steps not an array, 401, 404 campaign not found


POST /api/campaigns/:id/launch

Launches a campaign: queues sends for all contacts × first step, creates an Aimfox campaign for LinkedIn steps, then kicks the send worker. Only contacts without a prior reply receive sends.

Auth: Session cookie or Bearer token
Path params: id — campaign ID (integer)
Parameters: None
Quota: Monthly send cap enforced — returns 402 if launch would exceed it.
Response:

{
  "success": true,
  "data": {
    "status": "running",
    "queued": 47,
    "aimfox_campaign_id": "abc123",
    "aimfox_note": null
  }
}
Errors: 400 no steps / no contacts, 401, 402 monthly sends limit, 404


POST /api/campaigns/:id/pause

Pauses a running campaign (sets status to paused). Does not cancel already-queued sends.

Auth: Session cookie or Bearer token
Path params: id — campaign ID (integer)
Response:

{ "success": true }
Errors: 401


Tracking


GET /api/o/:token.gif

Open-pixel endpoint. Records an open event tied to the send identified by token, then returns a 1×1 transparent GIF. Embedded in outbound HTML emails as <img src="...">.

Auth: None (public — token is the access control)
Path params: token — 12-byte hex link token embedded in the email
Response: image/gif — 1×1 pixel (no-store, no-cache)


GET /api/r/:token

Click-tracking redirect. Records a click event then 302s to the destination URL. Links in outbound emails are wrapped to point here.

Auth: None (public — token is the access control)
Path params: token — 12-byte hex link token
Query params: u — destination URL (must start with https?://)
Response: 302 Found → destination URL
Errors: 400 missing or invalid destination URL


GET /api/sr/:token

Social post click-tracking redirect. Same as /api/r/:token but records a click metric on the social_post row rather than a campaign send event.

Auth: None (public — token is the access control)
Path params: token — 12-byte hex link token embedded in social post content
Query params: u — destination URL
Response: 302 Found → destination URL
Errors: 400 missing or invalid destination URL


Webhooks (Provider Callbacks)

These endpoints receive callbacks from outbound channel providers. They do not require user auth; the security is inherent in the provider's delivery (IP allowlisting or signature verification for Stripe).


POST /api/webhooks/emailit

Receives delivery, open, bounce, click, and reply events from the EmailIt email provider.

Auth: None (public — called by EmailIt)
Parameters (JSON body): Provider-specific event payload. The handler reads: - type or event — event type string (e.g. email.delivered, email.bounced, email.opened, reply) - message_id or id or data.id — message ID for correlation

On reply events: cancels queued sends for that contact and pushes contact to CloviCRM.
Response:

{ "success": true }


POST /api/webhooks/linkila

Receives click events from the Linkila link-tracking service.

Auth: None (public — called by Linkila)
Parameters (JSON body): Provider-specific. Reads token, ref, or metadata.token for correlation.
Response:

{ "success": true }


POST /api/webhooks/aimfox

Receives LinkedIn reply/message events from Aimfox. On reply events: cancels queued sends for the contact and pushes to CloviCRM.

Auth: None (public — called by Aimfox)
Parameters (JSON body): Provider-specific event payload. Reads: - type or event — must match reply|message|response - campaign_id or campaign.id — Aimfox campaign ID - lead_urn or profile_urn or lead.urn — LinkedIn profile URN

Response:

{ "success": true }


POST /api/webhooks/shortysms

Receives inbound SMS replies from ShortySMS or Twilio. Records an sms_inbound row and a reply event, handles STOP opt-out (TCPA), and pushes to CloviCRM.

Auth: None (public — called by ShortySMS/Twilio)
Parameters: Accepts both Twilio form fields (From, Body, To, MessageSid) and ShortySMS JSON (from, message, to, id).
Response: 200 JSON { "success": true } (Twilio-compatible)


SMS


POST /api/sms/test

Sends a single real SMS for testing. Defaults to the owner's own number (SHORTYSMS_PHONE_TO). For other numbers, the recipient must be an opted-in contact (TCPA). Records an ad-hoc send row for traceability.

Auth: Session cookie or Bearer token
Parameters (JSON body):

Field Type Required Notes
to string No Phone number; defaults to SHORTYSMS_PHONE_TO env var
body string No SMS body; max 800 chars; defaults to a test message
name string No Recipient name; defaults to 'Test'

Response:

{
  "success": true,
  "data": {
    "status": "sent",
    "provider": "shortysms",
    "msgId": "abc123",
    "send_id": 99,
    "to_masked": "1234***"
  }
}
Errors: 400 no recipient, 401, 403 recipient not opted-in, 502 provider error, 503 SMS not configured


GET /api/sms/status

Returns whether the SMS channel is configured and available.

Auth: Session cookie or Bearer token
Response:

{ "success": true, "data": { "configured": true, "provider": "shortysms" } }
Errors: 401


Stats & Analytics


GET /api/stats/overview

Returns high-level outreach KPIs aggregated across the tenant.

Auth: Session cookie or Bearer token
Response:

{
  "success": true,
  "data": {
    "active_campaigns": 3,
    "emails_sent": 1420,
    "sms_sent": 88,
    "replies": 47,
    "meetings": 5
  }
}
Errors: 401


GET /api/stats/funnel

Returns funnel stage counts for all-time sends.

Auth: Session cookie or Bearer token
Response:

{
  "success": true,
  "data": [
    { "stage": "Sent", "value": 1508 },
    { "stage": "Opened", "value": 312 },
    { "stage": "Clicked", "value": 88 },
    { "stage": "Replied", "value": 47 },
    { "stage": "Meetings", "value": 5 }
  ]
}
Errors: 401


GET /api/stats/channels

Returns per-channel breakdown of sent / opened / clicked / replied counts.

Auth: Session cookie or Bearer token
Response:

{
  "success": true,
  "data": [
    { "channel": "email", "sent": 1420, "opened": 310, "clicked": 85, "replied": 44 },
    { "channel": "linkedin", "sent": 88, "opened": 2, "clicked": 3, "replied": 3 }
  ]
}
Errors: 401


Content Generation (AI)


POST /api/content/generate

Generates outreach copy using the house LLM (Claude via ANTHROPIC_API_KEY). Returns 502 with an explanatory note if the API key is not configured.

Auth: Session cookie or Bearer token
Parameters (JSON body):

Field Type Required Notes
type string No Content type (e.g. email, linkedin_post, tweet, newsletter)
topic string No Subject/topic to write about
tone string No e.g. professional, casual
audience string No e.g. B2B decision makers

Response:

{ "success": true, "data": { "content": "Hi {{first_name}}, ...", "illustrative": false } }
Errors: 401, 502 AI unavailable


Cold-Email Grader (Public Lead Magnet)


POST /api/grader/analyze

Real deterministic rule-based email grader. No auth required, no email capture. Returns a score, letter grade, per-dimension scores, flags, and top-3 fix recommendations.

Auth: None (public, rate-limited: 30/min)
Parameters (JSON body):

Field Type Required Notes
subject string One or both required Max 500 chars
body string One or both required Max 20,000 chars

Example:

curl -X POST https://clovireach.com/api/grader/analyze \
  -H "Content-Type: application/json" \
  -d '{"subject":"Quick question","body":"Hi {{first_name}}, I noticed you lead the team at {{company}}..."}'
Response:
{
  "success": true,
  "data": {
    "overall": 82,
    "grade": "B",
    "subject": { "score": 90, "length": 14, "words": 2 },
    "body": { "score": 76, "words": 45, "links": 0, "sentences": 3 },
    "flags": [
      { "label": "No personalization token (e.g. {{first_name}}/{{company}})", "severity": "med" }
    ],
    "topFixes": [
      { "fix": "Add a personalization token...", "source": "rule", "impact": "high" }
    ],
    "method": "rule-based heuristics (deterministic)..."
  }
}
Errors: 400 no content provided


POST /api/grader/lead

Lead capture endpoint for the cold-email grader. Grades the email, attempts an AI rewrite via the house LLM, stores both in grader_leads, and returns the rewrite if available.

Auth: None (public, rate-limited: 30/min)
Parameters (JSON body):

Field Type Required Notes
email string Yes Lead's email address
subject string No Email to grade
body string No Email to grade
tone string No Rewrite tone; defaults to professional

Response (201):

{
  "success": true,
  "data": {
    "id": 5,
    "rewrite_status": "generated",
    "rewrite": "Subject: Quick question\n\nHi {{first_name}}...",
    "message": "Here is your AI rewrite. We've also saved it to email you a copy plus a sample multi-channel sequence."
  }
}
rewrite_status is one of generated, failed, pending.
Errors: 400 invalid email


CRM Push


POST /api/crm/push

Pushes a contact to CloviCRM as a lead or other kind. Accepts either an inline contact object or a contact_id referencing the local contacts table.

Auth: Session cookie or Bearer token
Parameters (JSON body):

Field Type Required Notes
contact_id int One or both Fetches from local contacts table
contact object One or both Inline contact object
kind string No Defaults to 'lead'
custom object No Extra metadata to pass through

Response:

{ "success": true, "data": { ... } }
Errors: 400 no contact supplied, 401


Connected Channel Accounts


GET /api/accounts

Returns connected outbound channel account status (email, LinkedIn, SMS, CRM). Reflects both database records and server-side API key availability.

Auth: Session cookie or Bearer token
Response:

{
  "success": true,
  "data": {
    "email":    { "status": "available", "provider": "emailit" },
    "linkedin": { "status": "available", "provider": "aimfox" },
    "sms":      { "status": "not_connected", "provider": "shortysms", "from": null },
    "crm":      { "status": "available", "provider": "clovicrm" }
  }
}
Errors: 401


POST /api/accounts/:channel/connect

Records a channel as connected, or updates an existing connection.

Auth: Session cookie or Bearer token
Path params: channel — channel name (e.g. email, linkedin, sms)
Parameters (JSON body):

Field Type Required
provider string No
ref_id string No

Response:

{ "success": true }
Errors: 401


Social / Viral Mode


GET /api/social/accounts

Returns connected social accounts and live Ocoya profile list. Reports honest channel availability based on configured API keys.

Auth: Session cookie or Bearer token
Response:

{
  "success": true,
  "data": {
    "accounts": [{ "id": 1, "channel": "linkedin", "provider": "ocoya", "label": "Vitaly - LinkedIn", "status": "connected" }],
    "channels": { "linkedin": { "configured": true }, "twitter": { "configured": false } },
    "ocoya_profiles": []
  }
}
Errors: 401


POST /api/social/accounts/:channel/connect

Records a social account connection (Ocoya workspace/profile).

Auth: Session cookie or Bearer token
Path params: channel — social channel (e.g. linkedin, twitter)
Parameters (JSON body):

Field Type Notes
provider string Defaults to 'ocoya'
label string Display label
ocoya_workspace_id string Overrides env default
ocoya_profile_id string Specific profile to use
ref_id string External reference

Response (201):

{ "success": true, "data": { "id": 1 } }
Errors: 401


POST /api/social/generate

Generates social post copy via the house LLM.

Auth: Session cookie or Bearer token
Parameters (JSON body):

Field Type Notes
type string tweettweet; anything else → linkedin_post
event or topic string Topic or event description
tone string Optional
audience string Optional

Response:

{ "success": true, "data": { "content": "Excited to share..." } }
Errors: 401, 502 AI unavailable


POST /api/social/posts

Creates a social post as draft, or publishes/schedules it via Ocoya. Links in content are automatically wrapped for click tracking. Honestly stays as draft on provider failure (no fake success).

Auth: Session cookie or Bearer token
Parameters (JSON body):

Field Type Notes
content string Required. Post text.
channels string or array Social channels (e.g. ["linkedin", "twitter"])
media_urls array Optional media attachment URLs
publish boolean or 'now' Publish immediately via Ocoya
scheduled_at ISO-8601 string Schedule for future publication
profile_ids array Ocoya profile IDs; falls back to DB records then env
campaign_id int Optional campaign association
event or source_event string Source event label
ocoya_workspace_id string Override workspace

Response (201):

{
  "success": true,
  "data": {
    "id": 10,
    "content": "...",
    "status": "published",
    "provider": "ocoya",
    "provider_post_id": "pg_abc123",
    "scheduled_at": null,
    "published_at": "2026-06-16T10:00:00Z"
  }
}
Errors: 400 no content, 401


GET /api/social/posts

Returns up to 200 social posts ordered by most recent.

Auth: Session cookie or Bearer token
Response:

{ "success": true, "data": [ { "id": 1, "content": "...", "status": "published", ... } ] }
Errors: 401


POST /api/social/posts/:id/publish

Publishes a draft post now (or schedules it) via Ocoya. Returns 502 honestly on provider error — never records fake success.

Auth: Session cookie or Bearer token
Path params: id — social post ID
Parameters (JSON body):

Field Type Notes
scheduled_at ISO-8601 string Schedule instead of publishing now
profile_ids array Override Ocoya profile IDs
ocoya_workspace_id string Override workspace

Response:

{ "success": true, "data": { "provider_post_id": "pg_abc123", "status": "published" } }
Errors: 401, 404, 502 provider error


GET /api/social/posts/:id/metrics

Returns per-post engagement: real tracked-link clicks from our own DB, plus provider analytics if available from Ocoya.

Auth: Session cookie or Bearer token
Path params: id — social post ID
Response:

{
  "success": true,
  "data": {
    "post_id": 10,
    "status": "published",
    "clicks": 14,
    "metrics": { "like": 22, "comment": 3 },
    "provider_engagement": null,
    "note": "No provider engagement available yet..."
  }
}
Errors: 401, 404


GET /api/social/overview

Returns aggregated social post KPIs (all-time, real data from DB).

Auth: Session cookie or Bearer token
Response:

{
  "success": true,
  "data": {
    "total_posts": 18,
    "published": 12,
    "scheduled": 2,
    "drafts": 4,
    "clicks": 88,
    "engagements": 105,
    "reach": 3200
  }
}
Errors: 401


GET /api/social/outcomes

Returns social metric events feed (up to 1,000 records), for CloviAnalytics integration.

Auth: Session cookie or Bearer token
Response:

{
  "success": true,
  "data": [
    { "id": 1, "post_id": 10, "type": "click", "value": 1, "source": "clovireach", "channels": ["linkedin"], "occurred_at": "..." }
  ]
}
Errors: 401


Plan & Quota


GET /api/plan

Returns the current user's plan, usage counts, and all plan limits. Billing is not yet live (early_access: true).

Auth: Session cookie or Bearer token
Response:

{
  "success": true,
  "data": {
    "plan": "free",
    "label": "Free",
    "early_access": true,
    "billing_live": false,
    "limits": { "contacts": 500, "sends_month": 1000, "campaigns": 3 },
    "usage": { "contacts": 42, "campaigns": 1, "sends_month": 88 },
    "all_plans": { "free": {...}, "starter": {...}, "pro": {...}, "business": {...} }
  }
}
Errors: 401


Per-User Third-Party Keys (AES-256-GCM encrypted)


POST /api/keys/:provider

Stores an encrypted API key for a third-party provider (e.g. a user's own Aimfox key). Encrypted with AES-256-GCM (CLOVIREACH_ENC_KEY). Never returned in plaintext.

Auth: Session cookie or Bearer token
Path params: provider — alphanumeric + underscore, 2–32 chars (e.g. aimfox, emailit)
Parameters (JSON body):

Field Type Required
key string Yes

Response:

{ "success": true, "data": { "provider": "aimfox", "stored": true } }
Errors: 400 invalid provider / missing key, 401


GET /api/keys

Lists which providers the user has a stored key for (presence only — values are never returned).

Auth: Session cookie or Bearer token
Response:

{
  "success": true,
  "data": [{ "provider": "aimfox", "configured": true, "updated_at": "..." }]
}
Errors: 401


DELETE /api/keys/:provider

Removes a stored provider key for the current user.

Auth: Session cookie or Bearer token
Path params: provider — provider name
Response:

{ "success": true }
Errors: 401


GDPR / Privacy


GET /api/gdpr/export

Downloads a full JSON export of all tenant data (contacts, campaigns, sends, events, social posts, SMS inbound). Triggers an audit log entry. Response is served as a downloadable JSON file.

Auth: Session cookie or Bearer token
Parameters: None
Response: Content-Disposition: attachment; filename="clovireach-export.json" — JSON object with all tenant tables
Errors: 401


DELETE /api/gdpr/account

Deletes the account and associated prospect data. Default is soft-delete with 90-day retention; ?purge=now performs an immediate hard cascade delete.

Auth: Session cookie or Bearer token
Query params: purge=now — immediate irreversible hard delete
Response:

{ "success": true, "message": "Account scheduled for deletion. Data retained 90 days for recovery, then permanently purged." }
Clears reach_token cookie.
Errors: 401


GET /api/gdpr/audit

Returns the last 500 audit log entries for the tenant (data access, mutations, logins, exports).

Auth: Session cookie or Bearer token
Response:

{
  "success": true,
  "data": [
    { "action": "data.access", "resource": "contacts", "detail": {"count": 42}, "ip": "...", "created_at": "..." }
  ]
}
Errors: 401


AI SDR


POST /api/sdr/draft-reply

Generates an AI draft reply to an inbound prospect message using the ms-voice-api (founder voice). Classifies intent, optionally splices a Lunacal booking link for warm replies, logs the draft to CloviCRM, and returns it for human review. Nothing is sent until approved.

Auth: Session cookie or Bearer token
Parameters (JSON body):

Field Type Required Notes
contact_id int Yes
reply_text string Yes Inbound message text
channel string No e.g. email, linkedin, sms
campaign_id int No Optional campaign context

Response:

{
  "success": true,
  "intent": "warm",
  "draft_reply": "Hey {{first_name}}, great to hear from you! Would Tuesday work for a quick 15-min call? [booking link]",
  "booking_link": "https://lunacal.ai/vitaly/15min",
  "sent": false,
  "note": "Draft generated. Review and send manually, or set auto_send=true per tenant to send automatically."
}
intent is one of: warm, cold, neutral, question.
Errors: 400 missing contact_id or reply_text, 401


Marketing Routes (from marketing_routes.js)


POST /api/lead

Public lead capture endpoint for the marketing site. Stores to site_leads table. Never echoes PII in response.

Auth: None (public, rate-limited: 5/min/IP)
Parameters (JSON body):

Field Type Required Notes
email string Yes
slug string No Platform identifier; defaults to 'clovireach'
name string No
source_page string No e.g. /pricing.html
consent boolean No GDPR/CAN-SPAM consent flag
utm_source string No Stored in payload JSON
utm_medium string No Stored in payload JSON
utm_campaign string No Stored in payload JSON
ref string No Stored in payload JSON

Response:

{ "success": true, "ok": true }
Errors: 400 invalid email


POST /api/v1/billing/checkout

Creates a CloviPay (Stripe) hosted checkout session with a 7-day trial. Returns a redirect URL. Returns 503 if CLOVIREACH_STRIPE_SECRET_KEY is not configured.

Auth: Session cookie or Bearer token (rate-limited: 10/5min/IP)
Parameters (JSON body):

Field Type Required Notes
plan string Yes lite, pro, or enterprise
cycle string No monthly (default) or annual

Response:

{ "success": true, "url": "https://checkout.stripe.com/...", "id": "cs_..." }
Errors: 400 invalid plan, 401, 404 user not found, 502 Stripe error, 503 billing not configured


POST /api/v1/billing/webhook

Stripe webhook receiver. Mounted with raw body parsing (before express.json). Verifies Stripe signature (stripe-signature header) and grants/revokes plan entitlements.

Auth: None — Stripe HMAC signature (CLOVIREACH_STRIPE_WEBHOOK_SECRET required)
Content-Type: application/json (raw bytes)
Handled events: - checkout.session.completed — sets user plan to trialing - customer.subscription.created / updated — sets plan + subscription status - customer.subscription.deleted — downgrades to free

Response:

{ "received": true }
Errors: 400 missing/invalid signature, 503 billing not configured


Notes

  • No public developer API exists. All routes above require either the cl_session SSO cookie or a reach_token local session. Bearer token is accepted but uses the same JWT format — there are no scoped API keys for third-party integrations yet.
  • Audit logging is recorded for all data access and mutations (1-year retention, auto-purged by nightly job).
  • CORS is strict-allowlist (clovireach.com, platform.clovitek.com, clovitek.com). No wildcard origin is permitted.
  • SMS sends are TCPA-gated: contacts must have sms_opt_in = true. STOP keyword replies automatically opt contacts out.
  • Worker internals: The send worker runs every 60 seconds. The sequence advancement worker runs every 10 minutes and advances contacts to their next step once the per-step day delay has elapsed.

Last updated: 2026-06-16