Developer Reference

Developer docs

Outbound as code. Signal queries, dry-run, vibejoy.yaml, cost ledger, reply triage, BYO sending. Drive it from Cursor/Claude (MCP), your terminal (CLI), or any REST client. Edit every line of copy before it sends.

MCP: Claude & Cursor
CLI
REST API
Config-as-code
Signal query DSL
n8n + Make.com

Quickstart

Get buyers found and a campaign live in under 5 minutes. Dry-run first if you want to preview volume without spending.

Fastest path for developers

1

Set up brand (URL or YAML)

Paste a site URL, or commit vibejoy.yaml with brand + signalQuery + mail provider.

2

Dry-run, then run

vibejoy query 'hiring AND funding' --dry-run then vibejoy run or vibejoy apply.

CLI

vibejoy setup url <url>vibejoy run --dry-runvibejoy apply

MCP (Agent)

signal_queryapply_configrun_pipeline

REST

POST /api/leads/signal-queryPOST /api/config/applyPOST /api/pipeline/run

Connect VibeJoy to Claude Desktop or Cursor. Your agent can then find leads, enrich contacts, and run full campaigns through natural-language prompts.

1

Get your API key

Go to Settings → Developer and click Generate API Key. It starts with vj_.

2

Add to your MCP config

Windows path tip: hold Shift and right-click vibejoy\mcp\index.js in Explorer → Copy as path. Then replace every \ with \\ in the JSON.

~/.cursor/mcp.json
{
  "mcpServers": {
    "vibejoy": {
      "command": "node",
      "args": ["C:\\Users\\Donna\\Desktop\\vibejoy\\mcp\\index.js"],
      "env": {
        "VIBEJOY_API_KEY": "vj_your_api_key_here",
        "VIBEJOY_API_URL": "https://api.vibejoy.io"
      }
    }
  }
}

For Claude Desktop on macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

For Claude Desktop on Windows: %APPDATA%\Claude\claude_desktop_config.json

3

Reload Cursor (or restart Claude Desktop)

In Cursor: Ctrl+Shift+P → Reload Window. The vibejoy tools will appear in your agent's tool list.

4

Run your first pipeline - one prompt does everything

Try these in order for fastest results:

"Set up my brand from my website: vibejoy.io"

setup_brand_from_url→ AI scrapes your site and fills brand + ICP

"Dry-run: hiring AND funding for my ICP"

signal_query→ preview count, nothing saved

"Apply my vibejoy.yaml and activate the pipeline"

apply_configrun_pipeline→ config applied, campaign live

"Show my credits, spend, and pricing"

get_costsget_pricing→ wallet + Free / $25 / Growth $249

Run 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

1. Setup brand
2. Connect SendGrid
3. Find leads
4. Enrich contacts
5. Create campaign
6. Activate emails
7. Schedule social
8. Monitor

Via the Dashboard

The dashboard walks you through campaign creation in 3 steps. Leads must be found and enriched first - the campaign builder pulls recipients directly from your enriched leads list.

Step 1: Find and enrich leadsLeads page

Go to Leads → click Find leads → choose a signal (hiring, funding, Reddit, LinkedIn, local) or run Auto-find. Intent leads on paid plans (Pay as you go or Growth) get an AI opener automatically. Click Enrich on any lead to get their verified email and contact details. You must enrich leads before they can be added to a campaign.

Step 2: Create the campaignCampaigns → New Campaign

Click New Campaign. Optionally override the target audience, goal, or add extra context for the AI - or leave blank to use your saved brand defaults. Click Generate Campaign. The AI writes a 3-email sequence, LinkedIn post, DM sequence, and ad headlines in your brand voice.

Step 3: Review and editStep 2 - Review & edit

Read through the generated emails, LinkedIn post, and DM sequence. Edit anything inline. Set the Email 1 send date - emails 2 and 3 are auto-scheduled 3 and 7 days later. The LinkedIn post date defaults to the same time as Email 1; clear it if you do not want it scheduled. Save as draft if you need more time.

Step 4: Add recipients from your leadsStep 3 - Schedule & send

Click "Import from leads →" to open the leads picker. This shows all your enriched leads with verified emails. Tick the ones you want to include and click Add. You can also add recipients manually (+ Add row) or paste from a CSV. If the list is empty, go back to the Leads page and enrich some leads first.

Step 5: ActivateStep 3 - Schedule & send

Check the Pre-launch checklist - all items must be green (brand set, recipients added, send date in the future, SendGrid connected). Click Activate campaign. Emails are scheduled and send automatically at the configured times - no external queue needed.

Leads must be enriched before they appear in the campaign builder

The "Import from leads" picker only shows leads with a verified email address. If the list is empty, go to Leads → Enrich first. Enrichment pulls the decision-maker's verified email and contact details - this is what makes personalised delivery possible.

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 and book more demos, targeting VP Sales at 50-500 person B2B SaaS companies. Tone: Direct, goal: Book demos."

setup_brand→ brand context saved, AI now writes for your exact ICP

"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 demo. Use all my enriched leads as recipients and activate it."

create_campaign→ 3-email sequence written, recipients added, campaign live - first replies incoming

"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, replies

Do 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. Your first replies come in before your next standup.

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 demos" \
  --icp "VP Sales at 50-500 person B2B SaaS companies" \
  --tone "Direct" \
  --goal "Book demos"

# 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 list

Via REST API (Node.js)

const BASE = 'https://api.vibejoy.io';
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 demos' // Book demos | Get signups | Build awareness | Book a call
  })
}).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} | AI openers: ${stats.openersGenerated} (${stats.openersCoverage}% coverage) | Emails sent: ${stats.totalEmailsSent}`);

Via REST API (Python)

import requests

BASE = "https://api.vibejoy.io"
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 demos"
})

# 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']}")

# Step 4: Check opener + send analytics
stats = requests.get(f"{BASE}/api/dashboard/stats", headers=HEADERS).json()
print(f"Leads: {stats['totalLeads']} | AI openers: {stats['openersGenerated']} ({stats['openersCoverage']}% coverage)")
print(f"Openers by signal: {stats.get('openersBySource', {})}")

AI Openers

An AI opener is a single sentence — under 20 words — that Claude writes for each individual lead based on the exact intent signal that found them. It opens the email. It is the difference between a reply and a delete.

The goal: make them think "this person gets exactly what I'm dealing with"

Signal: Hiring: SDR Manager

"Bringing on SDRs before the sequence is proven tends to multiply the noise, not the pipeline."

Signal: Funding: Series B announced

"The jump from Series A growth to Series B execution usually surfaces a very specific GTM problem."

Signal: Reddit: struggling with cold email open rates

"Hitting the wall where the list is fine but the angle just isn't landing is a specific kind of frustrating."

Signal: LinkedIn post: asking about hiring for growth

"The sequence that works at 20 reps usually needs a full rebuild by the time you hit 60."

Signal: GitHub: new devops repo pushed

"Teams that build the infra first and the ops playbook second usually hit the same gap around month three."

Signal: Contract: federal award $2M

"Winning a large government contract usually surfaces the vendor management overhead nobody planned for."

Why they work

  • Signal-specific, not generic. Each opener is written from the specific signal — the job title they're hiring for, the exact stage they raised, the specific thing they posted about. Not "I noticed you raised $X."
  • Never mentions the source. If someone vented on Reddit, the opener reflects that frustration without saying "I saw your Reddit post." It sounds like you thought of it yourself.
  • Peer tone, not sales tone. The system prompt instructs Claude: "sounds like a peer observation typed on a phone — not a sales line." No em dashes. No clichés. No quotes.
  • Under 20 words. Short enough to read in a second. Specific enough to stop the scroll.

Pay as you go and Growth only

AI openers are generated automatically for every intent-signal lead (job_board, funding, github, contracts, local_biz, yc, linkedin_post, reddit) on paid plans. The free plan does not include AI openers. Local / Maps discovery leads may not get openers as they have no intent signal text to write from.

How they work technically

  • Stored on the lead as personalizedOpener.
  • Generated immediately after lead discovery, in batches of 12, using Claude Sonnet.
  • Each signal type has its own prompt style — the hiring prompt focuses on the challenge that role reveals; the funding prompt acknowledges the milestone and pivots to a stage-specific problem; the Reddit/LinkedIn prompts reflect the frustration without citing the source.
  • You can edit any opener before the campaign sends — go to Leads, click a lead, edit the opener field.
  • Cost: $0.03 per opener (shown in your cost ledger as ai_opener).

Where to see them

Leads page

Each row shows the opener under Intent / opener. Click to read the full line + original signal.

Dashboard

Stat card: AI openers count, % of leads covered, and breakdown by signal source.

Usage page

All-time + monthly opener counts, coverage %, and openers by signal (hiring, funding, reddit, linkedin_post).

API

GET /api/dashboard/stats and GET /api/dashboard/usage return openersGenerated, openersCoverage, and openersBySource.

Stats API response

GET /api/dashboard/stats returns:

{
  "totalCampaigns": 4,
  "activeCampaigns": 1,
  "totalEmailsSent": 120,
  "linkedinPostsSent": 2,
  "totalLeads": 840,
  "enrichedLeads": 310,
  "openersGenerated": 210,
  "openersCoverage": 25,
  "openersBySource": {
    "job_board": 90,
    "funding": 55,
    "linkedin_post": 40,
    "reddit": 25
  }
}

GET /api/dashboard/usage?month=YYYY-MM includes the same opener fields under usage and allTime, plus limits.aiOpeners (whether your plan includes them).

What is not tracked yet

Reply rate or open rate broken down by opener vs no-opener is not available yet. Current analytics count how many openers were generated and by which signal - not send performance. Wire SendGrid engagement events if you need that later.

Editing openers

Click any opener on the Leads page to edit or write your own. Saved via PATCH /api/leads/:id with personalizedOpener. See Edit Copy for campaign emails, LinkedIn, Reddit, and reply drafts.

Edit Copy

AI drafts are a starting point. Edit everything before (or after) activation: campaign emails, LinkedIn posts/DMs, Reddit posts, ad headlines, lead openers, and triage reply drafts. Unsent messages pick up your edits; already-delivered messages are not resent.

Campaign create

Step 2 Review & edit: every email subject/body, LinkedIn, DMs, Reddit, ad headlines are editable before activate.

Campaign detail

Edit copy button on draft, scheduled, paused, or active campaigns. Save writes to PATCH /api/campaigns/:id { output }.

Leads

Click an opener (or Write opener) to edit personalizedOpener inline.

Reply triage

Dashboard: Edit reply, then Approve or Approve & send with your edited text.

MCP / API

update_campaign with output; PATCH /api/leads/:id; POST /api/replies/:id/approve { approvedReply }.

# Update campaign copy via API
PATCH /api/campaigns/:id
{
  "output": {
    "emails": {
      "email1": { "subject": "Quick question", "body": "…" }
    },
    "linkedinPost": "…"
  }
}

# Edit a lead opener
PATCH /api/leads/:id
{ "personalizedOpener": "Scaling eng usually means deploy friction hit the board deck." }

Filter Leads by Position

Once you have enriched leads, you can filter them by job title or position and add the filtered set directly to a new or existing campaign — without running the full pipeline again.

Three ways to filter and add

Dashboard

The "Filter leads by position" widget lets you type a role (e.g. "CMO", "VP Sales"), see matching enriched leads instantly, then create a new campaign or append them to an existing one.

Leads page

Use the "Filter by position…" input in the filter bar. All leads matching the role are shown. Select them and use "Add to campaign →" — a modal lets you pick new or existing campaign.

Campaign → Add leads

In any campaign's Recipients section, click "Add leads". Use the "Filter by role…" input inside the modal to narrow down enriched leads before selecting.

Adding to an existing campaign

When you add leads to an existing campaign that is already active or scheduled, the new recipients are appended — they receive the remaining emails in the sequence starting from the next unsent step. If the campaign is a draft, you can edit the copy before activating.

Via API

# Filter enriched leads by job title (server-side, paginated)
GET /api/leads?enriched=true&title=CMO&limit=100

# Append those lead IDs to an existing campaign
POST /api/campaigns/:id/add-leads
{ "leadIds": ["lead1", "lead2", ...] }

# Or create a brand-new campaign from a specific set of leads
POST /api/campaigns/generate
{
  "leadIds": ["lead1", "lead2"],
  "audience": "CMOs at B2B SaaS companies",
  "activate": true
}

Autopilot — Set & Forget

Configure once. VibeJoy runs the full outbound loop every hour, indefinitely — no manual intervention needed. The pipeline finds fresh hiring signals, enriches contacts, adds them to a running campaign, and emails them automatically.

What runs automatically

WhatFrequencyNotes
Job board scanEvery hourFinds companies actively hiring your target roles
Lead enrichmentEvery hourFinds decision-maker email via Apollo waterfall
Email 1Within minutes of enrichmentSent during 9am–7pm BST only
Email 23 days after Email 1Auto-scheduled per recipient
Email 37 days after Email 1Final soft close
Reply captureWithin 30 seconds of replyClassified + suggested response appears in dashboard

One-time setup checklist

  1. Set your brand in Settings → Brand or commit a vibejoy.yaml and run vibejoy apply
  2. Connect SendGrid (or SES) in Settings → Integrations — use a Restricted key with Mail Send permission
  3. Set up Reply tracking — add MX record and configure SendGrid Inbound Parse (instructions shown after connecting SendGrid)
  4. Apply your pipeline schedule via the vibejoy.yaml — or enable it from the API Docs page

vibejoy.yaml for full autopilot

vibejoy.yaml
version: 1

brand:
  businessName: "AppzJoy"
  offer: "Unlimited dev subscription — one flat monthly rate"
  icp: "SaaS founders hiring frontend or fullstack engineers"
  tone: Direct
  goal: "Get signups"

pipeline:
  skip: true   # schedule handles runs — don't fire one on apply

mail:
  provider: sendgrid

schedule:
  enabled: true
  mode: full_pipeline          # find → enrich → generate copy → send
  cadenceDays: 0.042           # hourly
  sources: [job_board]
  leadLimit: 7                 # 7 per run × 24 runs = ~168/day max
  dailyEmailCap: 500           # paid SendGrid plan
  targetRoles: [Founder, CTO, CEO, "Co-Founder", "VP Engineering"]

How the campaign works

VibeJoy maintains one active campaign per pipeline. New contacts found each hour are added as recipients to that campaign. Each new recipient gets the full 3-email sequence on its own schedule — Email 1 immediately, Email 2 after 3 days, Email 3 after 7 days. The campaign runs indefinitely — there is no end date unless you disable the schedule in your yaml.

Daily email cap prevents blasting. Sends are spread across business hours (9am–7pm BST). Roughly 7–17 new contacts are added per hourly run, so you send ~100–200 emails per day naturally.

Reply Triage Agent

Ingest inbound replies, classify them with Claude (interested, objection, question, OOO, unsubscribe…), get a suggested response, then approve or dismiss. Edit the suggested reply on the dashboard before approving. CLI and MCP expose the same workflow.

Automatic reply capture — SendGrid Inbound Parse

Prospect replies are delivered to your send.vibejoy.io mailbox via your existing email provider — but VibeJoy can only capture them automatically if they pass through SendGrid Inbound Parse. The setup below routes a copy of every reply through the webhook without touching your existing inbox.

1

Add MX record for a reply subdomain

In Cloudflare / GoDaddy / Namecheap — add this record. It creates reply.vibejoy.io without touching your existing send.vibejoy.io mailbox:

Type: MX  ·  Host: reply  ·  Value: mx.sendgrid.net  ·  Priority: 10
2

Configure SendGrid Inbound Parse

In SendGrid: Settings → Inbound Parse → Add Host & URL

Hostname: reply.vibejoy.io
URL: https://api.vibejoy.io/api/replies/inbound-parse?secret=YOUR_INBOUND_PARSE_SECRET
Spam check:   Raw MIME: off
3

Forward replies to the reply subdomain at your email provider

In your email provider (BillionMail / Zoho / Google Workspace), create a forward rule: all mail to contact@send.vibejoy.io is also forwarded to contact@reply.vibejoy.io. The copy hits SendGrid → the webhook fires → the reply appears in your dashboard. Your mailbox still receives the original.

Add env vars to your server

# In your backend .env
INBOUND_PARSE_SECRET=your-secret-token
INBOUND_REPLY_TO=reply@reply.vibejoy.io

The INBOUND_REPLY_TO address is added as the Reply-To header on every campaign email. Most email clients use Reply-To automatically; the forwarding rule above catches the minority that reply to the From address instead.

Get setup instructions via API

GET
/api/replies/inbound-parse/guide

Returns full Inbound Parse setup steps for your account including webhook URL and secret

Starter

REST

POST
/api/replies

Ingest reply (autoTriage: true by default)

{ "recipientEmail": "a@b.com", "body": "...", "subject": "Re: ..." }

GET
/api/replies

List replies (?status=new|triaged)

POST
/api/replies/:id/approve

Approve suggested (or edited) reply; send:true to deliver

{ "send": true }

POST
/api/replies/:id/dismiss

Dismiss reply

MCP tool: triage_replies with action list | ingest | approve | dismiss.

Dry-run Mode

Preview discovery volume without saving leads, creating campaigns, or sending email. Pass dryRun: true on the pipeline, signal query, or config apply endpoints.

# CLI
vibejoy run --dry-run --audience "Series A CTOs"
vibejoy query 'hiring AND funding' --dry-run
vibejoy apply --file vibejoy.yaml --dry-run

# API
POST /api/pipeline/run  { "dryRun": true, "sources": ["job_board"], "leadLimit": 50 }
POST /api/leads/signal-query  { "query": "hiring AND funding", "dryRun": true }

Config-as-code (vibejoy.yaml)

Commit outbound config next to your app. Apply brand, schedule, mail provider preference, signal query, and pipeline in one shot. Secrets (API keys) stay in Settings / env - never in YAML.

vibejoy.yaml
version: 1
brand:
  businessName: "Acme DevTools"
  offer: "Ship faster with fewer incidents"
  icp: "CTOs at Series A-C SaaS"
  tone: Direct
  goal: "Book calls"
pipeline:
  dryRun: false
  leadLimit: 50
  sources: [job_board, funding]
  titles: [CTO, Founder]
  activate: true
signalQuery: 'hiring AND (funding OR keyword:"devops")'
mail:
  provider: sendgrid   # or ses | postal
schedule:
  enabled: false
  targetCount: 100
  runAtHour: 8
vibejoy apply --file vibejoy.yaml --dry-run
vibejoy apply --file vibejoy.yaml

# API
POST /api/config/apply  { "yaml": "<file contents>", "dryRun": false }
GET  /api/config/schema

See vibejoy.example.yaml in the repo root. MCP: apply_config.

Custom Signal Query Language

Boolean combinators over intent atoms. Prefer this when you need intersections (e.g. hiring and recently funded) instead of OR-ing sources.

hiring

Companies hiring relevant eng/product roles

funding

Recently funded startups

linkedin

LinkedIn demand / posts

pain

Reddit / pain signals

maps

Google Maps company discovery

keyword:"..."

Google Search company discovery for a phrase

Operators: AND OR NOT and parentheses.

vibejoy query 'hiring AND (funding OR keyword:"platform eng")' --limit 40

# Or via pipeline
vibejoy run --query 'hiring AND funding' --dry-run

POST /api/leads/signal-query
{ "query": "hiring AND funding", "maxResults": 50 }

MCP: signal_query. Also pass signalQuery on run_pipeline.

Your costs & pricing

The ledger shows what you pay (credit debit), not VibeJoy infrastructure costs. Free trial usage is $0. Paid usage deducts from your wallet at published rates (~$0.05 per lead found).

OptionPriceCreditsNotes
Free$0100-lead trial
Pay as you go$25$25≈125 fully piped · ≈500 found · ≈50 campaigns
Growth$249/mo$320/mo≈1,600 fully piped leads / mo
vibejoy costs
vibejoy costs --month 2026-08
vibejoy pricing
vibejoy credits balance
vibejoy credits buy

GET /api/costs
GET /api/costs/entries
GET /api/costs/pricing
POST /api/paypal/credits   { "pack": "pack25" }

MCP: get_costs · get_pricing · buy_credits (returns PayPal URL). Dashboard: Buy $25 credits starts checkout immediately.

BYO Sending - SendGrid, AWS SES, Postal

VibeJoy is intelligence-first. You bring delivery: default SendGrid, or AWS SES / self-hosted Postal. Connect credentials in Settings (or via API). Campaign scheduler routes through your active provider.

GET
/api/integrations/mail

Current provider status

POST
/api/integrations/mail/provider

Set active provider

{ "provider": "ses" }

POST
/api/integrations/ses/connect

Connect AWS SES

{ "accessKeyId": "...", "secretAccessKey": "...", "region": "us-east-1", "fromEmail": "you@domain.com" }

POST
/api/integrations/postal/connect

Connect Postal

{ "apiUrl": "https://postal.yourdomain.com", "apiKey": "...", "fromEmail": "you@domain.com" }

SES requires @aws-sdk/client-sesv2 on the API server. Set mail.provider in vibejoy.yaml to prefer a provider (credentials still via Settings).

Daily Lead Pipeline - Wake Up to New Buyers Every Morning

Set VibeJoy on a schedule so it finds in-market buyers every day, enriches only the ones it hasn't seen before, and drips them into a live campaign - while you keep building. Replies arrive in your inbox without any manual prospecting.

How it works - the daily loop

Scheduler triggers
Find new companies
Skip duplicates (auto)
Enrich CEOs/CFOs only
Add to live campaign
Emails go out on schedule

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).

vibejoy-daily.sh
#!/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 * * * /home/yourname/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 * * * /home/yourname/vibejoy-daily.sh >> /var/log/vibejoy.log 2>&1

# Windows Task Scheduler - create a Basic Task:
# Program: node
# Arguments: C:\Users\YourName\Desktop\vibejoy-daily.sh
# Trigger: Daily at 7:00 AM

Option 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.

.github/workflows/daily-leads.yml
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 - your vj_ key from Settings
  • CAMPAIGN_ID - the campaign ID to keep feeding (run vibejoy campaign list to 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://api.vibejoy.io/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://api.vibejoy.io/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://api.vibejoy.io/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

Run my daily lead pipeline: 1. Find 25 companies in the UK actively hiring for CEO, CFO, or Finance Director roles - use the find_leads tool with signal: hiring 2. Find 25 more companies in the US hiring Managing Directors or Chief Executives 3. Enrich only the leads that haven't been enriched yet - target contacts with title CEO, CFO, or Managing Director, seniority c_suite 4. Add all newly enriched leads to campaign ID: YOUR_CAMPAIGN_ID (use create_campaign with allEnrichedLeads: true if the campaign doesn't exist yet, otherwise PATCH the existing one) 5. Tell me: how many new companies were found, how many were skipped as duplicates, how many contacts were enriched, and the total recipient count on the campaign.

"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"

update_campaignget_campaigns→ campaign updated, 47 total recipients

Option 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.

Cron (daily 7am)
HTTP: find leads (jobs)
HTTP: enrich (CEO/CFO)
HTTP: PATCH campaign
Slack: daily summary
1. Cron triggerSchedule: 0 7 * * 1-5 (Mon-Fri 7am)
2. HTTP Request - find leadsPOST /api/leads/intent/jobs · body: { jobTitles: ["CEO","CFO"], location: "UK", maxResults: 25 }
3. IF - skip if count = 0{{ $json.count > 0 }} - stops if no new leads found
4. HTTP Request - enrichPOST /api/leads/enrich · body: { unenrichedOnly: true, titles: ["CEO","CFO"], seniority: ["c_suite"] }
5. HTTP Request - update campaignPATCH /api/campaigns/CAMPAIGN_ID · body: { allEnrichedLeads: true }
6. Slack message"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 (skippedDuplicates in the response).
  • Watch your monthly lead quota - check the usage page or run vibejoy stats to 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:

A

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.

B

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:

Account labelA name for this account, e.g. "Main domain" or "Client A" - useful when you have multiple accounts
API keyPaste the SG. key you just copied
From nameWhat appears in the recipient's inbox - e.g. "Jane at Acme" or "Acme Team"
From emailMust match a verified sender in SendGrid - e.g. jane@acme.com

Via 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 ready

Via 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.com

Via REST API

curl -X POST https://api.vibejoy.io/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://api.vibejoy.io/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://api.vibejoy.io/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://api.vibejoy.io/api/integrations/sendgrid/accounts/ACCOUNT_ID \
  -H "Authorization: Bearer vj_your_api_key"

# Set a different default sender
curl -X PATCH https://api.vibejoy.io/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://api.vibejoy.io/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 specified

Deliverability 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

Social Scheduling (LinkedIn + Reddit)

Every campaign VibeJoy generates includes ready-to-schedule social content. The AI writes copy that makes your ICP feel understood - not pitched - so posts generate comments and DMs from potential buyers. 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 schedule

CLI

# Print to terminal
vibejoy campaign social --id CAMPAIGN_ID

# Or save to a file (recommended for long content)
vibejoy campaign social --id CAMPAIGN_ID --output social.txt

# Find your campaign ID first if needed
vibejoy campaign list

REST API

curl https://api.vibejoy.io/api/campaigns/CAMPAIGN_ID \
  -H "Authorization: Bearer vj_your_api_key"

# Response shape - campaign.output contains:
{
  "linkedinPost": "...",           // the LinkedIn post text
  "linkedinDMs": {
    "connectionRequest": "...",    // 280 char max - sent with the connection invite
    "dm1": "...",                  // send 2-3 days after they accept
    "dm2": "...",                  // send 5-7 days after dm1
    "dm3": "..."                   // send 10-14 days after dm2 (graceful exit if no reply)
  },
  "reddit": {
    "originalPost": {
      "subreddit": "r/sales",      // suggested subreddit
      "title": "...",
      "body": "..."
    },
    "replyTemplates": [
      { "scenario": "...", "body": "..." },
      { "scenario": "...", "body": "..." }
    ]
  }
}

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.

1

Create a Zernio account

Go to zernio.io and sign up for a free account.

2

Connect your LinkedIn profile in Zernio

Once logged in to Zernio:

  1. Click Connections in the left sidebar
  2. Find LinkedIn and click + Connect
  3. A LinkedIn OAuth popup appears - log in and grant access
  4. Your LinkedIn profile now appears in the Connections list with a green "Connected" status
3

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.

  1. In Zernio, go to Settings → API Keys
  2. Click Create API Key and give it a name (e.g. "VibeJoy")
  3. Copy the key - it starts with sk_ and is shown only once

Important: Copy it immediately - Zernio only shows the full key once. If you lose it, delete it and create a new one.

4

Copy your Zernio Account ID

This tells VibeJoy which LinkedIn profile to post from. Here is exactly where to find it:

1

Go to the Connections page in Zernio

2

Look for the long alphanumeric string next to your LinkedIn profile name - it looks like: 6a58876227846c0757346174

3

Click 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.

5

Paste both into VibeJoy Settings

Go to Settings → Integrations → LinkedIn via Zernio:

  1. Paste your Zernio API key (sk_...) into the first field
  2. Paste your Account ID into the second field
  3. Click Connect LinkedIn
  4. VibeJoy validates both and saves them encrypted - you'll see "Connected: Your Name"

Done. 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:

1

Connection request

Send with the connection invite - Max 280 characters - VibeJoy keeps it short and personal

2

Message 1 - value drop

2-3 days after they accept - Share a useful observation, no ask yet

3

Message 2 - soft ask

5-7 days after Message 1 - Gentle question or invitation to chat

4

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 - openers and emails sent?"

get_dashboard_stats→ leads, openersGenerated, openersCoverage, emails sent

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_here

Keep 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

PlanAPI calls/monthMCPCLIWebhooksSSE streaming
Free100--0-
Pay as you go50,00010
Growth50,00010

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)

Find your path in 2 seconds

Open File Explorer, navigate to the vibejoy\mcp folder, hold Shift and right-click index.js, then select Copy as path. Paste it into the args field - then replace every single \ with \\.

Example: if your Windows username is Donna the path is:C:\\Users\\Donna\\Desktop\\vibejoy\\mcp\\index.js

~/.cursor/mcp.json
{
  "mcpServers": {
    "vibejoy": {
      "command": "node",
      "args": ["C:\\Users\\Donna\\Desktop\\vibejoy\\mcp\\index.js"],
      "env": {
        "VIBEJOY_API_KEY": "vj_your_api_key_here",
        "VIBEJOY_API_URL": "https://api.vibejoy.io"
      }
    }
  }
}

After saving, press Ctrl+Shift+P → Reload Window in Cursor. The VibeJoy tools will appear in Agent mode.

Claude Desktop (macOS)

~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "vibejoy": {
      "command": "node",
      "args": ["/Users/yourname/vibejoy/mcp/index.js"],
      "env": {
        "VIBEJOY_API_KEY": "vj_your_api_key_here",
        "VIBEJOY_API_URL": "https://api.vibejoy.io"
      }
    }
  }
}

Claude Desktop (Windows)

File location: %APPDATA%\Claude\claude_desktop_config.json - paste this in your browser address bar to open the folder.

%APPDATA%\\Claude\\claude_desktop_config.json
{
  "mcpServers": {
    "vibejoy": {
      "command": "node",
      "args": ["C:\\Users\\Donna\\Desktop\\vibejoy\\mcp\\index.js"],
      "env": {
        "VIBEJOY_API_KEY": "vj_your_api_key_here",
        "VIBEJOY_API_URL": "https://api.vibejoy.io"
      }
    }
  }
}

Restart Claude Desktop after saving. VibeJoy tools appear automatically in Claude's tool list.

Available tools

run_pipeline

THE SIMPLEST TOOL. One call that does everything: finds in-market buyers, enriches their emails, writes AI copy, and activates the campaign. Use this instead of calling find_leads + enrich_contacts + create_campaign separately. Requires brand context and SendGrid connected.

Starter
targetAudience?: string - who to target (defaults to saved ICP)
sources?: string[] - ["job_board","funding","linkedin_post"] (default: job_board + funding)
leadLimit?: number - max leads per source query (default 30)
titles?: string[] - contact titles to enrich (default: CEO, Founder, CTO, VP Sales)
email1At?: string - ISO date for first email (default: tomorrow)
activate?: boolean - activate immediately (default: true)
dryRun?: boolean - plan only: nothing saved or sent
signalQuery?: string - boolean query e.g. hiring AND funding
setup_brand_from_url

Paste any URL - website, LinkedIn profile, Product Hunt, competitor site, or job listing. AI scrapes the page and auto-fills the entire brand profile. Fastest setup option.

All
url: string - any URL (with or without https://)
setup_brand_from_description

No website? Answer up to 3 plain-language questions and AI builds the brand profile. All fields optional - answer what you know.

All
what?: string - what you sell
who?: string - who you sell to
outcome?: string - the outcome customers get
check_integrations

Check 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_brand

Set 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.

All
businessName: string
offer: string
icp: string (ideal customer profile)
tone: "Bold" | "Warm" | "Direct" | "Professional" | "Conversational"
goal: "Book demos" | "Get signups" | "Build awareness" | "Book a call"
connect_sendgrid

Connect 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.

All
apiKey: 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_leads

Find in-market buyers by intent signal. Pass multiple queries in one call - either comma-separated in query ("SDR, RevOps Manager") or as a queries array - to search multiple job titles or keywords in parallel and get more results faster.

Starter
source: "local" | "job_board" | "reddit" | "funding" | "linkedin_post"
query: string - primary search query (for job_board, comma-separate multiple titles)
queries?: string[] - optional extra queries to run in parallel for the same source
location?: string - city/country (job_board and local)
limit?: number - max per query, default 50
find_leads_bulk

Find leads across multiple sources AND multiple queries in ONE parallel call. Use this instead of calling find_leads multiple times. Searches all specified sources simultaneously and returns combined, deduplicated results with a breakdown. The fastest way to build a large lead list.

Starter
searches: Array<{ source, queries: string[], location? }> - each entry is a source + array of queries to run in parallel
limit?: number - max leads per individual query, default 50
enrich_contacts

Enrich 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.

Starter
leadIds: string[] - array of _id values returned by find_leads
create_campaign

Generate an AI-written outreach sequence - 3 emails that move from door-opener to the yes-or-no, plus a LinkedIn post and Reddit content. The AI writes copy that makes prospects feel understood, which is what produces replies and books demo calls. Optionally activate immediately.

Starter
targetAudience: string - who the campaign targets (e.g. "fintech founders who just raised Series A")
campaignGoal: string - the outcome you want (e.g. "book a 15-minute demo call")
extraContext?: string - extra context to sharpen the copy
leadIds?: string[] - add specific leads as recipients
allEnrichedLeads?: boolean - add every enriched lead as a recipient
email1At?: string - ISO date for first email (defaults to tomorrow)
activate?: boolean - true to activate immediately (default false)
update_campaign

Keep building on an existing campaign - add more leads, rename it, reschedule send dates, or activate. Works on any non-completed campaign. Use this instead of create_campaign when you already have a campaign you want to grow.

Starter
campaignId: string - the campaign ID (use get_campaigns to find it)
name?: string - rename the campaign
leadIds?: string[] - add these lead IDs as new recipients (duplicates ignored)
allEnrichedLeads?: boolean - add every enriched lead as a recipient (duplicates ignored)
email1At?: string - reschedule when email 1 sends
activate?: boolean - activate after updating
pause?: boolean - pause a live campaign to stop further sends
get_campaigns

List all your campaigns with status, email sequences, reply rate, and delivery stats. No parameters required.

All
(none)
get_dashboard_stats

Fetch dashboard analytics: total leads, enriched leads, AI openers generated, opener coverage %, openers by signal source, campaigns, emails sent, LinkedIn sent. No parameters required.

All
(none)
get_brand_context

Read 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)
signal_query

Boolean signal expression over hiring, funding, linkedin, pain, maps, keyword:"…". Prefer over find_leads when you need AND/OR intersections.

Starter
query: string
maxResults?: number
dryRun?: boolean
location?: string
triage_replies

List, ingest, approve, or dismiss inbound replies. AI classifies and drafts a response.

Starter
action: "list" | "ingest" | "approve" | "dismiss"
body?, recipientEmail? (ingest)
replyId?, approvedReply?, send? (approve)
get_costs

Your spend and credit wallet — what you pay by operation. Includes Free / $25 PAYG / Growth $249 pricing.

All
month?: string (YYYY-MM)
get_pricing

Published plans and per-action rates (lead $0.05, enrich $0.12, etc.). Use for “how much does it cost?”

All
(none)
buy_credits

Start PayPal checkout for a $25 credit pack. Returns a URL the user must open to pay.

All
(none)
apply_config

Apply vibejoy.yaml (config-as-code). Pass YAML string; dryRun defaults true for safety.

Paid
yaml: string
dryRun?: boolean

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 100 companies: hiring SDRs or RevOps managers in the UK, AND that just raised Series A - do it in one search"

find_leads_bulk→ runs all searches in parallel, returns 100+ deduplicated leads in one shot

"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)→ scheduled, sends automatically at the configured times

"How many AI openers and emails have we sent?"

get_dashboard_stats→ leads, openersGenerated, openersCoverage, openersBySource, emails sent

"Show my credits and how pricing works"

get_costsget_pricing→ wallet + Free / $25 PAYG / Growth $249 + rates

"I'm low on credits — buy me another $25 pack"

buy_credits→ PayPal checkout URL to open

"Dry-run: hiring AND funding, then show my cost ledger"

signal_queryget_costs→ preview count + your $ spend + credits left

"List replies waiting for triage"

triage_replies→ classified inbox with suggested replies

Continuing an existing campaign

You never have to start over. Use update_campaign to keep adding leads to a campaign you already created. The agent automatically calls get_campaigns to look up the campaign ID - just refer to it by name and the agent does the rest. You can also find campaign IDs in the Campaigns page on your dashboard.

"Find 15 more funded startups in fintech"

find_leads (source: funding)→ 15 new leads added to your database

"Enrich them - senior contacts only"

enrich_contacts (seniority: c_suite, founder)→ 12 contacts enriched

"Add them to my July campaign"

get_campaignsupdate_campaign→ agent looks up the campaign by name, adds 12 recipients, total now 59

"Reschedule that campaign - send the first email next Monday at 9am"

update_campaign (email1At: ...)→ email 1 rescheduled, follow-ups auto-adjusted

"Actually, pause that campaign - I need to review the copy first"

update_campaign (pause: true)→ campaign paused, no further sends

Editing send dates from the dashboard

Open any campaign → Delivery timeline → Edit dates. You can update the LinkedIn post date and Email 1 date at any time, even after a campaign has been activated. Emails 2 and 3 are auto-recalculated (+3 and +7 days from Email 1). Hit Save and the cron scheduler picks up the new times on its next tick (within 60 seconds).

3 ways to find a campaign ID

  • Ask the agent - just say "my July campaign" and it calls get_campaigns automatically to look it up
  • Dashboard - open the Campaigns page, click any campaign, and the ID is shown in the URL and campaign detail
  • CLI - run vibejoy campaign list to print all campaigns with their IDs

AI 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 = 'https://api.vibejoy.io';
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 = "https://api.vibejoy.io"
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 analytics: leads, AI openers, coverage %, openers by signal, emails sent"""
  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('https://api.vibejoy.io/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 key

Commands

vibejoy find

Find intent-signal leads. Flags: --signal (hiring|reddit|funding|linkedin|local), --query, --location, --limit, --output (csv path)

Starter
vibejoy enrich

Enrich 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).

Starter
vibejoy campaign create

Create an AI campaign from a leads CSV. Flags: --leads, --name, --audience, --goal, --activate

Starter
vibejoy campaign update

Continue working on an existing draft - add leads, rename, reschedule, or activate. Flags: --id (required), --ids (comma-separated lead IDs), --all-enriched (add every enriched lead), --name (rename), --send-date (reschedule email 1), --activate

Starter
vibejoy campaign list

List all campaigns with status, sends, and reply rate

All
vibejoy campaign activate

Activate a draft campaign. Flags: --id

Starter
vibejoy campaign pause

Pause an active campaign to stop further sends. Flags: --id

Starter
vibejoy stats

Print analytics: leads, enriched, AI openers (+ coverage %), campaigns, emails sent

All
vibejoy run

Full pipeline. Flags: --audience, --sources, --limit, --titles, --send-at, --draft, --dry-run, --query (signal expression)

Starter
vibejoy apply

Config-as-code. Flags: --file vibejoy.yaml, --dry-run

Starter
vibejoy query

Boolean signal query. Args: <expr>. Flags: --limit, --dry-run, --location

Starter
vibejoy replies list

List triage inbox. Flags: --status

Starter
vibejoy replies ingest

Ingest + AI triage. Flags: --email, --body, --subject

Starter
vibejoy replies approve

Approve suggested reply. Flags: --send, --text

Starter
vibejoy costs

Your spend + credit wallet + pricing summary. Flags: --month YYYY-MM

All
vibejoy pricing

Plans (Free / $25 PAYG / Growth $249) and per-action rates

All
vibejoy credits balance

Show credit wallet balance

All
vibejoy credits buy

Buy a $25 credit pack (PayPal checkout URL)

All
vibejoy leads list

List saved leads. Flags: --enriched (true|false), --limit, --output

All
vibejoy auth

Manage your API key. Flags: --key, --show, --revoke

All

Full 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

# --- Continue an existing campaign (most common use case) ---
# First, find your campaign ID - 3 ways:
#   1. Dashboard → Campaigns page (shown in URL and campaign detail)
#   2. vibejoy campaign list  (prints all campaigns + IDs)
#   3. Ask your AI agent - it calls get_campaigns automatically

vibejoy campaign list
# Example output:
# ─────────────────────────────────────────────────────
# Name                        Status     Recipients  Created
# ─────────────────────────────────────────────────────
# August hiring push          draft      23          2026-07-20
# July RevOps campaign        scheduled  47          2026-07-15
# ─────────────────────────────────────────────────────

# Next day: find more leads and add them to the SAME campaign
vibejoy find --signal hiring --query "Revenue Operations Manager" --location "London" --limit 20
vibejoy enrich --unenriched

# Add new enriched leads to the existing campaign (no new campaign needed)
vibejoy campaign update --id CAMPAIGN_ID --all-enriched

# Or add specific leads by ID
vibejoy campaign update --id CAMPAIGN_ID --ids lead_id_1,lead_id_2,lead_id_3

# Rename and reschedule at the same time
vibejoy campaign update --id CAMPAIGN_ID --name "RevOps UK - Wave 2" --send-date 2026-08-01T09:00:00Z

# Update and activate in one step
vibejoy campaign update --id CAMPAIGN_ID --all-enriched --activate

# Pause a live campaign (stop further sends)
vibejoy campaign pause --id CAMPAIGN_ID

# --- 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 list

Cron / 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 stats

REST API

Base URL: https://api.vibejoy.io - all endpoints accept and return JSON. Authenticate with Authorization: Bearer vj_...

Setup (run once)

POST
/api/brand

Set brand context - required before campaign generation

{ "businessName": "...", "offer": "...", "icp": "...", "tone": "Direct", "goal": "Book demos" }

All
GET
/api/brand

Read current brand context

All
POST
/api/integrations/sendgrid/connect

Connect primary SendGrid account (legacy - single account)

{ "apiKey": "SG.xxx", "fromName": "Jane at Acme", "fromEmail": "jane@acme.com" }

All
DELETE
/api/integrations/sendgrid/disconnect

Disconnect primary SendGrid account

All
GET
/api/integrations/sendgrid/accounts

List all connected SendGrid accounts (primary + additional)

All
POST
/api/integrations/sendgrid/accounts

Add an additional SendGrid account (different domain or client)

{ "apiKey": "SG.xxx", "fromName": "...", "fromEmail": "...", "label": "Sales domain" }

All
DELETE
/api/integrations/sendgrid/accounts/:id

Remove a SendGrid account by its ID

All
PATCH
/api/integrations/sendgrid/accounts/:id/default

Set a SendGrid account as the default sender

All
POST
/api/integrations/zernio/connect

Connect 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" }

Growth
DELETE
/api/integrations/zernio/disconnect

Disconnect Zernio / LinkedIn scheduling

Growth

Lead discovery

POST
/api/leads/scrape

Local business search - find businesses by keyword and location (Google Maps via Apify)

{ "keyword": "software company", "location": "Austin", "maxResults": 50 }

All
POST
/api/leads/intent/jobs

Hiring signals - companies actively recruiting. Paid plans auto-generate AI openers. Pass multiple job titles for broader results.

{ "jobTitles": ["Software Engineer", "Engineering Manager"], "location": "London", "maxResults": 50 }

Paid
POST
/api/leads/intent/reddit

Pain signals - Reddit/Quora posts expressing your customer's problem. Paid plans auto-generate AI openers.

{ "keywords": "CI pipeline failing", "subreddits": ["devops"], "maxResults": 50 }

Paid
POST
/api/leads/intent/funding

Funding signals - recently funded startups (SEC EDGAR + tech news). Growth+ auto-generates AI openers.

{ "stages": ["seed", "series-a"], "maxResults": 50 }

Growth
POST
/api/leads/intent/linkedin-posts

LinkedIn demand signals - posts about your category. Growth+ auto-generates AI openers.

{ "keywords": "looking for developer tools", "maxResults": 50 }

Growth
POST
/api/leads/auto-find

ICP-driven multi-source find: intent signals + Maps + Google Search company discovery (companies with websites for enrich-by-domain). Optional AI openers on intent leads.

{ "targetCount": 500, "sources": ["hiring", "pain", "linkedin", "funding", "bulk"] }

Starter
POST
/api/leads/search-people

Direct people search by title, company, industry, and location

{ "jobTitle": "CTO", "location": "London", "companySize": "11-50" }

Starter
POST
/api/leads/fill-websites

Batch-resolve missing company websites (Clearbit + Google Search) so contacts can be enriched by domain

{ }

All
POST
/api/leads/signal-query

Boolean signal query (hiring AND funding, keyword:"devops", etc.)

{ "query": "hiring AND funding", "maxResults": 50, "dryRun": false }

Starter
POST
/api/pipeline/run

Full pipeline with dryRun + signalQuery support

{ "sources": ["job_board"], "leadLimit": 30, "dryRun": false }

Starter
POST
/api/config/apply

Apply vibejoy.yaml config-as-code

{ "yaml": "version: 1\\n...", "dryRun": true }

Starter
GET
/api/costs

Your spend + credit wallet + pricing summary (?month=YYYY-MM)

All
GET
/api/costs/pricing

Plans: Free, Pay as you go $25, Growth $249/mo — plus per-action rates

All
GET
/api/replies

Reply triage inbox

Starter
POST
/api/replies

Ingest inbound reply for AI triage

{ "recipientEmail": "a@b.com", "body": "..." }

Starter
GET
/api/integrations/mail

Mail provider status (sendgrid|ses|postal)

All

Enrichment

POST
/api/leads/enrich

Enrich multiple leads in parallel (up to 6 concurrent). Returns when all complete.

{ "leadIds": ["id1", "id2"] }

Starter
POST
/api/leads/enrich-stream

Same as /enrich but streams Server-Sent Events. Each event: { type, enriched, failed, total, leadName }.

{ "leadIds": ["id1", "id2"] }

Starter
POST
/api/leads/enrich-single

Enrich one lead, returns updated doc immediately.

{ "leadId": "id1" }

Starter

Leads management

GET
/api/leads

Paginated list. Query params: page, limit, status, enriched (true|false)

All
PATCH
/api/leads/:id

Update editable fields on a lead

{ "website": "...", "email": "...", "contactName": "..." }

All
DELETE
/api/leads/:id

Delete a single lead

All

Campaigns

POST
/api/campaigns/generate

Generate AI-written email sequence in your brand voice

{ "targetAudience": "...", "campaignGoal": "...", "extraContext": "..." }

Starter
GET
/api/campaigns

List all campaigns with status, email sequences, and delivery stats

All
GET
/api/campaigns/:id

Single campaign - full emails, subject lines, and per-recipient delivery status

All
POST
/api/campaigns/:id/activate

Activate a draft - schedules all emails to send automatically at the configured times

Starter
POST
/api/campaigns/:id/pause

Pause an active campaign (stops further sends)

Starter
DELETE
/api/campaigns/:id

Delete a campaign

All

Account & stats

GET
/api/dashboard/stats

Analytics: totalLeads, enrichedLeads, openersGenerated, openersCoverage, openersBySource, campaigns, emails sent, LinkedIn sent

All
GET
/api/dashboard/usage

Monthly usage + all-time totals including AI openers. Query: ?month=YYYY-MM. Returns usage.openersGenerated, openersBySource, openersCoverage, limits.aiOpeners

All
GET
/api/auth/me

Current user: plan, brand context, integration status, API key

All
POST
/api/auth/api-key

Generate or regenerate your API key. Returns new vj_ key.

All
DELETE
/api/auth/api-key

Revoke your API key immediately

All
GET
/api/brand

Read your brand context (name, offer, ICP, tone, goal)

All

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.found

A new intent-signal lead was discovered and saved

Starter
lead.enriched

Contact enrichment completed for a lead (email + title added)

Starter
campaign.created

A new campaign was generated (emails + LinkedIn post ready)

Starter
campaign.activated

A campaign was activated and emails are scheduled to send automatically

Starter
campaign.email_sent

An individual email was delivered to a recipient

Growth
campaign.reply_received

A reply to a campaign email was detected

Growth

Payload 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 Engineering at Acme Corp",
    "personalizedOpener": "Hiring a Head of Engineering usually means the old deploy process just became a board-level problem."
  }
}

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

Schedule (Mon 8am)
VibeJoy: find hiring leads
VibeJoy: enrich contacts
VibeJoy: create campaign
Slack: notify team

n8n HTTP Request node - find leads

MethodPOST
URLhttps://api.vibejoy.io/api/leads/intent/jobs
AuthenticationHeader Auth → Authorization: Bearer vj_...
Body (JSON){ "jobTitles": ["RevOps Manager"], "location": "London", "maxResults": 25 }

n8n HTTP Request node - create campaign

MethodPOST
URLhttps://api.vibejoy.io/api/campaigns/generate
AuthenticationHeader Auth → Authorization: Bearer vj_...
Body (JSON){ "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: https://api.vibejoy.io/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 module

Zapier (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.

Get your API key →