Developer Docs
ZenMode REST API
Push LinkedIn automation tasks and read your campaign data programmatically. Available on every paid plan except Novice.
Authentication
All API requests require a Bearer token. Generate an API key from your dashboard.
curl -H "Authorization: Bearer zm_your_api_key_here" \
https://www.zen-mode.io/api/v1/tasksKeep your API key secret. It is shown only once when created. If compromised, revoke it from the dashboard and generate a new one.
Scoping: your API key is your identity — every response is scoped to the key's own account. Resource ids passed as parameters (e.g. campaign_id) can only reference your own resources; anything else returns 404.
Plan requirement: the API is available on every active paid plan except Novice. On the Novice tier (or without an active plan) every endpoint returns:
HTTP 403
{
"error": "API access requires an active paid plan above the Novice tier. Upgrade your plan at https://www.zen-mode.io/dashboard/billing to use the API.",
"code": "plan_upgrade_required"
}Rate Limits
API Requests
100 per minute
Task Creation
30–50 per day per LinkedIn account (by account tier)
Exceeding limits returns 429 Too Many Requests.
Endpoints
/api/v1/tasks— Create a taskQueue a LinkedIn action. The desktop app picks it up automatically.
Request Body
{
"action_type": "connect",
"linkedin_account_id": "acc_abc123",
"payload": {
"profile_url": "https://linkedin.com/in/jane-doe",
"message": "Hi Jane, I'd love to connect!"
},
"priority": 0,
"max_retries": 3
}Action Types
connect — Send a connection request (with optional message)
message — Send a direct message to an existing connection
follow — Follow a profile
view_profile — View a profile (warm-up action)
withdraw_connection — Withdraw a pending connection request
Response (201)
{
"task": {
"id": "task_m1abc_x7k2p9qr",
"action_type": "connect",
"status": "pending",
"linkedin_account_id": "acc_abc123",
"payload": { ... },
"priority": 0,
"created_at": "2026-04-02T10:30:00.000Z"
}
}/api/v1/tasks— List tasksRetrieve your tasks with optional filters.
Query Parameters
status — Filter by status: pending, processing, completed, failed, cancelled
linkedin_account_id — Filter by LinkedIn account
limit — Results per page (default 50, max 100)
offset — Pagination offset
curl -H "Authorization: Bearer zm_..." \
"https://www.zen-mode.io/api/v1/tasks?status=completed&limit=10"/api/v1/tasks/:id— Get task statuscurl -H "Authorization: Bearer zm_..." \
https://www.zen-mode.io/api/v1/tasks/task_m1abc_x7k2p9qr/api/v1/tasks/:id— Cancel a pending taskOnly tasks in pending or queued status can be cancelled.
curl -X DELETE -H "Authorization: Bearer zm_..." \
https://www.zen-mode.io/api/v1/tasks/task_m1abc_x7k2p9qrWebhooks
Register a webhook URL to receive POST callbacks when tasks complete or fail.
/api/v1/webhooks— Register a webhook{
"url": "https://your-server.com/zenmode-webhook",
"events": ["task.completed", "task.failed"]
}URL must use HTTPS. Valid events: task.completed, task.failed.
/api/v1/webhooks— List webhooks/api/v1/webhooks?id=123— Delete a webhookWebhook Payload
When a task completes or fails, ZenMode sends a signed POST to your webhook URL.
Headers
X-ZenMode-Signature — HMAC-SHA256 hex digest of the request body, signed with your webhook secret
X-ZenMode-Event — Event type (e.g. task.completed)
Body
{
"event": "task.completed",
"data": {
"task_id": "task_m1abc_x7k2p9qr",
"action_type": "connect",
"status": "completed",
"result": { "success": true },
"linkedin_account_id": "acc_abc123"
},
"timestamp": "2026-04-02T10:35:00.000Z"
}Verifying Signatures
const crypto = require('crypto');
function verifyWebhook(body, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Code Examples
cURL
# Create a connection request task
curl -X POST https://www.zen-mode.io/api/v1/tasks \
-H "Authorization: Bearer zm_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"action_type": "connect",
"linkedin_account_id": "acc_abc123",
"payload": {
"profile_url": "https://linkedin.com/in/jane-doe",
"message": "Hi Jane, would love to connect!"
}
}'
# Check task status
curl https://www.zen-mode.io/api/v1/tasks/task_m1abc_x7k2p9qr \
-H "Authorization: Bearer zm_your_api_key"
# List completed tasks
curl "https://www.zen-mode.io/api/v1/tasks?status=completed&limit=20" \
-H "Authorization: Bearer zm_your_api_key"Python
import requests
API_KEY = "zm_your_api_key"
BASE_URL = "https://www.zen-mode.io/api/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}
# Create a task
task = requests.post(f"{BASE_URL}/tasks", headers=headers, json={
"action_type": "connect",
"linkedin_account_id": "acc_abc123",
"payload": {
"profile_url": "https://linkedin.com/in/jane-doe",
"message": "Hi Jane, would love to connect!"
}
}).json()
print(f"Task created: {task['task']['id']}")
# Poll for completion
import time
while True:
status = requests.get(
f"{BASE_URL}/tasks/{task['task']['id']}", headers=headers
).json()
if status["task"]["status"] in ("completed", "failed"):
print(f"Task {status['task']['status']}: {status['task'].get('result')}")
break
time.sleep(10)JavaScript / Node.js
const API_KEY = "zm_your_api_key";
const BASE_URL = "https://www.zen-mode.io/api/v1";
async function createTask(actionType, profileUrl, message) {
const res = await fetch(`${BASE_URL}/tasks`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
action_type: actionType,
payload: { profile_url: profileUrl, message },
}),
});
return res.json();
}
// Send a connection request
const { task } = await createTask(
"connect",
"https://linkedin.com/in/jane-doe",
"Hi Jane, would love to connect!"
);
console.log("Task ID:", task.id);
// Register a webhook to get notified
await fetch(`${BASE_URL}/webhooks`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://your-server.com/webhook",
events: ["task.completed", "task.failed"],
}),
});Read API — Analytics, Status & Leads
Read your own campaign data programmatically. Same Bearer-key authentication as the task endpoints; every response is scoped to the API key's account.
GET/api/v1/analytics
Campaign analytics: sent, accepted, replied, meetings booked, acceptance rate, reply rate, funnel, daily time-series, and a per-campaign comparison. Query params: days (1–365, default 30, or all) and campaign_id (optional, must be your own campaign).
curl -H "Authorization: Bearer zm_your_api_key_here" \
"https://www.zen-mode.io/api/v1/analytics?days=30"Example response:
{
"window": { "days": 30 },
"campaign_id": null,
"summary": {
"total_contacts": 412,
"sent": 305,
"accepted": 121,
"replied": 38,
"meetings_booked": 6,
"acceptance_rate_pct": 40,
"reply_rate_pct": 31,
"reply_rate_denominator": "accepted"
},
"funnel": { "sent": 305, "accepted": 121, "replied": 38, "meetings_booked": 6 },
"daily": [
{ "date": "2026-07-28", "connections_sent": 18, "connections_accepted": 7,
"replies_received": 3, "meetings_booked": 0 }
],
"campaigns": [
{ "id": 42, "name": "UK SaaS Founders", "status": "active",
"total_contacts": 180, "sent_count": 140, "connected_count": 61,
"replied_count": 19, "meeting_count": 3 }
]
}Note: reply rate is replied ÷ accepted (the response echoes reply_rate_denominator). Message open rates are not available — LinkedIn provides no prospect-side open data, so we don't invent one. The campaigns comparison is included only when no campaign_id filter is set.
GET/api/v1/status
Live desktop runtime status: whether the desktop app is active (heartbeat within 10 minutes), its current phase (idle / starting / scraping / connecting / checking_replies / monitoring / error), connections sent today, and per-LinkedIn-account action counts vs limits when running multi-account.
curl -H "Authorization: Bearer zm_your_api_key_here" \
https://www.zen-mode.io/api/v1/statusExample response (desktop running; when it isn't: { "desktop_active": false, "accounts": [] }):
{
"desktop_active": true,
"last_seen": "2026-07-29T14:58:12.000Z",
"phase": "connecting",
"linkedin_status": "normal",
"active_campaign_count": 2,
"connections_sent_today": 14,
"accounts": [
{ "display_name": "Jane Doe", "is_running": true, "phase": "connecting",
"campaign_id": 42, "daily_actions": 14, "daily_limit": 30,
"weekly_actions": 61, "weekly_limit": 100, "linkedin_status": "normal",
"started_at": "2026-07-29T13:02:44.000Z",
"last_action_at": "2026-07-29T14:55:03.000Z" }
]
}GET/api/v1/leads
Per-lead read across your campaigns: name, title, company, LinkedIn URL, status (pending / warming_up / sent / connected / replied / meeting_booked / failed …), replied_at, follow-up step reached, and calendar_clicked (the tracked calendar-link redirect — the only click signal that exists; general link click tracking and "who opened" are not available). Query params: campaign_id, status, updated_since (ISO 8601), limit (max 500), offset.
curl -H "Authorization: Bearer zm_your_api_key_here" \
"https://www.zen-mode.io/api/v1/leads?status=replied&limit=100"Example response:
{
"leads": [
{ "id": 37110, "campaign_id": 42, "name": "Omar Molina",
"title": "COO", "company": "Acme Ops", "location": "Mexico City, Mexico",
"linkedin_url": "https://www.linkedin.com/in/example",
"status": "replied", "follow_up_number": 1, "calendar_clicked": false,
"connection_sent_at": "2026-07-16T19:02:11.000Z",
"connection_accepted_at": "2026-07-17T17:58:25.000Z",
"replied_at": "2026-07-20T15:12:40.000Z",
"created_at": "2026-07-15T18:04:41.000Z",
"updated_at": "2026-07-20T15:12:40.000Z" }
],
"pagination": { "limit": 100, "offset": 0, "returned": 1 }
}Task Lifecycle
pending — Task queued, waiting for desktop app to pick up
processing — Desktop app is executing the task
completed — Action performed successfully on LinkedIn
failed — Action failed (check result for details)
cancelled — Task was cancelled before processing
Error Codes
| Status | Meaning |
|---|---|
| 400 | Bad request — missing or invalid fields |
| 401 | Unauthorized — missing or invalid API key |
| 403 | Forbidden — no active subscription |
| 404 | Not found — task or webhook doesn't exist |
| 429 | Rate limit exceeded — wait 60 seconds |
| 500 | Server error |
Need help?
Generate your API key and start sending tasks in minutes.