clickfunnels-sdk
v1.0.2
Published
Simple ClickFunnels 2.0 SDK - Add leads and check tag access
Maintainers
Readme
Unofficial ClickFunnels SDK by Wynter Jones
Simple, LLM-friendly Node.js SDK for ClickFunnels 2.0 - Add leads and check tag access
A lightweight SDK focused on the most common ClickFunnels operations: adding contacts with tags and checking if contacts have specific tags for access control.
Important: For backend use only, do not use client side.
Features
- ✅ Add contacts with attributes and tags (by tag ID)
- ✅ Check if contact has specific tags (by tag ID)
- ✅ List teams, workspaces, and tags (to easily discover IDs)
- ✅ TypeScript support with full type definitions
- ✅ LLM-friendly with clear documentation
- ✅ Automatic contact upsert (create or update)
Installation
npm install clickfunnels-sdkQuick Start
const ClickFunnels = require("clickfunnels-sdk");
const cf = new ClickFunnels({
apiKey: "your-api-key",
workspaceId: 456,
subdomain: "yourworkspace",
});
// List tags to get their IDs
const tags = await cf.listTags();
// [{ id: 1, name: 'Premium Member' }, { id: 2, name: 'Newsletter' }]
// Add contact with tag IDs
const result = await cf.addContact({
email_address: "[email protected]",
first_name: "John",
last_name: "Doe",
tag_ids: [1, 2], // Tag IDs
});
// Check if contact has tag (by ID)
const hasAccess = await cf.checkContactHasTag("[email protected]", 1);
if (hasAccess) {
console.log("✅ User has access");
}Getting Your Credentials
Step 1: Get API Key
- Go to ClickFunnels Team Settings → Developer Portal
- Create a Platform Application
- Copy your API Access Token
Step 2: Get Workspace ID and Subdomain
Use the SDK methods to discover your workspace configuration:
// Step 1: Create a temporary SDK instance with just your API key
const tempCf = new ClickFunnels({
apiKey: "your-api-key",
workspaceId: 0, // temporary
subdomain: "temp", // temporary
});
// Step 2: Get your team ID
const teams = await tempCf.listTeams();
console.log(teams); // [{ id: 123, name: "My Team" }]
// Step 3: Get your workspace ID and subdomain using the team ID
const workspaces = await tempCf.listWorkspaces(123);
console.log(workspaces); // [{ id: 456, subdomain: "myworkspace" }]
// Step 4: Create your actual SDK instance with the correct values
const cf = new ClickFunnels({
apiKey: "your-api-key",
workspaceId: 456,
subdomain: "myworkspace",
});Step 3: Environment Variables
Create a .env file:
CLICKFUNNELS_API_KEY=your-api-key-here
CLICKFUNNELS_WORKSPACE_ID=456
CLICKFUNNELS_SUBDOMAIN=yourworkspaceNote: teamId is optional and only needed if you want to use listWorkspaces() without passing a teamId parameter.
API Reference
Constructor
new ClickFunnels(config);Parameters:
apiKey(string, required): Your ClickFunnels API access tokenworkspaceId(number, required): Your workspace IDsubdomain(string, required): Your workspace subdomainteamId(number, optional): Your team ID (only needed forlistWorkspaces()helper)userAgent(string, optional): Custom User-Agent header
listTeams()
List all teams accessible with your API key.
const teams = await cf.listTeams();
// Returns: [{ id: 123, name: "My Team", public_id: "abc123", ... }]listWorkspaces(teamId?)
List all workspaces for a team.
// Provide teamId as parameter
const workspaces = await cf.listWorkspaces(123);
// Returns: [{ id: 456, name: "My Workspace", subdomain: "myworkspace", ... }]
// Or use teamId from config (if provided during initialization)
const cfWithTeam = new ClickFunnels({
apiKey: "your-api-key",
teamId: 123,
workspaceId: 456,
subdomain: "yourworkspace",
});
const workspaces2 = await cfWithTeam.listWorkspaces();Note: Either teamId parameter or teamId in config is required for this method.
listTags()
List all tags in your workspace. Use this to get tag IDs!
const tags = await cf.listTags();
// Returns: [
// { id: 1, name: "Premium Member", color: "#FF5733", ... },
// { id: 2, name: "Newsletter", color: "#3498DB", ... }
// ]addContact(attributes, tagIds?)
Add or update a contact with attributes and tags.
Parameters:
attributes(object): Contact informationemail_address(string, required): Contact emailfirst_name(string): First namelast_name(string): Last namephone_number(string): Phone numbertime_zone(string): Timezonewebsite_url(string): Website URLlinkedin_url(string): LinkedIn profile URLinstagram_url(string): Instagram profile URLtwitter_url(string): Twitter profile URLfb_url(string): Facebook profile URLtag_ids(number[]): Array of tag IDs to applycustom_attributes(object): Custom attributes as key-value pairs
tagIds(number[], optional): Array of tag IDs to apply (alternative to tag_ids in attributes)
Returns: Promise with contact and applied tags
const tags = await cf.listTags();
const premiumTagId = tags.find((t) => t.name === "Premium Member").id;
// Method 1: Using tag_ids in attributes (recommended)
const result = await cf.addContact({
email_address: "[email protected]",
first_name: "Jane",
last_name: "Smith",
phone_number: "+1234567890",
website_url: "https://example.com",
linkedin_url: "https://www.linkedin.com/in/janesmith",
custom_attributes: { plan: "premium" },
tag_ids: [premiumTagId], // Use tag ID, not name
});
// Method 2: Using separate tagIds parameter (backward compatible)
const result2 = await cf.addContact(
{
email_address: "[email protected]",
first_name: "Jane",
},
[premiumTagId]
);
console.log("Contact ID:", result.contact.id);
console.log("Applied tags:", result.appliedTags);checkContactHasTag(email, tagId)
Check if a contact has a specific tag (by tag ID).
Parameters:
email(string): Contact email to checktagId(number): Tag ID to verify (get fromlistTags())
Returns: Promise - true if contact has tag, false otherwise
const tags = await cf.listTags();
const premiumTagId = tags.find((t) => t.name === "Premium Member").id;
const hasAccess = await cf.checkContactHasTag(
"[email protected]",
premiumTagId
);
if (hasAccess) {
console.log("✅ User has premium access");
} else {
console.log("❌ User does not have premium access");
}Usage Examples
Example 1: Grant Access Based on Tag
// pages/api/verify-access.js (Next.js)
const ClickFunnels = require("clickfunnels-sdk");
const cf = new ClickFunnels({
apiKey: process.env.CLICKFUNNELS_API_KEY,
workspaceId: parseInt(process.env.CLICKFUNNELS_WORKSPACE_ID),
subdomain: process.env.CLICKFUNNELS_SUBDOMAIN,
});
export default async function handler(req, res) {
const { email_address } = req.body;
// Get the Premium tag ID
const tags = await cf.listTags();
const premiumTag = tags.find((t) => t.name === "Premium Member");
if (!premiumTag) {
return res.status(500).json({ error: "Premium tag not found" });
}
// Check access using tag ID
const hasAccess = await cf.checkContactHasTag(email_address, premiumTag.id);
res.json({ access: hasAccess });
}Example 2: Add Lead from Form Submission
// pages/api/signup.js (Next.js)
export default async function handler(req, res) {
const { email, firstName, lastName } = req.body;
// Get tag IDs
const tags = await cf.listTags();
const newLeadTag = tags.find((t) => t.name === "New Lead");
const newsletterTag = tags.find((t) => t.name === "Newsletter");
// Add contact with tags
await cf.addContact({
email_address: email,
first_name: firstName,
last_name: lastName,
custom_attributes: {
source: "website-signup",
signup_date: new Date().toISOString(),
},
tag_ids: [newLeadTag.id, newsletterTag.id],
});
res.json({ success: true });
}Example 3: Complete Workflow with Tag Lookup
async function onboardNewCustomer(email, plan) {
// Get available tags
const tags = await cf.listTags();
// Find tag IDs by name
const planTag = tags.find((t) => t.name === `${plan} Plan`);
const activeTag = tags.find((t) => t.name === "Active Customer");
if (!planTag || !activeTag) {
throw new Error("Required tags not found");
}
// Add customer with tags
const result = await cf.addContact({
email_address: email,
custom_attributes: {
plan,
onboarded_at: new Date().toISOString(),
},
tag_ids: [planTag.id, activeTag.id],
});
console.log("Customer onboarded:", result.contact.id);
// Verify tags were applied
const hasPlanTag = await cf.checkContactHasTag(email, planTag.id);
const hasActiveTag = await cf.checkContactHasTag(email, activeTag.id);
return {
success: hasPlanTag && hasActiveTag,
contactId: result.contact.id,
};
}Example 4: Middleware for Protected Routes
// middleware/requirePremium.js
async function requirePremiumAccess(req, res, next) {
const email = req.user.email; // from your auth system
const tags = await cf.listTags();
const premiumTag = tags.find((t) => t.name === "Premium Member");
const hasAccess = await cf.checkContactHasTag(email, premiumTag.id);
if (!hasAccess) {
return res.status(403).json({
error: "Premium membership required",
});
}
next();
}TypeScript Support
Full TypeScript definitions are included:
import ClickFunnels, {
ClickFunnelsConfig,
Contact,
Tag,
AddContactResponse,
} from "clickfunnels-sdk";
const config: ClickFunnelsConfig = {
apiKey: "your-api-key",
workspaceId: 456,
subdomain: "yourworkspace",
};
const cf = new ClickFunnels(config);
const tags: Tag[] = await cf.listTags();
const result: AddContactResponse = await cf.addContact({
email_address: "[email protected]",
tag_ids: [1, 2],
});Testing
This SDK includes a comprehensive test suite to ensure reliability.
Running Tests
# Run all tests
npm test
# Run tests in watch mode (for development)
npm run test:watch
# Run tests with coverage report
npm run test:coverageTest Coverage
The test suite includes 32 tests covering:
- Constructor validation - Ensures all required config parameters are validated
- listTeams() - Team listing and error handling
- listWorkspaces() - Workspace listing with custom/default team IDs
- listTags() - Tag listing with various response formats
- addContact() - Contact creation, updating, and tag application
- checkContactHasTag() - Tag verification and filtering
- Error handling - API errors, timeouts, network failures
- Edge cases - Empty responses, missing data, custom fields
Current coverage: ~92% (statements, branches, functions, lines)
Test Technologies
- Jest - Testing framework
- ts-jest - TypeScript support
- axios-mock-adapter - HTTP request mocking
Tests run entirely offline using mocked HTTP requests, so no API credentials are required.
Important Notes
Tag IDs vs Tag Names
This SDK uses tag IDs (not tag names) for addContact() and checkContactHasTag(). This is by design because:
- Tag IDs are stable and unique
- Tag names can change in the ClickFunnels UI
- The ClickFunnels API uses IDs for applied tags
Always use listTags() first to get the tag IDs you need:
// ✅ CORRECT - Use tag ID
const tags = await cf.listTags();
const tagId = tags.find((t) => t.name === "Premium").id;
await cf.checkContactHasTag("[email protected]", tagId);
// ❌ WRONG - Don't use tag name
await cf.checkContactHasTag("[email protected]", "Premium"); // Won't work!Contact Field Names
This SDK uses the official ClickFunnels API field names:
email_address(notemail)custom_attributes(notcustom_fields)tag_idsfor applying tags within the contact object
These match the ClickFunnels 2.0 API specification exactly.
Contact Upsert Behavior
addContact() automatically handles create-or-update:
- If email_address exists: Updates the contact's attributes
- If email_address doesn't exist: Creates a new contact
- Tags are always applied (won't duplicate if already applied)
Error Handling
The SDK throws ClickFunnelsError for API errors:
try {
await cf.addContact({
email_address: "[email protected]",
tag_ids: [999],
});
} catch (error) {
if (error.name === "ClickFunnelsError") {
console.error("Status:", error.statusCode);
console.error("Message:", error.message);
console.error("Details:", error.details);
}
}For AI/LLM Assistants
When using this SDK in AI coding assistants (Claude, Cursor, Replit, etc):
// 1. Install package
// npm install clickfunnels-sdk
// 2. Import and initialize
const ClickFunnels = require("clickfunnels-sdk");
const cf = new ClickFunnels({
apiKey: process.env.CLICKFUNNELS_API_KEY,
workspaceId: parseInt(process.env.CLICKFUNNELS_WORKSPACE_ID),
subdomain: process.env.CLICKFUNNELS_SUBDOMAIN,
});
// 3. Always get tag IDs first
const tags = await cf.listTags();
// 4. Use tag IDs (not names) for operations
const tagId = tags.find((t) => t.name === "Your Tag Name").id;
await cf.addContact({
email_address: "[email protected]",
tag_ids: [tagId],
});
await cf.checkContactHasTag("[email protected]", tagId);License
MIT
Support
For issues, questions, or contributions, please open an issue on GitHub.
