API documentation
Connect Pabbly, Zapier, an AI client over MCP, or your own integration to one Liteo workspace. This page covers authentication, polling, the record endpoints used by Zapier, and the MCP server.
- Base URL
- https://elated-rat-401.convex.site/v1
- Auth
- Bearer token (connection code)
- Format
- JSON
Authentication
In Liteo, open Settings, then Integrations. Choose Pabbly or Zapier and create a connection. Copy the connection code when Liteo shows it.
Send that code as a Bearer token with every request. Keep it private. Liteo only shows it once.
Authorization: Bearer YOUR_CONNECTION_CODEThe connection code is a workspace API key. It can read records and make changes through the write endpoints below. The Pabbly polling endpoints only read records.
An admin creates a workspace connection and chooses the default owner for new records. Keep the key on your server or in the integration's credential store. Never put it in a URL or public page.
To replace a key, create another, update your workflows, then revoke the old key in Settings → Integrations. Revocation stops requests using that key. It leaves existing CRM records in place.
Test a connection
GET/v1/me
Confirms that the connection code works and shows which Liteo workspace it can access.
curl "https://elated-rat-401.convex.site/v1/me" \
-H "Authorization: Bearer YOUR_CONNECTION_CODE"{
"keyName": "Pabbly",
"scope": "service",
"workspaceName": "Your workspace"
}New contacts
GET/v1/pabbly/contacts
Returns the newest contacts first. The response uses an items array so Pabbly can check each contact as a separate record.
Add ?limit=100 to return up to 100 contacts. The default is 25.
curl "https://elated-rat-401.convex.site/v1/pabbly/contacts?limit=100" \
-H "Authorization: Bearer YOUR_CONNECTION_CODE"{
"items": [
{
"id": "contact_123",
"createdAt": 1788804000000,
"firstName": "Ada",
"lastName": "Lovelace",
"workEmail": "ada@example.com",
"workPhone": "+1 555 0100",
"jobTitle": "Founder",
"type": "lead",
"businessId": "",
"tagIds": "",
"customFields": "{}",
"assignedToId": "user_123",
"needsReview": false
}
],
"nextCursor": null
}| Field | Meaning |
|---|---|
| id | The contact's unique Liteo ID |
| createdAt | When the contact was created, as a Unix timestamp in milliseconds |
| firstName | First name |
| lastName | Last name |
| workEmail | Work email address |
| workPhone | Work phone number |
| jobTitle | Job title |
| type | lead or customer |
| businessId | The linked business ID, or an empty string |
| tagIds | Comma-separated tag IDs |
| customFields | Custom fields as a JSON string |
| assignedToId | The Liteo member who owns the contact |
| previousOwnerId | The previous owner ID, or an empty string |
| needsReview | Whether the contact needs review |
New deals
GET/v1/pabbly/deals
Returns the newest deals first in an items array. Use the same connection code and optional limit as the contacts endpoint.
curl "https://elated-rat-401.convex.site/v1/pabbly/deals?limit=100" \
-H "Authorization: Bearer YOUR_CONNECTION_CODE"{
"items": [{
"id": "deal_123",
"name": "Website project",
"value": 2500,
"currency": "USD",
"stageId": "stage_123",
"stageName": "Proposal",
"stageColor": "",
"pipelineId": "pipeline_123",
"pipelineName": "Sales",
"nextFollowUp": "2026-09-15",
"businessId": "",
"businessName": "",
"primaryContactName": "",
"closedAt": 0,
"lostReason": "",
"stageMovedAt": 1788804000000
}],
"nextCursor": null
}| Field | Meaning |
|---|---|
| id | The deal's unique Liteo ID |
| name | Deal name |
| value | Deal value |
| currency | Three-letter currency code |
| stageId | Pipeline stage ID |
| stageName | Pipeline stage name |
| stageColor | Pipeline stage color, or an empty string |
| pipelineId | Pipeline ID |
| pipelineName | Pipeline name |
| nextFollowUp | Next follow-up date as YYYY-MM-DD, or an empty string |
| businessId | The linked business ID, or an empty string |
| businessName | The linked business name, or an empty string |
| primaryContactName | The primary contact name, or an empty string |
| closedAt | Unix timestamp in milliseconds when the deal closed, or 0 |
| lostReason | Why the deal was lost, or an empty string |
| stageMovedAt | Unix timestamp in milliseconds when the stage changed, or 0 |
Polling limits
In Pabbly, read the items array and use id to identify each record. Keep that ID unchanged between checks so the same record only starts your workflow once.
Set limit=100 for the largest Pabbly polling window. A single check returns at most 100 records. If more than 100 records arrive between checks, older records can be missed.
Both Pabbly endpoints return nextCursor for older records, or null when there are no more. Pass it as cursor on the next request. Your connector must follow it explicitly; Pabbly's polling step may only read the first page.
The Zapier connector checks up to 1,000 recent records per poll, in pages of 100. It can miss older records when more than 1,000 arrive between checks. Zapier may also hold large batches for confirmation.
Your Pabbly or Zapier plan controls how often checks run. Test with your expected volume before relying on polling. For a complete import or recovery, use the paginated record endpoints below and store processed record IDs.
List and find records
These endpoints serve Zapier and custom integrations. They return data and nextCursor. Contacts and deals arrive newest first. Use each record's _id as its stable ID.
| Field | Meaning |
|---|---|
| GET /v1/contacts | List contacts. Add email for an exact, case-normalized email search. |
| GET /v1/deals | List deals, including pipeline and stage details. |
| GET /v1/businesses | List businesses. |
| GET /v1/tasks | List tasks. |
| GET /v1/pipelines | List pipelines and their stages. Use their IDs when creating a deal. |
| GET /v1/pipelines/{id}/stages | List stages in one pipeline, in stage order. |
| GET /v1/tags | List existing tags. Use their IDs or exact names when tagging a contact. |
| GET /v1/members | List active workspace members for assigning records. |
| GET /v1/contacts/{id} | Read one contact. |
| GET /v1/businesses/{id} | Read one business. |
| GET /v1/deals/{id} | Read one deal. |
List requests accept limit from 1 to 100 (default 25) and an optional cursor. Pass nextCursor unchanged into the next request. Stop when nextCursor is null.
Member and stage lists return _id and name for each option. Pass a member's _id unchanged as assignedToId. Member pages can be empty while nextCursor still points to more members; keep following the cursor until it is null.
curl --get "https://elated-rat-401.convex.site/v1/contacts" \
-H "Authorization: Bearer YOUR_CONNECTION_CODE" \
--data-urlencode "email=ada@example.com" \
--data-urlencode "limit=100"{
"data": [{
"_id": "contact_123",
"_creationTime": 1788804000000,
"firstName": "Ada",
"lastName": "Lovelace",
"workEmail": "ada@example.com",
"workPhone": "+1 555 0100",
"jobTitle": "Founder",
"type": "lead",
"businessId": null,
"tags": [],
"customFields": {},
"assignedToId": "member_123"
}],
"nextCursor": null
}This example shows common contact fields; responses can include more summary fields. A search with no matches returns an empty data array. Single-record reads return the record directly, or 404.
Email and phone are optional when you create a contact. Contact responses return them as workEmail and workPhone. Deal requests use title; deal responses call it name. Response timestamps use Unix milliseconds.
IDs in these examples are placeholders. Use IDs returned by your own workspace. Do not use Pabbly's flattened records as write request bodies.
Create and update records
Send JSON with Content-Type: application/json and your Bearer key. Successful writes return HTTP 200 with the resulting record directly, without a data wrapper.
Optional fields can be omitted. Updates preserve omitted fields. Unknown fields are rejected. Related records and assignedToId must belong to the same workspace; the assigned member must be active.
POST/v1/contacts
| Field | Meaning |
|---|---|
| firstName, lastName | Required non-empty strings. |
| email, phone | Optional strings. |
| jobTitle | Optional string. |
| type | lead or customer. Defaults to lead for a new contact. |
| businessId | Optional business ID, or null. |
| assignedToId | Optional active member identifier. Omit to use the key's default owner. |
curl "https://elated-rat-401.convex.site/v1/contacts" \
-H "Authorization: Bearer YOUR_CONNECTION_CODE" \
-H "Content-Type: application/json" \
--data '{"firstName":"Ada","lastName":"Lovelace"}'PATCH/v1/contacts/{id}
Accepts the same fields as contact creation. Send at least 1 field. Omit type to preserve the contact's current status. Set businessId to null to remove the business link.
{"jobTitle":"Founder"}POST/v1/contacts/{id}/tags
Send exactly 1 of tagId or tagName. The tag must already exist in the workspace. The response is the updated contact. Adding a tag that is already attached leaves it attached once.
{"tagName":"Event lead"}Zapier's find-or-create and upsert actions first search GET /v1/contacts?email=…. They then create or update the matching contact. This sequence is not an atomic upsert; parallel workflows can race.
POST/v1/businesses
| Field | Meaning |
|---|---|
| name | Required non-empty string. |
| website, email, phone | Optional strings. |
| type | lead or customer. Defaults to lead. |
| assignedToId | Optional active member identifier. |
{"name":"Example Studio","website":"https://example.com","type":"lead"}POST/v1/deals
| Field | Meaning |
|---|---|
| title, pipelineId, stageId | Required strings. The stage must belong to the pipeline. |
| value | Optional non-negative number, default 0. Currency comes from the workspace. |
| contactId, businessId | Optional related-record IDs. |
| assignedToId | Optional active member identifier. |
| expectedCloseDate | Optional YYYY-MM-DD date. This legacy parameter sets the next follow-up date, returned as nextFollowUp. |
{"title":"Website project","pipelineId":"pipeline_123","stageId":"stage_123","value":2500,"expectedCloseDate":"2026-09-15"}The response includes _id, name, value, currency, pipelineId, stageId and nextFollowUp, plus related-record details. PATCH /v1/deals/{id} accepts title, value, stageId, assignedToId, expectedCloseDate and customFields. Send at least 1 field.
In a deal update, set expectedCloseDate to null to clear the next follow-up date.
POST/v1/tasks
| Field | Meaning |
|---|---|
| title | Required non-empty string. |
| dueDate | Optional YYYY-MM-DD date, or null for no due date. |
| contactId, businessId, dealId | Optional related-record IDs. |
| assignedToId | Optional active member identifier. |
{"title":"Follow up","dueDate":"2026-09-15","contactId":"contact_123"}The response includes _id, title and dueDate, plus task and related-record details. Dates are calendar dates, without a timezone. Direct API requests must send YYYY-MM-DD, not a timestamp.
Ordinary record creation has no idempotency guarantee. If a response is lost, check for the record before retrying. A retry can create another record.
Import leads
POST/v1/inbound/leads
Use this endpoint for automated lead handoffs. Send a unique source event ID in the Liteo-Idempotency-Key header. Repeating the same event returns the first result instead of creating another contact.
| Field | Meaning |
|---|---|
| contact | Required object. firstName is required; lastName, email, phone, and jobTitle are optional. |
| externalIdentity | Provider, accountId, and externalId. Required when the contact has no email or phone. |
| externalIdentity.profileUrl | Optional HTTPS profile URL. |
| business | Optional business with a required name and optional website, email, and phone. |
| tags | Optional list of existing tag names. |
| assignTo | Optional email address for an active workspace member. |
| source | Optional source label for the activity log. |
curl "https://elated-rat-401.convex.site/v1/inbound/leads" \
-H "Authorization: Bearer YOUR_CONNECTION_CODE" \
-H "Liteo-Idempotency-Key: sendpilot-event-123" \
-H "Content-Type: application/json" \
--data '{"contact":{"firstName":"Ada"},"externalIdentity":{"provider":"sendpilot","accountId":"workspace_123","externalId":"lead_123","profileUrl":"https://www.linkedin.com/in/ada"},"source":"Sendpilot"}'Liteo matches externalIdentity first, then email, then phone. A later event for the same external identity updates empty contact fields and keeps existing values.
For Sendpilot, map eventId to Liteo-Idempotency-Key. Map workspaceId to accountId and data.leadId to externalId. Use data.campaignId to list the campaign leads, paginate until id equals data.leadId, then map the matched lead’s name, company, title, and LinkedIn URL.
Filter on the matched lead’s customLeadStatus before calling Liteo; do not filter on the webhook’s newStatus. An ordinary reply should not create a contact. See the Sendpilot setup guide for the complete recipe.
For Instantly, subscribe to lead_interested and look up the exact campaign lead by campaign_id and lead_email. Map workspace to accountId and the matched lead’s id to externalId. Use instantly:{workspace}:{lead_id}:lead_interested as the idempotency key. See the Instantly setup guide for the complete recipe.
Link a business and contact
To save a form submission with a company and a person, find or create the business first. Then pass its _id as businessId when creating or updating the contact.
POST/v1/businesses/find-or-create
Accepts the business creation fields, including customFields. A supplied website matches by its normalized domain. Without a website, matching uses the trimmed, case-sensitive business name.
Matching and creation happen in 1 transaction. The response includes the business and a created boolean. Existing businesses keep their values; multiple matches return 409 so you can resolve the duplicate.
{"name":"Example Studio","website":"https://example.com"}Use the returned business _id in the contact request:
{"firstName":"Ada","lastName":"Lovelace","email":"ada@example.com","businessId":"business_123"}PATCH/v1/businesses/{id}
Accepts name, website, email, phone, type, assignedToId and customFields. Send at least 1 field. Omitted fields keep their current values.
Custom fields
Contact, business and deal writes accept a customFields object. Their record responses include customFields keyed by each field's storage key. Zapier shows the field names when you configure a create or update action.
GET/v1/custom-fields?entityType=contact
entityType can be contact, business or deal. The data array contains up to 100 active definitions with id, key, name, type and options.
{"data":[{"id":"field_123","key":"cf_budget","name":"Budget","type":"number","options":[]}]}In a write, identify each field by its definition id or storage key. Only fields defined for that record type and workspace are accepted. Updates preserve unmapped fields; null clears a value. Zapier leaves blank inputs unmapped.
{"customFields":{"field_123":500,"cf_source":null}}Values must match the field type. Numbers must be finite, dates must use YYYY-MM-DD, and dropdown values must match an option. Email fields require an email address; link fields require an HTTP or HTTPS URL.
A write accepts at most 100 custom values. Text values are limited to 1,000 characters; the total request body limit still applies.
POST/v1/custom-fields
| Field | Meaning |
|---|---|
| entityType | contact, business or deal. |
| name | Required field name, up to 100 characters. |
| type | text, number, date, select, link, email or phone. |
| options | Required for select fields: an array of up to 50 distinct strings, each at most 50 characters. |
The connection owner must currently be a workspace admin. For a service connection, this means the person who created the key. The default assignee's role doesn't grant this permission.
An identical name, type and option list returns the existing definition. A conflicting definition returns 409; insufficient admin access returns 403. Creating a field defines it for the workspace. Set its value separately in a record action.
{"entityType":"contact","name":"Budget","type":"number"}Record changes
GET/v1/events?entityType=contact&eventType=updated
Change history powers updated-record, stage-change, completion and tag triggers. Each change has its own event ID, even when a record changes twice. History starts when this feature is deployed and is retained for 7 days.
| Field | Meaning |
|---|---|
| updated | Contacts, businesses, deals and tasks. Includes changedFields and previousValues. |
| stage_changed | Deals only. Includes the destination stageId and pipelineId. |
| completed | Tasks only. Completing a reopened task creates another event. |
| tag_added, tag_removed | Contacts, businesses and deals. Includes tagId. |
| limit, cursor | Use the same pagination parameters as record lists. Large payloads can produce shorter pages; always follow nextCursor. |
{
"data":[{
"id":"event_123","recordId":"contact_123","entityType":"contact",
"eventType":"updated","occurredAt":"2026-09-09T08:00:00.000Z",
"changedFields":["jobTitle"],
"record":{"firstName":"Ada","lastName":"Lovelace","jobTitle":"Founder"},
"previousValues":{"jobTitle":"Director"}
}],
"nextCursor":null
}This example shows selected record fields. Deleted records are excluded from delivery. Zapier lets you watch selected fields, including individual custom fields, or leave the selection empty for any supported change.
Changed values and previousValues reflect that event. Other record fields show the current record when delivered.
Custom changes include customFields and customFields.storage_key in changedFields. Zapier filters these after fetching each page. Internal maintenance changes don't trigger updated-record events.
A Zap checks up to 1,000 events in at most 10 requests. Large payloads can reduce that count. Changes outside that window, or older than 7 days, can be missed. These are polling triggers, so delivery follows your Zap's polling schedule.
Due dates
GET/v1/due?eventType=task_due&timezone=Asia%2FBangkok
| Field | Meaning |
|---|---|
| task_due | Incomplete tasks due today. |
| task_overdue | Incomplete tasks with a due date before today. |
| deal_follow_up_due | Open deals with a follow-up due today. Won and lost deals are excluded. |
| timezone | An IANA timezone, such as Asia/Bangkok or Europe/London. Defaults to UTC. |
These endpoints use calendar dates. Today starts at midnight in the selected timezone, including daylight-saving changes. Responses include id, recordId, eventDate and timezone alongside the record fields.
The event id combines the record and its due date. Zapier uses it to run once per record and date, rather than every day it stays overdue. Moving the date creates a new ID; reopening a task with the same date keeps the old ID.
Task Due and Deal Follow-Up Due only include today's records. Keep the Zap enabled so it polls during that day. Task Overdue includes earlier dates. Each Zap checks at most 1,000 matching records per poll.
MCP server
POSThttps://app.liteo.io/api/mcp
Liteo has an MCP server, so an AI client can work in your workspace through named tools instead of raw HTTP. It speaks Streamable HTTP at a single endpoint. Every tool runs as a real workspace member, with that member's permissions.
ChatGPT, claude.ai, and Claude Desktop connect with OAuth. Add the endpoint as a connector and connect: the client registers itself, you sign in to Liteo, pick the workspace, and approve.
Discovery follows the MCP authorization spec, with dynamic client registration and PKCE (S256). Access tokens last 1 hour. Refresh tokens last 30 days and rotate on every use.
Claude Code, Codex, and Cursor can also sign in this way. They and your scripts can send an API key as a Bearer token instead. Create the key in Settings → Integrations → For developers, and keep it in an environment variable, never in a committed file. For Claude Code, a project .mcp.json reads it like this:
{
"mcpServers": {
"liteo": {
"type": "http",
"url": "https://app.liteo.io/api/mcp",
"headers": { "Authorization": "Bearer ${LITEO_MCP_API_KEY}" }
}
}
}Call get_workspace first to confirm the workspace and the member the connection acts as. Tool names are snake_case.
Lists take pageSize (25 by default, 100 at most) and cursor, and return { items, nextCursor }. Dates are YYYY-MM-DD. Create tools take an optional idempotencyKey, so a retry returns the first result instead of a duplicate.
| Field | Meaning |
|---|---|
| crm | Search, read, create and update contacts, businesses, deals, tasks, and notes. Pipelines, members, tags, and custom fields. |
| operations | Customer sites, locations, installed products, service visits, and service history. |
| catalog | Read, create, update, and archive products, consumables, and services. |
| commerce | Deal lines, draft quotes, and draft invoices. |
| automations | List automations and their runs. Reading one in full, creating, changing, turning on or off, and deleting them needs a workspace admin. |
| forms | Read forms and submissions. Creating, changing, switching off, and deleting them needs a workspace admin. New forms start off, and a person turns them on in Liteo. |
Admin tools need an admin who signed in or uses a personal key. Workspace keys can't use them.
76 tools in all. If a client limits how many tools it shows, load a subset with ?toolsets=crm,operations. get_workspace and search_crm are always included.
Removals that are hard to undo need confirm set to true on the call: archive_site, remove_installed_product, delete_automation, and delete_form. Quotes and invoices stay drafts. Sending, accepting, issuing, voiding, crediting, and payments happen in Liteo only. The server never deletes contacts, businesses, or deals, and it never creates or turns on webhook actions.
Every call passes through the same membership check, billing lock, and request limits as the REST API. Reads work without an active plan; writes don't. Connected apps are listed in Settings → Integrations → For developers, under Connected apps, where you can disconnect them.
The setup guide walks through each client, and the MCP server feature page shows what it looks like in use.
Request limits
Each key allows 60 requests per minute on trial and 240 on an active paid plan. A separate limit of 120 requests per minute applies to a source IP, including failed authentication attempts.
A key can use at most 6,250 requests per UTC day. Other keys in the workspace may share that daily allowance. Every page request counts toward the limits.
On HTTP 429, wait for the number of seconds in Retry-After before retrying. Responses can include Liteo-RateLimit-Limit, Liteo-RateLimit-Remaining and Liteo-RateLimit-Reset (Unix seconds).
JSON request bodies must be 32 KB or smaller. String values must be 2,000 characters or fewer; individual fields can have tighter limits. Write access requires a writable workspace plan.
Errors
Liteo returns a standard HTTP status and a short JSON message. A request ID is included so support can trace the problem.
{
"error": {
"code": "unauthorized",
"message": "The connection code is missing, invalid, or revoked.",
"requestId": "request_123"
}
}| Field | Meaning |
|---|---|
| 400 | The request is invalid. |
| 401 | The connection code is missing or invalid. |
| 402 | The workspace needs an active plan for write requests. |
| 403 | The key no longer has access to the workspace. |
| 404 | The route or workspace record was not found. |
| 409 | The request conflicts with the current record state. |
| 429 | Too many requests. Wait, then try again. |
| 500 | The request could not be completed. Contact support with the request ID. |
| 503 | The request is already being processed. Wait before retrying. |
Need help?
Email hello@liteo.io. Include the request ID if an API call failed.