VibeJoy API & Developer Docs
Control your entire outbound pipeline from your AI agent, CLI, or any REST client. Find leads by intent signal, enrich contacts, generate campaigns, and fire email sequences without touching the dashboard.
Quickstart
Get your first leads in under 5 minutes. Pick your preferred interface:
Connect VibeJoy to Claude Desktop or Cursor. Your agent can then find leads, enrich contacts, and run full campaigns through natural-language prompts.
Get your API key
Go to Settings → Developer and click Generate API Key. It starts with vj_.
Add to your global Cursor MCP config
{
"mcpServers": {
"vibejoy": {
"command": "node",
"args": ["/path/to/vibejoy/mcp/index.js"],
"env": {
"VIBEJOY_API_KEY": "vj_your_api_key_here",
"VIBEJOY_API_URL": "http://localhost:3001"
}
}
}
}For Claude Desktop on macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
For Claude Desktop on Windows: %APPDATA%\Claude\claude_desktop_config.json
Reload Cursor (or restart Claude Desktop)
In Cursor: Ctrl+Shift+P → Reload Window. The vibejoy tools will appear in your agent's tool list.
Prompt your agent
Try any of these to confirm it's working:
"What are my VibeJoy dashboard stats?"
get_dashboard_stats"What's my brand context?"
get_brand_context"Find 10 companies hiring SDRs in London"
find_leadsRun a Full Campaign
Three things required before a campaign sends — all fully automatable
1. Brand context — set once via MCP (setup_brand), CLI (vibejoy brand setup), or dashboard.
2. SendGrid — connect once via MCP (connect_sendgrid), CLI (vibejoy sendgrid connect), or dashboard.
3. Recipients + send date — pass allEnrichedLeads: true (or leadIds[]) to create_campaign or vibejoy campaign create --all-leads. No dashboard required.
All three steps work via MCP, CLI, and REST API. A single agent prompt or CLI command can run the full pipeline end to end.
Complete pipeline
Via MCP — full setup and campaign in one conversation
The agent handles everything. A fresh account goes from zero to live campaign without touching the dashboard:
"Check my VibeJoy setup — is everything ready to run campaigns?"
check_integrations→ shows brand context status, SendGrid status, LinkedIn status"Set up my brand: business is Acme CRM, we help SDR teams automate outreach, targeting VP Sales at 50-500 person B2B SaaS companies. Tone: Direct, goal: Book calls."
setup_brand→ brand context saved, AI now has your voice"Connect SendGrid with key SG.xxx, sending from 'Jane at Acme' <jane@acme.com>"
connect_sendgrid→ SendGrid connected, email delivery ready"Find 25 companies in London actively hiring SDRs"
find_leads→ 25 leads saved with websites and LinkedIn"Enrich those leads — I need decision-maker emails"
enrich_contacts→ verified emails, job titles, LinkedIn profiles"Write a campaign for sales leaders at companies scaling their SDR team. Goal: book a 20-minute call. Use all my enriched leads as recipients and activate it."
create_campaign→ 3-email sequence written, all enriched leads added, campaign activated — no dashboard needed"Show me the social content for that campaign so I can schedule it"
get_campaign_social→ LinkedIn post, DM sequence, Reddit post and reply templates"What are my campaign stats?"
get_campaignsget_dashboard_stats→ sends, opens, repliesDo it all in one prompt (after initial setup)
"Find 25 funded Series A startups, enrich their contacts, write a campaign to book demos, use all enriched leads as recipients, and activate it."
The agent chains find_leads → enrich_contacts → create_campaign (with allEnrichedLeads + activate) automatically. No dashboard, no manual steps.
Via CLI
# ── ONE-TIME SETUP ──────────────────────────────────────────────────────────
# Set up brand context (required before campaigns)
vibejoy brand setup \
--name "Acme CRM" \
--offer "We help SDR teams automate outreach and book more meetings" \
--icp "VP Sales at 50-500 person B2B SaaS companies" \
--tone "Direct" \
--goal "Book calls"
# Connect SendGrid (required before email delivery)
vibejoy sendgrid connect \
--key "SG.your_sendgrid_key" \
--from-name "Jane at Acme" \
--from-email "jane@acme.com"
# Verify everything is ready
vibejoy status
# ── REPEATABLE CAMPAIGN PIPELINE ─────────────────────────────────────────────
# Step 1: Find companies hiring for roles your SaaS automates
vibejoy find --signal hiring --query "RevOps Manager" --location "United Kingdom" --limit 25 --output leads.csv
# Step 2: Enrich — target only the decision-maker you want
vibejoy enrich --unenriched --titles "CEO,Founder,CTO"
# Or filter by seniority level (c_suite, founder, vp, director, manager, owner)
# vibejoy enrich --unenriched --seniority "c_suite,founder"
# Or enrich a specific batch, targeting CFOs only
# vibejoy enrich --file leads.csv --titles "CFO,Finance Director" --output enriched.csv
# Step 3: Create campaign, add all enriched leads, and activate in one command
vibejoy campaign create \
--audience "RevOps managers scaling their CRM stack" \
--goal "book a 20-minute product demo" \
--all-leads \
--email1-date 2026-07-10 \
--activate
# Step 4: Get social content (LinkedIn + Reddit) to schedule via Zernio
vibejoy campaign social --id CAMPAIGN_ID --output social.txt
# Step 5: Monitor stats
vibejoy stats
vibejoy campaign listVia REST API (Node.js)
const BASE = 'http://localhost:3001';
const KEY = 'vj_your_api_key_here';
const headers = { 'Authorization': `Bearer ${KEY}`, 'Content-Type': 'application/json' };
// ── ONE-TIME SETUP ────────────────────────────────────────────────────────────
// Step 0a: Set up brand context (do this once — the AI uses it for all campaigns)
await fetch(`${BASE}/api/brand`, {
method: 'POST', headers,
body: JSON.stringify({
businessName: 'Acme CRM',
offer: 'We help SDR teams automate outreach and book more meetings',
icp: 'VP Sales at 50-500 person B2B SaaS companies',
tone: 'Direct', // Bold | Warm | Direct | Professional | Conversational
goal: 'Book calls' // Get leads | Get signups | Build awareness | Book calls
})
}).then(r => r.json());
// Step 0b: Connect SendGrid (do this once — required for email delivery)
await fetch(`${BASE}/api/integrations/sendgrid/connect`, {
method: 'POST', headers,
body: JSON.stringify({
apiKey: 'SG.your_sendgrid_api_key',
fromName: 'Jane at Acme',
fromEmail: 'jane@acme.com' // must be a verified sender in SendGrid
})
}).then(r => r.json());
// ── REPEATABLE CAMPAIGN PIPELINE ──────────────────────────────────────────────
// Step 1: Find hiring signal leads
const { leads } = await fetch(`${BASE}/api/leads/intent/jobs`, {
method: 'POST', headers,
body: JSON.stringify({ jobTitles: ['RevOps Manager'], location: 'United Kingdom', maxResults: 25 })
}).then(r => r.json());
console.log(`Found ${leads.length} leads`);
const leadIds = leads.map(l => l._id);
// Step 2: Enrich contacts (parallel, streams progress if you use /enrich-stream)
const { enriched, newLeadsCreated } = await fetch(`${BASE}/api/leads/enrich`, {
method: 'POST', headers,
body: JSON.stringify({ leadIds })
}).then(r => r.json());
console.log(`Enriched ${enriched} leads, ${newLeadsCreated} extra contacts found`);
// Step 3: Generate + add recipients + activate in one call
// Pass allEnrichedLeads: true to auto-add every enriched lead
// Pass activate: true to go live immediately (requires SendGrid connected)
const { campaignId, name, output, activated, recipientCount } = await fetch(`${BASE}/api/campaigns/generate`, {
method: 'POST', headers,
body: JSON.stringify({
targetAudience: 'RevOps managers scaling their tech stack',
campaignGoal: 'book a 20-minute product demo',
extraContext: 'Mention we integrate with their existing CRM',
allEnrichedLeads: true, // add all enriched leads
email1At: new Date(Date.now() + 24*60*60*1000).toISOString(), // send tomorrow
activate: true, // go live immediately
})
}).then(r => r.json());
console.log(`Campaign: ${name} (ID: ${campaignId})`);
console.log(`Recipients: ${recipientCount} | Activated: ${activated}`);
console.log(`Email 1 subject: ${output.emails?.email1?.subject}`);
// Step 5: Check stats any time
const stats = await fetch(`${BASE}/api/dashboard/stats`, { headers }).then(r => r.json());
console.log(`Total leads: ${stats.totalLeads} | Emails sent: ${stats.totalEmailsSent}`);Via REST API (Python)
import requests
BASE = "http://localhost:3001"
KEY = "vj_your_api_key_here"
HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
# ── ONE-TIME SETUP ────────────────────────────────────────────────────────────
# Set up brand context
requests.post(f"{BASE}/api/brand", headers=HEADERS, json={
"businessName": "Acme CRM",
"offer": "We help SDR teams automate outreach and book more meetings",
"icp": "VP Sales at 50-500 person B2B SaaS companies",
"tone": "Direct",
"goal": "Book calls"
})
# Connect SendGrid
requests.post(f"{BASE}/api/integrations/sendgrid/connect", headers=HEADERS, json={
"apiKey": "SG.your_sendgrid_api_key",
"fromName": "Jane at Acme",
"fromEmail": "jane@acme.com"
})
# ── REPEATABLE CAMPAIGN PIPELINE ──────────────────────────────────────────────
# Step 1: Find leads
r = requests.post(f"{BASE}/api/leads/intent/jobs",
headers=HEADERS,
json={"jobTitles": ["RevOps Manager"], "location": "United Kingdom", "maxResults": 25})
leads = r.json()["leads"]
lead_ids = [l["_id"] for l in leads]
print(f"Found {len(lead_ids)} leads")
# Step 2: Enrich
r = requests.post(f"{BASE}/api/leads/enrich", headers=HEADERS, json={"leadIds": lead_ids})
print(r.json()) # { enriched, newLeadsCreated, failed }
# Step 3: Generate + add all enriched leads + activate in one call
from datetime import datetime, timedelta
tomorrow = (datetime.utcnow() + timedelta(days=1)).isoformat() + "Z"
r = requests.post(f"{BASE}/api/campaigns/generate", headers=HEADERS, json={
"targetAudience": "RevOps managers scaling their tech stack",
"campaignGoal": "book a 20-minute product demo",
"allEnrichedLeads": True, # auto-add every enriched lead as recipient
"email1At": tomorrow, # send tomorrow
"activate": True # go live immediately (requires SendGrid)
})
data = r.json()
print(f"Campaign: {data['name']} | Recipients: {data['recipientCount']} | Live: {data['activated']}")Daily Lead Pipeline — Find CEOs, CFOs & Decision-Makers Automatically
Set VibeJoy to run on a schedule so it finds new leads every day, enriches only the ones it hasn't seen before, and drips them into a live campaign — all without you touching anything.
How it works — the daily loop
Duplicate detection is built in — if VibeJoy finds a company it already has, it skips it. Only genuinely new leads are enriched and added.
Option 1 — CLI + cron (simplest for developers)
Save this as a shell script and schedule it with cron (Linux/macOS) or Task Scheduler (Windows).
#!/bin/bash
# Daily VibeJoy pipeline — finds CEOs/CFOs at companies hiring for relevant roles,
# enriches new ones, and adds them to a live campaign.
# Schedule: run daily at 7am with cron: 0 7 * * * /path/to/vibejoy-daily.sh
set -e # stop on any error
echo "[$(date)] Starting daily VibeJoy pipeline..."
# ── Step 1: Find new companies (duplicates are skipped automatically) ──────────
vibejoy find \
--signal hiring \
--query "CFO OR Chief Financial Officer OR Finance Director" \
--location "United Kingdom" \
--limit 25
vibejoy find \
--signal hiring \
--query "CEO OR Chief Executive OR Managing Director" \
--location "United States" \
--limit 25
# ── Step 2: Enrich only leads not yet enriched, targeting CFOs/CEOs ────────────
vibejoy enrich \
--unenriched \
--titles "CEO,CFO,Chief Executive,Chief Financial Officer,Managing Director,Finance Director" \
--seniority "c_suite,founder"
# ── Step 3: Add new enriched leads to the existing live campaign ───────────────
# Replace CAMPAIGN_ID with your actual campaign ID (vibejoy campaign list)
vibejoy campaign activate \
--id CAMPAIGN_ID \
--all-leads
echo "[$(date)] Pipeline complete."Make it executable and schedule it:
# Make executable
chmod +x vibejoy-daily.sh
# Schedule via cron (Linux/macOS) — runs every day at 7am
crontab -e
# Add this line:
0 7 * * * /path/to/vibejoy-daily.sh >> /var/log/vibejoy.log 2>&1
# Windows Task Scheduler — create a Basic Task:
# Program: node
# Arguments: C:\path\to\vibejoy-daily.sh
# Trigger: Daily at 7:00 AMOption 2 — GitHub Actions (zero-infrastructure scheduling)
No server needed. GitHub runs the workflow on your schedule for free on public repos, and free minutes on private repos are generous.
name: Daily Lead Discovery
on:
schedule:
- cron: '0 7 * * 1-5' # 7am UTC, Monday to Friday
workflow_dispatch: # also allows manual trigger from GitHub UI
jobs:
discover:
runs-on: ubuntu-latest
steps:
- name: Install VibeJoy CLI
run: npm install -g vibejoy-cli # or: npm install -g .
- name: Authenticate
run: vibejoy auth --key ${{ secrets.VIBEJOY_API_KEY }}
env:
VIBEJOY_API_KEY: ${{ secrets.VIBEJOY_API_KEY }}
- name: Find CEOs at companies hiring senior roles
run: |
vibejoy find --signal hiring --query "CEO OR Chief Executive" --location "United Kingdom" --limit 20
vibejoy find --signal hiring --query "CFO OR Finance Director" --location "United States" --limit 20
- name: Enrich only new leads (CEOs and CFOs only)
run: |
vibejoy enrich --unenriched \
--titles "CEO,CFO,Chief Executive,Finance Director" \
--seniority "c_suite,founder"
- name: Add to campaign and re-activate
run: vibejoy campaign activate --id ${{ secrets.CAMPAIGN_ID }} --all-leads
env:
CAMPAIGN_ID: ${{ secrets.CAMPAIGN_ID }}GitHub Secrets to add
In your GitHub repo: Settings → Secrets → New repository secret
VIBEJOY_API_KEY— yourvj_key from SettingsCAMPAIGN_ID— the campaign ID to keep feeding (runvibejoy campaign listto find it)
Option 3 — REST API + any scheduler (cron-job.org, Zapier, n8n)
If you don't want to run code, use a free HTTP scheduler like cron-job.org to hit the API directly. Chain calls in order:
// Step 1: Find new companies hiring for senior roles (run daily)
// VibeJoy automatically skips companies it already has — safe to run every day
const findRes = await fetch('https://your-api.com/api/leads/intent/jobs', {
method: 'POST',
headers: { 'Authorization': 'Bearer vj_your_key', 'Content-Type': 'application/json' },
body: JSON.stringify({
jobTitles: ['CEO', 'CFO', 'Chief Financial Officer', 'Managing Director'],
location: 'United Kingdom',
maxResults: 25
})
}).then(r => r.json());
console.log(`Found ${findRes.count} new leads, skipped ${findRes.skippedDuplicates} duplicates`);
// Step 2: Enrich only unenriched leads — target CEO/CFO titles specifically
const enrichRes = await fetch('https://your-api.com/api/leads/enrich', {
method: 'POST',
headers: { 'Authorization': 'Bearer vj_your_key', 'Content-Type': 'application/json' },
body: JSON.stringify({
unenrichedOnly: true,
titles: ['CEO', 'CFO', 'Chief Executive Officer', 'Chief Financial Officer', 'Managing Director', 'Finance Director'],
seniority: ['c_suite', 'founder']
})
}).then(r => r.json());
console.log(`Enriched ${enrichRes.enriched} contacts`);
// Step 3: Add all enriched leads to a live campaign (safe to re-run — no duplicates added)
const updateRes = await fetch('https://your-api.com/api/campaigns/CAMPAIGN_ID', {
method: 'PATCH',
headers: { 'Authorization': 'Bearer vj_your_key', 'Content-Type': 'application/json' },
body: JSON.stringify({ allEnrichedLeads: true })
}).then(r => r.json());
console.log(`Campaign now has ${updateRes.recipientCount} recipients`);Option 4 — MCP / AI Agent (conversational recurring prompt)
Paste this prompt in Claude or Cursor each morning, or schedule it with an agent runner:
Daily agent prompt — copy and run each morning
"Find 25 UK companies hiring CEOs or CFOs — skip any I already have"
find_leads→ 18 new leads, 7 skipped (already in your list)"Enrich the new unenriched leads — I only want CEO or CFO contacts"
enrich_contacts→ 14 contacts enriched with decision-maker emails"Add all enriched leads to my outreach campaign and give me the new recipient count"
create_campaignget_campaigns→ campaign updated, 47 total recipientsOption 5 — n8n workflow (full no-code automation)
Build this in n8n and schedule it with the Cron trigger node. Set to run Monday-Friday at 7am.
Schedule: 0 7 * * 1-5 (Mon-Fri 7am)POST /api/leads/intent/jobs · body: { jobTitles: ["CEO","CFO"], location: "UK", maxResults: 25 }{{ $json.count > 0 }} — stops if no new leads foundPOST /api/leads/enrich · body: { unenrichedOnly: true, titles: ["CEO","CFO"], seniority: ["c_suite"] }PATCH /api/campaigns/CAMPAIGN_ID · body: { allEnrichedLeads: true }"Daily pipeline: {{ $node[2].json.count }} new leads, {{ $node[4].json.enriched }} enriched, {{ $node[5].json.recipientCount }} in campaign"Tips for a healthy daily pipeline
- Duplicate detection is automatic — running the same find query daily is safe. VibeJoy skips companies it already has and tells you how many were skipped (
skippedDuplicatesin the response). - Watch your monthly lead quota — check the usage page or run
vibejoy statsto see how many leads you've used this month. - Vary your queries — rotate job titles and locations week by week to reach different segments without exhausting one source.
- Use a "drip" campaign — keep one evergreen campaign active and keep adding enriched leads to it. Email 1 fires 24h after a lead is added, emails 2 and 3 follow automatically.
- Start small — 10-15 leads per day is a better starting point than 50. Quality over volume when warming up a sending domain.
SendGrid Setup
SendGrid delivers your campaign emails. You connect your own account so emails come from your domain with your sender reputation — not a shared VibeJoy pool. Setup takes about 5 minutes. You can connect multiple accounts (different domains or different clients) and choose which one each campaign sends from.
SendGrid free tier
The free plan includes 100 emails/day with no credit card required. That covers most early-stage outreach campaigns. Paid plans start at $19.95/month for higher volumes.
Step 1 — Create a SendGrid account
Go to sendgrid.com and sign up. No credit card needed for the free plan.
Step 2 — Verify your sender identity
This is the address that appears in the "From" field of every email. SendGrid will reject any send from an unverified address — this causes silent failures.
In SendGrid: go to Settings → Sender Authentication. Choose one of:
Single Sender Verification — quickest (2 min)
Click Verify a Single Sender, fill in your name, email, and company. SendGrid sends you a confirmation email. Click the link. Done. Best for getting started fast.
Domain Authentication — better deliverability (10 min)
Adds SPF + DKIM DNS records to your domain. Emails send from you@yourdomain.com with full authentication. Significantly improves inbox placement. Recommended if you have access to your DNS settings.
In SendGrid: Settings → Sender Authentication → Authenticate Your Domain. Follow the wizard — it generates the exact DNS records to add.
Important: If you skip sender verification, emails will silently fail to send. Always verify before connecting to VibeJoy.
Step 3 — Create a SendGrid API key
In SendGrid: Settings → API Keys → Create API Key.
Name it anything (e.g. "VibeJoy")
Choose Restricted Access
Enable Mail Send → Full Access
Enable Suppressions → Read Access (for managing unsubscribes)
Click Create & View and copy the key immediately — it starts with SG. and is shown only once
Step 4 — Connect to VibeJoy
Connect via dashboard, MCP, CLI, or REST API. All methods are equivalent.
Via Dashboard (easiest)
Go to Settings → Integrations → SendGrid. Fill in the form:
SG. key you just copiedjane@acme.comVia MCP (ask your agent)
"Connect SendGrid with key SG.your_key, sending from 'Jane at Acme' <jane@acme.com>"
connect_sendgrid→ validates key, saves account, email delivery readyVia CLI
vibejoy sendgrid connect \
--key "SG.your_sendgrid_api_key" \
--from-name "Jane at Acme" \
--from-email "jane@acme.com"
# Verify it worked
vibejoy status
# ✅ SendGrid email — connected, sending from jane@acme.comVia REST API
curl -X POST https://your-api.com/api/integrations/sendgrid/connect \
-H "Authorization: Bearer vj_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"apiKey": "SG.your_sendgrid_api_key",
"fromName": "Jane at Acme",
"fromEmail": "jane@acme.com"
}'Multiple SendGrid accounts (multi-domain or agency use)
You can connect multiple SendGrid accounts — useful if you have different sending domains (e.g. outreach@acme.com and team@acme.io) or if you manage campaigns for different clients.
Adding a second account (Dashboard)
Go to Settings → Integrations → SendGrid and click + Add account. Fill in the label, API key, from name, and from email for the new domain. Each account can have its own API key.
Adding accounts via REST API
# List all connected accounts
curl https://your-api.com/api/integrations/sendgrid/accounts \
-H "Authorization: Bearer vj_your_api_key"
# → { accounts: [{ _id, label, fromName, fromEmail, isDefault }, ...] }
# Add a second account (different domain)
curl -X POST https://your-api.com/api/integrations/sendgrid/accounts \
-H "Authorization: Bearer vj_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"label": "EU domain",
"apiKey": "SG.another_key",
"fromName": "Acme Europe",
"fromEmail": "hello@acme.eu"
}'
# Remove an account
curl -X DELETE https://your-api.com/api/integrations/sendgrid/accounts/ACCOUNT_ID \
-H "Authorization: Bearer vj_your_api_key"
# Set a different default sender
curl -X PATCH https://your-api.com/api/integrations/sendgrid/accounts/ACCOUNT_ID/default \
-H "Authorization: Bearer vj_your_api_key"Selecting sender per campaign
When creating a campaign in the dashboard, a Send from dropdown appears on the schedule step if you have more than one account — pick which one to use. Via REST API, pass sendgridAccountId in the campaign PATCH:
curl -X PATCH https://your-api.com/api/campaigns/CAMPAIGN_ID \
-H "Authorization: Bearer vj_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"sendgridAccountId": "ACCOUNT_ID"
}'
# Campaigns default to your primary/default account if not specifiedDeliverability tips
- Use domain authentication rather than single sender if possible — it significantly improves inbox placement
- Start with 20-50 emails per day and ramp up gradually to warm your domain
- Each sending domain (different
fromEmail) should be separately verified in SendGrid - Never send to unverified or purchased lists — this damages your sender reputation
- Add an unsubscribe link to stay CAN-SPAM and GDPR compliant — VibeJoy includes this automatically
Authentication
All API requests require an API key. Generate one from Settings → Developer.
Pass your key in the Authorization header on every request:
Authorization: Bearer vj_your_api_key_hereKeep your key secret
Never commit it to source control. Store it in an environment variable. If it's leaked, regenerate immediately from Settings.
Limits by plan
| Plan | API calls/month | MCP | CLI | Webhooks | SSE streaming |
|---|---|---|---|---|---|
| Free | 100 | — | — | 0 | — |
| Starter | 2,000 | ✓ | ✓ | 2 | ✓ |
| Growth | 10,000 | ✓ | ✓ | 10 | ✓ |
| Pro | Unlimited | ✓ | ✓ | Unlimited | ✓ |
MCP Server
The VibeJoy MCP server exposes your full lead engine as tools that any MCP-compatible AI can call — Claude Desktop, Cursor Agent, or any app that speaks the Model Context Protocol.
Setup
Cursor (global config — recommended)
{
"mcpServers": {
"vibejoy": {
"command": "node",
"args": ["/absolute/path/to/vibejoy/mcp/index.js"],
"env": {
"VIBEJOY_API_KEY": "vj_your_api_key_here",
"VIBEJOY_API_URL": "http://localhost:3001"
}
}
}
}Reload the Cursor window (Ctrl+Shift+P → Reload Window) after saving.
Claude Desktop (macOS)
{
"mcpServers": {
"vibejoy": {
"command": "node",
"args": ["/absolute/path/to/vibejoy/mcp/index.js"],
"env": {
"VIBEJOY_API_KEY": "vj_your_api_key_here",
"VIBEJOY_API_URL": "http://localhost:3001"
}
}
}
}Claude Desktop (Windows)
{
"mcpServers": {
"vibejoy": {
"command": "node",
"args": ["C:\\path\\to\\vibejoy\\mcp\\index.js"],
"env": {
"VIBEJOY_API_KEY": "vj_your_api_key_here",
"VIBEJOY_API_URL": "http://localhost:3001"
}
}
}
}Available tools
check_integrationsCheck your setup status: whether brand context is configured, SendGrid is connected, and LinkedIn is linked. Run this first to confirm everything is ready before creating a campaign.
All(none)
setup_brandSet your brand context — business name, offer, ICP, tone, and campaign goal. The AI uses this as its voice for every campaign. Must be completed before campaign generation.
AllbusinessName: string offer: string icp: string (ideal customer profile) tone: "Bold" | "Warm" | "Direct" | "Professional" | "Conversational" goal: "Get leads" | "Get signups" | "Build awareness" | "Book calls"
connect_sendgridConnect a SendGrid account for email delivery. Required before campaigns can send emails. The sender email must be verified in your SendGrid account. You can call this tool multiple times to add multiple accounts (different domains or clients) — they all appear in Settings and can be selected per campaign.
AllapiKey: string (starts with SG.) fromName: string (display name in From field) fromEmail: string (verified sender address) label?: string (e.g. "Sales domain" or "Client A" — helps identify the account)
find_leadsFind leads by intent signal type. Returns leads saved to your database with _id fields.
Startersource: "local" | "job_board" | "reddit" | "funding" | "linkedin_post" query: string (keyword, job title, pain phrase, funding stage) location?: string (city/country — for local and job_board) limit?: number (default 25, max 100)
enrich_contactsEnrich leads with verified emails, job titles, LinkedIn profiles, and phone numbers. Returns counts of enriched leads and any new contacts discovered at the same company.
StarterleadIds: string[] — array of _id values returned by find_leads
create_campaignGenerate an AI-written email sequence in your brand voice. Optionally activate immediately for SendGrid delivery.
StartertargetAudience: string — who the campaign targets campaignGoal: string — desired outcome (e.g. "book a demo") extraContext?: string — any extra context for the AI activate?: boolean — true to activate immediately (default false)
get_campaignsList all your campaigns with status, email sequences, reply rate, and delivery stats. No parameters required.
All(none)
get_dashboard_statsFetch this month's usage: total leads, enriched leads, campaigns created, emails sent, LinkedIn sent. No parameters required.
All(none)
get_brand_contextRead your current brand context: business name, offer, ideal customer profile, tone, and campaign goal. Used as the AI's voice for all campaign generation.
All(none)
Example agent conversation
This is a real transcript of what happens when you talk to Cursor Agent with VibeJoy connected:
"What's my brand context?"
get_brand_context→ shows your business name, ICP, offer, tone"Find 20 SaaS companies that just raised a Series A"
find_leads (source: funding, query: series-a)→ 20 leads saved with websites"Enrich them — I need decision-maker emails"
enrich_contacts→ verified emails + titles via B2B database"Write a 3-email campaign targeting their new CTO/VP Eng. Goal is a product demo."
create_campaign→ AI writes in your brand voice"Looks good. Activate it."
create_campaign (activate: true)→ queued in SendGrid, starts sending"How many emails have we sent this month?"
get_dashboard_stats→ full usage breakdownAI Agents
Beyond MCP, you can use VibeJoy's REST API directly from any AI agent framework — LangChain, LlamaIndex, OpenAI function calling, CrewAI, or your own custom agent loop.
Cursor Agent mode
With the MCP configured, Cursor's Agent mode can run your entire outbound pipeline autonomously. Just describe what you want:
Cursor Agent prompt
"Every Monday morning, find 30 companies that are actively hiring for the role my SaaS automates. Enrich them. Check if we already have a campaign targeting this audience — if not, write a new one and activate it. Then update a Notion table with this week's campaign stats."
→ Agent calls: get_campaigns → find_leads → enrich_contacts → create_campaign → (Notion MCP)
OpenAI function calling
import OpenAI from 'openai';
import fetch from 'node-fetch';
const openai = new OpenAI();
const VJ_KEY = process.env.VIBEJOY_API_KEY;
const VJ_BASE = 'http://localhost:3001';
const h = { Authorization: `Bearer ${VJ_KEY}`, 'Content-Type': 'application/json' };
const tools = [
{
type: 'function',
function: {
name: 'find_leads',
description: 'Find leads by intent signal',
parameters: {
type: 'object',
properties: {
source: { type: 'string', enum: ['job_board','reddit','funding','linkedin_post','local'] },
query: { type: 'string' },
location: { type: 'string' },
limit: { type: 'number' }
},
required: ['source','query']
}
}
},
{
type: 'function',
function: {
name: 'enrich_contacts',
description: 'Enrich leads with verified emails, job titles, and LinkedIn. Filter by titles or seniority.',
parameters: {
type: 'object',
properties: {
leadIds: { type: 'array', items: { type: 'string' } },
titles: { type: 'array', items: { type: 'string' }, description: 'Job title keywords e.g. ["CEO","Founder"]' },
seniority: { type: 'array', items: { type: 'string' }, description: 'Seniority levels e.g. ["c_suite","vp"]' }
},
required: ['leadIds']
}
}
}
];
async function callVibeJoy(name, args) {
if (name === 'find_leads') {
const res = await fetch(`${VJ_BASE}/api/leads/intent/jobs`, {
method: 'POST', headers: h,
body: JSON.stringify({ jobTitles: [args.query], location: args.location, maxResults: args.limit || 25 })
});
return res.json();
}
if (name === 'enrich_contacts') {
const res = await fetch(`${VJ_BASE}/api/leads/enrich`, {
method: 'POST', headers: h,
body: JSON.stringify({ leadIds: args.leadIds, titles: args.titles || [], seniority: args.seniority || [] })
});
return res.json();
}
}
// Agent loop
let messages = [{ role: 'user', content: 'Find 10 companies hiring sales ops in London, then enrich them.' }];
while (true) {
const res = await openai.chat.completions.create({ model: 'gpt-4o', tools, messages });
const msg = res.choices[0].message;
messages.push(msg);
if (res.choices[0].finish_reason === 'stop') {
console.log(msg.content);
break;
}
for (const call of msg.tool_calls || []) {
const result = await callVibeJoy(call.function.name, JSON.parse(call.function.arguments));
messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) });
}
}LangChain agent
from langchain.agents import create_openai_functions_agent, AgentExecutor
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
import requests, os
BASE = "http://localhost:3001"
H = {"Authorization": f"Bearer {os.environ['VIBEJOY_API_KEY']}", "Content-Type": "application/json"}
@tool
def find_leads(source: str, query: str, location: str = "", limit: int = 25) -> dict:
"""Find leads by intent signal. source: job_board | reddit | funding | linkedin_post | local"""
r = requests.post(f"{BASE}/api/leads/intent/jobs", headers=H,
json={"jobTitles": [query], "location": location, "maxResults": limit})
return r.json()
@tool
def enrich_contacts(lead_ids: list[str]) -> dict:
"""Enrich leads with verified email, title, and LinkedIn"""
r = requests.post(f"{BASE}/api/leads/enrich", headers=H, json={"leadIds": lead_ids})
return r.json()
@tool
def get_dashboard_stats() -> dict:
"""Get this month's usage stats"""
return requests.get(f"{BASE}/api/dashboard/stats", headers=H).json()
llm = ChatOpenAI(model="gpt-4o")
agent = create_openai_functions_agent(llm, [find_leads, enrich_contacts, get_dashboard_stats])
executor = AgentExecutor(agent=agent, tools=[find_leads, enrich_contacts, get_dashboard_stats], verbose=True)
executor.invoke({"input": "Find 20 companies hiring RevOps managers in the UK and enrich them"})Real-time enrichment progress (SSE)
Use the streaming endpoint to get live progress events during enrichment — useful in agent UIs, dashboards, or long-running scripts.
// Node.js: stream enrichment progress
const response = await fetch('http://localhost:3001/api/leads/enrich-stream', {
method: 'POST',
headers: { 'Authorization': 'Bearer vj_your_key', 'Content-Type': 'application/json' },
body: JSON.stringify({ leadIds: [...] })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).split('\n')) {
if (!line.startsWith('data: ')) continue;
const evt = JSON.parse(line.slice(6));
if (evt.type === 'progress') {
console.log(`[${evt.enriched + evt.failed}/${evt.total}] ${evt.leadName}`);
}
if (evt.type === 'done') {
console.log(`Done: ${evt.enriched} enriched, ${evt.newLeadsCreated} new contacts, ${evt.failed} not found`);
}
}
}CLI
The VibeJoy CLI lets you run lead discovery from your terminal, pipe output into other tools, and integrate VibeJoy into scripts and CI workflows.
Install & authenticate
npm install -g vibejoy
vibejoy auth --key vj_your_api_key_here
vibejoy auth --show # verify stored keyCommands
vibejoy findFind intent-signal leads. Flags: --signal (hiring|reddit|funding|linkedin|local), --query, --location, --limit, --output (csv path)
Startervibejoy enrichEnrich leads. --unenriched enriches every lead not yet enriched. --titles "CEO,CTO" targets specific job title keywords. --seniority "c_suite,founder" filters by seniority level. Also accepts --file (csv), --ids, --output (csv).
Startervibejoy campaign createCreate an AI campaign from a leads CSV. Flags: --leads, --name, --audience, --goal, --activate
Startervibejoy campaign listList all campaigns with status, sends, and reply rate
Allvibejoy campaign activateActivate a draft campaign. Flags: --id
Startervibejoy campaign pausePause an active campaign. Flags: --id
Startervibejoy statsPrint this month's usage stats: leads, enriched, campaigns, sends, replies
Allvibejoy leads listList saved leads. Flags: --enriched (true|false), --limit, --output
Allvibejoy authManage your API key. Flags: --key, --show, --revoke
AllFull pipeline examples
# --- Hiring signal pipeline ---
# Find companies hiring for the role your SaaS automates
vibejoy find --signal hiring --query "Revenue Operations Manager" --location "United Kingdom" --limit 50 --output /tmp/leads.csv
# Enrich all leads not yet enriched (simplest — no file needed)
vibejoy enrich --unenriched
# Or enrich a specific CSV file
# vibejoy enrich --file /tmp/leads.csv --output /tmp/enriched.csv
# Generate and activate campaign with all enriched leads
vibejoy campaign create \
--audience "RevOps managers scaling their CRM stack" \
--goal "book a 20-minute product demo" \
--all-leads \
--email1-date 2026-07-15 \
--activate
# --- Pain signal pipeline ---
# Find people on Reddit expressing your customer's pain
vibejoy find --signal reddit --query "cold email not getting replies" --limit 30 --output /tmp/pain.csv
# Enrich all unenriched leads
vibejoy enrich --unenriched
# Create and activate campaign
vibejoy campaign create \
--audience "founders frustrated with cold email deliverability" \
--goal "start a conversation about outbound consistency" \
--all-leads \
--email1-date 2026-07-15 \
--activate
# --- Check results ---
vibejoy stats
vibejoy campaign listCron / CI integration
# .github/workflows/weekly-leads.yml
name: Weekly lead run
on:
schedule:
- cron: '0 8 * * 1' # Monday 8am UTC
jobs:
find-and-enrich:
runs-on: ubuntu-latest
steps:
- run: npm install -g vibejoy
- run: vibejoy auth --key ${VIBEJOY_API_KEY}
env:
VIBEJOY_API_KEY: ${{ secrets.VIBEJOY_API_KEY }}
- run: |
EMAIL_DATE=$(date -u -d "+2 days" +%Y-%m-%d 2>/dev/null || date -u -v+2d +%Y-%m-%d)
vibejoy find --signal hiring --query "SDR" --limit 50 --output leads.csv
vibejoy enrich --unenriched
vibejoy campaign create \
--audience "Sales managers scaling outbound without growing headcount" \
--goal "start a conversation about pipeline efficiency" \
--all-leads \
--email1-date "$EMAIL_DATE" \
--activate
- run: vibejoy statsREST API
Base URL: http://localhost:3001 (self-hosted) or https://api.vibejoy.io (cloud)
All endpoints accept and return JSON. Authenticate with Authorization: Bearer vj_...
Setup (run once)
/api/brandSet brand context — required before campaign generation
{ "businessName": "...", "offer": "...", "icp": "...", "tone": "Direct", "goal": "Book calls" }
/api/brandRead current brand context
/api/integrations/sendgrid/connectConnect primary SendGrid account (legacy — single account)
{ "apiKey": "SG.xxx", "fromName": "Jane at Acme", "fromEmail": "jane@acme.com" }
/api/integrations/sendgrid/disconnectDisconnect primary SendGrid account
/api/integrations/sendgrid/accountsList all connected SendGrid accounts (primary + additional)
/api/integrations/sendgrid/accountsAdd an additional SendGrid account (different domain or client)
{ "apiKey": "SG.xxx", "fromName": "...", "fromEmail": "...", "label": "Sales domain" }
/api/integrations/sendgrid/accounts/:idRemove a SendGrid account by its ID
/api/integrations/sendgrid/accounts/:id/defaultSet a SendGrid account as the default sender
/api/integrations/zernio/connectConnect LinkedIn via your own Zernio account — requires your API key (sk_...) from Settings → API Keys, and Account ID from the Connections page, both at zernio.io
{ "apiKey": "sk_your_zernio_api_key", "accountId": "6a58876227846c0757346174" }
/api/integrations/zernio/disconnectDisconnect Zernio / LinkedIn scheduling
Lead discovery
/api/leads/scrapeLocal business search — find businesses by keyword and location
{ "keyword": "accountants", "location": "Edinburgh", "maxResults": 50 }
/api/leads/intent/jobsHiring signals — companies actively recruiting
{ "jobTitles": ["SDR", "RevOps"], "location": "London", "maxResults": 25 }
/api/leads/intent/redditPain signals — Reddit posts expressing your customer's problem
{ "keywords": "cold email deliverability", "subreddits": ["sales"], "maxResults": 25 }
/api/leads/intent/fundingFunding signals — recently funded startups with budget to spend
{ "stages": ["seed", "series-a"], "maxResults": 25 }
/api/leads/intent/linkedin-postsLinkedIn demand signals — posts about your category
{ "keywords": "outbound sales automation", "maxResults": 25 }
/api/leads/search-peopleDirect people search by title, company, industry, and location
{ "jobTitle": "VP Sales", "location": "London", "companySize": "11-50" }
Enrichment
/api/leads/enrichEnrich multiple leads in parallel (up to 6 concurrent). Returns when all complete.
{ "leadIds": ["id1", "id2"] }
/api/leads/enrich-streamSame as /enrich but streams Server-Sent Events. Each event: { type, enriched, failed, total, leadName }.
{ "leadIds": ["id1", "id2"] }
/api/leads/enrich-singleEnrich one lead, returns updated doc immediately.
{ "leadId": "id1" }
Leads management
/api/leadsPaginated list. Query params: page, limit, status, enriched (true|false)
/api/leads/:idUpdate editable fields on a lead
{ "website": "...", "email": "...", "contactName": "..." }
/api/leads/:idDelete a single lead
Campaigns
/api/campaigns/generateGenerate AI-written email sequence in your brand voice
{ "targetAudience": "...", "campaignGoal": "...", "extraContext": "..." }
/api/campaignsList all campaigns with status, email sequences, and delivery stats
/api/campaigns/:idSingle campaign — full emails, subject lines, and per-recipient delivery status
/api/campaigns/:id/activateActivate a draft — queues all emails in SendGrid
/api/campaigns/:id/pausePause an active campaign (stops further sends)
/api/campaigns/:idDelete a campaign
Account & stats
/api/dashboard/statsThis month's usage: leads, enriched, campaigns, emails sent, LinkedIn sent
/api/auth/meCurrent user: plan, brand context, integration status, API key
/api/auth/api-keyGenerate or regenerate your API key. Returns new vj_ key.
/api/auth/api-keyRevoke your API key immediately
/api/brandRead your brand context (name, offer, ICP, tone, goal)
Webhooks
VibeJoy can POST to your endpoint whenever a lead is found, enriched, or a campaign milestone is hit. Configure webhook URLs in Settings → Webhooks.
Available events
lead.foundA new intent-signal lead was discovered and saved
Starterlead.enrichedContact enrichment completed for a lead (email + title added)
Startercampaign.createdA new campaign was generated (emails + LinkedIn post ready)
Startercampaign.activatedA campaign was activated and emails are queued for sending
Startercampaign.email_sentAn individual email was delivered to a recipient
Growthcampaign.reply_receivedA reply to a campaign email was detected
GrowthPayload shape
{
"event": "lead.enriched",
"timestamp": "2026-07-08T09:00:00Z",
"data": {
"_id": "lead_abc123",
"businessName": "Acme Corp",
"contactName": "Jane Smith",
"contactTitle": "Head of Sales",
"email": "jane@acme.com",
"linkedinUrl": "https://linkedin.com/in/janesmith",
"website": "https://acme.com",
"intentSource": "job_board",
"intentSignal": "Hiring: Head of Sales at Acme Corp"
}
}Verifying signatures
Each request includes X-VibeJoy-Signature — an HMAC-SHA256 of the raw body using your webhook secret.
import crypto from 'crypto';
export function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// Express example
app.post('/webhook/vibejoy', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-vibejoy-signature'];
if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const { event, data } = JSON.parse(req.body);
console.log(event, data);
res.sendStatus(200);
});n8n & Make.com
Wire VibeJoy intent signals into your existing automation workflows using the HTTP Request node. No custom integration required.
Example n8n workflow
n8n HTTP Request node — find leads
POSThttp://localhost:3001/api/leads/intent/jobsHeader Auth → Authorization: Bearer vj_...{ "jobTitles": ["RevOps Manager"], "location": "London", "maxResults": 25 }n8n HTTP Request node — create campaign
POSThttp://localhost:3001/api/campaigns/generateHeader Auth → Authorization: Bearer vj_...{ "targetAudience": "RevOps managers", "campaignGoal": "book a demo" }Make.com (Integromat)
Use the HTTP module in Make.com with the same base URL and Bearer token. Map leads[] from the find response into any downstream module.
# Make.com HTTP module: find leads
URL: http://localhost:3001/api/leads/intent/jobs
Method: POST
Headers: Authorization = Bearer vj_your_key
Body type: Raw (JSON)
Body: {"jobTitles": ["SDR"], "location": "London", "maxResults": 25}
# Map leads[].businessName, leads[].website, leads[]._id
# to your CRM, Airtable, or Notion moduleZapier (via webhook)
Use Zapier's Webhooks by Zapier action with a POST request to any VibeJoy endpoint. Chain: Zap trigger → POST to /api/leads/intent/jobs → loop results → POST to your CRM.
Questions or issues?
Raise issues or ask questions via the dashboard. Docs are updated continuously.
Social Scheduling (LinkedIn + Reddit)
Every campaign VibeJoy generates includes ready-to-schedule social content. The AI writes conversation-starting copy — not pitches — designed to open genuine discussions with your ICP. You review it, then schedule it.
What gets generated for every campaign
LinkedIn post
150-200 words, brand voice, observation-led — no hard sell, max 3 hashtags
LinkedIn DM sequence
Connection request note + 3 follow-up DMs spaced over 2 weeks
Reddit original post
Value-first post for your ICP subreddit — adds insight, does not pitch
Reddit reply templates
2 ready-to-use replies for relevant threads in your subreddit
Part A — Get your social content
After creating a campaign, retrieve the social content via your preferred interface:
Dashboard
Go to Campaigns → open your campaign → Social tab. You'll see the LinkedIn post, DM sequence, Reddit post, and reply templates — each with a copy button.
MCP (ask your agent)
"Show me the social content for my latest campaign — the LinkedIn post, DMs, and Reddit copy"
get_campaignsget_campaign_social→ returns all social copy, ready to review and scheduleCLI
REST API
Part B — Connect Zernio for LinkedIn scheduling
Zernio is the service that handles LinkedIn post and DM scheduling from VibeJoy. You connect it once and it stays connected.
Zernio is optional. If you prefer to copy and paste content manually into LinkedIn or another scheduler, you can skip this. Social content is always available in the dashboard Social tab.
Create a Zernio account
Go to zernio.io and sign up for a free account.
Connect your LinkedIn profile in Zernio
Once logged in to Zernio:
Create your Zernio API key
VibeJoy uses your own Zernio API key to schedule on your behalf — so billing, rate limits, and LinkedIn authentication all stay within your account.
sk_and is shown only onceImportant: Copy it immediately — Zernio only shows the full key once. If you lose it, delete it and create a new one.
Copy your Zernio Account ID
This tells VibeJoy which LinkedIn profile to post from. Here is exactly where to find it:
Go to the Connections page in Zernio
Look for the long alphanumeric string next to your LinkedIn profile name — it looks like:
6a58876227846c0757346174Click the copy icon next to it to copy the full string — that is your Account ID
Tip: The Zernio dashboard URL often contains your Account ID — e.g.
zernio.io/dashboard/6a58876227846c0757346174. The ID in the URL is your Account ID.Paste both into VibeJoy Settings
Go to Settings → Integrations → LinkedIn via Zernio:
sk_...) into the first fieldDone. VibeJoy now schedules LinkedIn posts through your own Zernio account. You only do this once.
Part C — Schedule your content
LinkedIn post
Copy the LinkedIn post text from the campaign Social tab. In Zernio, create a new Post, paste the copy, and set a publish time.
Best times for B2B LinkedIn posts
Tuesday, Wednesday, Thursday — 8am to 10am your audience's timezone. Avoid weekends and Monday mornings.
LinkedIn DM sequence
The DM sequence has four parts. Send them manually or through a LinkedIn automation tool:
Connection request
Send with the connection invite — Max 280 characters — VibeJoy keeps it short and personal
Message 1 — value drop
2-3 days after they accept — Share a useful observation, no ask yet
Message 2 — soft ask
5-7 days after Message 1 — Gentle question or invitation to chat
Message 3 — graceful exit
10-14 days after Message 2 (no reply) — Close the loop warmly, leave the door open
For fully automated DM sequences, tools like Expandi, Waalaxy, or Dripify can send the full sequence automatically. Copy the four messages from VibeJoy into their sequence builder.
Reddit
Original post
The AI suggests a subreddit based on your ICP. Go to that subreddit, verify the rules allow discussion posts (most do), and submit. The post adds value first — it does not pitch your product directly.
Reply templates
Search the subreddit for threads matching the scenario in each reply template (e.g. "struggling with SDR ramp time"). When you find one, paste the reply and edit it to match the specific thread context. Never paste unedited — Reddit readers spot templates immediately.
Reddit self-promotion rules
Most subreddits allow 1 self-promotional post for every 9 community contributions (the 9:1 rule). VibeJoy's posts are written to be value-first and community-friendly, but always check the subreddit sidebar for their specific rules before posting.
End-to-end via MCP — one conversation
Once Zernio is connected, a single agent conversation handles everything from lead discovery to content retrieval:
"Find 20 companies hiring SDRs in London, enrich their contacts targeting VP Sales or Head of Sales, write a campaign, and activate it"
find_leadsenrich_contactscreate_campaign→ leads found, enriched, campaign written and activated"Show me the social content for that campaign so I can schedule it"
get_campaign_social→ LinkedIn post + DM sequence + Reddit post + reply templates"What are my dashboard stats — how many emails have been sent?"
get_dashboard_stats→ leads, campaigns, emails sent, opens