Skip to content
WorksBuddy Logo
WorksBuddy
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

This document describes only the public endpoints — the routes that are reachable without a logged-in LIO session cookie / standard app JWT, i.e. everything an external integration, partner service, provider webhook, or customer-facing link can call directly.

All of these are registered on the public router (mounted at /api/v1) plus a handful of root-level infrastructure endpoints. Internal, authenticated app APIs (the /api/... routes behind authenticate) are intentionally not covered here.

This is reference documentation only. It does not change any behavior.


1. Conventions#

Base URL#

https://api-lio.worksbuddy.ai/api/v1

Root-level endpoints (health, OIDC, a few webhooks) sit at the host root, not under /api/v1; each is called out below.

Content type#

Request and response bodies are JSON (Content-Type: application/json), except:

  • Provider webhooks that sign the raw bytes (Slack, Teams, Zoom, Meta, WhatsApp, Evox, Calendly, Didit, WorksBuddy Voice, Ciao, SSO) — sent as raw/form bodies.
  • Meta data-deletion callback — application/x-www-form-urlencoded.
  • Email-tracking pixels — respond with an image / HTTP redirect, not JSON.

Standard response envelope#

Most endpoints return:

{ "success": true, "message": "...", "data": { }, "total": 0, "page": 1, "limit": 10, "totalPages": 0 }

Errors:

{ "success": false, "message": "..." }

or, on some endpoints:

{ "success": false, "error": { "message": "...", "type": "...", "code": null } }

A few of the oldest lead endpoints return { message, data } without a success field — noted where that applies.

Common HTTP status codes#

CodeMeaning
200OK
201Created
400Bad request (missing required query/body)
401Missing / invalid credential
403Valid credential but not allowed (bad key, IP block, wrong org)
404Resource not found (or org/user could not be resolved)
409Duplicate (lead already exists; survey already answered)
410Gone (expired survey link)
422Validation error
429Rate limited

Rate limits#

  • A global Redis-backed limiter guards /api and /api/v1 (webhooks, SSE, static uploads and OAuth/OIDC flows are skipped inside it).
  • A dedicated public limiter exists (300 req/min) and is applied per-route on the booking, quote and VoC survey links (see each section for the specific cap).
  • Every public request/response pair is written to integration_logs (direction IN) by the inbound logger — except the quote-link routes, which are skipped so the token and customer details are never logged.

2. Authentication schemes#

The public API uses several different credential types. Each endpoint below names which one it expects. Secrets are configured via environment variables — values are never shown here.

#SchemeHow the caller sends itResolvesUsed by
AAPI KeyHeader x-api-key: <raw key>Looks up the hashed key in api_keys (must be active, unexpired; optional IP allow-list) → organization_idPOST /lead
BAPI-Key JWTHeader Authorization: Bearer <jwt> signed with jwtSecretKey, payload carries sso_idFinds the user by sso_id → org + roleGET /lead/lead-list
CShared-Secret JWTHeader Authorization: Bearer <jwt> signed with sharedSecretKey, payload { sso_id, organization_id, source? }Verifies claims, resolves the user/org/lead/add, /user/list, /deal/* (shared), /feed/public/add, /ai-email/reply-webhook, /intent-signal
DPublic JWTHeader Authorization: Bearer <jwt> signed with jwtSecretKey, payload carries org_idVerifies the org only, then acts as the org's oldest user (the creator)/public/lead/create, /public/lead/activity, /ciao-calls/*
ERevo JWTHeader Authorization: Bearer <jwt> signed with automationJwtSecret, claim sub = <user email>Finds the LIO user by email → orgAuto-detected on every withRevoFallback(...) route (below)
FEvox static keyHeader x-evox-key: <static key> plus organization_id in query or bodyCompares against evoxStaticKeyGET /evox/leads
GWeb-form keyHeader x-api-key: <raw key> matching the form's stored key, :form_link in the pathMatches the web form row → orgGET /web-form/:form_link
HURL tokenA 32-byte token / HMAC token in the path (no header)The token (or its stored hash) is the only credentialBooking, Quote, VoC survey, Email tracking
IProvider signatureProvider-specific signature header over the raw body (e.g. X-Twilio-Signature, X-Hub-Signature-256), verified inside the controllern/a — the provider is the callerAll webhooks in §9

The Revo fallback (withRevoFallback)#

Many routes are wrapped in withRevoFallback(primaryAuth). The behaviour is:

  1. If the Authorization Bearer token verifies against automationJwtSecret, it is a Revo JWT (scheme E) and Revo auth runs.
  2. Otherwise the wrapped primary scheme runs (A, C, or the standard app authenticate).
  3. A JWT whose header alg is not HS256/RS256 (e.g. alg: none) is rejected outright with 401.

So a route documented as "Shared-Secret JWT or Revo JWT" accepts either.

Note: routes wrapped as withRevoFallback(authenticate) fall back to the standard LIO app JWT when the token is not a Revo token. They are listed here because they live on the public /api/v1 router and are reachable by Revo without an app session, but a non-Revo caller still needs a valid LIO user token.


3. Leads#

3.1 Create lead (strict)#

POST /api/v1/lead
  • Auth: API Key (A) or Revo JWT (E).
  • Body (unknown keys allowed → auto-created as custom fields):
FieldTypeNotes
emailstringRequired. Valid email.
first_name, last_namestring2–50 chars, optional.
phone_numberstringoptional; also creates a Contact when present
company, job_title, websitestringwebsite accepts a bare domain (https:// is prepended); company is upserted
address, city, state, country, pin_codestringoptional
status, priority, lead_sourcenumber or name stringnames are resolved to the org's lookup ids; Revo callers always get source REVO; otherwise defaults to Website source / Medium priority
scoreint 0–100optional
estimated_valuenumberoptional
assigned_toint (user id)optional; web-form round-robin may override
linkedin_urlstringoptional (bare domain accepted)
notesstringoptional
tagsstring[]tag names; created if missing, stored as ids
custom_fieldsobjectoptional nested custom data
merge1on duplicate → 200 "Lead merged successfully"
duplicate1ignore duplicate and create anyway
  • Duplicate rule: exact match on first_name + last_name + email + phone_number within the org; plus a single-field email check when the org's dedup policy is email / email_phone.
  • Success 201: { "message": "Lead created successfully", "data": { ...lead } }
  • Errors: 422 (validation), 409 ("This lead already exists in the system" / "A lead with this email already exists").

3.2 Create lead (shared-secret variant)#

POST /api/v1/lead/add

Same controller and body as §3.1, but Auth: Shared-Secret JWT (C) or Revo JWT (E).

3.3 Create lead (permissive)#

POST /api/v1/lead/create
  • Auth: standard LIO app JWT or Revo JWT (E).
  • Required: at least one of email or linkedin_url. Everything else optional.
  • Dedup runs on whichever identifier(s) are present (email exact; LinkedIn by slug, matched with an ILIKE on /in/<slug> or /pub/<slug>).
  • Accepts a domain alias (company_website / domain / domain_url) mapped onto website. Name is derived from the email/LinkedIn slug when not given.
  • Body knobs merge: 1 / duplicate: 1 behave as in §3.1.
  • Success 201: { "success": true, "message": "Lead created successfully", "data": { ...lead } }
  • Duplicate: 200 (merge) or 409 with data: { id, email, linkedin_url }.

3.4 Create lead (public JWT)#

POST /api/v1/public/lead/create

Same permissive controller as §3.3, but Auth: Public JWT (D) — the token's org_id selects the organization; the org's oldest user is recorded as creator.

3.5 Add activity to a lead (public JWT)#

POST /api/v1/public/lead/activity
  • Auth: Public JWT (D).
  • Body:
FieldTypeNotes
lead_idintRequired. Must belong to the token's org.
activity_typestringRequired.
activity_descriptionstringRequired.
type_idintoptional
  • Success 201: { "success": true, "message": "Activity added successfully" }
  • Errors: 422 (validation), 404 (lead not found in org).

3.6 List leads#

GET /api/v1/lead/lead-list
  • Auth: API-Key JWT (B) or Revo JWT (E).
  • Query: limit (default 10), page (default 1), search_value, order_by (one of id,created_at,updated_at,first_name,last_name,email,score), order_in (ASC/DESC).
  • Scope: org-scoped; non-admin callers see only leads they created or are assigned.
  • Success 200:
{ "success": true, "message": "Leads fetched successfully",
  "data": [ { "id": 1, "first_name": "...", "email": "...", "status": 1, "...": "..." } ],
  "total": 42, "page": 1, "limit": 10, "totalPages": 5 }

3.7 Change lead status by name#

POST /api/v1/lead/:id/change-status
  • Auth: standard LIO app JWT or Revo JWT (E).
  • Path: :id — lead id.
  • Body: { "status": "<status name>" } (resolved case-insensitively against the org's status lookups, then the global default). Runs the normal status-change side-effects.
  • Errors: 422 (invalid id / unknown status), 404 (lead not found).

4. Deals#

4.1 List deals#

GET /api/v1/deal/list
  • Auth: Shared-Secret JWT (C) or Revo JWT (E).
  • Query: limit, page, order_by, order_in, lead_id, pipeline_id, stage_id, search_value (matches title/description), start_date + end_date.
  • Scope: only deals whose lead belongs to the caller's org.
  • Success 200: standard list envelope; each row is flattened with deal_id, deal_title, and the lead's first_name,last_name,email,phone,company,city,state,country.

4.2 Move deal to a stage (by id)#

POST /api/v1/deal/:id/move-stage
  • Auth: Shared-Secret JWT (C) or Revo JWT (E).
  • Body: { "stage_id": <int> } (required; must belong to the deal's pipeline).
  • Success 200: { "success": true, "message": "Deal moved to stage successfully", "data": { "deal_id", "stage_id", "stage_name" } }
  • Errors: 422 (invalid stage), 404 (deal not found), 403 (deal not in caller's org).

4.3 Change deal stage (by name)#

POST /api/v1/deal/:id/change-stage
  • Auth: Shared-Secret JWT (C) or Revo JWT (E).
  • Body: { "stage": "Won" | "Lost" | "Negotiation" } (case-insensitive; resolved to a stage of the deal's pipeline). Stamps won_at the first time the deal enters a won stage.
  • Success 200: { "success": true, "message": "Deal stage updated successfully", "data": { "deal_id", "stage_id", "stage_name" } }

4.4 Create deal (by names)#

POST /api/v1/deal/create
  • Auth: standard LIO app JWT or Revo JWT (E).
  • Body:
FieldTypeNotes
lead_idintRequired. Must exist in the org.
titlestringRequired. 2–50 chars.
valuenumber ≥ 1Required.
expected_close_datedateRequired.
pipelinestring nameoptional; defaults to the org's first active pipeline
stagestring nameoptional; defaults to the pipeline's first stage
descriptionstringoptional, ≤ 1000 chars
  • Success 201:
{ "success": true, "message": "Deal created successfully",
  "data": { "id", "lead_id", "title", "value", "expected_close_date", "description",
            "pipeline": { "id", "name" }, "stage": { "id", "name" } } }
  • Errors: 422 (validation / unknown pipeline/stage), 404 (lead not found), 400 (pipeline deal limit reached).

5. Feed, Users, Web form#

5.1 Add activity to the feed#

POST /api/v1/feed/add
  • Auth: standard LIO app JWT or Revo JWT (E).
  • Body: model_id, model_type (lead → Lead, deal → LeadDeal), activity_type, activity_description (all required); optional failure_reason, type_id, image.
  • Success 201: { "success": true, "message": "Activity logged successfully", "data": { "id", "source" } }

5.2 Add activity to the feed (shared-secret)#

POST /api/v1/feed/public/add

Same as §5.1, but Auth: Shared-Secret JWT (C) or Revo JWT (E); the caller is resolved from the token's sso_id + organization_id.

5.3 List users in the organization#

GET /api/v1/user/list
  • Auth: Shared-Secret JWT (C) or Revo JWT (E).
  • Success 200: { "success": true, "data": [ { "id": 12, "name": "..." } ] } (ids numeric).

5.4 View a public web form#

GET /api/v1/web-form/:form_link
  • Auth: Web-form key (G) — x-api-key header must match the form's stored key.
  • Success 200: { "success": true, "data": { "id", "form_name", "form_link", "form_html", "api_key_raw" } }
  • Errors: 401 (key missing), 403 (wrong key for this form), 404 (form not found).

6. Integrations (Evox, intent, automation)#

6.1 Evox — fetch leads#

GET /api/v1/evox/leads
  • Auth: Evox static key (F) — x-evox-key header and organization_id (query or body).
  • Query: page, limit (max 100), search, order_by, order_in, and filters status, priority, lead_source, assigned_to (comma-separated ids), city, state, country, company, score_min, score_max, created_from, created_to. "Deleted"-status leads are excluded.
  • Errors: 401 (key missing), 403 (invalid key), 400 (organization_id missing).

6.2 Ingest an intent signal#

POST /api/v1/intent-signal
  • Auth: Shared-Secret JWT (C) or Revo JWT (E).
  • Body:
FieldTypeNotes
lead_idint/stringRequired. Must exist in the org.
signal_typestringRequired. One of: pricing_page_visit, demo_requested, form_submitted, reply_received, email_opened, email_opened_3x, link_clicked, case_study_download, manual
signal_detailanyoptional
sourcestringoptional (default webhook)
occurred_atdateoptional
  • Errors: 401 (no org in token), 400 (missing/unsupported fields), 404 (lead not found).

6.3 Automation trigger — recent events (Revo preview)#

GET /api/v1/automation/triggers/:triggerKey/recent
  • Auth: standard LIO app JWT or Revo JWT (E).
  • Returns the caller's recent real events for a workflow trigger in the Revo sample_output envelope; empty → { "data": [] }.

6.4 Evox Ciao calls#

POST /api/v1/ciao-calls/prepare
POST /api/v1/ciao-calls/result
  • Auth: Public JWT (D) — the token's org is used; its id is an SSO uuid.
  • prepare returns a booking_ref before Evox dials a lead; result forwards the CallResult (adds a lead note and books a meeting for Evox calls).

6.5 AI email reply webhook#

POST /api/v1/ai-email/reply-webhook
  • Auth: Shared-Secret JWT (C).
  • Marks AI email drafts as replied when an inbound email references a previously-sent draft's message_id.

7. Email tracking (token in URL)#

Public by design — these URLs are embedded in emails sent to recipients. Auth is the HMAC signature inside the URL token (validated in the controller); tampering is rejected.

GET /api/v1/track/open/:token     # 1×1 tracking pixel; logs email_opened / email_opened_3x
GET /api/v1/track/click/:token    # logs link_clicked, then 302-redirects to the real URL

These power the pages a lead/customer opens from an email — no login. The emailed token (only its hash is stored) is the sole credential. Each group has its own rate limiter.

GET  /api/v1/public/quote/:token                  # view the quote (first open marks it "viewed")
POST /api/v1/public/quote/:token/accept           # { name, email, agree: true }
POST /api/v1/public/quote/:token/decline          # { reason?, name? }
POST /api/v1/public/quote/:token/request-changes  # { message, name?, requested_changes?[] }
  • requested_changes[] items: { position, quantity?, note? }.
  • Success 200: { "success": true, "message": "...", "data": { ...public quote } }.
  • Errors: 404 (invalid link), 422 (validation). A repeat submission returns 200 "Your response was already recorded." (idempotent). These routes are excluded from integration_logs.

8.2 Voice-of-Customer survey (rate limit: 20 / 10 min per IP)#

GET  /api/v1/voc/survey/:token            # fetch the survey (no CRM data exposed)
POST /api/v1/voc/survey/:token            # { score (int, required), comment? }
POST /api/v1/voc/survey/:token/opt-out    # unsubscribe from survey emails
  • Errors: 404 invalid link, 410 expired, 409 already answered, 422 missing score.

8.3 Public booking page (rate limit: 300 / min)#

GET  /api/v1/booking/:slug                 # rep profile: { rep.name, durations, timezone }
GET  /api/v1/booking/:slug/availability    # ?date=YYYY-MM-DD&duration=30&tz=... (or from/to range)
POST /api/v1/booking/:slug/confirm         # book a slot
  • The :slug identifies the rep (booking_enabled must be true); the rep's email is never exposed.
  • confirm body: { start, end, name, email } (required) plus company, phone, notes. It match-or-creates the prospect as a lead in the rep's org and creates an online meeting (Meet/Teams link).
  • Errors: 404 (page not found), 422 (missing fields / invalid slot).

9. Webhooks (provider → LIO)#

These are inbound endpoints that external providers call. You don't call them as a client; you configure the provider with the URL. Each is authenticated by a provider-specific signature over the raw request body (scheme I), verified inside the controller — do not add a Bearer/API-key to these.

Telephony & voice (under /api/v1, verified by provider signature)#

POST /api/v1/twilio/voice/outbound            # X-Twilio-Signature
POST /api/v1/twilio/status-callback
POST /api/v1/twilio/recording-callback
POST /api/v1/twilio/transcription-callback
POST /api/v1/plivo/webhooks/answer            # X-Plivo-Signature-V3
POST /api/v1/plivo/webhooks/status
POST /api/v1/plivo/webhooks/hangup
POST /api/v1/plivo/webhooks/recording
POST /api/v1/plivo/webhooks/transcription
POST /api/v1/plivo/webhooks/compliance

Inbox sync#

POST /api/v1/inbox/webhook/microsoft   # ?validationToken on creation; later POSTs signed via clientState
POST /api/v1/inbox/webhook/gmail       # ?token=<shared secret> (Google Pub/Sub push)

External workflow callbacks#

POST /api/v1/webhooks/workflow/...     # HMAC-signed (workflowWebhookAuth), raw body

Provider webhooks registered at the host root (raw body + HMAC)#

POST /api/v1/webhooks/slack            # Slack signature
POST /api/v1/webhooks/teams            # Teams / Bot Framework JWT
POST /api/v1/webhooks/zoom             # x-zm-signature
POST /api/v1/webhooks/meta             # X-Hub-Signature-256 (Facebook Lead Ads)
POST /api/v1/webhooks/lead-source      # generic lead-source (99acres, MagicBricks, …); per-org token in path
POST /api/v1/webhooks/evox             # X-Evox-Signature
POST /api/v1/webhooks/calendly         # Calendly-Webhook-Signature
POST /api/v1/webhooks/didit            # X-Signature (KYC)
POST /api/v1/webhooks/whatsapp         # per-org x-hub-signature-256 (AiSensy)
POST /api/v1/webhooks/worksbuddy-voice # HMAC
POST /api/v1/ciao                      # Ciao demo-booking, HMAC over raw bytes
POST /api/v1/webhooks/calling-payment  # Stripe signature (prepaid calling wallet)
POST /api/v1/webhooks/meta-data-deletion  # Meta data-deletion (urlencoded signed_request)

SSO integration (host root)#

POST /api/webhooks/users   # SSO user provisioning (X-SSO signature, raw body)
POST /api/webhooks         # same SSO user webhook at the fleet-default path
*    /api/v1/sso/...        # SSO ↔ product plan-sync (X-SSO-Signature on writes)

10. Infrastructure & identity (host root, no app auth)#

Health probes#

GET /health/live    # 200 { "status": "ok" } — process is up
GET /health/ready   # 200 ready / 503 not-ready, with { checks: { db, redis } }

OIDC / OAuth (MCP connector identity provider)#

GET  /.well-known/openid-configuration
GET  /.well-known/oauth-authorization-server
GET  /.well-known/jwks.json
GET  /oauth/jwks
POST /oauth/register        # dynamic client registration
POST /oauth/token           # OIDC token (also /oauth/access-token)
POST /oauth/access-token
GET  /oauth/userinfo
GET  /oauth/authorize       # 302 → frontend consent screen (needs MCP_CONSENT_URL)
POST /api/v1/oauth/token    # MCP OAuth token (auth code + PKCE; no Bearer, per RFC 6749 §3.2)

Static uploads#

GET /uploads/:storedName    # public file download; original filename + inline/attachment disposition

SSE streams (token in query / Authorization — not app-session)#

GET  /api/events                          # ?token=<jwt> — server-sent events for the org
POST /api/leads/email/bulk/stream         # Authorization: Bearer <jwt> — bulk enrichment SSE

11. Quick auth cheat-sheet#

Endpoint(s)Credential
POST /leadx-api-key (or Revo JWT)
POST /lead/add, /user/list, /deal/list, /deal/:id/move-stage, /deal/:id/change-stage, /feed/public/add, /ai-email/reply-webhook, /intent-signalShared-secret Bearer JWT (or Revo JWT)
GET /lead/lead-listAPI-key Bearer JWT with sso_id (or Revo JWT)
POST /public/lead/create, /public/lead/activity, /ciao-calls/*Public Bearer JWT with org_id
POST /lead/create, /deal/create, /feed/add, /lead/:id/change-status, GET /automation/triggers/:k/recentLIO app JWT (or Revo JWT)
GET /evox/leadsx-evox-key + organization_id
GET /web-form/:form_linkx-api-key matching the form
Booking / Quote / VoC survey / trackingtoken in the URL
All §9 webhooksprovider signature over raw body
Health / OIDC / uploadsnone