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.

bash
curl -H "Authorization: Bearer zm_your_api_key_here" \
  https://www.zen-mode.io/api/v1/tasks

Keep 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:

json
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

POST/api/v1/tasksCreate a task

Queue a LinkedIn action. The desktop app picks it up automatically.

Request Body

json
{
  "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)

json
{
  "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"
  }
}
GET/api/v1/tasksList tasks

Retrieve 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

bash
curl -H "Authorization: Bearer zm_..." \
  "https://www.zen-mode.io/api/v1/tasks?status=completed&limit=10"
GET/api/v1/tasks/:idGet task status
bash
curl -H "Authorization: Bearer zm_..." \
  https://www.zen-mode.io/api/v1/tasks/task_m1abc_x7k2p9qr
DELETE/api/v1/tasks/:idCancel a pending task

Only tasks in pending or queued status can be cancelled.

bash
curl -X DELETE -H "Authorization: Bearer zm_..." \
  https://www.zen-mode.io/api/v1/tasks/task_m1abc_x7k2p9qr

Webhooks

Register a webhook URL to receive POST callbacks when tasks complete or fail.

POST/api/v1/webhooksRegister a webhook
json
{
  "url": "https://your-server.com/zenmode-webhook",
  "events": ["task.completed", "task.failed"]
}

URL must use HTTPS. Valid events: task.completed, task.failed.

GET/api/v1/webhooksList webhooks
DELETE/api/v1/webhooks?id=123Delete a webhook

Webhook 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

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

javascript
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

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

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

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

bash
curl -H "Authorization: Bearer zm_your_api_key_here" \
  "https://www.zen-mode.io/api/v1/analytics?days=30"

Example response:

json
{
  "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.

bash
curl -H "Authorization: Bearer zm_your_api_key_here" \
  https://www.zen-mode.io/api/v1/status

Example response (desktop running; when it isn't: { "desktop_active": false, "accounts": [] }):

json
{
  "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.

bash
curl -H "Authorization: Bearer zm_your_api_key_here" \
  "https://www.zen-mode.io/api/v1/leads?status=replied&limit=100"

Example response:

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

pendingprocessingcompleted
orprocessingfailed

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

StatusMeaning
400Bad request — missing or invalid fields
401Unauthorized — missing or invalid API key
403Forbidden — no active subscription
404Not found — task or webhook doesn't exist
429Rate limit exceeded — wait 60 seconds
500Server error

Need help?

Generate your API key and start sending tasks in minutes.