This is the abridged developer documentation for Klozeo
# Klozeo API Documentation — Lead Database for Developers
> Store, score, and query your leads programmatically. Native MCP integration for Claude, Cursor, and your custom AI workflows.
Klozeo is an API-first database for business leads. Connect your data sources, score leads automatically, and query them in natural language using Claude, Cursor, or any MCP-compatible AI client. REST API Full CRUD for leads, notes, attributes, scoring rules, and webhooks. Cursor-based pagination and 20+ filter operators for precise queries. MCP Integration 18 specialized MCP tools. Connect Claude, Cursor, Cline, Smithery, or any MCP-compatible client — then query your leads in plain English. Lead Scoring Expression-based scoring rules. Automatically score leads 0–100 based on any combination of fields and custom attributes. Auto-Deduplication Push the same lead twice? Klozeo detects duplicates automatically and merges the data — no manual cleanup required. [Learn how →](/guides/deduplication/)
# 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.
# API Changelog — Release Notes
> Release history for the Klozeo API.
## v1.3.0 — April 2026 [Section titled “v1.3.0 — April 2026”](#v130--april-2026) You can now track where each lead stands in your pipeline without opening a dialog or writing a line of code. * **Lead status** — every lead now has a status field: `new`, `contacted`, `qualified`, `disqualified`, `converted`. Update it directly from the leads table with a single click — no form to open, no page to reload. * **Filter by status** — add a status filter to any view to focus on the leads that matter right now. Works with all existing filters and exports. * **API + MCP support** — set or update status via `PATCH /leads/:id`, any of the four SDKs, or directly from your AI assistant with the `update_lead` tool. *** ## v1.2.0 — April 2026 [Section titled “v1.2.0 — April 2026”](#v120--april-2026) The MCP server now exposes **33 tools**, up from 18. New additions: * **Batch operations** — `batch_create_leads`, `batch_update_leads`, `batch_delete_leads`. Create or update up to 500 leads in one call. Deduplication runs per item in batch create, so you can safely re-import data without worrying about duplicates. * **Attributes CRUD** — `list_attributes`, `create_attribute`, `update_attribute`, `delete_attribute`. Manage custom fields on any lead directly from your AI assistant. * **Scoring management** — `create_scoring_rule`, `get_scoring_rule`, `update_scoring_rule`, `delete_scoring_rule`, `update_all_scores`. Build and iterate on scoring rules without opening the dashboard. * **API Key management** — `list_api_keys`, `create_api_key`, `delete_api_key`. * **Note editing** — `update_note` to edit note content after the fact. Batch webhook events (`leads.created`, `leads.updated`, `leads.deleted`, `leads.scored`) fire a single grouped notification per operation instead of N individual ones. → [Full tool reference](/mcp/overview/) *** ## v1.1.0 — April 2026 [Section titled “v1.1.0 — April 2026”](#v110--april-2026) You can now integrate Klozeo directly from your language’s package manager — no raw HTTP needed. * **TypeScript / JavaScript** — `npm install @klozeo/sdk` * **Python** — `pip install klozeo` * **Go** — `go get github.com/lbframe/klozeo-sdk-go` * **Rust** — `cargo add klozeo` All four clients share the same API surface: a fluent filter builder, automatic pagination, batch operations, and typed errors. Pages are fetched transparently as you iterate — you never write cursor logic yourself. → [TypeScript SDK documentation](/sdks/typescript/) · [Python](/sdks/python/) · [Go](/sdks/go/) · [Rust](/sdks/rust/) The MCP integration now exposes 18 tools, up from 13 at launch. The five additions cover scoring and webhooks: you can now list and apply scoring rules, and manage webhook endpoints — all from your AI assistant without leaving the conversation. *** ## v1.0.0 — March 2026 [Section titled “v1.0.0 — March 2026”](#v100--march-2026) Initial release. * **Leads API** — full CRUD: create, read, update, delete. Fields include name, email, phone, city, website, source, rating, tags, score, and custom attributes. * **Notes API** — attach and manage timestamped notes on any lead. * **Attributes API** — extend any lead with typed custom fields (text, number, boolean, list) without schema migrations. * **Scoring Rules API** — define expression-based rules to compute a 0–100 score per lead automatically. * **Webhooks API** — subscribe to lead events and receive real-time HTTP notifications. * **Export** — download your full lead database as CSV, JSON, or XLSX with full filter and sort support. * **Cursor-based pagination** — keyset pagination with `limit`, `cursor`, `sort_by`, and `sort_order` parameters. Opaque cursors, no offset math. * **Filtering** — 20+ operators covering text, numeric, tag arrays, location radius, and dynamic attributes. Multiple filters combine with AND/OR logic. * **Auto-deduplication** — 4-tier duplicate detection on every `POST /leads`: by email, source ID, phone+name similarity, or name+city. Merges use Last Touch Wins strategy. * **Batch operations** — create, update, or delete up to 100 leads per request (500 on Pro). * **MCP integration** — 13 tools at launch for Claude, Cursor, Cline, and any MCP-compatible client. OAuth 2.0 + PKCE authorization — no manual key copy-paste required.
# Frequently Asked Questions
> Frequently asked questions about Klozeo.
## General [Section titled “General”](#general) **What is Klozeo?** Klozeo is an API-first lead database. You store, score, filter, and query business leads via REST API or natural language through MCP-compatible AI clients (Claude, Cursor, Cline, etc.). **What’s the difference between `updated_at` and `last_interaction_at`?** * `updated_at` — changes only when a structural field (name, email, city, etc.) is modified. * `last_interaction_at` — updates on every API push, including merges where no field changed. Use `last_interaction_at` to sort by “freshness of interest” independently of data changes. **Are timestamps in milliseconds or seconds?** All timestamps are **Unix seconds** (not milliseconds). For JavaScript: `new Date(timestamp * 1000)`. ## Authentication [Section titled “Authentication”](#authentication) **Where do I get an API key?** Sign in at [klozeo.com/dashboard](https://klozeo.com/dashboard) → **API Keys** → **Add new**. The raw key is shown once — copy it immediately. **What if I didn’t copy my API key in time?** Delete the key from the dashboard and create a new one — takes 10 seconds. **Can I have multiple API keys?** Yes. Create as many keys as you need (e.g., one per integration). Revoke any key individually without affecting the others. ## Leads [Section titled “Leads”](#leads) **What happens when I push a duplicate lead?** Klozeo runs 4-tier deduplication automatically. High-confidence matches (email, source\_id) are merged using “Last Touch Wins”. See the [Deduplication guide](/guides/deduplication/). **Does batch create run deduplication?** No — `POST /leads/batch` skips duplicate detection for performance. Use individual `POST /leads` calls if you need deduplication, or pre-deduplicate your dataset before importing. **What is the free plan limit?** Free accounts can store up to **100 leads** and make **100 API requests per 10 minutes**. Upgrade to Pro for up to 500 leads and 1,000 requests/10 min. Need more than 500 leads? [Contact us](mailto:hello@klozeo.com) about an Enterprise plan with custom limits. **Can I import leads in bulk?** Yes — use `POST /leads/batch` (up to 100 on Free, 500 on Pro) or import CSV/JSON directly from the dashboard. **What happens if I hit the lead limit?** New leads return `403 leads_limit_reached`. Existing leads and all other operations continue normally. Upgrade to Pro to increase your limit. ## MCP [Section titled “MCP”](#mcp) **Which AI clients are supported?** Claude Desktop, Claude Code, Cursor, Cline, Roo Code, Smithery, and Goose. Any MCP-compatible client that supports HTTP or SSE transports will work. **Do I need to share my API key with the AI client?** For most clients (Claude Code, Cursor, Cline, Goose): no — a browser-based authorization flow generates and stores the key automatically. Claude Desktop currently requires a manual key in its config. ## Pricing & billing [Section titled “Pricing & billing”](#pricing--billing) **Is a credit card required for the free plan?** No. The free plan requires no payment information. **How do I upgrade from Free to Pro?** Go to Settings in your dashboard and click “Upgrade to Pro”. You’ll be redirected to the Stripe checkout. Your plan is activated immediately after payment. **What are the differences between Free and Pro?** Free: 100 leads, 100 API requests per 10 minutes. Pro: 500 leads, 1,000 API requests per 10 minutes. **Can I cancel my subscription?** Yes, at any time from Settings → Billing. You keep Pro access until the end of the billing period. **What payment methods are accepted?** Credit and debit cards via Stripe (Visa, Mastercard, American Express).
# Authentication
> How to authenticate with the Klozeo API using API keys.
All Klozeo API endpoints (except `/register` and `/login`) require authentication via an API key. ## Get your API key [Section titled “Get your API key”](#get-your-api-key) Sign in to your dashboard at [klozeo.com/dashboard](https://klozeo.com/dashboard), navigate to **API Keys**, and click **Add new**. Your API key is shown **once** at creation time — Klozeo stores only a hashed version for security. Copy it immediately and save it somewhere safe (a password manager, an `.env` file). **Missed it?** Delete the key and create a new one — takes 10 seconds. ## Using your API key [Section titled “Using your API key”](#using-your-api-key) Pass the key in the `X-API-Key` header on every request: ```bash curl -H "X-API-Key: sk_live_your_api_key_here" \ https://api.klozeo.com/api/v1/leads ``` ## Key format [Section titled “Key format”](#key-format) All API keys follow the format `sk_live_<16-byte hex>` (example: `sk_live_a1b2c3d4e5f60718`). ## MCP / OAuth clients [Section titled “MCP / OAuth clients”](#mcp--oauth-clients) **Using an AI client (Claude, Cursor, Cline…)?** You don’t need to touch your API key. MCP clients use a browser-based authorization flow — sign in to your Klozeo account once and the client stores a key automatically. See [MCP Setup →](/mcp/overview/) ## Security [Section titled “Security”](#security) * Store your key in an environment variable — never hardcode it in source code. * Revoke compromised keys immediately from the dashboard — any other keys you’ve created will continue working. * All API keys give full access to your account data. If you need isolation between integrations, create one key per integration and revoke them independently.
# Your first request
> Make your first API call to Klozeo in under 2 minutes.
1. **Create a free account** at [klozeo.com](https://klozeo.com) and generate an API key. 2. **Create your first lead:** ```bash curl -X POST https://api.klozeo.com/api/v1/leads \ -H "X-API-Key: sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Corporation", "source": "website", "email": "contact@acme.com", "city": "San Francisco" }' ``` Response (`201 Created`): ```json { "id": "cl_01234567-89ab-cdef-0123-456789abcdef", "name": "Acme Corporation", "source": "website", "email": "contact@acme.com", "city": "San Francisco", "score": 0, "created_at": 1703520000, "updated_at": 1703520000, "last_interaction_at": 1703520000 } ``` A few things to note in this response: * **`score: 0`** is expected — scores are calculated once you [configure scoring rules](/guides/lead-scoring/). * **Timestamps** (`created_at`, `updated_at`, `last_interaction_at`) are **Unix seconds** (not milliseconds). For JavaScript: `new Date(1703520000 * 1000)` → December 25, 2023. 3. **List your leads:** ```bash curl https://api.klozeo.com/api/v1/leads \ -H "X-API-Key: sk_live_your_key" ``` 4. **Filter by city:** ```bash curl "https://api.klozeo.com/api/v1/leads?filter=and.eq.city.San+Francisco" \ -H "X-API-Key: sk_live_your_key" ``` ## Base URL [Section titled “Base URL”](#base-url) All API endpoints are prefixed with: ```plaintext https://api.klozeo.com/api/v1 ``` ## Next steps [Section titled “Next steps”](#next-steps) * [Learn filtering](/guides/filtering/) — 20+ operators including `within_radius` for geo search * [Set up MCP](/mcp/overview/) — let Claude query your leads in natural language * [Configure scoring rules](/guides/lead-scoring/) — automatically score leads 0–100
# Introduction
> What Klozeo is, what problems it solves, and how to navigate this documentation.
Klozeo is an **API-first lead database** built for teams that want to manage, score, and query their business leads programmatically — including through AI assistants like Claude and Cursor. If you’ve ever found yourself juggling lead data across spreadsheets, CRMs, and custom scripts, Klozeo gives you a single, structured place to store that data and a powerful API to work with it. *** ## What problem does Klozeo solve? [Section titled “What problem does Klozeo solve?”](#what-problem-does-klozeo-solve) Most tools that store lead data were designed for humans — forms, tables, manual imports. They’re hard to automate, hard to query precisely, and impossible to connect to AI workflows without significant glue code. Klozeo is designed for a different world: * **Your pipeline pushes leads via API** — from scrapers, webhooks, or integrations * **Your rules score them automatically** — based on criteria you define in plain expressions * **Your AI assistant queries them in natural language** — using the built-in MCP integration You stay in control. Klozeo handles the storage, deduplication, scoring, and exposure. *** ## Core concepts [Section titled “Core concepts”](#core-concepts) Before diving into the API, here are the five concepts you’ll encounter everywhere: ### Leads [Section titled “Leads”](#leads) A lead is any business or person you want to track. Each lead has standard fields (name, email, phone, website, city…) plus any custom fields you add via **attributes**. Leads are identified by a prefixed ID: `cl_`. ### Attributes [Section titled “Attributes”](#attributes) Attributes are **custom fields** you attach to a lead. They let you extend the default schema without any migrations — add `cuisine`, `employee_count`, `last_meeting_date`, or any field your workflow needs. ### Scoring rules [Section titled “Scoring rules”](#scoring-rules) A scoring rule is an **expression** that assigns a numeric weight to a lead property. Klozeo evaluates all your rules and computes a score from 0 to 100 for each lead. Example: *“Add 20 points if the lead has a website. Add 30 more if their city is Paris.”* ### Deduplication [Section titled “Deduplication”](#deduplication) When you push a new lead, Klozeo checks automatically whether it already exists — by email, source ID, phone, or name+city — and merges it rather than creating a duplicate. You get clean data without extra logic on your side. ### MCP integration [Section titled “MCP integration”](#mcp-integration) MCP (Model Context Protocol) lets AI clients like Claude or Cursor connect directly to Klozeo and query your leads in natural language. Instead of writing API calls, you ask: *“Show me the top 10 leads in Berlin with a score above 70.”* *** ## How the documentation is organized [Section titled “How the documentation is organized”](#how-the-documentation-is-organized) | Section | What you’ll find | | ------------------- | -------------------------------------------------------------------- | | **Getting Started** | Authentication, your first API call, rate limits | | **API Reference** | Every endpoint, parameter, and response format | | **Guides** | Deep dives: filtering, pagination, deduplication, batch ops, scoring | | **MCP Integration** | How to connect Claude, Cursor, Cline, and other AI clients | | **SDKs** | Official TypeScript, Python, Go, and Rust client libraries | | **Resources** | FAQ and changelog | *** ## Where to start [Section titled “Where to start”](#where-to-start) **I want to make my first API call →** [Authentication](/getting-started/authentication/) then [Your first request](/getting-started/first-request/) **I want to connect Claude or Cursor →** [MCP Overview](/mcp/overview/) **I want to understand how scoring works →** [Lead Scoring guide](/guides/lead-scoring/) **I want to push leads in bulk →** [Batch Operations](/guides/batch-operations/) **I have a specific question →** [FAQ](/faq/) *** ## API at a glance [Section titled “API at a glance”](#api-at-a-glance) ```plaintext Base URL: https://api.klozeo.com/api/v1 Auth: X-API-Key: Format: JSON ``` All endpoints follow REST conventions. Errors return a consistent `{ "error": "code", "message": "..." }` shape. Lead IDs are prefixed (`cl_`), note IDs are prefixed (`note_`), everything else is a plain UUID.
# LLM Context Files
> Machine-readable documentation files for LLMs and AI agents — llms.txt, llms-full.txt, and llms-small.txt.
Klozeo publishes machine-readable documentation at build time so that AI assistants and agents can ingest it directly, without scraping HTML. This is based on the [llmstxt.org](https://llmstxt.org/) convention. *** ## Available files [Section titled “Available files”](#available-files) | File | URL | What’s in it | | ---------------- | ------------------------------------------------------------------------ | ----------------------------------------------------- | | `llms.txt` | [docs.klozeo.com/llms.txt](https://docs.klozeo.com/llms.txt) | Index — links to all other files with descriptions | | `llms-full.txt` | [docs.klozeo.com/llms-full.txt](https://docs.klozeo.com/llms-full.txt) | Complete documentation, all pages in markdown | | `llms-small.txt` | [docs.klozeo.com/llms-small.txt](https://docs.klozeo.com/llms-small.txt) | Compact version — tips, notes, and whitespace removed | ### Category subsets [Section titled “Category subsets”](#category-subsets) For large context windows where you only need a specific area: | File | URL | Content | | --------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | API Reference | [docs.klozeo.com/\_llms-txt/api-reference.txt](https://docs.klozeo.com/_llms-txt/api-reference.txt) | Leads, Notes, Attributes, Scoring Rules, Webhooks, Export | | MCP Integration | [docs.klozeo.com/\_llms-txt/mcp-integration.txt](https://docs.klozeo.com/_llms-txt/mcp-integration.txt) | Claude Desktop, Claude Code, Cursor, Cline setup guides | | SDKs | [docs.klozeo.com/\_llms-txt/sdks.txt](https://docs.klozeo.com/_llms-txt/sdks.txt) | TypeScript, Python, Go, Rust SDK documentation | *** ## When to use which file [Section titled “When to use which file”](#when-to-use-which-file) **Building something with the full API** → use `llms-full.txt` or the `api-reference.txt` subset. **Tight context window** → use `llms-small.txt` — same coverage, \~40% smaller. **Connecting via MCP** → use `mcp-integration.txt` — has all the setup steps for every supported client. **Asking an AI assistant a quick question** → point it at `llms.txt` first — it will pick the right subset. *** ## Usage examples [Section titled “Usage examples”](#usage-examples) ### Passing context in a prompt [Section titled “Passing context in a prompt”](#passing-context-in-a-prompt) ```plaintext Read https://docs.klozeo.com/llms-full.txt then help me implement lead filtering with the Go SDK. ``` ### Claude Code / cursor rules [Section titled “Claude Code / cursor rules”](#claude-code--cursor-rules) Add to your project’s `CLAUDE.md` or `.cursor/rules`: ```markdown Klozeo API docs: https://docs.klozeo.com/llms-full.txt ``` ### Using the MCP server [Section titled “Using the MCP server”](#using-the-mcp-server) If you’ve set up the [Klozeo MCP integration](/mcp/overview/), your AI client already has live access to your lead data. The context files complement MCP — they document the API itself, not your data.
# Rate Limits
> Understanding Klozeo API rate limits per plan.
The API allows a fixed number of requests per 10-minute window, per API key. The window slides continuously — it doesn’t reset on a fixed clock interval. **In practice:** if you make 100 requests in the first minute on the Free plan, you must wait \~9 minutes before your first request exits the window and you can send another. ## Limits by plan [Section titled “Limits by plan”](#limits-by-plan) | Plan | Requests | Window | | ---- | -------- | ---------- | | Free | 100 | 10 minutes | | Pro | 1,000 | 10 minutes | ## Response headers [Section titled “Response headers”](#response-headers) Every API response includes rate limit headers: | Header | Description | | ----------------------- | ----------------------------------------------------------------- | | `X-RateLimit-Limit` | Max requests allowed in the window | | `X-RateLimit-Remaining` | Requests remaining in the current window | | `Retry-After` | Seconds to wait before retrying (only present on `429` responses) | ## Rate limit exceeded [Section titled “Rate limit exceeded”](#rate-limit-exceeded) When the limit is exceeded, the API returns `429 Too Many Requests`: ```json { "error": "rate_limit_exceeded", "message": "Rate limit exceeded", "code": "rate_limit_exceeded" } ``` Unauthenticated requests (missing `X-API-Key`) are not counted — they return `401` directly. ## Handling 429 in code [Section titled “Handling 429 in code”](#handling-429-in-code) Read the `Retry-After` header and pause before retrying. Use exponential backoff for repeated 429s: ```python import time, requests def api_call_with_retry(url, headers, max_retries=3): for attempt in range(max_retries): resp = requests.get(url, headers=headers) if resp.status_code == 429: retry_after = int(resp.headers.get("Retry-After", 60)) time.sleep(retry_after * (2 ** attempt)) # exponential backoff continue return resp raise Exception("Rate limit retries exhausted") ``` ## Upgrade [Section titled “Upgrade”](#upgrade) Upgrade to Pro from your [dashboard](https://klozeo.com/dashboard) to get 10× the rate limit.
# Batch Operations
> Create, update, or delete up to 500 leads in a single request.
Batch endpoints reduce API calls and improve throughput for bulk operations. ## Limits [Section titled “Limits”](#limits) | Plan | Max per batch | | ---- | ------------- | | Free | 100 | | Pro | 500 | Exceeding the limit returns `400 Bad Request` — the entire request is rejected and no leads are created. ## Batch Create [Section titled “Batch Create”](#batch-create) ```plaintext POST /leads/batch ``` Returns `201 Created` if all succeed, `207 Multi-Status` on partial failure. > `207 Multi-Status` means at least one lead failed. **Always check the `errors` array** even when the status is 207. Entries in `created` were saved successfully. ```json { "leads": [ { "name": "Lead 1", "source": "import", "city": "Athens" }, { "name": "Lead 2", "source": "import", "city": "Berlin" } ] } ``` Deduplication runs per item (same logic as single `POST /leads`). Duplicate merges count as `success` but do not consume a slot from your plan’s leads limit. Response: ```json { "created": [ { "index": 0, "id": "cl_...", "created_at": 1703520000 }, { "index": 1, "id": "cl_existing...", "created_at": 1703510000, "duplicate": true } ], "errors": [{ "index": 2, "message": "name is required" }], "total": 3, "success": 2, "failed": 1 } ``` The `duplicate: true` field on a created item means the incoming lead was merged into an existing record — no new lead was created. ## 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...", "cl_ccc..."], "data": { "source": "Website", "tags": ["reviewed"] } } ``` ## Batch Delete [Section titled “Batch Delete”](#batch-delete) ```plaintext DELETE /leads/batch ``` ```json { "ids": ["cl_aaa...", "cl_bbb..."] } ``` ## Webhooks [Section titled “Webhooks”](#webhooks) Batch operations fire a single grouped webhook event instead of one per lead: | Operation | Event | Payload | | ------------ | --------------- | ------------------------------------ | | Batch create | `leads.created` | `{ ids: [...], count: N }` | | Batch update | `leads.updated` | `{ ids: [...], count: N }` | | Batch delete | `leads.deleted` | `{ ids: [...], count: N }` | | Bulk rescore | `leads.scored` | `{ leads: [{id, score}], count: N }` | Single-lead operations still fire the singular events (`lead.created`, `lead.updated`, etc.). ## Tips [Section titled “Tips”](#tips) * Use `207` responses to identify and retry failed entries by `index`. * Items that are deduplicated (merged) count as `success` with `duplicate: true` — they don’t fail and don’t consume a leads slot. * For very large datasets (10k+ leads), split into multiple batch requests and respect your plan’s [rate limits](/getting-started/rate-limits/).
# Deduplication
> How Klozeo automatically detects and merges duplicate leads.
Every `POST /leads` request runs automatic duplicate detection before inserting. This prevents polluting your database with duplicate entries from different sources. ## Detection priority [Section titled “Detection priority”](#detection-priority) | Priority | Criterion | Confidence | Action | | -------- | ---------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------- | | 1 | `email` match (case-insensitive) | High | Merge into existing | | 2 | `source_id` exact match | High | Merge into existing | | 3 | `phone` match + name similarity > 80% | Medium | Merge into existing (similarity uses normalized string comparison — “Acme Corp” and “ACME Corporation” may not match) | | 4 | `name` + `city` match (case-insensitive) | Low | Create new, flag with `potential_duplicate_id` | ## Merge strategy: Last Touch Wins [Section titled “Merge strategy: Last Touch Wins”](#merge-strategy-last-touch-wins) When a duplicate is detected at priority 1–3: * Non-empty fields from the **incoming** request **overwrite** the existing lead. * Fields not present in the request are **left unchanged**. * `last_interaction_at` is **always bumped** — even if no field changed. This models “freshness of interest”: every new push from a source registers as a new interaction. ## Response on duplicate [Section titled “Response on duplicate”](#response-on-duplicate) ```json { "id": "cl_existing...", "message": "Duplicate detected, existing lead updated", "created_at": 1703520000, "duplicate": true } ``` HTTP status is `200 OK` (not 201) on a merge. ## Potential duplicate (low confidence) [Section titled “Potential duplicate (low confidence)”](#potential-duplicate-low-confidence) On a name+city match only, a new lead is created but the response includes a hint: ```json { "id": "cl_new...", "potential_duplicate_id": "cl_similar...", "message": "Lead created successfully" } ``` You can review these in the dashboard: navigate to **Leads**, search for the `potential_duplicate_id`, and manually merge or dismiss. ## `updated_at` vs `last_interaction_at` [Section titled “updated\_at vs last\_interaction\_at”](#updated_at-vs-last_interaction_at) | Field | Updates when | | --------------------- | --------------------------------------------------------- | | `updated_at` | A structural field is modified (name, email, city, etc.) | | `last_interaction_at` | Any push arrives — including merges with no field changes | Use `sort_by=last_interaction_at&sort_order=DESC` to find the leads with the most recent activity, independent of data changes.
# Filter Operators — Complete Reference
> Complete reference for all 20+ filter operators in Klozeo.
All list and export endpoints accept repeatable `filter` query parameters. ## Format [Section titled “Format”](#format) ```plaintext filter=logic.operator.field.value ``` | Part | Values | | ---------- | --------------------------------------------------------------------- | | `logic` | `and`, `or` | | `operator` | See tables below | | `field` | Any lead field (see list below), or `attr:name` for custom attributes | | `value` | Filter value (omit for no-value operators like `is_empty`) | ### How `and` / `or` logic works [Section titled “How and / or logic works”](#how-and--or-logic-works) Each filter carries its own logic prefix — there is no global “AND mode” or “OR mode”. * Filters prefixed with `and` **must all match** (like SQL `AND`). * Filters prefixed with `or` match if **any one** of them is true (like SQL `OR`). * When mixing both: all `and` filters must pass **and** at least one `or` filter must pass. | Example | Meaning | | ------------------------------------------------------------ | -------------------------------- | | `filter=and.eq.city.Paris` | city must be Paris | | `filter=and.eq.city.Paris&filter=and.gte.rating.4` | city is Paris **AND** rating ≥ 4 | | `filter=and.is_not_empty.email&filter=or.is_not_empty.phone` | has email **OR** has phone | ## Filterable fields [Section titled “Filterable fields”](#filterable-fields) | Field | Type | Notes | | ------------- | -------- | --------------------------------------- | | `name` | text | | | `email` | text | | | `phone` | text | | | `city` | text | | | `source` | text | | | `source_id` | text | | | `website` | text | | | `rating` | number | | | `score` | number | Computed score 0–100 | | `tags` | array | Use array operators | | `location` | location | Use location operators | | `attr:` | varies | Custom attribute (e.g., `attr:cuisine`) | ## Text operators [Section titled “Text operators”](#text-operators) | Operator | Description | | -------------- | -------------------------------------- | | `eq` | Equals (case-insensitive) | | `neq` | Not equals | | `contains` | Contains substring | | `not_contains` | Does not contain | | `starts_with` | Starts with | | `ends_with` | Ends with | | `is_empty` | Null or empty string | | `is_not_empty` | Not null and not empty | | `in` | Equals one of (comma-separated values) | ## Numeric operators [Section titled “Numeric operators”](#numeric-operators) | Operator | Description | | -------- | --------------------- | | `eq` | Equals | | `neq` | Not equals | | `gt` | Greater than | | `gte` | Greater than or equal | | `lt` | Less than | | `lte` | Less than or equal | ## Array operators (for `tags`) [Section titled “Array operators (for tags)”](#array-operators-for-tags) | Operator | Description | | -------------------- | ---------------------------- | | `array_contains` | Array contains value | | `array_not_contains` | Array does not contain value | | `array_empty` | Array is empty | | `array_not_empty` | Array is not empty | ## Location operators (for `location`) [Section titled “Location operators (for location)”](#location-operators-for-location) | Operator | Description | Value format | | --------------- | ---------------------- | ------------ | | `within_radius` | Within N km of a point | `lat,lng,km` | | `is_set` | Has coordinates | — | | `is_not_set` | Missing coordinates | — | ## Custom attributes [Section titled “Custom attributes”](#custom-attributes) Use `attr:field_name` as the field: ```bash ?filter=and.eq.attr:cuisine.Italian ?filter=and.gt.attr:employees.100 ``` ## Examples [Section titled “Examples”](#examples) ```bash # City = Athens AND rating >= 4 ?filter=and.eq.city.Athens&filter=and.gte.rating.4 # Has email OR has phone ?filter=and.is_not_empty.email&filter=or.is_not_empty.phone # Tagged as "restaurant" ?filter=and.array_contains.tags.restaurant # Within 5km of Athens city center # Value format: lat,lng,radius_km ?filter=and.within_radius.location.37.98,23.73,5 # Source is either "website" or "api" ?filter=and.in.source.website,api # Leads with a custom "cuisine" attribute = Italian ?filter=and.eq.attr:cuisine.Italian ```
# Lead Scoring
> Automatically score leads 0–100 using expression-based rules.
Lead scoring assigns a numeric score (0–100) to each lead based on configurable rules. Scores are stored and queryable. ## How it works [Section titled “How it works”](#how-it-works) 1. Create one or more scoring rules — each rule is a mathematical expression. 2. Each rule produces a value; all rule values are combined (weighted sum, clamped to 0–100). 3. Trigger recalculation for a single lead or all leads. ## Create a rule [Section titled “Create a rule”](#create-a-rule) ```bash curl -X POST https://api.klozeo.com/api/v1/scoring-rules \ -H "X-API-Key: sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Rating score", "expression": "rating * 10", "weight": 1.0 }' ``` ## Expression reference [Section titled “Expression reference”](#expression-reference) Expressions can reference any standard lead field by name, or custom attributes using `attr_`. | Type | Syntax | Example | | ---------------- | -------------------------------------------- | --------------------- | | Arithmetic | `+`, `-`, `*`, `/` | `rating * 10` | | Comparison | `>`, `>=`, `<`, `<=`, `==` | `rating > 4` | | Ternary | `condition ? value_if_true : value_if_false` | `rating > 4 ? 30 : 0` | | Standard field | Field name as-is | `rating` | | Custom attribute | `attr_` | `attr_employees` | | Missing field | Returns `0` if the field is null or absent | — | ### Examples [Section titled “Examples”](#examples) ```plaintext rating * 20 attr_review_count > 100 ? 30 : 0 (rating * 10) + (attr_review_count > 50 ? 20 : 0) attr_employees > 500 ? 40 : 20 ``` ## Recalculate [Section titled “Recalculate”](#recalculate) ```bash # Single lead curl -X POST https://api.klozeo.com/api/v1/leads/cl_.../score \ -H "X-API-Key: sk_live_your_key" # All leads curl -X POST https://api.klozeo.com/api/v1/leads/score/batch \ -H "X-API-Key: sk_live_your_key" ``` ## Filter by score [Section titled “Filter by score”](#filter-by-score) ```bash # Leads with score >= 70 ?filter=and.gte.score.70 # High-value leads (score >= 80, has email) ?filter=and.gte.score.80&filter=and.is_not_empty.email ``` ## Score display in dashboard [Section titled “Score display in dashboard”](#score-display-in-dashboard) The dashboard uses the following thresholds to color-code lead scores: | Score | Color | | ----- | ----- | | ≥ 70 | Green | | ≥ 40 | Amber | | < 40 | Red |
# Use Klozeo in your n8n workflows
> Replace Airtable as your lead database in n8n — real write-back, built-in deduplication, and a clean REST API with no SQL nodes.
If you’re running n8n workflows that push leads into Airtable, you’ve hit this wall: Airtable works fine as a read source, but writing back — updating a field after an enrichment step, marking a lead as qualified after a scoring run — is fragile. You’re fighting rate limits, patchy UPSERT support, and an API that wasn’t designed for programmatic deduplication. Klozeo is a REST API built specifically for this. Same HTTP Request nodes in n8n, no SQL required, deduplication on every write. *** ## Authentication [Section titled “Authentication”](#authentication) Add an `X-API-Key` header to every request. Get your key from the dashboard under **API Keys**. In n8n, store it as a **Credential** (HTTP Header Auth): | Field | Value | | ----- | ------------- | | Name | `X-API-Key` | | Value | `sk_live_...` | *** ## Create or update a lead [Section titled “Create or update a lead”](#create-or-update-a-lead) `POST /leads` runs deduplication before every insert. If a lead with the same email already exists, it merges instead of creating a duplicate — and returns the existing lead’s ID. **n8n HTTP Request node:** | Setting | Value | | ------- | ------------------------------------- | | Method | POST | | URL | `https://api.klozeo.com/api/v1/leads` | | Auth | HTTP Header Auth (your credential) | | Body | JSON | ```json { "name": "{{ $json.company_name }}", "source": "n8n-webhook", "email": "{{ $json.email }}", "city": "{{ $json.city }}", "source_id": "{{ $json.external_id }}" } ``` Use `source_id` to pass your external system’s ID — Klozeo uses it as a second dedup key. Safe to call on every workflow run. **Response when created (201):** ```json { "id": "cl_...", "message": "Lead created successfully" } ``` **Response when merged (200):** ```json { "id": "cl_existing...", "message": "Duplicate detected, existing lead updated", "duplicate": true } ``` Either way, `id` is valid and you can use it in downstream nodes. *** ## Update a lead [Section titled “Update a lead”](#update-a-lead) Once you have a `cl_` ID — from a previous create step, a webhook, or a lookup — you can patch any field: ```plaintext PUT https://api.klozeo.com/api/v1/leads/{{ $json.id }} ``` ```json { "status": "qualified", "rating": 4.5 } ``` Only include fields you want to change. `status` accepts: `new`, `contacted`, `qualified`, `disqualified`, `converted`. This is the write-back that Airtable makes painful. One node, no workarounds. *** ## Filter leads [Section titled “Filter leads”](#filter-leads) ```plaintext GET https://api.klozeo.com/api/v1/leads?filter=and.eq.status.new&filter=and.gte.score.60&limit=100 ``` Filter format: `logic.operator.field.value`. Chain multiple `filter` params with `&`. Common operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `is_empty`, `not_empty`. In n8n, set the URL with **Query Parameters** in the node: | Parameter | Value | | --------- | ------------------- | | `filter` | `and.eq.status.new` | | `filter` | `and.gte.score.60` | | `limit` | `100` | n8n sends both `filter` params — Klozeo combines them with AND logic. Paginate with `cursor` from the response’s `next_cursor` field. Use a **Loop** node + an **IF** node on `has_more` to walk all pages. *** ## Batch create [Section titled “Batch create”](#batch-create) If you’re importing a list — form submissions, scraped data, a CSV — use batch create: ```plaintext POST https://api.klozeo.com/api/v1/leads/batch ``` ```json { "leads": [ { "name": "Acme", "source": "import", "email": "contact@acme.com" }, { "name": "Globex", "source": "import", "email": "info@globex.com" } ] } ``` Up to 100 leads per request (Free) or 500 (Pro). Check the `errors` array in the response — a `207 Multi-Status` means some failed, not all. *** ## Migrate from Airtable [Section titled “Migrate from Airtable”](#migrate-from-airtable) **Option 1 — CSV import (quickest)** 1. Export your Airtable base as CSV. 2. In Klozeo: **Import** → upload the CSV → map your columns. 3. Deduplication runs on every row — safe to re-import if you’re unsure. **Option 2 — n8n workflow** Useful if you want to migrate incrementally or keep both in sync during a transition. 1. **Airtable node** → list all records. 2. **Split In Batches** (100 at a time). 3. **HTTP Request** → `POST /leads/batch` to Klozeo. 4. Optional: store the Airtable record ID in `source_id` so Klozeo can match them on future runs. *** ## What you gain [Section titled “What you gain”](#what-you-gain) Switching Klozeo in as your write target: * **Dedup on every write** — no more `UPSERT` gymnastics or checking-then-inserting in two nodes. * **Write-back without friction** — update status, rating, or any field from any point in your workflow. * **Scoring rules** — define expressions once, scores update automatically on every lead change. No formula columns to maintain. * **Webhooks** — subscribe to `lead.created`, `lead.updated` etc. to trigger downstream workflows instead of polling. → [API Reference — Leads](/api/leads/) · [Filtering guide](/guides/filtering/) · [Scoring Rules](/api/scoring-rules/)
# Cursor-Based Pagination Guide
> How cursor-based pagination works in Klozeo.
Klozeo uses **cursor-based (keyset) pagination** — not offset-based. This prevents duplicate or skipped results when data changes between pages. ## Parameters [Section titled “Parameters”](#parameters) | Parameter | Default | Max | Description | | --------- | ------- | ---- | ----------------------------------- | | `limit` | 50 | 1000 | Results per page | | `cursor` | — | — | Opaque token from previous response | ## Response fields [Section titled “Response fields”](#response-fields) ```json { "leads": [...], "next_cursor": "eyJpZCI6ImNsXzAxMjM...", "has_more": true, "count": 50 } ``` | Field | Description | | ------------- | -------------------------------------------------------- | | `next_cursor` | Pass as `cursor` in next request. Null if no more pages. | | `has_more` | `true` if there are more results | | `count` | Number of results in this page | ## Usage [Section titled “Usage”](#usage) ```bash # First page curl "https://api.klozeo.com/api/v1/leads?limit=50" \ -H "X-API-Key: sk_live_your_key" # Next page curl "https://api.klozeo.com/api/v1/leads?limit=50&cursor=eyJpZCI6ImNsXzAxMjM..." \ -H "X-API-Key: sk_live_your_key" ``` ## Important [Section titled “Important”](#important) * The cursor is **opaque** — do not parse or construct it manually. * Cursors encode the current sort position. Changing `sort_by` or `sort_order` invalidates the cursor. * Filters are preserved across pages — pass the same `filter` params with each request. * For complete dataset export without pagination, use the [Export endpoint](/api/export/) instead.
# Claude Code
> Connect Claude Code CLI to your Klozeo lead database via MCP.
Claude Code supports MCP servers natively. Klozeo uses the streamable HTTP transport (preferred). 1. Add the Klozeo MCP server to your Claude Code config: ```bash claude mcp add klozeo --transport http https://mcp.klozeo.com/mcp ``` On first connection, Claude Code opens a browser for OAuth authorization. Sign in with your Klozeo account — an API key is issued automatically. 2. Verify the connection: ```bash claude mcp list ``` You should see `klozeo` with status `connected` and 33 tools. 3. Use Klozeo tools in any Claude Code session: ```plaintext /mcp klozeo list_leads query="restaurants in Paris" ``` Or simply describe what you need in natural language — Claude will use the appropriate MCP tool automatically. ## Manual API key setup [Section titled “Manual API key setup”](#manual-api-key-setup) If you prefer to use an existing API key instead of OAuth: ```bash claude mcp add klozeo \ --transport http \ --header "Authorization: Bearer sk_live_your_key" \ https://mcp.klozeo.com/mcp ```
# Claude Desktop
> Connect Claude Desktop to your Klozeo lead database via MCP.
> **Note:** Claude Desktop requires a manually configured API key — browser-based OAuth is not yet supported by Claude Desktop’s MCP implementation. 1. Open Claude Desktop settings → **Developer** → **Edit Config**. 2. Add the Klozeo MCP server to `claude_desktop_config.json`: ```json { "mcpServers": { "klozeo": { "type": "http", "url": "https://mcp.klozeo.com/mcp", "headers": { "Authorization": "Bearer sk_live_your_key" } } } } ``` Replace `sk_live_your_key` with your actual API key from the [dashboard](https://klozeo.com/dashboard). 3. Restart Claude Desktop. 4. In any new conversation, you’ll see **Klozeo** listed in the tools/context area. You should see 33 tools listed — if you don’t, see Troubleshooting below. 5. Try a natural language query: > *“Find all leads in Paris with a rating above 4 and send me the top 5”* ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) * **Tools not showing:** Restart Claude Desktop after config changes. Make sure you are on Claude Desktop v0.10 or later (which supports remote HTTP MCP servers). * **Auth error:** Verify your `Authorization` header value is correct and the API key has not been revoked from the dashboard. * **Connection refused:** Check that `mcp.klozeo.com` is reachable from your network.
# Cline / Roo Code / Smithery
> Connect Cline, Roo Code, or Smithery to Klozeo via MCP.
## Cline & Roo Code [Section titled “Cline & Roo Code”](#cline--roo-code) Both Cline and Roo Code support MCP servers via the VS Code extension settings. In your VS Code `settings.json` (`Cmd+Shift+P` → “Open User Settings (JSON)”): ```json { "cline.mcpServers": { "klozeo": { "url": "https://mcp.klozeo.com/mcp", "headers": { "Authorization": "Bearer sk_live_your_key" } } } } ``` Restart VS Code. Klozeo tools appear in the Cline/Roo sidebar under **MCP Tools**. ## Smithery [Section titled “Smithery”](#smithery) Klozeo is available as a managed MCP server on Smithery: 1. Go to [smithery.ai](https://smithery.ai) and search for **Klozeo**. 2. Click **Connect** and authorize with your Klozeo account. 3. Copy the generated connection string (it looks like `https://mcp.smithery.ai/klozeo?apiKey=...`) into the **Server URL** field of your Smithery workspace configuration. ## Goose [Section titled “Goose”](#goose) Add to your Goose config (`~/.config/goose/config.yaml`): ```yaml mcp: servers: - name: klozeo url: https://mcp.klozeo.com/mcp headers: Authorization: "Bearer sk_live_your_key" ``` Restart Goose. Run `goose mcp list` to verify the `klozeo` server appears with 33 tools.
# Cursor
> Connect Cursor to your Klozeo lead database via MCP.
1. Open Cursor settings (`Cmd+,`) → **Features** → **MCP Servers** → **Add server**. 2. Configure the Klozeo server: | Field | Value | | ----- | ---------------------------- | | Name | `klozeo` | | Type | `http` | | URL | `https://mcp.klozeo.com/mcp` | 3. Click **Save**. Cursor will open your browser for OAuth authorization. 4. Sign in to your Klozeo account. An API key is generated and stored automatically. 5. In any Cursor chat, Klozeo tools are now available. Try: > *“List all leads imported this month and score them”* ## Manual setup (API key) [Section titled “Manual setup (API key)”](#manual-setup-api-key) If OAuth is not available, add the API key as a header in the MCP config (`~/.cursor/mcp.json`): ```json { "mcpServers": { "klozeo": { "url": "https://mcp.klozeo.com/mcp", "headers": { "Authorization": "Bearer sk_live_your_key" } } } } ```
# MCP Overview
> Connect Claude, Cursor, and any MCP-compatible AI client to your Klozeo lead database.
Klozeo implements the **Model Context Protocol (MCP)** — an open standard that lets AI assistants directly query and manipulate your lead database using natural language. ## Authentication is automatic [Section titled “Authentication is automatic”](#authentication-is-automatic) When you connect for the first time, your MCP client opens a browser window — sign in to your Klozeo account and authorize access. An API key is generated and stored automatically. You won’t need to copy-paste anything. If you prefer to use an existing API key directly (for example, in a CI environment), each client setup guide includes manual instructions. ## Transport [Section titled “Transport”](#transport) Klozeo uses the **streamable HTTP transport** (preferred by default). Claude Desktop is the exception — it requires the **SSE transport** because OAuth is not supported in that client. All other clients (Claude Code, Cursor, Cline, Roo Code, Goose, Smithery) use streamable HTTP. ## Available tools (33) [Section titled “Available tools (33)”](#available-tools-33) ### Leads [Section titled “Leads”](#leads) | Tool | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------- | | `list_leads` | List leads with filters, sorting, and pagination. Pass `format: csv/json/xlsx` to export all records without pagination | | `get_lead` | Get a specific lead by ID | | `create_lead` | Create a new lead (deduplication automatic) | | `update_lead` | Update lead fields | | `delete_lead` | Delete a lead — requires `confirm: true` | | `create_lead_tag` | Add a tag to a lead (idempotent) | ### Batch Operations [Section titled “Batch Operations”](#batch-operations) | Tool | Description | | -------------------- | ----------------------------------------------------------------------- | | `batch_create_leads` | Create up to 100/500 leads in one call (deduplication applied per item) | | `batch_update_leads` | Apply the same field update to multiple leads | | `batch_delete_leads` | Delete multiple leads — requires `confirm: true` | ### Notes [Section titled “Notes”](#notes) | Tool | Description | | ------------- | ---------------------------------------------- | | `create_note` | Add a timestamped note to a lead | | `list_notes` | List all notes for a lead | | `update_note` | Edit the content of an existing note | | `delete_note` | Delete a note by ID — requires `confirm: true` | ### Attributes [Section titled “Attributes”](#attributes) | Tool | Description | | ------------------ | --------------------------------------------------------- | | `list_attributes` | List all custom attributes for a lead | | `create_attribute` | Add a custom attribute (text, number, bool, list, object) | | `update_attribute` | Update the value of an existing attribute | | `delete_attribute` | Remove a custom attribute — requires `confirm: true` | ### Scoring [Section titled “Scoring”](#scoring) | Tool | Description | | --------------------- | ----------------------------------------------------- | | `list_scoring_rules` | List all scoring rules | | `create_scoring_rule` | Create a new scoring rule with an expression | | `get_scoring_rule` | Get a single scoring rule by ID | | `update_scoring_rule` | Update a scoring rule’s name, expression, or priority | | `delete_scoring_rule` | Delete a scoring rule — requires `confirm: true` | | `update_lead_score` | Recalculate and persist the score for a single lead | | `update_all_scores` | Recalculate and persist scores for all leads | ### Webhooks [Section titled “Webhooks”](#webhooks) | Tool | Description | | ---------------- | -------------------------------------------------------- | | `list_webhooks` | List all webhook subscriptions | | `create_webhook` | Create a new webhook subscription | | `delete_webhook` | Delete a webhook subscription — requires `confirm: true` | ### API Keys [Section titled “API Keys”](#api-keys) | Tool | Description | | ---------------- | -------------------------------------------- | | `list_api_keys` | List all API keys for your account | | `create_api_key` | Create a new named API key | | `delete_api_key` | Revoke an API key — requires `confirm: true` | ### Analytics [Section titled “Analytics”](#analytics) | Tool | Description | | ----------------- | --------------------------------- | | `get_stats` | Get aggregate account statistics | | `list_categories` | List all distinct lead categories | | `list_cities` | List all distinct cities | ## How tool responses work [Section titled “How tool responses work”](#how-tool-responses-work) Every tool returns structured JSON, which means AI agents can act on the result without guessing. **Success:** ```json { "data": { "id": "cl_abc123", "name": "Café du Marché", ... } } ``` **Error:** ```json { "error": { "code": "RESOURCE_NOT_FOUND", "message": "Lead not found.", "status": 404 } } ``` Error codes are stable and machine-readable: `RESOURCE_NOT_FOUND`, `VALIDATION_ERROR`, `UNAUTHORIZED`, `CONFLICT`, `RATE_LIMITED`, `INTERNAL_ERROR`. An agent hitting a rate limit can read `RATE_LIMITED` and decide to retry — it doesn’t have to parse a prose string. ## Destructive operations require confirmation [Section titled “Destructive operations require confirmation”](#destructive-operations-require-confirmation) `delete_lead`, `delete_note`, and `delete_webhook` won’t execute unless `confirm: true` is passed. This means an AI agent can’t accidentally delete a record because it misread your intent — it has to be explicitly instructed to confirm. If `confirm` is missing or false, the tool returns: ```json { "error": { "code": "CONFIRMATION_REQUIRED", "message": "Set confirm: true to delete.", "status": 400 } } ``` In practice, you just tell the AI “delete lead X, confirm it” and it handles the parameter correctly. ## Sorting [Section titled “Sorting”](#sorting) `list_leads` accepts a `sort` parameter (not `sort_by`) for the field name, and `sort_order` for direction (`ASC` or `DESC`). Pass `format: csv/json/xlsx` to export all records without pagination. ```plaintext List leads sorted by score descending ``` Claude will pass `sort: "score", sort_order: "DESC"` automatically. ## Designed for agent reliability [Section titled “Designed for agent reliability”](#designed-for-agent-reliability) Most MCP servers are written for humans to browse — long descriptions, inconsistent verb choices, free-form error strings. That works fine in a UI. In an agentic context, it costs tokens and causes tool selection mistakes. A few specific decisions we made: **Consistent verb prefixes.** Every tool name starts with `list_`, `get_`, `create_`, `update_`, or `delete_`. No synonyms, no exceptions. Models trained on natural language have strong priors on these verbs, which means fewer wrong tool selections and fewer retries. **Short, factual descriptions.** Tool descriptions are capped at 80 characters. Parameter descriptions at 40. No examples, no preamble (“This tool allows you to…”). Just the action and the object. This keeps the tool manifest compact — 33 tools load comfortably within a single context window with room to spare for your actual task. **Machine-readable errors.** Every error has a stable `code` field (`RESOURCE_NOT_FOUND`, `RATE_LIMITED`, etc.). An agent can branch on the code without parsing prose. This matters when building multi-step workflows where one tool’s failure should inform the next step. **Explicit confirmation for destructive ops.** Delete tools require `confirm: true`. An agent that misunderstood “remove duplicates” won’t silently wipe records — it will get a `CONFIRMATION_REQUIRED` error and surface it to you before doing anything irreversible. **Single source of truth for schemas.** Each tool’s parameter schema is defined once and shared across the MCP interface, the API, and the SDK types. Adding or changing a field touches one place — there’s no separate “MCP version” of the schema that can drift out of sync with what the API actually accepts. These aren’t architectural novelties — they’re just the conventions the [MCP spec](https://modelcontextprotocol.io) recommends, applied consistently. The result is a server that agents use correctly on the first try more often, which means less back-and-forth and fewer unexpected outcomes for you. ## Setup guides [Section titled “Setup guides”](#setup-guides) Follow the guide for your AI client: * [Claude Desktop](/mcp/claude-desktop/) * [Claude Code](/mcp/claude-code/) * [Cursor](/mcp/cursor/) * [Cline / Roo Code / Smithery / Goose](/mcp/cline-roo-smithery/)
# Go SDK
> Official Go client for the Klozeo API.
```bash go get github.com/lbframe/klozeo-sdk-go ``` Requires Go 1.23+. ## Quick start [Section titled “Quick start”](#quick-start) ```go import klozeo "github.com/lbframe/klozeo-sdk-go" client := klozeo.New("sk_live_your_api_key") // Create a lead // Optional string fields use pointers to distinguish "not set" from "empty string" lead, err := client.Create(ctx, &klozeo.Lead{ Name: "Acme Corp", Source: "website", City: "Berlin", Email: klozeo.Ptr("contact@acme.com"), }) // List with filters leads, err := client.List(ctx, klozeo.City().Eq("Berlin"), klozeo.Rating().Gte(4.0), klozeo.Sort(klozeo.FieldRating, klozeo.Desc), klozeo.Limit(20), ) // Iterate all pages (Go 1.23+ range-over-function iterator) for lead, err := range client.Iterator(ctx, klozeo.City().Eq("Berlin")) { if err != nil { log.Fatal(err) } fmt.Println(lead.Name) } // Export to CSV reader, err := client.Export(ctx, klozeo.ExportCSV) ``` ## OR logic [Section titled “OR logic”](#or-logic) Combine `Or()` with standard filters in the same `List()` call: ```go leads, err := client.List(ctx, klozeo.City().Eq("Paris"), // AND city = Paris klozeo.Or().Rating().Gte(4.0), // OR rating >= 4 ) ``` ## Error handling [Section titled “Error handling”](#error-handling) ```go import "errors" resp, err := client.Create(ctx, &klozeo.Lead{Name: "Acme", Source: "web"}) if err != nil { var apiErr *klozeo.APIError if errors.As(err, &apiErr) { switch apiErr.Code { case klozeo.ErrRateLimit: // retry after apiErr.RetryAfter case klozeo.ErrNotFound: // lead not found case klozeo.ErrUnauthorized: // invalid API key } } } ``` ## Notes [Section titled “Notes”](#notes) ```go note, err := client.AddNote(ctx, leadID, "Called back — very interested.") notes, err := client.Notes(ctx, leadID) err = client.DeleteNote(ctx, noteID) ``` ## Batch operations [Section titled “Batch operations”](#batch-operations) ```go // Create up to 100 leads at once results, err := client.BatchCreate(ctx, leads) // Delete by IDs err = client.BatchDelete(ctx, []string{"cl_...", "cl_..."}) ``` ## More [Section titled “More”](#more) Full API reference: [pkg.go.dev/github.com/lbframe/klozeo-sdk-go](https://pkg.go.dev/github.com/lbframe/klozeo-sdk-go)
# Python SDK
> Official Python client for the Klozeo API.
```bash pip install klozeo ``` Requires Python 3.10+. ## Quick start [Section titled “Quick start”](#quick-start) ```python from klozeo import ( Klozeo, Lead, text_attr, number_attr, city, rating, SortField, SortOrder ) client = Klozeo("sk_live_your_api_key") # Create a lead lead = client.leads.create(Lead( name="Acme Corp", source="website", city="Berlin", email="contact@acme.com", attributes=[ text_attr("industry", "Software"), number_attr("employees", 500), ] )) # List with filters leads = client.leads.list( filters=[city.eq("Berlin"), rating.gte(4.0)], sort_by=SortField.RATING, sort_order=SortOrder.DESC, limit=20, ) # Iterate all pages automatically for lead in client.leads.iterate(filters=[city.eq("Berlin")]): print(lead.name) # Export to CSV import csv, io data = client.leads.export(format="csv") reader = csv.DictReader(io.StringIO(data)) ``` ## Handling errors [Section titled “Handling errors”](#handling-errors) ```python from klozeo import KlozeoError, RateLimitError, AuthError, NotFoundError try: lead = client.leads.create(Lead(name="Acme", source="web")) except RateLimitError as e: print(f"Rate limited. Retry after {e.retry_after}s") except AuthError: print("Invalid or revoked API key") except NotFoundError: print("Lead not found") except KlozeoError as e: print(f"API error {e.status}: {e.message}") ``` ## More [Section titled “More”](#more) Full source and examples: [github.com/lbframe/klozeo-sdk-python](https://github.com/lbframe/klozeo-sdk-python) Package on PyPI: [pypi.org/project/klozeo](https://pypi.org/project/klozeo)
# Rust SDK
> Official Rust client for the Klozeo API.
```toml [dependencies] klozeo = "0.1" tokio = { version = "1", features = ["full"] } ``` Requires Rust 1.75+. Async/await via `tokio`, HTTP via `reqwest`. ## Quick start [Section titled “Quick start”](#quick-start) ```rust use klozeo::{Client, Lead}; #[tokio::main] async fn main() -> Result<(), klozeo::Error> { let client = Client::new("sk_live_your_api_key"); // Create a lead let resp = client.leads().create( Lead::builder() .name("Acme Corporation") .source("website") .city("San Francisco") .email("contact@acme.com") .rating(4.5) .tags(vec!["enterprise".into(), "saas".into()]) .build(), ).await?; println!("Created: {}", resp.id); // List with filters use klozeo::filters::{city, rating, sort}; let leads = client.leads().list(vec![ city().eq("Berlin"), rating().gte(4.0), sort().by("rating").desc(), ]).await?; // Stream all pages automatically use futures::StreamExt; let mut stream = client.leads().stream(vec![city().eq("Berlin")]); while let Some(lead) = stream.next().await { println!("{}", lead?.name); } Ok(()) } ``` ## Client options [Section titled “Client options”](#client-options) ```rust use klozeo::{Client, ClientConfig}; use std::time::Duration; let client = Client::with_config( "sk_live_your_api_key", ClientConfig::builder() .base_url("https://custom.api.com") // default: https://api.klozeo.com/api/v1 .timeout(Duration::from_secs(30)) .max_retries(3) // retries on 429 / 5xx .build(), ); ``` ## Filters [Section titled “Filters”](#filters) ```rust use klozeo::filters::{city, rating, tags, or, attr}; let leads = client.leads().list(vec![ city().eq("Paris"), or().city().eq("Lyon"), // OR city = Lyon rating().gte(4.0), tags().contains("enterprise"), attr("industry").eq("Software"), // dynamic attribute ]).await?; ``` ## Attribute helpers [Section titled “Attribute helpers”](#attribute-helpers) ```rust use klozeo::Attribute; let lead = Lead::builder() .name("Acme") .source("web") .attributes(vec![ Attribute::text("industry", "Software"), Attribute::number("employees", 500.0), Attribute::bool("verified", true), Attribute::list("products", vec!["CRM".into(), "ERP".into()]), ]) .build(); ``` ## Error handling [Section titled “Error handling”](#error-handling) ```rust use klozeo::Error; match client.leads().get("cl_nonexistent").await { Err(Error::NotFound) => println!("Lead not found"), Err(Error::Unauthorized) => println!("Invalid API key"), Err(Error::RateLimit { retry_after }) => { println!("Rate limited. Retry after {retry_after}s"); } Err(Error::Api { status, message, .. }) => { println!("API error {status}: {message}"); } Ok(lead) => println!("Got: {}", lead.name), } ``` ## Notes [Section titled “Notes”](#notes) ```rust let note = client.notes().create(lead_id, "Called back — very interested.").await?; let notes = client.notes().list(lead_id).await?; client.notes().delete(note_id).await?; ``` ## Batch operations [Section titled “Batch operations”](#batch-operations) ```rust // Create up to 100 leads at once let results = client.leads().batch_create(leads).await?; // Delete by IDs client.leads().batch_delete(vec!["cl_...", "cl_..."]).await?; ``` ## More [Section titled “More”](#more) Full source and examples: [github.com/lbframe/klozeo-sdk-rust](https://github.com/lbframe/klozeo-sdk-rust) Package on crates.io: [crates.io/crates/klozeo](https://crates.io/crates/klozeo)
# TypeScript SDK
> Official TypeScript / JavaScript client for the Klozeo API.
```bash npm install @klozeo/sdk ``` Requires Node.js 18+ (or any modern browser). Ships ESM + CJS with full TypeScript types. ## Quick start [Section titled “Quick start”](#quick-start) ```typescript import { Klozeo } from "@klozeo/sdk"; const client = new Klozeo("sk_live_your_api_key"); // Create a lead const resp = await client.leads.create({ name: "Acme Corporation", source: "website", city: "San Francisco", email: "contact@acme.com", rating: 4.5, tags: ["enterprise", "saas"], }); console.log(`Created: ${resp.id}`); // List with filters import { city, rating } from "@klozeo/sdk"; const leads = await client.leads.list({ filters: [city().eq("Berlin"), rating().gte(4.0)], sortBy: "rating", sortOrder: "desc", limit: 20, }); // Iterate all pages automatically for await (const lead of client.leads.iterate({ filters: [city().eq("Berlin")], })) { console.log(lead.name); } // Export to CSV const csv = await client.leads.export({ format: "csv" }); ``` ## Client options [Section titled “Client options”](#client-options) ```typescript const client = new Klozeo("sk_live_your_api_key", { baseUrl: "https://custom.api.com", // default: https://api.klozeo.com/api/v1 timeout: 30_000, // ms, default 30 000 maxRetries: 3, // retries on 429 / 5xx }); ``` ## Filters [Section titled “Filters”](#filters) ```typescript import { city, country, rating, score, tags, or, attr, } from "@klozeo/sdk"; const leads = await client.leads.list({ filters: [ city().eq("Paris"), or().city().eq("Lyon"), // OR city = Lyon rating().gte(4.0), tags().contains("enterprise"), attr("industry").eq("Software"), // dynamic attribute ], }); ``` ## Attribute helpers [Section titled “Attribute helpers”](#attribute-helpers) ```typescript import { textAttr, numberAttr, boolAttr, listAttr } from "@klozeo/sdk"; await client.leads.create({ name: "Acme", source: "web", attributes: [ textAttr("industry", "Software"), numberAttr("employees", 500), boolAttr("verified", true), listAttr("products", ["CRM", "ERP"]), ], }); ``` ## Error handling [Section titled “Error handling”](#error-handling) ```typescript import { KlozeoError, RateLimitError, AuthError, NotFoundError } from "@klozeo/sdk"; try { const lead = await client.leads.create({ name: "Acme", source: "web" }); } catch (err) { if (err instanceof RateLimitError) { console.log(`Rate limited. Retry after ${err.retryAfter}s`); } else if (err instanceof AuthError) { console.log("Invalid or revoked API key"); } else if (err instanceof NotFoundError) { console.log("Lead not found"); } else if (err instanceof KlozeoError) { console.log(`API error ${err.status}: ${err.message}`); } } ``` ## Notes [Section titled “Notes”](#notes) ```typescript const note = await client.notes.create(leadId, "Called back — very interested."); const notes = await client.notes.list(leadId); await client.notes.delete(noteId); ``` ## Batch operations [Section titled “Batch operations”](#batch-operations) ```typescript // Create up to 100 leads at once const results = await client.leads.batchCreate(leads); // Delete by IDs await client.leads.batchDelete(["cl_...", "cl_..."]); ``` ## More [Section titled “More”](#more) Full source and examples: [github.com/lbframe/klozeo-sdk-typescript](https://github.com/lbframe/klozeo-sdk-typescript) Package on npm: [npmjs.com/package/@klozeo/sdk](https://www.npmjs.com/package/@klozeo/sdk)