Skip to content
WorksBuddy Logo
WorksBuddy
Pricing
Lio icon

Lio · Lead Manager

AI lead management: capture, qualify, and work every lead.

Base path/api · /api/v1AuthX-API-Key · CRM_ keyFormatJSON · snake_case

Create leads in LIO from your own systems over a simple REST API.


Overview#

API nameLIO API
Versionv1
Base URLhttps://api-lio.worksbuddy.ai/api/v1
ProtocolHTTPS only
FormatJSON (application/json)
AuthenticationX-API-Key: CRM_...
Who can create keysOrganization admins, from the LIO app
Key lifetimeYour choice: a specific expiry date, or never

What the API does#

The LIO API lets external systems push new leads into LIO. Send a lead from your website form, landing page, chatbot, or partner app, and it lands in your LIO account, ready for your team to work.

Everything a key touches stays inside your organization. A key cannot read, change, or delete your CRM data, and cannot reach another organization.

What you can build#

  • Lead capture: push leads from web forms, landing pages, chatbots, or partner systems straight into LIO.
  • Enrichment on ingest: send status, priority, source, score, estimated value, assignee, tags, and any custom fields with the lead.
  • De-duplication: LIO detects duplicates on create and can merge instead.

Get a key#

Sign in to LIO as an organization admin and open the API Keys area in Settings. Create a key, optionally set an expiry and an IP allow-list, then copy it. The full key is shown once. See Managing your keys.


Table of contents#

  1. Quick start
  2. Authentication
  3. Response format
  4. Error codes
  5. Rate limits
  6. Access restrictions
  7. Create a lead
  8. Managing your keys
  9. Security
  10. Code samples
  11. Troubleshooting
  12. Limitations

1. Quick start#

Step 1. Create a key in the LIO app (Settings, API Keys). See Managing your keys.

Step 2. Store it in your secret manager. The key is shown once and cannot be recovered later.

Step 3. Create a lead.

curl -X POST "https://api-lio.worksbuddy.ai/api/v1/lead" \
  --header "X-API-Key: CRM_YOUR_API_KEY_HERE" \
  --header "Content-Type: application/json" \
  --data '{
    "first_name": "Jordan",
    "last_name": "Rivera",
    "email": "jordan@example.com",
    "phone_number": "+15551234567",
    "company": "Example Co",
    "lead_source": "Website"
  }'

Step 4. Read the response. On success you get 201 with { "message": "Lead created successfully", "data": { ...the lead } }.


2. Authentication#

Send your key in the X-API-Key header on every request. It is a bare key, not an Authorization: Bearer token.

X-API-Key: CRM_YOUR_API_KEY_HERE

That is the whole contract. No timestamp, no signature, no extra header.

Key format#

CRM_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...
PropertyValue
PrefixCRM_
Body96 hexadecimal characters
ShownOnce, at creation
Stored by LIOHashed. The raw key is never stored and cannot be retrieved.

If you lose a key, regenerate it or create a new one and delete the old one.

When a key stops working#

A key is refused when it is deactivated, deleted, past its expiry date, or when the request comes from an IP address outside the key's allow-list (if one is set).

Testing your key#

Send the request from a server or a terminal. The endpoint takes only the X-API-Key header, so a browser session has no effect on it.


3. Response format#

Read each endpoint's sample for its exact shape.

Create success#

{
  "message": "Lead created successfully",
  "data": { }
}

Errors#

Authentication errors are shaped { "error": "..." }. Validation errors are { "message": [ ... ] }. Duplicate errors carry a status_code. See Error codes.


4. Error codes#

HTTPBody shapeMeaningWhat to do
401{ "error": "API key missing" }No X-API-Key header.Send the header.
403{ "error": "Invalid or expired API key" }Key is unknown, deactivated, or past expiry.Regenerate or create a new key.
403{ "error": "IP not allowed" }Request IP is not in the key's allow-list.Call from an allowed IP, or widen the list.
409{ "status_code": 409, "message": "This lead already exists in the system" }A lead with the same name, email, and phone already exists.Send duplicate: 1 to force a new lead, or merge: 1 to merge.
409{ "status_code": 409, "message": "A lead with this email already exists" }Email already on a lead.Same as above.
422{ "message": [ "email must be a valid email" ] }Validation failed. message is an array of field messages.Fix the fields.
429{ "success": false, "message": "Too many requests. Please try again later." }Rate limited.Wait for the Retry-After header, then retry.
500{ "success": false, "message": "..." }Server error.Retry with backoff.

Retry: 429, 500, 502, 503, 504 with exponential backoff and jitter. Never retry 401, 403, 409, or 422. They will not succeed on a repeat.


5. Rate limits#

A rate limit protects the service. Build your integration to handle throttling gracefully rather than assuming a fixed ceiling.

When you are limited#

You get 429 with a Retry-After header in seconds:

{ "success": false, "message": "Too many requests. Please try again later." }

Staying under the limit#

  • Handle 429 and honor Retry-After from day one.
  • Batch your ingestion instead of firing everything at once.
  • If several systems push leads into LIO, coordinate them so they do not exhaust the shared limit together.

6. Access restrictions#

An API key can create leads in your organization, and nothing else. It is refused on every other route, including sign-in and sessions, key management, and any other CRM resource.

A key cannot read your CRM, cannot create or revoke another key, cannot sign in as anybody, and cannot change what your organization is billed. Those actions happen in the LIO app.


7. Create a lead#

POST /lead
Full URLhttps://api-lio.worksbuddy.ai/api/v1/lead
AuthenticationX-API-Key: CRM_...
Content typeapplication/json

Creates a lead in your organization. Extra keys you send that are not listed below are stored as custom fields on the lead. You can also send a nested custom_fields object.

status, priority, and lead_source accept either a numeric id or a name. Names are matched against your organization's values; an unknown lead_source name is created for you. A bare-domain website or linkedin_url (no scheme) has https:// prepended.

Request parameters

ParameterData typeRequiredDescription
first_namestringNo2 to 50 characters.
last_namestringNo2 to 50 characters.
emailstringNoValid email.
phone_numberstringNo
addressstringNo
citystringNo
statestringNo
countrystringNo
pin_codestringNo
companystringNo
job_titlestringNo
websitestringNoURL. Bare domains get https://.
linkedin_urlstringNoURL.
statusint or stringNoStatus id or name. Defaults to your first status.
priorityint or stringNolow, medium, high, urgent, or the id. Default medium.
lead_sourceint or stringNoSource id or name. Default Website. Unknown names are created.
scoreintNo0 to 100.
estimated_valuenumberNo
assigned_tointNoUser id to assign the lead to.
notesstringNo
tagsarrayNo
custom_fieldsobjectNoCustom field values. Unknown top-level keys are folded in here too.
duplicateintNo1 forces creation even if a duplicate is detected.
mergeintNo1 merges into the existing lead instead of erroring.

Request sample

curl -X POST "https://api-lio.worksbuddy.ai/api/v1/lead" \
  --header "X-API-Key: CRM_YOUR_API_KEY_HERE" \
  --header "Content-Type: application/json" \
  --data '{
    "first_name": "Jordan",
    "last_name": "Rivera",
    "email": "jordan@example.com",
    "phone_number": "+15551234567",
    "company": "Example Co",
    "job_title": "Head of Ops",
    "lead_source": "Website",
    "priority": "high",
    "estimated_value": 25000,
    "tags": ["inbound", "demo-request"],
    "utm_campaign": "spring-launch"
  }'

Here utm_campaign is not a known field, so it is stored as a custom field.

Response body

FieldData typeDescription
messagestringLead created successfully.
dataLeadThe created lead. Key fields below.

The Lead object (key fields)

FieldData typeDescription
idint
first_name / last_namestring or null
email / phone_numberstring or null
company / job_title / websitestring or null
address / city / state / country / pin_codestring or null
statusintLookup id. status_text gives the label.
priorityint1 low, 2 medium, 3 high, 4 urgent. priority_text gives the label.
lead_sourceintLookup id. lead_source_text gives the label.
scoreint0 to 100.
estimated_valuenumber
assigned_toint or null
linkedin_urlstring or null
notesstring or null
tagsarray
custom_fieldsobjectAny extra fields you sent.
created_at / updated_atdatetime

Response sample 201

{
  "message": "Lead created successfully",
  "data": {
    "id": 5573,
    "first_name": "Jordan",
    "last_name": "Rivera",
    "email": "jordan@example.com",
    "phone_number": "+15551234567",
    "company": "Example Co",
    "job_title": "Head of Ops",
    "status": 1,
    "priority": 3,
    "lead_source": 1,
    "score": 0,
    "estimated_value": 25000,
    "tags": ["inbound", "demo-request"],
    "custom_fields": { "utm_campaign": "spring-launch" },
    "created_at": "2026-09-02T09:31:00.000Z",
    "updated_at": "2026-09-02T09:31:00.000Z"
  }
}

Merge sample 200

When you send merge: 1 and a duplicate exists:

{ "status_code": 200, "message": "Lead merged successfully" }

Errors

CodeBody
401{ "error": "API key missing" }
403{ "error": "Invalid or expired API key" }
403{ "error": "IP not allowed" }
409{ "status_code": 409, "message": "This lead already exists in the system" }
409{ "status_code": 409, "message": "A lead with this email already exists" }
422{ "message": [ "email must be a valid email" ] }

8. Managing your keys#

Keys are created and managed in the LIO app. Open the API Keys area in Settings as an organization admin.

Create#

Give the key a name and, optionally, an expiry date and an IP allow-list. The full key (CRM_...) is shown once, with a copy button. Once you close the dialog it cannot be shown again.

View#

The list shows each key's name, its status, who created it, and its expiry.

StatusMeaning
ActiveWorking normally.
ExpiredPast its expiry date. Every request returns 403.
DisabledTurned off. Every request returns 403.

Regenerate#

Rotates the key in place. A new raw key is issued and shown once; the old value stops working immediately. Use this when a key may have leaked.

Delete#

Removes the key. It stops working immediately.

Rotation#

To rotate with an overlap window:

  1. Create a second key with the same settings and a dated name, for example Website ingest (2026-09).
  2. Deploy the new key to your integration.
  3. Once nothing is using the old key, delete it.

Rotate immediately after any suspected exposure, and whenever someone with access to the secret leaves the team.


9. Security#

A key can create leads in your organization. Treat it as a secret.

  • Never put a key in frontend code. Anything shipped to a browser is readable by anyone who opens developer tools. Call the API from your server.
  • Never commit a key. Not to git, a Dockerfile, or a CI config. Use your platform's secret store.
  • Never send a key in a URL. Query strings end up in logs. Use the X-API-Key header.
  • Set an expiry. A key that never expires is a key you will forget about.
  • Use the IP allow-list when your caller has stable IP addresses.
  • One key per integration. Shared keys make it impossible to tell what broke.
  • If a key leaks, regenerate or delete it first, then investigate. Both take effect on the next request.

10. Code samples#

Node.js#

const BASE = 'https://api-lio.worksbuddy.ai/api/v1';
const KEY = process.env.LIO_API_KEY; // CRM_...

async function createLead(lead) {
  const res = await fetch(`${BASE}/lead`, {
    method: 'POST',
    headers: {
      'X-API-Key': KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(lead),
  });

  if (res.status === 429) {
    const retryAfter = Number(res.headers.get('Retry-After') || 60);
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    return createLead(lead);
  }

  const body = await res.json().catch(() => ({}));

  if (!res.ok) {
    const message = Array.isArray(body.message) ? body.message.join(', ') : (body.message || body.error);
    const err = new Error(message || `LIO API ${res.status}`);
    err.status = res.status;
    throw err;
  }

  return body.data;
}

const lead = await createLead({
  first_name: 'Jordan',
  email: 'jordan@example.com',
  lead_source: 'Website',
  priority: 'high',
});

Python#

import os
import time
import requests

BASE = "https://api-lio.worksbuddy.ai/api/v1"
KEY = os.environ["LIO_API_KEY"]  # CRM_...

session = requests.Session()
session.headers.update({"X-API-Key": KEY})


def create_lead(lead):
    for _ in range(5):
        res = session.post(f"{BASE}/lead", json=lead, timeout=30)

        if res.status_code == 429:
            time.sleep(int(res.headers.get("Retry-After", 60)))
            continue

        body = res.json()

        if not res.ok:
            message = body.get("message") or body.get("error")
            raise RuntimeError(f"LIO API {res.status_code}: {message}")

        return body["data"]

    raise RuntimeError("LIO API: rate limited after 5 attempts")


lead = create_lead({"first_name": "Jordan", "email": "jordan@example.com", "lead_source": "Website"})

PHP#

<?php
$base = 'https://api-lio.worksbuddy.ai/api/v1';
$key  = getenv('LIO_API_KEY'); // CRM_...

function create_lead(array $lead): array {
    global $base, $key;

    $ch = curl_init($base . '/lead');
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => 'POST',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'X-API-Key: ' . $key,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS     => json_encode($lead),
    ]);

    $raw    = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $decoded = json_decode($raw, true);

    if ($status >= 400) {
        $message = $decoded['message'] ?? ($decoded['error'] ?? 'unknown');
        if (is_array($message)) { $message = implode(', ', $message); }
        throw new RuntimeException("LIO API {$status}: {$message}");
    }

    return $decoded['data'];
}

$lead = create_lead(['first_name' => 'Jordan', 'email' => 'jordan@example.com', 'lead_source' => 'Website']);

11. Troubleshooting#

SymptomCauseFix
401 API key missingNo X-API-Key header, or it was sent as Authorization: Bearer.Send X-API-Key: CRM_....
403 Invalid or expired API keyKey is unknown, disabled, past expiry, or copied with trailing whitespace.Copy the full CRM_... key and trim it, or regenerate.
403 IP not allowedThe key has an IP allow-list and your caller's IP is not on it.Add the IP, or remove the allow-list.
409 This lead already existsSame name, email, and phone as an existing lead.Send duplicate: 1 to force, or merge: 1 to merge.
422 with a message arrayA field failed validation.Read the messages and fix those fields.
Extra fields "disappeared"Unknown fields are stored as custom fields, not top-level columns.Read them back from data.custom_fields.
Rate limited despite low volumeOther systems may be sharing the same limit.Coordinate your callers so they do not compete for the shared limit.

12. Limitations#

  • Lead creation only. The API creates leads. There is no read, update, or delete, and no access to other CRM resources.
  • Key management is done in the app. A key cannot create, rotate, or revoke another key.
  • One environment. Test against a throwaway lead source or key.

Support#

For integration questions, contact your WorksBuddy account team.


Last updated: 2026-09-02