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.
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:
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:
{
"success": true,
"data": { "id": 1, "email": "user@example.com", "name": "Jane Doe", "plan": "free", "role": "user" }
}
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"}'
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"}'
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:
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:
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:
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:
{
"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"
}
]
}
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"}'
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'
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": "..." }]
}
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):
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:
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": "..." }]
}
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):
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 }
]
}
}
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:
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
}
}
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:
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:
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:
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:
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***"
}
}
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:
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
}
}
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 }
]
}
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 }
]
}
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:
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}}..."}'
{
"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)..."
}
}
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:
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" }
}
}
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:
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": []
}
}
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):
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 | tweet → tweet; anything else → linkedin_post |
event or topic |
string | Topic or event description |
tone |
string | Optional |
audience |
string | Optional |
Response:
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"
}
}
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:
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:
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..."
}
}
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
}
}
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": "..." }
]
}
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": {...} }
}
}
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:
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:
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:
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." }
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": "..." }
]
}
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:
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:
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:
Errors:400 missing/invalid signature, 503 billing not configured
Notes¶
- No public developer API exists. All routes above require either the
cl_sessionSSO cookie or areach_tokenlocal 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