# LeadTable External API v4

REST API for agencies to manage customers, tables (campaigns), leads, webhooks, files, pages, and WhatsApp.

**Authentication:** every route needs the headers `x-api-key` and `email`.

**IDs:** MongoDB ObjectId strings. Timestamps are ISO-8601 unless noted.

**Tables vs campaigns:** a campaign is a table. v4 uses `/table` paths. Request fields that still say `campaignID` accept a table ID.

**Lead profile (`leadData`):** `GET /lead/{leadID}` adds `lead.leadData` with `mappedData` (column name → answer), `rawData` (question title → answer), and `dataArray` (`{ title, value }[]`). List and search routes do not add `leadData`.

**Webhooks:** topics are `newLead`, `changeStatus`, `updateLead`, `deleteLead`, `whatsappMessage`. Layers are `agency`, `customer`, `table`. Live deliveries use a JSON body with `event`, IDs, contact fields, and `leadData` (`mappedData` + `rawData`, no `dataArray`). `GET /pollWebhook/{campaignID}/{topic}` returns one shortened test sample, not the full live body.

**Files:** upload as `multipart/form-data` with `file` and `id` (lead ID).  Download links are **only valid for 5 minutes**

Server: `https://api.lead-table.com/api/v4/`

Auth: headers `x-api-key` and `email` on every route.

## GET /auth
Check authentication

Returns `{ auth: true }` when the API key and email are valid.

operationId: `getAuth`

Responses:
- 200: Authenticated `{ auth: true }`
- 400: BadRequest
- 401: Unauthorized

## GET /agency
Get the authenticated agency

Returns the agency that belongs to the API key, including aggregated stats.

operationId: `getAgency`

Responses:
- 200: Agency `{ agency: Agency }`
- 401: Unauthorized
- 404: NotFound

## GET /customer/list
List customers

Paginated list of customers in the agency. Each item includes `stats`.

operationId: `listCustomers`

Parameters:
- query `page` (integer, optional)
- query `limit` (integer, optional)

Responses:
- 200: Paginated customers `{ pagination: Pagination, customers: CustomerListItem[] }`
- 400: BadRequest
- 401: Unauthorized

## POST /customer/create
Create a customer

Creates a customer if the agency has a free slot. Optional: increment slots, create customer logins, and attach pages.

operationId: `createCustomer`

Request body (required): `CustomerCreateRequest`

Responses:
- 200: Customer created `CustomerCreateResponse`
- 403: Forbidden
- 500: InternalServerError

## GET /customer/{customerID}
Get a customer

Returns one customer you can access, including `stats`.

operationId: `getCustomer`

Parameters:
- path `customerID` (string, required)

Responses:
- 200: Customer `{ customer: Customer }`
- 400: BadRequest
- 403: Forbidden
- 404: NotFound

## POST /customer/addPage
Add a page to a customer

Copies a page instance from a page template onto a customer. Send a value for every required page variable.

operationId: `addCustomerPage`

Request body (required): `CustomerAddPageRequest`

Responses:
- 200: Page added `{ pageInstanceID: string }`
- 400: BadRequest
- 404: NotFound

## DELETE /customer/{customerID}/page/{pageInstanceID}
Remove a page from a customer

Deletes the page instance. The page must belong to the customer and to your agency.

operationId: `removeCustomerPage`

Parameters:
- path `customerID` (string, required)
- path `pageInstanceID` (string, required)

Responses:
- 200: Page removed `CustomerRemovePageResponse`
- 400: BadRequest
- 403: Forbidden
- 404: NotFound

## GET /table/list/{customerID}
List tables of a customer

Paginated tables (campaigns) of a customer. Limit is capped at 100. Each item includes `stats`, `totalLeads`, `leadsCount`, and `webhookToken`.

operationId: `listTablesByCustomer`

Parameters:
- path `customerID` (string, required)
- query `page` (integer, optional)
- query `limit` (integer, optional)

Responses:
- 200: Paginated tables `{ pagination: Pagination, campaigns: TableListItem[] }`
- 400: BadRequest
- 404: NotFound

## GET /table/{tableID}
Get a table

Returns one table you can access, including `stats` and `webhookToken`.

operationId: `getTable`

Parameters:
- path `tableID` (string, required) — Table (campaign) ID

Responses:
- 200: Table `{ table: Table }`
- 400: BadRequest
- 403: Forbidden
- 404: NotFound

## GET /table/{tableID}/leads
List leads of a table

Paginated leads of a table. Limit is capped at 200. Entries do not include `leadData`. Use GET `/lead/{leadID}` for profile data.

operationId: `listLeadsByTable`

Parameters:
- path `tableID` (string, required) — Table (campaign) ID
- query `page` (integer, optional)
- query `limit` (integer, optional)

Responses:
- 200: Paginated leads `{ pages: LeadPages, leads: LeadListItem[] }`
- 400: BadRequest
- 403: Forbidden
- 404: NotFound

## POST /table/create
Create a table

Creates a table for a customer you can access. You can set statuses, a status template, pre-qualification, and lead-delete visibility. A table is saved only after a status template exists. Send `status` to create a new template, or `statusTemplateID` to use an existing one. If you send neither, the first template of the agency is used. If the agency has no template, the response is 404: create at least one status template (https://help.lead-table.com/de/articles/9739351-status-felder-und-statusvorlagen). Returns a `webhookToken`. To create a lead, send an HTTP POST with JSON to `https://api.lead-table.com/api/webhook/{webhookToken}`.

operationId: `createTable`

Request body (required): `TableCreateRequest`

Responses:
- 200: Table created `TableCreateResponse`
- 403: Forbidden
- 404: NotFound

## POST /table/create/{templateID}
Create a table from a template

Creates a table from an agency table template. The template sets status, views, and automations. The customer must belong to the agency and must be active. Returns a `webhookToken`. To create a lead, send an HTTP POST with JSON to `https://api.lead-table.com/api/webhook/{webhookToken}`.

operationId: `createTableFromTemplate`

Parameters:
- path `templateID` (string, required)

Request body (required): `{ customerID: string, occupation: string }`

Responses:
- 200: Table created `TableCreateFromTemplateResponse`
- 403: Forbidden
- 404: NotFound

## GET /table/templates/list
List table templates

Agency table templates, newest first. Each item has `id`, `name`, and `icon` only.

operationId: `listTableTemplates`

Parameters:
- query `page` (integer, optional)
- query `limit` (integer, optional)
- query `search` (string, optional)

Responses:
- 200: Paginated templates `{ templates: TableTemplate[], pagination: Pagination }`
- 401: Unauthorized

## GET /status/templates/list
List status templates

Reusable status templates for the agency, sorted by name. Campaign copies are not included. Use a status `id` with `POST /lead/changeStatus`. `leadInboxStatus` is the inbox status id, or `null`.

operationId: `listStatusTemplates`

Responses:
- 200: Status templates `StatusTemplateList`
- 401: Unauthorized

## POST /lead/create
Create a lead

Creates a lead in a table. Each `data` item is a question (`key`) and answer (`value`). Set `notifications` to `true` to run automations.

operationId: `createLead`

Request body (required): `LeadCreateRequest`

Responses:
- 200: Lead created `{ id: string }`
- 400: BadRequest
- 404: NotFound

## GET /lead/{leadID}
Get a lead

Returns the lead including `lead.leadData` (`mappedData`, `rawData`, `dataArray`) when the table exists.

operationId: `getLead`

Parameters:
- path `leadID` (string, required)
- query `plainDescription` (boolean, optional) — If `true`, HTML is stripped from `description`.

Responses:
- 200: Lead `{ lead: Lead }`
- 400: BadRequest
- 404: NotFound

## PUT /lead/{leadID}
Create or update a lead field

Creates or updates one profile field. If the question does not exist, the API adds it. Set `setVisibleInProfile` to `true` to show the field in the lead profile.

operationId: `upsertLeadField`

Parameters:
- path `leadID` (string, required)

Request body (required): `UpdateLeadFieldRequest`

Responses:
- 200: Lead updated `{ success: true, lead: LeadWrite }`
- 400: BadRequest
- 404: NotFound

## PUT /lead/{leadID}/description
Update lead description

Overwrites the lead description. Maximum length is 25000 characters after HTML sanitizing.

operationId: `updateLeadDescription`

Parameters:
- path `leadID` (string, required)

Request body (required): `{ description: string }`

Responses:
- 200: Lead updated `{ lead: LeadWrite }`
- 400: BadRequest
- 404: NotFound

## GET /lead/{leadID}/files
List files of a lead

Returns the files uploaded to a lead: `filename`, `filesize`, `mimetype` and `blobURL`. Use `blobURL` as the file id to download a file with `GET /lead/{leadID}/files/{fileID}/content`.

`downloadURL` is an older, ready-made link that is valid for about 55 minutes and does not name the file. Prefer the download endpoint.

### Example: download all files of a lead

Set `API_KEY`, `AGENCY_EMAIL` and `LEAD_ID`. The files land in `lead-files/<LEAD_ID>/`.

**Node.js** (18 or newer), save as `download.mjs` and run `node download.mjs`

```js
import { createWriteStream } from "node:fs";
import { mkdir } from "node:fs/promises";
import { basename, join } from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";

const { API_BASE = "https://api.lead-table.com/api/v4", API_KEY, AGENCY_EMAIL, LEAD_ID } = process.env;
const headers = { "x-api-key": API_KEY, email: AGENCY_EMAIL };
const dir = join("lead-files", LEAD_ID);
await mkdir(dir, { recursive: true });

const listing = await fetch(`${API_BASE}/lead/${LEAD_ID}/files`, { headers });
if (!listing.ok) throw new Error(`Listing failed: HTTP ${listing.status}`);
const { files } = await listing.json();

for (const file of files) {
  const res = await fetch(`${API_BASE}/lead/${LEAD_ID}/files/${file.blobURL}/content`, { headers });
  if (!res.ok) throw new Error(`${file.filename}: HTTP ${res.status}`);
  await pipeline(Readable.fromWeb(res.body), createWriteStream(join(dir, basename(file.filename))));
  console.log("ok ", file.filename);
}
```

**Python** (3.8 or newer), save as `download.py` and run `python3 download.py`

```python
import json, os, shutil, urllib.request

API_BASE = os.environ.get("API_BASE", "https://api.lead-table.com/api/v4")
lead_id = os.environ["LEAD_ID"]
headers = {"x-api-key": os.environ["API_KEY"], "email": os.environ["AGENCY_EMAIL"]}
out = os.path.join("lead-files", lead_id)
os.makedirs(out, exist_ok=True)


def get(path):
    return urllib.request.urlopen(urllib.request.Request(f"{API_BASE}{path}", headers=headers))


with get(f"/lead/{lead_id}/files") as res:
    files = json.load(res)["files"]

for f in files:
    target = os.path.join(out, os.path.basename(f["filename"]))
    with get(f"/lead/{lead_id}/files/{f['blobURL']}/content") as res, open(target, "wb") as fh:
        shutil.copyfileobj(res, fh)
    print("ok ", f["filename"])
```

operationId: `listLeadFiles`

Parameters:
- path `leadID` (string, required)

Responses:
- 200: Files `{ files: LeadFile[] }`
- 400: BadRequest
- 403: Forbidden
- 404: NotFound

## GET /lead/{leadID}/files/{fileID}/content
Download a file of a lead

Redirects (`302`) to a download link that is valid for 60 seconds. Follow the redirect and save the body. The response carries the real `Content-Type` and a `Content-Disposition` header with the file name. Send your `x-api-key` and `email` headers with this request only. Most HTTP clients follow the redirect by default.

operationId: `downloadLeadFile`

Parameters:
- path `leadID` (string, required)
- path `fileID` (string, required) — The `blobURL` of the file from the listing.

Responses:
- 302: Redirect to the file. The `Location` header holds the 60 second link.
- 400: BadRequest
- 403: Forbidden
- 404: NotFound

## POST /lead/addFile
Upload a file to a lead

Uploads one file and attaches it to the lead.

operationId: `addFileToLead`

Request body (required): `{ file: string(binary), id: string }`

Responses:
- 200: File uploaded `{ success: boolean, message?: string, data?: object }`
- 400: BadRequest
- 404: NotFound
- 500: InternalServerError

## POST /lead/changeStatus
Change lead status

Sets a new status. The status must belong to the table status template. Set `sendMails` to `true` to run automations.

operationId: `changeLeadStatus`

Request body (required): `LeadStatusChangeRequest`

Responses:
- 200: Status changed `SuccessResponse`
- 400: BadRequest
- 404: NotFound

## POST /lead/move
Move or copy leads

Moves or copies one or more leads to another table. `toCustomerID` must match the target table owner. You can map statuses or set one `newStatus`.

operationId: `moveLeads`

Request body (required): `LeadMoveRequest`

Responses:
- 200: Leads moved or copied `SuccessResponse`
- 400: BadRequest
- 404: NotFound

## GET /lead/search/byEmail/{email}
Search leads by email

Paginated search across agency customers. The email is URL-encoded in the path and matched in lowercase. Results do not include `leadData`.

operationId: `searchLeadByEmail`

Parameters:
- path `email` (string, required) — Email address, URL-encoded
- query `page` (integer, optional)
- query `limit` (integer, optional)

Responses:
- 200: Paginated leads `{ pagination: Pagination, leads: LeadSearchItem[] }`
- 400: BadRequest
- 404: NotFound

## POST /attachWebhook
Attach a webhook

Registers a URL for a topic and layer. For `table` send `campaignID`. For `customer` send `customerID`. Store `externalHookId` so you can remove the webhook later. `layer` defaults to `table`.

operationId: `attachWebhook`

Request body (required): `AttachWebhookRequest`

Responses:
- 200: Webhook attached `{ message?: string, externalHookId: string }`
- 400: BadRequest
- 403: Forbidden
- 404: NotFound

## DELETE /removeWebhook
Detach a webhook

Removes a webhook. Prefer `externalHookId` from attach. Without it, send `url`, `layer`, and `campaignID` (table) or `customerID` (customer). Use `Content-Type: application/json`. Some HTTP clients drop DELETE bodies.

operationId: `removeWebhook`

Request body (required): `RemoveWebhookRequest`

Responses:
- 200: Webhook removed `{ removed: boolean }`
- 400: BadRequest
- 401: Unauthorized

## GET /pollWebhook/{campaignID}/{topic}
Poll a webhook test sample

Returns a one-element array with a shortened test payload. This is not the full live webhook body.

operationId: `pollWebhook`

Parameters:
- path `campaignID` (string, required) — Table (campaign) ID
- path `topic` ("newLead" | "changeStatus" | "updateLead" | "whatsappMessage", required) — Test topic for pollWebhook

Responses:
- 200: Array with one test payload `WebhookTestEvent[]`
- 400: BadRequest
- 404: NotFound

## POST /whatsapp/message/send
Send a WhatsApp message

Sends a text message to a lead or an unassigned conversation. Send either `leadID` or `unassignedConversationID`. The customer must have a WhatsApp number. The 24-hour window and DSGVO rules still apply.

operationId: `sendWhatsappMessage`

Request body (required): `WhatsappSendRequest`

Responses:
- 200: Message sent `SuccessResponse`
- 400: BadRequest
- 404: NotFound

## GET /pages/templates/list
List page templates

Agency page templates with page instance IDs and required variables. Use these IDs in `POST /customer/addPage` or in `pages` on customer create.

operationId: `listPortalTemplates`

Responses:
- 200: Portal templates `{ templates: PortalTemplate[] }`
- 401: Unauthorized

## Models

### SuccessResponse

`{ success: true }`

### Pagination

`{ totalItems: integer, totalPages: integer, currentPage: integer, itemsPerPage: integer }`

### LeadPages

`{ totalLeads: integer, totalPages: integer, currentPage: integer, leadsPerPage: integer }`

### LastLead

`{ _id?: string, createdAt?: string(date-time) }`

### LeadStats

`{ totalLeads?: integer, newLeads?: integer, leadsLast30Days?: integer, lastLead?: LastLead, lastChange?: string(date-time), unusedLeads?: integer, oldLeads?: integer }`

### StatusStats

`{ counts?: { id?: string, count?: integer }[] }`

### WhatsAppStats

`{ unreadMessages?: integer, totalMessages?: integer, lastMessageDate?: string(date-time) }`

### CustomerWhatsAppStats

``

### AgencyStats

`{ agencyCampaignsStats?: { campaignCount?: integer, campaignLast30Days?: integer }, agencyCustomersStats?: { customerCount?: integer, customerLast30Days?: integer }, statusStats?: StatusStats, leadsStats?: LeadStats, whatsAppStats?: CustomerWhatsAppStats }`

### CustomerStats

`{ statusStats?: StatusStats, leadsStats?: LeadStats, campaignCount?: integer, whatsAppStats?: CustomerWhatsAppStats }`

### TableStats

`{ nextRefresh?: string(date-time), leadsStats?: LeadStats, leadsStatsCustomerLogin?: LeadStats, statusStats?: StatusStats, whatsAppStats?: WhatsAppStats, whatsAppStatsCustomerLogin?: WhatsAppStats }`

### Agency

`{ _id: string, email: string(email), domain: string, createdAt: string(date-time), stats: AgencyStats }`

### CustomerListItem

`{ _id?: string, name?: string, archived?: boolean, createdAt?: string(date-time), stats?: CustomerStats }`

### Customer

`{ _id: string, name: string, description: string, archived: boolean, inactive: boolean, createdAt: string(date-time), ownerAgency: string, stats: CustomerStats }`

### CustomerCreateRequest

`{ name: string, description?: string, allowAutomaticSlotIncrement?: boolean, customerLogins?: { email: string(email) }[], subuserAccess?: { all: boolean, subusers?: string[] }, pages?: { pageInstanceID: string, variables: object }[] }`

### PageHelperResultData

`{ pageInstanceID: string }`

### CustomerLoginHelperResultData

`{ email: string(email), magicLink: string, id: string }`

### PageHelperResult

`{ success: boolean, data: PageHelperResultData, errorMessage?: string, statusCode?: integer }`

### CustomerLoginHelperResult

`{ success: boolean, data: CustomerLoginHelperResultData, errorMessage?: string, statusCode?: integer }`

### CustomerCreateResponse

`{ id: string, name: string, description?: string, createdAt: string(date-time), pages?: PageHelperResult[], customerLogins?: CustomerLoginHelperResult[], upgradeResult?: { previousSlotCount?: integer, newSlotCount?: integer } }`

### TableListItem

`{ _id?: string, occupation?: string, archived?: boolean, createdAt?: string(date-time), lastChange?: string(date-time), stats?: TableStats, totalLeads?: integer, leadsCount?: integer, webhookToken?: string }`

### Table

`{ _id: string, occupation: string, archived: boolean, inactive: boolean, createdAt: string(date-time), ownerCustomer: string, ownerAgency: string, stats: TableStats, webhookToken: string }`

### TableCreateRequest

`{ customerID: string, occupation: string, funnelLink?: string, preQualify?: boolean, hiddenStatuses?: string[], deleteLeads?: boolean, status?: { title: string, color: string }[], statusTemplateID?: string }`

### TableCreateResponse

`{ id: string, occupation: string, customerID: string, customerName?: string, webhookToken: string, createdAt: string(date-time) }`

### TableCreateFromTemplateResponse

`{ name: string, tableID: string, webhookToken: string }`

### TableTemplate

`{ id?: string, name?: string, icon?: string }`

### StatusTemplateStatus

`{ id: string, name: string, color: string }`

### StatusTemplate

`{ id: string, name: string, leadInboxStatus: string, statuses: StatusTemplateStatus[] }`

### StatusTemplateList

`{ templates: StatusTemplate[] }`

### ProfileFieldRow

`{ title?: string, value?: string }`

### LeadLeadData

`{ mappedData?: object, rawData?: object, dataArray?: ProfileFieldRow[] }`

### Lead

`{ _id?: string, ownerAgency?: string, ownerCustomer?: string, ownerTable?: string, name?: string, phone?: string, email?: string(email), createdAt?: string(date-time), lastChange?: string(date-time), source?: string, status?: string, description?: string, funnelData?: object, history?: object[], leadData?: LeadLeadData }`

### LeadWrite

`{ _id?: string, ownerAgency?: string, ownerCustomer?: string, ownerTable?: string, name?: string, phone?: string, email?: string(email), createdAt?: string(date-time), source?: string, status?: string, description?: string, funnelData?: object, history?: object[], archived?: boolean, inactive?: boolean }`

### LeadListItem

`{ _id?: string, ownerCustomer?: string, ownerTable?: string, name?: string, phone?: string, email?: string(email), createdAt?: string(date-time), source?: string, status?: string, funnelData?: object, history?: object[], archived?: boolean, inactive?: boolean }`

### LeadSearchItem

`{ _id?: string, ownerCustomer?: string, ownerTable?: string, name?: string, phone?: string, email?: string(email), createdAt?: string(date-time), source?: string, status?: string, description?: string }`

### LeadCreateRequest

`{ campaignID: string, data: { key: string, value?: string }[], notifications?: boolean }`

### UpdateLeadFieldRequest

`{ question: string, answer: string, setVisibleInProfile?: boolean }`

### LeadStatusChangeRequest

`{ leadID: string, campaignID: string, statusID: string, sendMails?: boolean }`

### LeadMoveRequest

`{ mode: "move" | "copy", leadsIDs: string[], fromCampaignID: string, toCampaignID: string, toCustomerID: string, sendMails?: boolean, newStatus?: string, statusReplacement?: { oldStatus: string, newStatus: string }[] }`

### LeadFile

`{ filename: string, filesize: number, mimetype: string, blobURL: string, createdAt?: string(date-time), downloadURL: string }`

### WebhookTopic

`"newLead" | "changeStatus" | "updateLead" | "deleteLead" | "whatsappMessage"`

### WebhookLayer

`"agency" | "customer" | "table"`

### AttachWebhookRequest

`{ url: string, topic: WebhookTopic, layer?: WebhookLayer, campaignID?: string, customerID?: string }`

### RemoveWebhookRequest

`{ externalHookId?: string, url?: string, layer?: WebhookLayer, campaignID?: string, customerID?: string }`

### WebhookTestEvent

`WebhookTestNoLead | WebhookTestNewLead | WebhookTestChangeStatus | WebhookTestUpdateLead | WebhookTestWhatsapp`

### WebhookTestNoLead

`{ event?: "noLead", campaignID?: string, campaignName?: string, customerID?: string, customerName?: string, timestamp?: integer(int64) }`

### WebhookTestNewLead

`{ id: string }`

### WebhookTestChangeStatus

`{ leadID?: string, campaignID?: string, campaignName?: string, customerID?: string, customerName?: string, oldStatus?: string, oldStatusID?: string, newStatus?: string, newStatusID?: string, status?: string, statusID?: string, leadEmail?: string, leadPhone?: string, leadName?: string, responsible?: string, message?: string }`

### WebhookTestUpdateLead

`{ event?: "updateDescription", leadID?: string, campaignID?: string, campaignName?: string, customerID?: string, customerName?: string, status?: string, statusID?: string, leadEmail?: string, leadPhone?: string, leadName?: string, lastChange?: integer(int64), createdAt?: string(date-time), timestamp?: integer(int64) }`

### WebhookTestWhatsapp

`{ leadID?: string, campaignID?: string, campaignName?: string, customerID?: string, customerName?: string, leadEmail?: string, leadPhone?: string, leadName?: string, message?: string }`

### WhatsappSendRequest

`{ message: string, leadID?: string, unassignedConversationID?: string }`

### CustomerAddPageRequest

`{ customerID: string, pageInstanceID: string, variables: object }`

### CustomerRemovePageResponse

`{ pageInstancesToDelete?: { _id?: string, ownerCustomer?: string }[], removeAllInstances?: boolean }`

### PortalTemplate

`{ templateID?: string, name?: string, pages?: { pageInstanceID?: string, name?: string, variables?: object }[] }`

