API Reference: Complete REST API reference for leads, notes, attributes, scoring rules, webhooks, and export
# Attributes
> API reference for dynamic lead attributes — add any custom field to your leads.
Dynamic attributes extend the standard lead schema with arbitrary custom fields. Five types are supported: | Type | Use for | Example value | | -------- | --------------------------------- | ------------------------------- | | `text` | Single string values | `"Italian"` | | `number` | Numeric values (integer or float) | `500` | | `bool` | True/false flags | `true` | | `list` | Arrays of strings | `["CRM", "ERP"]` | | `object` | Nested key-value data | `{ "linkedin": "https://..." }` | *** ## List Attributes [Section titled “List Attributes”](#list-attributes) ```plaintext GET /leads/{id}/attributes ``` **Response `200 OK`:** ```json [ { "id": "uuid", "name": "industry", "type": "text", "value": "Software" }, { "id": "uuid", "name": "employees", "type": "number", "value": 500 }, { "id": "uuid", "name": "verified", "type": "bool", "value": true }, { "id": "uuid", "name": "products", "type": "list", "value": ["CRM", "ERP"] }, { "id": "uuid", "name": "social", "type": "object", "value": { "linkedin": "https://..." } } ] ``` *** ## Create Attribute [Section titled “Create Attribute”](#create-attribute) ```plaintext POST /leads/{id}/attributes ``` ```json { "name": "cuisine", "type": "text", "value": "Italian" } ``` **Response `201 Created`:** Full attribute object. *** ## Update Attribute [Section titled “Update Attribute”](#update-attribute) ```plaintext PUT /leads/{id}/attributes/{attr_id} ``` ```json { "value": "Mediterranean" } ``` **Response `200 OK`:** Updated attribute object. *** ## Delete Attribute [Section titled “Delete Attribute”](#delete-attribute) ```plaintext DELETE /leads/{id}/attributes/{attr_id} ``` **Response `204 No Content`.** *** ## Filtering by attribute [Section titled “Filtering by attribute”](#filtering-by-attribute) Use `attr:field_name` in filter expressions: ```bash ?filter=and.eq.attr:cuisine.Italian ?filter=and.gt.attr:employees.100 ``` See [Filtering](/guides/filtering/) for all supported operators.
# Export Leads — CSV, JSON, XLSX
> Export leads to CSV, JSON, or XLSX — with full filter and sort support.
Export all leads (or a filtered subset) to CSV, JSON, or XLSX. Unlike the List endpoint, export returns **all** matching records in a single response — no pagination, no cursor. Use this for data backups, bulk processing, or feeding external tools. ## Endpoint [Section titled “Endpoint”](#endpoint) ```plaintext GET /leads/export ``` ## Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | Description | | ------------ | ------ | -------------------------------------------------- | | `format` | string | `csv`, `json`, or `xlsx` (required) | | `filter` | string | Same filter expressions as List Leads (repeatable) | | `sort_by` | string | Sort field | | `sort_order` | string | `ASC` or `DESC` | ## Examples [Section titled “Examples”](#examples) ```bash # Export all leads as CSV curl -o leads.csv \ "https://api.klozeo.com/api/v1/leads/export?format=csv" \ -H "X-API-Key: sk_live_your_key" # Export Paris leads with rating ≥ 4, sorted by rating descending curl -o leads.xlsx \ "https://api.klozeo.com/api/v1/leads/export?format=xlsx&filter=and.eq.city.Paris&filter=and.gte.rating.4&sort_by=rating&sort_order=DESC" \ -H "X-API-Key: sk_live_your_key" ``` ## Response [Section titled “Response”](#response) | Format | Content-Type | Notes | | ------ | ------------------------------------------------------------------- | ------------------------------ | | `csv` | `text/csv` | UTF-8 encoded, comma-separated | | `json` | `application/json` | Array of full lead objects | | `xlsx` | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | Excel-compatible |
# Leads
> Full API reference for lead management — CRUD, batch operations, scoring, and export.
**Base URL:** `https://api.klozeo.com/api/v1` **Auth:** `X-API-Key: sk_live_...` required on all requests. ## ID format [Section titled “ID format”](#id-format) Lead IDs use the prefix `cl_` (e.g., `cl_01234567-89ab-cdef-0123-456789abcdef`). Always include the full ID with the prefix in API requests. ## Lead object [Section titled “Lead object”](#lead-object) | Field | Type | Notes | | --------------------- | --------- | ------------------------------------------------------------------------------------------------------ | | `id` | string | Format: `cl_`. Read-only. | | `name` | string | **Required** | | `source` | string | **Required** | | `email` | string | Optional | | `phone` | string | Optional | | `city` | string | Optional | | `website` | string | Optional | | `rating` | number | Optional. Useful in scoring expressions. | | `tags` | string\[] | Optional array | | `score` | number | 0–100. Computed via scoring rules. | | `source_id` | string | Optional. External ID used for deduplication. | | `status` | string | Pipeline status. One of: `new`, `contacted`, `qualified`, `disqualified`, `converted`. Default: `new`. | | `created_at` | integer | Unix seconds. Read-only. | | `updated_at` | integer | Unix seconds. Updated only when a structural field changes. | | `last_interaction_at` | integer | Unix seconds. Updated on every push or merge. | All timestamps are **Unix seconds** (not milliseconds). For JavaScript: `new Date(timestamp * 1000)`. *** ## List Leads [Section titled “List Leads”](#list-leads) ```plaintext GET /leads ``` | Parameter | Type | Default | Description | | ------------ | ------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `filter` | string | — | Repeatable. Format: `logic.operator.field.value`. Example: `filter=and.eq.city.Paris`. See [Filtering guide →](/guides/filtering/) | | `sort_by` | string | — | Field to sort by | | `sort_order` | string | `ASC` | `ASC` or `DESC` | | `limit` | integer | 50 | Max results (max: 1000) | | `cursor` | string | — | Opaque pagination token from previous response | **Response `200 OK`:** ```json { "leads": [{ "id": "cl_...", "name": "Acme", "source": "website", "score": 87, "..." }], "next_cursor": "eyJpZCI6ImNsXzAxMjM...", "has_more": true, "count": 50 } ``` *** ## Create Lead [Section titled “Create Lead”](#create-lead) ```plaintext POST /leads ``` Required fields: `name`, `source`. All other fields are optional. ```json { "name": "Acme Corporation", "source": "website", "email": "contact@acme.com", "city": "San Francisco", "rating": 4.5, "tags": ["enterprise"], "attributes": [ { "name": "industry", "type": "text", "value": "Software" }, { "name": "employees", "type": "number", "value": 500 } ] } ``` Automatic deduplication runs before every insert. The response varies depending on the outcome: ```json // New lead created (201 Created) { "id": "cl_...", "message": "Lead created successfully", "created_at": 1703520000 } // Duplicate detected and merged (200 OK) { "id": "cl_existing...", "message": "Duplicate detected, existing lead updated", "created_at": 1703520000, "duplicate": true } // Low-confidence match — new lead created with a hint (201 Created) { "id": "cl_new...", "message": "Lead created successfully", "potential_duplicate_id": "cl_similar..." } ``` **Deduplication priority:** | Priority | Criterion | Confidence | Action | | -------- | -------------------------------- | ---------- | ----------------------------------- | | 1 | `email` match (case-insensitive) | High | Merge → 200 + `"duplicate": true` | | 2 | `source_id` match (exact) | High | Merge → 200 + `"duplicate": true` | | 3 | `phone` + name similarity > 80% | Medium | Merge → 200 + `"duplicate": true` | | 4 | `name` + `city` match | Low | Create + `"potential_duplicate_id"` | Merge strategy: **Last Touch Wins** — non-empty incoming fields overwrite existing. `last_interaction_at` is always bumped. **Error `403`** when the free plan lead limit (100) is reached. *** ## Get Lead [Section titled “Get Lead”](#get-lead) ```plaintext GET /leads/{id} ``` **Response `200 OK`:** Full lead object (see schema above). **Error `404`** if the lead does not exist or belongs to a different account. *** ## Update Lead [Section titled “Update Lead”](#update-lead) ```plaintext PUT /leads/{id} ``` Partial update — only include fields to change. Returns the updated lead. ```json { "rating": 4.8, "city": "New York", "tags": ["enterprise", "updated"] } ``` **Error `404`** if the lead does not exist or belongs to a different account. *** ## Delete Lead [Section titled “Delete Lead”](#delete-lead) ```plaintext DELETE /leads/{id} ``` **Response `204 No Content`.** **Error `404`** if the lead does not exist or belongs to a different account. *** ## Batch Create [Section titled “Batch Create”](#batch-create) ```plaintext POST /leads/batch ``` > **⚠️ Deduplication not included** Batch create skips duplicate detection for performance. If you need deduplication, use `POST /leads` individually or pre-deduplicate your dataset before importing. Up to **100 leads** (Free) or **500 leads** (Pro) per request. Exceeding the limit returns `400 Bad Request` — the entire request is rejected and no leads are created. ```json { "leads": [ { "name": "Lead 1", "source": "import", "city": "Athens" }, { "name": "Lead 2", "source": "import", "city": "London" } ] } ``` **Response `201 Created`** (all succeeded) or **`207 Multi-Status`** (partial failure): > `207 Multi-Status` means at least one lead failed. **Always check the `errors` array**, even when the request returned 207 and not a 4xx/5xx. Entries in `created` were saved successfully. ```json { "created": [{ "index": 0, "id": "cl_...", "created_at": 1703520000 }], "errors": [{ "index": 1, "message": "Duplicate source_id" }], "total": 2, "success": 1, "failed": 1 } ``` *** ## Batch Update [Section titled “Batch Update”](#batch-update) ```plaintext PUT /leads/batch ``` Apply the same partial update to multiple leads: ```json { "ids": ["cl_aaa...", "cl_bbb..."], "data": { "source": "LinkedIn" } } ``` *** ## Batch Delete [Section titled “Batch Delete”](#batch-delete) ```plaintext DELETE /leads/batch ``` ```json { "ids": ["cl_aaa...", "cl_bbb..."] } ``` *** ## Recalculate Score [Section titled “Recalculate Score”](#recalculate-score) ```plaintext POST /leads/{id}/score ``` Recalculates and persists the score for a single lead based on current scoring rules. *** ## Bulk Recalculate Scores [Section titled “Bulk Recalculate Scores”](#bulk-recalculate-scores) ```plaintext POST /leads/score/batch ``` Recalculates scores for all leads in your account. *** ## Export [Section titled “Export”](#export) ```plaintext GET /leads/export?format=csv ``` See the [Export reference](/api/export/) for full details. *** ## Common errors [Section titled “Common errors”](#common-errors) | Status | Code | Description | | ------ | --------------------- | ---------------------------------------------------------------------------------------- | | 400 | `bad_request` | Invalid request body or parameters (e.g., missing required fields, batch limit exceeded) | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `leads_limit_reached` | Free plan lead limit reached — upgrade to Pro | | 404 | `not_found` | Lead not found or belongs to a different account | | 429 | `rate_limit_exceeded` | Rate limit exceeded — see [Rate Limits](/getting-started/rate-limits/) | | 500 | `internal_error` | Internal server error |
# Lead Notes API Reference
> API reference for lead notes — create, list, update, and delete.
Notes are free-text annotations attached to a lead. Note IDs use the prefix `note_` (e.g., `note_01234567-89ab-cdef-0123-456789abcdef`). All timestamps are Unix seconds. *** ## List Notes [Section titled “List Notes”](#list-notes) ```plaintext GET /leads/{id}/notes ``` **Response `200 OK`:** ```json [ { "id": "note_01234567-89ab-cdef-0123-456789abcdef", "lead_id": "cl_...", "content": "Spoke with CEO, very interested in Pro plan.", "created_at": 1703520000, "updated_at": 1703520000 } ] ``` *** ## Create Note [Section titled “Create Note”](#create-note) ```plaintext POST /leads/{id}/notes ``` ```json { "content": "Follow-up scheduled for next week." } ``` **Response `201 Created`:** Full note object. *** ## Update Note [Section titled “Update Note”](#update-note) ```plaintext PUT /notes/{note_id} ``` ```json { "content": "Follow-up completed. Deal closed." } ``` **Response `200 OK`:** Updated note object. Ownership is verified — you can only update notes that belong to your leads. Returns `404` if the note doesn’t exist or belongs to a different account. *** ## Delete Note [Section titled “Delete Note”](#delete-note) ```plaintext DELETE /notes/{note_id} ``` **Response `204 No Content`.**
# Scoring Rules
> API reference for lead scoring rules — create expression-based rules to score leads 0–100.
Scoring rules evaluate expressions against lead fields and attributes to produce a numeric score (0–100). ## How scores are calculated [Section titled “How scores are calculated”](#how-scores-are-calculated) Each rule produces a value via its `expression`. The final lead score is the **weighted sum** of all rule values, clamped to 0–100: ```plaintext score = clamp(rule1.value × rule1.weight + rule2.value × rule2.weight + ..., 0, 100) ``` **`weight`** lets you control how much each rule contributes. A rule with `weight: 2.0` counts twice as much as one with `weight: 1.0`. Start with `1.0` for all rules and adjust once you see score distributions. See the [Lead Scoring guide](/guides/lead-scoring/) for expression syntax and examples. *** ## List Scoring Rules [Section titled “List Scoring Rules”](#list-scoring-rules) ```plaintext GET /scoring-rules ``` **Response `200 OK`:** Array of scoring rule objects. *** ## Create Scoring Rule [Section titled “Create Scoring Rule”](#create-scoring-rule) ```plaintext POST /scoring-rules ``` ```json { "name": "High rating bonus", "expression": "rating * 10", "weight": 1.0 } ``` **Response `201 Created`:** Full scoring rule object. *** ## Get Scoring Rule [Section titled “Get Scoring Rule”](#get-scoring-rule) ```plaintext GET /scoring-rules/{id} ``` *** ## Update Scoring Rule [Section titled “Update Scoring Rule”](#update-scoring-rule) ```plaintext PUT /scoring-rules/{id} ``` ```json { "expression": "rating * 15", "weight": 1.5 } ``` *** ## Delete Scoring Rule [Section titled “Delete Scoring Rule”](#delete-scoring-rule) ```plaintext DELETE /scoring-rules/{id} ``` **Response `204 No Content`.** *** ## Applying scores [Section titled “Applying scores”](#applying-scores) After creating or modifying rules, recalculate scores: * Single lead: `POST /leads/{id}/score` * All leads: `POST /leads/score/batch`
# Webhooks
> API reference for webhooks — receive real-time events when leads change.
Webhooks deliver HTTP POST notifications to your endpoint when events occur. ## Supported events [Section titled “Supported events”](#supported-events) ### Single-item events [Section titled “Single-item events”](#single-item-events) | Event | Triggered when | | -------------- | ---------------------------- | | `lead.created` | A new lead is created | | `lead.updated` | A lead is updated | | `lead.deleted` | A lead is deleted | | `lead.scored` | A lead score is recalculated | | `note.created` | A note is added to a lead | | `note.updated` | A note is edited | | `note.deleted` | A note is deleted | ### Batch events [Section titled “Batch events”](#batch-events) Batch operations fire one grouped event instead of N individual events. | Event | Triggered when | Payload shape | | --------------- | ------------------------------ | ------------------------------------ | | `leads.created` | `POST /leads/batch` succeeds | `{ ids: [...], count: N }` | | `leads.updated` | `PUT /leads/batch` succeeds | `{ ids: [...], count: N }` | | `leads.deleted` | `DELETE /leads/batch` succeeds | `{ ids: [...], count: N }` | | `leads.scored` | `POST /leads/score/batch` runs | `{ leads: [{id, score}], count: N }` | *** ## List Webhooks [Section titled “List Webhooks”](#list-webhooks) ```plaintext GET /webhooks ``` *** ## Create Webhook [Section titled “Create Webhook”](#create-webhook) ```plaintext POST /webhooks ``` ```json { "url": "https://your-server.com/hooks/klozeo", "events": ["lead.created", "lead.updated"], "secret": "optional-signing-secret" } ``` **Response `201 Created`:** Full webhook object. If a `secret` is provided, each request includes an `X-Klozeo-Signature` header (HMAC-SHA256 of the raw request body, hex-encoded). *** ## Delete Webhook [Section titled “Delete Webhook”](#delete-webhook) ```plaintext DELETE /webhooks/{id} ``` **Response `204 No Content`.** *** ## Payload format [Section titled “Payload format”](#payload-format) All timestamps are **Unix seconds**. ```json { "event": "lead.created", "timestamp": 1703520000, "data": { "id": "cl_...", "name": "Acme", "source": "website", "score": 0, "created_at": 1703520000 } } ``` The `data` object is the full lead (or note) that triggered the event. *** ## Verifying signatures [Section titled “Verifying signatures”](#verifying-signatures) When a `secret` is set, verify each incoming request to prevent spoofed events: ```python import hmac, hashlib def verify_webhook(payload_bytes: bytes, signature_header: str, secret: str) -> bool: expected = hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature_header) # In your request handler: body = request.get_data() # raw bytes, before any parsing sig = request.headers.get("X-Klozeo-Signature", "") if not verify_webhook(body, sig, "your-secret"): return 401 ``` Node.js ```javascript const crypto = require("crypto"); function verifyWebhook(rawBody, signatureHeader, secret) { const expected = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader)); } ``` *** ## Delivery & reliability [Section titled “Delivery & reliability”](#delivery--reliability) * **Your endpoint must return a `2xx` status** within 5 seconds to acknowledge receipt. * **Design your handler to be idempotent** — the same event may be delivered more than once in rare cases (network failures, retries). * If your endpoint is unreachable or returns a non-2xx status, deliveries are retried with exponential backoff.