DeskOS Leads Management: Public API Guide
Welcome to the DeskOS Leads Management Public API. This guide covers everything you need to start pushing leads from external sources (Facebook Lead Ads, Google Ads, website forms, landing pages, and more) directly into your DeskOS CRM, as well as updating and removing them programmatically.
It also covers outbound webhooks, which let DeskOS notify your systems in real time as leads are created, move through your pipeline, and convert. Together the two directions give you a full two-way sync.
Overview
Most coworking operators collect leads from multiple channels: Facebook campaigns, Google Ads forms, website contact forms, walk-in enquiry sheets synced to Google Sheets, and so on. The Public Leads API gives you a single, secure endpoint to funnel all of these into DeskOS automatically.
How it works:
- The DeskOS team generates a secure API key for your organization.
- You connect your lead sources (Facebook, Google Sheets, website forms, etc.) to the DeskOS API using tools like Zapier, Pabbly Connect, Make (Integromat), or direct HTTP calls.
- Every lead flows into DeskOS in real time: auto-assigned, auto-tagged, with follow-ups created automatically.
- Optionally, you register a webhook so DeskOS pushes events back out to you whenever a lead changes stage or converts.
No more copy-pasting from spreadsheets. No more leads slipping through the cracks.
Environments & Server URLs
DeskOS provides two environments for the API:
| Environment | Purpose | Server URL |
|---|---|---|
| Test / Staging | Development and testing | Provided by the DeskOS team |
| Production | Live lead data | Provided by the DeskOS team |
The DeskOS team will provide you with the server URLs for both your test and production environments during onboarding. All endpoint paths documented below are relative to the server URL you receive.
For example, if your production server URL is https://api.yourworkspace.deskos.com, then the lead creation endpoint would be:
https://api.yourworkspace.deskos.com/api/v1/leads-management/external/leads/
Always test your integration against the staging environment first before switching to production.
Authentication
Every API request must include a valid API key. The DeskOS team will generate this key for your organization and share it with you securely. Treat your API key like a password. Do not share it publicly or commit it to version control.
Sending Your API Key
You can include the key in any of these three ways (pick whichever works best with your integration tool):
Method 1: Authorization Header (Recommended)
Authorization: Api-Key pk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
Method 2: X-API-Key Header
X-API-Key: pk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
Method 3: Query Parameter
https://<your-server-url>/api/v1/leads-management/external/leads/?api_key=pk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
The query parameter method is convenient for quick testing in Zapier or Pabbly but we recommend using headers for production integrations.
API Key Details
- Format:
pk_live_followed by 40 hexadecimal characters. - Scope: Each key is tied to your organization. It can be restricted to specific campuses or allowed to create leads across all your campuses.
- Default campus: Your key can have a default campus configured so you don't need to pass
campus_idwith every request. - Default source: Your key can have a default lead source (e.g., "Website") so leads are auto-tagged even when
source_nameisn't sent. - Rate limit: Each key has a daily request limit (default: 1,000 leads/day). Contact the DeskOS team if you need a higher limit.
- Security: The raw key is shown only once when it's created. DeskOS stores a hashed version, so they cannot retrieve it for you. If you lose it, a new key must be generated.
API Endpoints
Base Path
/api/v1/leads-management/external/
All endpoints below are relative to your server URL + this base path.
1. Create a Lead
POST /api/v1/leads-management/external/leads/
Creates a single lead in DeskOS. If a lead with the same email already exists on the same campus, it updates the existing lead instead of creating a duplicate (this behavior can be turned off).
Request Headers
Content-Type: application/json
X-API-Key: pk_live_your_key_here
Minimal Request Body
Only first_name is required. Send as much or as little data as you have:
{
"first_name": "Rahul"
}
Full Request Body (All Fields)
{
"first_name": "Rahul",
"last_name": "Sharma",
"email": "rahul.sharma@acmecorp.com",
"phone": "+919876543210",
"alternate_phone": "+919876543211",
"company_name": "Acme Corp",
"job_title": "CTO",
"linkedin_url": "https://linkedin.com/in/rahulsharma",
"lead_type": "company",
"source_name": "Facebook Lead Ad",
"industry_name": "Technology",
"tags": "hot-lead, q1-campaign, enterprise",
"space_preference": "Private Cabin",
"required_seats": 10,
"move_in_date": "2026-04-01",
"estimated_value": 500000.00,
"monthly_budget": 150000.00,
"notes": "Looking for a 10-seater private cabin with meeting room access. Prefers ground floor.",
"requirements": "Private cabin, 24/7 access, dedicated parking",
"pain_points": "Current office lease ending in 2 months, need quick move-in",
"referral_source": "Referred by Vikram Mehta",
"campus_id": 3,
"deduplicate": true,
"external_id": "FB-LEAD-98765",
"custom_fields": {
"crm_id": "SF-00123",
"budget_range": "1L-2L",
"preferred_floor": "Ground"
}
}
Field Reference
Required Fields
| Field | Type | Description |
|---|---|---|
first_name | string (max 100) | Lead's first name |
Contact Information (Optional)
| Field | Type | Default | Description |
|---|---|---|---|
last_name | string (max 100) | "" | Lead's last name |
email | null | Email address. Used for deduplication: if a lead with this email already exists on the campus, the existing record is updated instead of creating a duplicate. | |
phone | string (max 20) | "" | Primary phone number. Any format accepted (e.g., +919876543210, 098765 43210). |
alternate_phone | string (max 20) | null | Secondary phone number |
Company & Professional (Optional)
| Field | Type | Default | Description |
|---|---|---|---|
company_name | string (max 200) | null | Company or organization name |
job_title | string (max 100) | null | Job title or designation |
linkedin_url | URL | null | LinkedIn profile URL |
Lead Classification (Optional)
| Field | Type | Default | Description |
|---|---|---|---|
lead_type | enum | "individual" | Type of lead. Options: individual, company, visitor |
source_name | string (max 100) | null | Name of the lead source (e.g., "Facebook Lead Ad", "Google Ads", "Website Contact Form"). Matched case-insensitively against the sources configured for the campus. A name that doesn't match an existing source is filed under "Unknown Source" rather than creating a new one - so set your sources up in DeskOS first, and keep the spelling consistent. If source_name is omitted entirely, the API key's default source is used, falling back to "Public API". |
industry_name | string (max 100) | null | Industry the lead belongs to (e.g., "Technology", "Healthcare"). Automatically created if it doesn't exist. |
tags | string (max 255) | null | Comma-separated tags for categorization. The system automatically appends api-lead to all leads created via this API. |
Space Requirements (Optional)
| Field | Type | Default | Description |
|---|---|---|---|
space_preference | string (max 100) | null | Type of space the lead is looking for (e.g., "Private Cabin", "Hot Desk", "Dedicated Desk", "Virtual Office") |
required_seats | integer (min: 1) | null | Number of seats or desks needed |
move_in_date | date (YYYY-MM-DD) | null | When the lead wants to start |
Budget & Deal (Optional)
| Field | Type | Default | Description |
|---|---|---|---|
estimated_value | decimal | null | Estimated total deal value |
monthly_budget | decimal | null | Monthly budget the lead has indicated |
Notes & Details (Optional)
| Field | Type | Default | Description |
|---|---|---|---|
notes | text | null | Free-text notes about the lead |
requirements | text | null | Detailed space or service requirements |
pain_points | text | null | Challenges or problems the lead is facing |
referral_source | string (max 255) | null | Who or what referred this lead |
Custom Fields (Optional)
| Field | Type | Default | Description |
|---|---|---|---|
custom_fields | object | {} | Key-value pairs where each key is a custom field's slug (machine name) as configured in your DeskOS campus settings. Values are always strings. Unknown slugs are silently ignored. |
"custom_fields": {
"crm_id": "SF-00123",
"budget_range": "1L-2L",
"preferred_floor": "Ground"
}
Go to DeskOS → Leads → Settings → Custom Fields. Each field has a machine name (slug) shown alongside its label. Use that slug as the key here.
Routing & Control (Optional)
| Field | Type | Default | Description |
|---|---|---|---|
campus_id | integer | null | ID of the target campus in DeskOS. If not provided, falls back to: (1) the API key's default campus, or (2) your org's only campus if there's just one. |
deduplicate | boolean | true | When true, if a lead with the same email already exists on this campus, the existing record is updated (blank fields are filled in, notes and tags are appended) rather than creating a duplicate. Set to false to always create new records. |
external_id | string (max 255) | null | Your system's unique ID for this lead (e.g., Facebook's lead ID, your CRM's record ID). Stored in the lead's tags as ext:<your_id> for cross-referencing. |
Success Response: New Lead Created
Status: 201 Created
{
"success": true,
"is_new": true,
"lead": {
"id": 142,
"first_name": "Rahul",
"last_name": "Sharma",
"email": "rahul.sharma@acmecorp.com",
"phone": "+919876543210",
"company_name": "Acme Corp",
"job_title": "CTO",
"lead_type": "company",
"source_name": "Facebook Lead Ad",
"status_name": "New",
"campus_name": "Whitefield Hub",
"tags": "hot-lead, q1-campaign, enterprise, ext:FB-LEAD-98765, api-lead",
"notes": "Looking for a 10-seater private cabin with meeting room access. Prefers ground floor.\n\nRequirements: Private cabin, 24/7 access, dedicated parking",
"space_preference": "Private Cabin",
"required_seats": 10,
"estimated_value": "500000.00",
"monthly_budget": "150000.00",
"created_at": "2026-06-06T10:30:00.000000Z",
"updated_at": "2026-06-06T10:30:00.000000Z",
"is_new": true,
"custom_fields": {
"crm_id": "SF-00123",
"budget_range": "1L-2L",
"preferred_floor": "Ground"
}
}
}
Success Response: Existing Lead Updated (Dedup)
Status: 200 OK
When deduplicate is true and a lead with the same email already exists:
{
"success": true,
"is_new": false,
"lead": {
"id": 98,
"first_name": "Rahul",
"last_name": "Sharma",
"email": "rahul.sharma@acmecorp.com",
"phone": "+919876543210",
"company_name": "Acme Corp",
"job_title": "CTO",
"lead_type": "individual",
"source_name": "Website Contact Form",
"status_name": "Contacted",
"campus_name": "Whitefield Hub",
"tags": "website, api-lead, hot-lead, q1-campaign, enterprise, ext:FB-LEAD-98765, api-lead",
"notes": "Original notes from first submission.\n\n---\n\n[API Update 2026-06-06 10:30]\nLooking for a 10-seater private cabin with meeting room access.",
"space_preference": "Private Cabin",
"required_seats": 10,
"estimated_value": "500000.00",
"monthly_budget": "150000.00",
"created_at": "2026-02-10T14:20:00.000000Z",
"updated_at": "2026-06-06T10:30:00.000000Z",
"is_new": false,
"custom_fields": {
"crm_id": "SF-00123",
"budget_range": "1L-2L"
}
}
}
How deduplication works:
- Only blank or empty fields on the existing lead are filled in with new data. Existing values are never overwritten.
- Tags are appended (not replaced).
- Notes are appended with a timestamp separator.
- Custom fields are upserted: provided keys are written, existing keys not in the request are left untouched.
- An activity log entry is created recording which fields were updated.
2. Bulk Create Leads
POST /api/v1/leads-management/external/leads/bulk/
Submit up to 50 leads in a single request. Useful for batch imports from Google Sheets or syncing a backlog of leads.
Request Body
{
"leads": [
{
"first_name": "Rahul",
"last_name": "Sharma",
"email": "rahul@acmecorp.com",
"phone": "+919876543210",
"source_name": "Google Ads",
"required_seats": 10,
"custom_fields": { "crm_id": "SF-001" }
},
{
"first_name": "Priya",
"last_name": "Patel",
"email": "priya@techstart.io",
"phone": "+919876543211",
"company_name": "TechStart",
"source_name": "Google Ads",
"space_preference": "Hot Desk",
"required_seats": 5
},
{
"first_name": "Amit",
"source_name": "Facebook Lead Ad",
"notes": "Interested in virtual office"
}
]
}
Each object in the leads array accepts the exact same fields as the single create endpoint, including custom_fields.
Success Response
Status: 200 OK
{
"success": true,
"total_submitted": 3,
"created": 3,
"updated": 0,
"failed": 0,
"results": [
{ "index": 0, "lead_id": 143, "is_new": true },
{ "index": 1, "lead_id": 144, "is_new": true },
{ "index": 2, "lead_id": 145, "is_new": true }
],
"errors": []
}
Partial Failure Response
If some leads pass validation and others don't, the valid ones are still created:
{
"success": true,
"total_submitted": 4,
"created": 2,
"updated": 1,
"failed": 1,
"results": [
{ "index": 0, "lead_id": 143, "is_new": true },
{ "index": 1, "lead_id": 144, "is_new": true },
{ "index": 3, "lead_id": 98, "is_new": false }
],
"errors": [
{
"index": 2,
"errors": {
"email": ["Enter a valid email address."]
}
}
]
}
The index in both results and errors corresponds to the position of the lead in your original leads array (0-based).
3. Retrieve a Lead
GET /api/v1/leads-management/external/leads/<lead_id>/
Returns the full detail of a single lead, including all fields and custom fields. The lead must belong to your organization.
Request Headers
X-API-Key: pk_live_your_key_here
Success Response
Status: 200 OK
{
"success": true,
"lead": {
"id": 142,
"first_name": "Rahul",
"last_name": "Sharma",
"email": "rahul.sharma@acmecorp.com",
"phone": "+919876543210",
"alternate_phone": null,
"company_name": "Acme Corp",
"job_title": "CTO",
"linkedin_url": null,
"lead_type": "company",
"source_name": "Facebook Lead Ad",
"industry_name": "Technology",
"status_name": "Proposal Sent",
"status_color": "#f39c12",
"campus_name": "Whitefield Hub",
"assigned_to_name": "Sneha Kapoor",
"tags": "hot-lead, q1-campaign, api-lead",
"notes": "Looking for a 10-seater private cabin.",
"requirements": "Private cabin, 24/7 access",
"pain_points": "Lease ending in 2 months",
"referral_source": null,
"space_preference": "Private Cabin",
"required_seats": 10,
"move_in_date": "2026-04-01",
"estimated_value": "500000.00",
"monthly_budget": "150000.00",
"is_active": true,
"is_active_deal": false,
"is_lost_deal": false,
"last_contacted_at": "2026-06-05T14:30:00.000000Z",
"created_at": "2026-06-06T10:30:00.000000Z",
"updated_at": "2026-06-06T11:45:00.000000Z",
"custom_fields": {
"crm_id": "SF-00123",
"budget_range": "1L-2L"
}
}
}
cURL Example
curl -X GET "https://<your-server-url>/api/v1/leads-management/external/leads/142/" \
-H "X-API-Key: pk_live_your_key_here"
5. Update a Lead
PATCH /api/v1/leads-management/external/leads/<lead_id>/
Partially updates an existing lead. Only the fields you include in the request body are changed. Everything else is left exactly as it is.
The lead must belong to your organization (the API key enforces this automatically).
Request Headers
Content-Type: application/json
X-API-Key: pk_live_your_key_here
Request Body
All fields are optional. Send only what you want to change:
{
"phone": "+919999999999",
"company_name": "Acme Corp (Updated)",
"status_name": "Proposal Sent",
"monthly_budget": 200000.00,
"custom_fields": {
"crm_id": "SF-00456",
"preferred_floor": "First"
}
}
Field Reference
| Field | Type | Description |
|---|---|---|
first_name | string (max 100) | Lead's first name |
last_name | string (max 100) | Lead's last name |
email | Lead's email address | |
phone | string (max 20) | Primary phone number |
alternate_phone | string (max 20) | Secondary phone number |
company_name | string (max 200) | Company or organization name |
job_title | string (max 100) | Job title or designation |
linkedin_url | URL | LinkedIn profile URL |
lead_type | enum | individual, company, or visitor |
source_name | string (max 100) | Lead source name. Must match an existing source in DeskOS. Unrecognized names are ignored. |
industry_name | string (max 100) | Industry name. Auto-created if it doesn't exist. |
status_name | string (max 100) | Pipeline status name. Must match an existing status for this campus. Unrecognized names are ignored. |
tags | string (max 255) | Comma-separated tags. Replaces the existing tags value entirely. |
space_preference | string (max 100) | Type of space preferred |
required_seats | integer (min: 1) | Number of seats needed |
move_in_date | date (YYYY-MM-DD) | Desired move-in date |
estimated_value | decimal | Estimated total deal value |
monthly_budget | decimal | Monthly budget |
notes | text | Free-text notes. Replaces the existing notes value entirely. |
requirements | text | Space/service requirements |
pain_points | text | Challenges the lead is facing |
referral_source | string (max 255) | Referral information |
custom_fields | object | Key-value pairs of custom field slugs → values. Only the keys you include are updated; other custom fields are untouched. |
The PATCH endpoint overwrites the fields you send (unlike the deduplication path in POST /leads/, which only fills blank fields). Use PATCH when you explicitly want to change a value that already exists.
Success Response
Status: 200 OK
{
"success": true,
"lead": {
"id": 142,
"first_name": "Rahul",
"last_name": "Sharma",
"email": "rahul.sharma@acmecorp.com",
"phone": "+919999999999",
"company_name": "Acme Corp (Updated)",
"job_title": "CTO",
"lead_type": "company",
"source_name": "Facebook Lead Ad",
"status_name": "Proposal Sent",
"campus_name": "Whitefield Hub",
"tags": "hot-lead, q1-campaign, enterprise, api-lead",
"notes": "Looking for a 10-seater private cabin with meeting room access.",
"space_preference": "Private Cabin",
"required_seats": 10,
"estimated_value": "500000.00",
"monthly_budget": "200000.00",
"created_at": "2026-06-06T10:30:00.000000Z",
"updated_at": "2026-06-06T11:45:00.000000Z",
"is_new": false,
"custom_fields": {
"crm_id": "SF-00456",
"budget_range": "1L-2L",
"preferred_floor": "First"
}
}
}
cURL Example
curl -X PATCH "https://<your-server-url>/api/v1/leads-management/external/leads/142/" \
-H "Content-Type: application/json" \
-H "X-API-Key: pk_live_your_key_here" \
-d '{
"status_name": "Proposal Sent",
"monthly_budget": 200000,
"custom_fields": {
"crm_id": "SF-00456"
}
}'
6. Delete a Lead
DELETE /api/v1/leads-management/external/leads/<lead_id>/
Deactivates a lead, removing it from active pipeline views. This is a soft delete: the lead record is preserved in the database (so historical activities, proposals, and reports remain intact) but it is marked inactive and hidden from the CRM interface.
The lead must belong to your organization (the API key enforces this automatically).
Request Headers
X-API-Key: pk_live_your_key_here
No request body is needed.
Success Response
Status: 204 No Content
No response body is returned on success.
cURL Example
curl -X DELETE "https://<your-server-url>/api/v1/leads-management/external/leads/142/" \
-H "X-API-Key: pk_live_your_key_here"
Deleting via the API marks the lead as inactive. It does not permanently erase the record. If you need to permanently remove a lead (e.g., for a GDPR erasure request), do so through the DeskOS CRM interface.
7. Add an Activity / Note
POST /api/v1/leads-management/external/leads/<lead_id>/activities/
Logs a new activity against a lead: a note, call, meeting, email, WhatsApp message, etc. This is the right endpoint when your external system records an interaction and you want it to appear in the DeskOS activity feed.
Request Headers
Content-Type: application/json
X-API-Key: pk_live_your_key_here
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
activity_type | enum | Yes | Type of activity. See values below. |
subject | string (max 255) | Yes | Short summary of the activity |
description | text | No | Full details / body of the note or activity |
performed_at | datetime (ISO 8601) | No | When the activity happened. Defaults to now. |
duration | integer | No | Duration in minutes (useful for calls and meetings) |
activity_type values:
| Value | Meaning |
|---|---|
note | Internal note |
call | Phone call |
email | Email sent or received |
meeting | In-person or video meeting |
whatsapp | WhatsApp message |
visit | Physical visit to the campus |
task | Task or to-do item |
other | Anything else |
Example: Log a call
{
"activity_type": "call",
"subject": "Discovery call with Rahul",
"description": "Discussed requirements in detail. Interested in 10 seats, needs ground floor. Will review proposal by Friday.",
"performed_at": "2026-06-06T11:30:00Z",
"duration": 25
}
Example: Add a note
{
"activity_type": "note",
"subject": "Budget confirmed",
"description": "Rahul confirmed the monthly budget is ₹1.5L. Decision maker is the CFO, and Rahul will loop them in."
}
Success Response
Status: 201 Created
{
"success": true,
"activity": {
"id": 891,
"activity_type": "call",
"activity_type_display": "Call",
"subject": "Discovery call with Rahul",
"description": "Discussed requirements in detail. Interested in 10 seats, needs ground floor.",
"performed_by_name": null,
"performed_at": "2026-06-06T11:30:00.000000Z",
"duration": 25,
"created_at": "2026-06-06T11:45:00.000000Z"
}
}
performed_by_name will be null for activities logged via the API since there is no associated DeskOS team member. Activities logged by team members inside DeskOS will show their name.
cURL Example
curl -X POST "https://<your-server-url>/api/v1/leads-management/external/leads/142/activities/" \
-H "Content-Type: application/json" \
-H "X-API-Key: pk_live_your_key_here" \
-d '{
"activity_type": "note",
"subject": "Budget confirmed",
"description": "Monthly budget confirmed at 1.5L."
}'
8. Retrieve Activities
GET /api/v1/leads-management/external/leads/<lead_id>/activities/
Returns the paginated activity list for a lead, newest first. Includes all activities, both those logged via the API and those logged internally by your team inside DeskOS.
Query Parameters
| Parameter | Default | Description |
|---|---|---|
page | 1 | Page number |
page_size | 20 | Results per page (max 100) |
Success Response
Status: 200 OK
{
"success": true,
"lead_id": 142,
"total": 7,
"page": 1,
"page_size": 20,
"activities": [
{
"id": 891,
"activity_type": "call",
"activity_type_display": "Call",
"subject": "Discovery call with Rahul",
"description": "Discussed requirements. 10 seats, ground floor preferred.",
"performed_by_name": null,
"performed_at": "2026-06-06T11:30:00.000000Z",
"duration": 25,
"created_at": "2026-06-06T11:45:00.000000Z"
},
{
"id": 874,
"activity_type": "other",
"activity_type_display": "Other",
"subject": "Lead created via public API",
"description": "Lead submitted through API key: Zapier - Facebook Leads",
"performed_by_name": null,
"performed_at": "2026-06-06T10:30:00.000000Z",
"duration": null,
"created_at": "2026-06-06T10:30:00.000000Z"
}
]
}
cURL Example
curl -X GET "https://<your-server-url>/api/v1/leads-management/external/leads/142/activities/?page=1&page_size=20" \
-H "X-API-Key: pk_live_your_key_here"
9. Retrieve Status History
GET /api/v1/leads-management/external/leads/<lead_id>/history/
Returns the complete status-change timeline for a lead, newest first. Every time a lead moves from one pipeline stage to another, whether changed by a team member or via the API, an entry is recorded here.
Success Response
Status: 200 OK
{
"success": true,
"lead_id": 142,
"history": [
{
"id": 56,
"from_status": {
"name": "Contacted",
"color": "#3498db"
},
"to_status": {
"name": "Proposal Sent",
"color": "#f39c12"
},
"changed_at": "2026-06-06T11:45:00.000000Z"
},
{
"id": 31,
"from_status": {
"name": "New",
"color": "#2ecc71"
},
"to_status": {
"name": "Contacted",
"color": "#3498db"
},
"changed_at": "2026-06-06T10:50:00.000000Z"
},
{
"id": 12,
"from_status": null,
"to_status": {
"name": "New",
"color": "#2ecc71"
},
"changed_at": "2026-06-06T10:30:00.000000Z"
}
]
}
from_status is null for the very first entry (the initial status assigned when the lead was created).
cURL Example
curl -X GET "https://<your-server-url>/api/v1/leads-management/external/leads/142/history/" \
-H "X-API-Key: pk_live_your_key_here"
10. Health Check / Validate API Key
GET /api/v1/leads-management/external/health/
Use this endpoint to verify your API key is working and see its configuration. Great for testing during setup.
Request
curl -X GET "https://<your-server-url>/api/v1/leads-management/external/health/" \
-H "X-API-Key: pk_live_your_key_here"
Response
Status: 200 OK
{
"status": "ok",
"key_name": "Zapier - Facebook Leads",
"organization": "CoWork Solutions Pvt Ltd",
"default_campus": "Whitefield Hub",
"allowed_campuses": "all",
"default_source": "Facebook",
"daily_limit": 1000,
"requests_today": 42
}
| Field | Description |
|---|---|
status | Always "ok" if the key is valid |
key_name | Label assigned to this API key |
organization | Your organization name in DeskOS |
default_campus | The campus leads go to if campus_id isn't specified |
allowed_campuses | List of campus names this key can target, or "all" |
default_source | Default lead source applied when source_name isn't sent |
daily_limit | Maximum leads this key can create per day |
requests_today | How many requests have been made today |
Custom Fields
DeskOS allows your team to define campus-specific custom fields on leads. For example, a "CRM ID" field to link back to your external system, a "Budget Range" dropdown, or a "Preferred Floor" text field.
Setting Up Custom Fields
Custom fields are managed in DeskOS at Leads → Settings → Custom Fields. Each field has:
- Label: the human-readable name shown in the CRM (e.g., "Budget Range")
- Slug / machine name: the key you use in API calls (e.g.,
budget_range) - Type: text, number, date, dropdown, etc.
Using Custom Fields in the API
Pass custom field values in the custom_fields object on any create or update request, keyed by slug:
{
"first_name": "Rahul",
"custom_fields": {
"crm_id": "SF-00123",
"budget_range": "1L-2L",
"preferred_floor": "Ground"
}
}
- Slugs that don't match a configured field for the campus are silently ignored, and no error is returned.
- On create (or dedup update), custom field values are upserted.
- On PATCH, only the keys you include are updated. Other custom fields are left untouched.
- All values are sent as strings, even for number or date fields. DeskOS stores them as-is.
Custom Fields in Responses
All lead responses include a custom_fields object:
"custom_fields": {
"crm_id": "SF-00123",
"budget_range": "1L-2L",
"preferred_floor": "Ground"
}
If no custom field values have been set for a lead, custom_fields will be an empty object {}.
Webhooks (Outbound Events)
Everything above is inbound - you pushing data into DeskOS. Webhooks are the other direction: DeskOS calls you whenever a lead is created, updated, moves pipeline stage, is deleted, or converts into a member.
This is what you want if you're building a two-way sync, posting new leads into Slack, or triggering an n8n / Make workflow the moment a deal is won.
Webhooks use the same API key as the rest of this API - no separate credential to manage.
How It Works
- You register a target URL with DeskOS, choosing which events you care about.
- DeskOS returns a signing secret (shown once, at registration).
- Whenever a matching event happens, DeskOS
POSTs a JSON payload to your URL, signed with that secret. - Your endpoint verifies the signature, returns a
2xx, and does whatever you need.
11. Register a Webhook
POST /api/v1/leads-management/external/webhooks/
Request Headers
Content-Type: application/json
X-API-Key: pk_live_your_key_here
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
target_url | URL (max 500) | Yes | Where DeskOS will POST events. Must be publicly reachable over HTTPS. |
events | array of strings | No | Which events to receive. Omit or send an empty array to subscribe to all events. Unknown event names are rejected with a 400. |
{
"target_url": "https://automation.yourcompany.com/webhooks/deskos-leads",
"events": ["lead.created", "lead.status_changed", "lead.converted"]
}
Success Response
Status: 201 Created
{
"success": true,
"webhook": {
"id": "9f8c1e2a-4b7d-4f31-9a6e-2c5d8b0e7143",
"target_url": "https://automation.yourcompany.com/webhooks/deskos-leads",
"events": ["lead.created", "lead.status_changed", "lead.converted"],
"secret": "whsec_3f9a2b7c1d4e6a8b0c2d4e6f8a0b2c4d6e8f0a2b4c6d8e0f",
"is_active": true,
"created_at": "2026-06-06T10:30:00.000000Z"
}
}
The secret is how you prove an incoming request really came from DeskOS. Store it somewhere safe (an environment variable, your automation tool's credential store) the moment you receive it.
There is no duplicate check on target_url. If you re-register the same URL you will receive every event twice, each with a different secret. Call GET /webhooks/ first to check, and delete the old subscription before registering a replacement.
12. List Your Webhooks
GET /api/v1/leads-management/external/webhooks/
Returns every subscription for your organization, active or not.
Status: 200 OK
{
"success": true,
"webhooks": [
{
"id": "9f8c1e2a-4b7d-4f31-9a6e-2c5d8b0e7143",
"target_url": "https://automation.yourcompany.com/webhooks/deskos-leads",
"events": ["lead.created", "lead.status_changed", "lead.converted"],
"secret": "whsec_3f9a2b7c1d4e6a8b0c2d4e6f8a0b2c4d6e8f0a2b4c6d8e0f",
"is_active": true,
"created_at": "2026-06-06T10:30:00.000000Z"
}
]
}
Unlike API keys, webhook secrets can be retrieved - they're returned in full by this endpoint. If you lose one, list your webhooks rather than re-registering.
13. Delete a Webhook
DELETE /api/v1/leads-management/external/webhooks/<webhook_id>/
Permanently removes the subscription. Deliveries stop immediately. Unlike leads, this is a hard delete - the subscription record is gone, not deactivated.
Status: 204 No Content on success, 404 if the ID doesn't exist or belongs to another organization.
curl -X DELETE "https://<your-server-url>/api/v1/leads-management/external/webhooks/9f8c1e2a-4b7d-4f31-9a6e-2c5d8b0e7143/" \
-H "X-API-Key: pk_live_your_key_here"
Event Types
| Event | Fires when |
|---|---|
lead.created | A new lead is created - through this API, the DeskOS CRM, a web form, or an ad integration. |
lead.updated | Any field on an existing lead changes (other than a status change). See the caveat below. |
lead.status_changed | The lead moves from one pipeline stage to another. Carries the old and new status in changes. |
lead.deleted | The lead is soft-deleted (marked inactive) or hard-deleted from the database. |
lead.converted | The lead is converted into a member/company. Carries the resulting company in changes. |
lead.updated is chattier than you'd expectA single event fires per save, and DeskOS saves a lead more than once during some flows. Creating a lead through this API with auto-assignment enabled emits lead.created and then lead.updated (the second save writes assigned_to). Internal bookkeeping such as last_contacted_at also emits lead.updated.
If you only care about meaningful business moments, subscribe to lead.created, lead.status_changed, and lead.converted and leave lead.updated off. If you do need it, make your handler idempotent and diff against your own copy of the record.
Only one event fires per save. When several conditions are true at once, precedence is: deleted > created > status_changed > updated.
Payload Format
Every delivery has the same envelope:
{
"event": "lead.status_changed",
"occurred_at": "2026-06-06T11:45:00.123456+00:00",
"organization_id": 12,
"lead": {
"id": 142,
"lead_type": "company",
"first_name": "Rahul",
"last_name": "Sharma",
"email": "rahul.sharma@acmecorp.com",
"phone": "+919876543210",
"company_name": "Acme Corp",
"job_title": "CTO",
"status": "Proposal Sent",
"source": "Facebook Lead Ad",
"priority": "high",
"estimated_value": "500000.00",
"expected_close_date": "2026-07-15",
"tags": "hot-lead, q1-campaign, api-lead",
"campus_id": 3,
"campus_name": "Whitefield Hub",
"assigned_to": "Sneha Kapoor",
"is_active": true,
"created_at": "2026-06-06T10:30:00.000000+00:00",
"updated_at": "2026-06-06T11:45:00.000000+00:00"
},
"changes": {
"from_status": "Contacted",
"to_status": "Proposal Sent"
}
}
Envelope Fields
| Field | Description |
|---|---|
event | One of the five event names above |
occurred_at | ISO 8601 timestamp of when the event fired |
organization_id | Your DeskOS organization ID |
lead | Snapshot of the lead after the change |
changes | Event-specific context. {} for events that don't carry any. |
The lead Object
| Field | Type | Notes |
|---|---|---|
id | integer | DeskOS lead ID - use this with PATCH /leads/<id>/ to write back |
lead_type | string | individual, company, or visitor |
first_name, last_name | string | |
email, phone | string / null | |
company_name, job_title | string / null | |
status | string / null | Pipeline stage name |
source | string / null | Lead source name |
priority | string | low, medium, high, or urgent |
estimated_value | string / null | Decimal serialized as a string |
expected_close_date | date / null | YYYY-MM-DD |
tags | string | Comma-separated |
campus_id, campus_name | integer / string | Which campus the lead belongs to |
assigned_to | string / null | Full name of the assigned team member |
is_active | boolean | false after a soft delete |
created_at, updated_at | datetime | ISO 8601, UTC |
The webhook payload uses status and source, while the REST endpoints above return status_name and source_name. The values are the same - just watch the key names when mapping fields.
These keys are stable. New fields may be added over time, but existing ones will not be renamed or removed, so parse defensively (ignore unknown keys) rather than pinning to an exact shape.
Per-Event changes
lead.status_changed
"changes": {
"from_status": "Contacted",
"to_status": "Proposal Sent"
}
from_status is null when the lead had no previous status.
lead.converted
"changes": {
"company_id": 87,
"company_name": "Acme Corp",
"proposal_id": "3c9d1f4a-8b2e-4d6f-9a1c-5e7b0d3f8a26",
"won_value_monthly": "150000.00"
}
proposal_id is null if the lead was converted without a proposal.
lead.created, lead.updated, lead.deleted carry "changes": {}.
Request Headers
Every delivery includes:
| Header | Example | Purpose |
|---|---|---|
Content-Type | application/json | |
User-Agent | DeskOS-Webhooks/1.0 | Identifies DeskOS traffic in your logs |
X-DeskOS-Event | lead.status_changed | Route without parsing the body |
X-DeskOS-Delivery | a4f1c8e2-... | Unique per delivery attempt - use this for idempotency |
X-DeskOS-Signature | sha256=3f9a2b7c... | HMAC-SHA256 of the raw body, keyed with your secret |
Verifying the Signature
Anyone who learns your URL can post to it. Always verify the signature before acting on a payload.
The signature is HMAC-SHA256(secret, raw_request_body), hex-encoded, prefixed with sha256=.
DeskOS signs the exact bytes it sends, serialized with compact separators and sorted keys. If you parse the JSON and re-serialize it before hashing, your signature will not match - even though the data is identical.
Capture the raw body before any JSON parsing. Most frameworks make this available (request.body in Django/Flask, req.rawBody in Express with a verify hook, the "raw body" option in n8n's Webhook node).
Python (Flask)
import hashlib
import hmac
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_secret_here"
@app.route("/webhooks/deskos-leads", methods=["POST"])
def deskos_webhook():
signature = request.headers.get("X-DeskOS-Signature", "")
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
request.get_data(), # raw bytes, NOT request.json
hashlib.sha256,
).hexdigest()
# Constant-time compare guards against timing attacks
if not hmac.compare_digest(signature, expected):
abort(401)
payload = request.get_json()
event = payload["event"]
lead = payload["lead"]
if event == "lead.status_changed":
print(f"{lead['first_name']} moved to {payload['changes']['to_status']}")
return "", 200
Node.js (Express)
const express = require("express");
const crypto = require("crypto");
const app = express();
const WEBHOOK_SECRET = "whsec_your_secret_here";
// Keep the raw body around so we can verify against the exact bytes sent
app.use(
express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
},
})
);
app.post("/webhooks/deskos-leads", (req, res) => {
const signature = req.get("X-DeskOS-Signature") || "";
const expected =
"sha256=" +
crypto.createHmac("sha256", WEBHOOK_SECRET).update(req.rawBody).digest("hex");
const valid =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const { event, lead, changes } = req.body;
if (event === "lead.converted") {
console.log(`${lead.company_name} converted to company ${changes.company_id}`);
}
res.sendStatus(200);
});
Delivery, Retries & Failure Handling
| Behaviour | Detail |
|---|---|
| Timeout | 10 seconds. Respond quickly - queue slow work rather than doing it inline. |
| Success | Any 2xx response. Anything else (or a timeout) counts as a failure. |
| Retries | Up to 5, with exponential backoff: 10s, 20s, 40s, 80s, 160s. |
| Delivery guarantee | At-least-once. A slow-but-successful response can still be retried, so you may receive the same event twice. |
| Auto-disable | After 15 consecutive failed events (all retries exhausted), the subscription is deactivated and deliveries stop until it's re-registered. |
| Ordering | Not guaranteed. Retries mean a later event can arrive before an earlier one - use occurred_at to order, and updated_at to discard stale writes. |
| Transactional safety | Events are only sent after the database change commits. A rolled-back save never produces a webhook. |
Because delivery is at-least-once, store the X-DeskOS-Delivery header value and skip anything you've already processed. Alternatively, key on lead.id + occurred_at.
Checking Delivery Health
Your DeskOS admin can see every subscription's health in the CRM under Leads → Settings → Integrations, including consecutive failure count, last successful delivery, last failure, and the last error message. That's the first place to look if events stop arriving.
Scope & Limitations
- Subscriptions are organization-wide, not campus-scoped. You receive events for leads at every campus in your organization. If you only care about one campus, filter on
lead.campus_idin your handler. - Webhook endpoints don't count against your daily rate limit. Registering, listing, and deleting subscriptions are free; only lead operations are metered.
- Soft deletes fire
lead.deleted. There's no separate "deactivated" event - checklead.is_activeif you need to distinguish. - Target URLs must be publicly reachable. DeskOS calls out from its own servers, so
localhostand private-network addresses won't work. Use a tunnel (ngrok, Cloudflare Tunnel) for local development.
Quick Setup Example
# 1. Register, subscribing to the three high-signal events
curl -X POST "https://<your-server-url>/api/v1/leads-management/external/webhooks/" \
-H "Content-Type: application/json" \
-H "X-API-Key: pk_live_your_key_here" \
-d '{
"target_url": "https://automation.yourcompany.com/webhooks/deskos-leads",
"events": ["lead.created", "lead.status_changed", "lead.converted"]
}'
# 2. Save the returned "secret" into your handler's config.
# 3. Create a test lead - your endpoint should receive lead.created
curl -X POST "https://<your-server-url>/api/v1/leads-management/external/leads/" \
-H "Content-Type: application/json" \
-H "X-API-Key: pk_live_your_key_here" \
-d '{"first_name": "Webhook", "last_name": "Test", "email": "webhook-test@example.com"}'
# 4. Confirm the subscription is healthy
curl -X GET "https://<your-server-url>/api/v1/leads-management/external/webhooks/" \
-H "X-API-Key: pk_live_your_key_here"
Error Responses
400: Validation Error
Returned when the request body fails validation.
{
"success": false,
"errors": {
"first_name": ["This field is required."],
"email": ["Enter a valid email address."],
"required_seats": ["Ensure this value is greater than or equal to 1."]
}
}
400: Campus Resolution Failed
Returned when the API can't determine which campus to create the lead on.
{
"success": false,
"errors": {
"campus_id": [
"Could not determine campus. Either pass 'campus_id' in the request or set a default campus on the API key."
]
}
}
401: Authentication Failed
Returned when the API key is missing, invalid, or deactivated.
{
"detail": "Invalid API key."
}
{
"detail": "API key is deactivated."
}
404: Lead Not Found
Returned by the update and delete endpoints when the lead ID doesn't exist or doesn't belong to your organization.
{
"success": false,
"errors": {
"lead_id": ["Lead not found or not accessible."]
}
}
429: Rate Limit Exceeded
Returned when the daily request limit for the API key has been reached.
{
"detail": "Daily rate limit exceeded for this API key."
}
What Happens Automatically
When a lead is created through this API, DeskOS runs the same automation pipeline as internally created leads:
| Automation | What Happens |
|---|---|
| Default status | The lead is assigned your campus's default status (e.g., "New"). |
| Source tagging | The lead source is resolved from source_name, the API key's default, or auto-created as "Public API". |
| Auto-assignment | If enabled in your campus's lead settings, the lead is automatically assigned to the team member with the fewest active leads. |
| Follow-up creation | If enabled, a follow-up task is created and assigned to the team member, due in X days (configurable in lead settings). |
| Assignment alert | The assigned team member receives an alert about the new lead. |
| Activity logging | An activity entry is created recording that the lead was submitted via the public API. Updates and deletes are also logged. |
| Deduplication | If a lead with the same email exists on the campus, the existing record is updated instead of creating a duplicate. |
| Tags | All API-created leads automatically receive the api-lead tag. If you pass external_id, it's stored as ext:<your_id>. |
Auto-assignment, follow-up creation, and follow-up timing are controlled by your campus's Lead Settings in the DeskOS admin panel. Contact your DeskOS admin to adjust these.
Integration Guides
Zapier: Facebook Lead Ads → DeskOS
This is the most common integration. When someone fills out your Facebook Lead Ad form, Zapier catches it and pushes it to DeskOS.
Step 1: Create a new Zap in Zapier.
Step 2: Set the Trigger to Facebook Lead Ads → New Lead.
Step 3: Set the Action to Webhooks by Zapier → Custom Request.
Step 4: Configure the action:
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://<your-server-url>/api/v1/leads-management/external/leads/ |
| Payload Type | json |
| Headers | Content-Type: application/json |
| Headers | X-API-Key: pk_live_your_key_here |
Step 5: Set the Data body, mapping your Facebook form fields:
{
"first_name": "{{first_name}}",
"last_name": "{{last_name}}",
"email": "{{email}}",
"phone": "{{phone_number}}",
"company_name": "{{company_name}}",
"source_name": "Facebook Lead Ad",
"notes": "Campaign: {{campaign_name}} | Ad Set: {{adset_name}} | Ad: {{ad_name}}",
"external_id": "{{id}}",
"custom_fields": {
"fb_campaign": "{{campaign_name}}",
"fb_lead_id": "{{id}}"
}
}
Capture the Facebook campaign and ad names in the notes field so your sales team knows exactly which ad brought the lead in. You can also store them in custom fields for structured filtering later.
Step 6: Test the Zap against the staging environment first, then switch the URL to production.
Pabbly Connect: Google Ads → DeskOS
Step 1: Create a new workflow in Pabbly Connect.
Step 2: Set the Trigger to Google Ads → New Lead Form Response.
Step 3: Add an Action → API / Webhook (Custom).
Step 4: Configure:
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://<your-server-url>/api/v1/leads-management/external/leads/ |
| Headers | Content-Type: application/json, X-API-Key: pk_live_your_key_here |
| Body | JSON (map your fields below) |
Step 5: Map the body:
{
"first_name": "{{first_name}}",
"last_name": "{{last_name}}",
"email": "{{email}}",
"phone": "{{phone}}",
"source_name": "Google Ads",
"notes": "Campaign: {{campaign_name}}",
"external_id": "{{lead_id}}"
}
Make (Integromat): Any Source → DeskOS
Step 1: Add your trigger module (Facebook, Google Sheets, Typeform, etc.).
Step 2: Add an HTTP → Make a Request module.
Step 3: Configure:
| Setting | Value |
|---|---|
| URL | https://<your-server-url>/api/v1/leads-management/external/leads/ |
| Method | POST |
| Body Type | Raw |
| Content Type | JSON (application/json) |
| Headers | X-API-Key: pk_live_your_key_here |
Step 4: Set the request body with your mapped fields.
Google Sheets → DeskOS (via Zapier / Pabbly)
If your team collects walk-in enquiries in a Google Sheet:
Trigger: Google Sheets → New Row
Action: Webhook to DeskOS API
{
"first_name": "{{Column A - Name}}",
"email": "{{Column B - Email}}",
"phone": "{{Column C - Phone}}",
"company_name": "{{Column D - Company}}",
"required_seats": "{{Column E - Seats}}",
"source_name": "Walk-in Enquiry",
"notes": "{{Column F - Remarks}}"
}
If your sheet has a full name in one column (e.g., "Rahul Sharma"), map it to first_name and leave last_name empty. DeskOS will store the full string as the first name, and you can always clean it up later.
Direct HTTP Call: cURL
# Create a lead
curl -X POST "https://<your-server-url>/api/v1/leads-management/external/leads/" \
-H "Content-Type: application/json" \
-H "X-API-Key: pk_live_your_key_here" \
-d '{
"first_name": "Rahul",
"last_name": "Sharma",
"email": "rahul@acmecorp.com",
"phone": "+919876543210",
"company_name": "Acme Corp",
"source_name": "Website Contact Form",
"required_seats": 10,
"monthly_budget": 150000,
"custom_fields": { "crm_id": "SF-00123" }
}'
# Update a lead (by DeskOS lead ID)
curl -X PATCH "https://<your-server-url>/api/v1/leads-management/external/leads/142/" \
-H "Content-Type: application/json" \
-H "X-API-Key: pk_live_your_key_here" \
-d '{
"status_name": "Proposal Sent",
"monthly_budget": 200000,
"custom_fields": { "crm_id": "SF-00456" }
}'
# Delete a lead (soft-delete)
curl -X DELETE "https://<your-server-url>/api/v1/leads-management/external/leads/142/" \
-H "X-API-Key: pk_live_your_key_here"
Direct HTTP Call: Python
import requests
BASE_URL = "https://<your-server-url>/api/v1/leads-management/external"
API_KEY = "pk_live_your_key_here"
HEADERS = {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
}
# Create a lead
response = requests.post(
f"{BASE_URL}/leads/",
headers=HEADERS,
json={
"first_name": "Rahul",
"last_name": "Sharma",
"email": "rahul@acmecorp.com",
"phone": "+919876543210",
"company_name": "Acme Corp",
"source_name": "Website Contact Form",
"required_seats": 10,
"monthly_budget": 150000,
"custom_fields": {"crm_id": "SF-00123"},
},
)
lead_id = response.json()["lead"]["id"]
# Update the lead
requests.patch(
f"{BASE_URL}/leads/{lead_id}/",
headers=HEADERS,
json={
"status_name": "Proposal Sent",
"custom_fields": {"crm_id": "SF-00456"},
},
)
# Soft-delete the lead
requests.delete(f"{BASE_URL}/leads/{lead_id}/", headers=HEADERS)
Direct HTTP Call: JavaScript / Node.js
const BASE_URL = "https://<your-server-url>/api/v1/leads-management/external";
const HEADERS = {
"Content-Type": "application/json",
"X-API-Key": "pk_live_your_key_here",
};
// Create a lead
const createRes = await fetch(`${BASE_URL}/leads/`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
first_name: "Rahul",
last_name: "Sharma",
email: "rahul@acmecorp.com",
phone: "+919876543210",
company_name: "Acme Corp",
source_name: "Website Contact Form",
required_seats: 10,
monthly_budget: 150000,
custom_fields: { crm_id: "SF-00123" },
}),
});
const { lead } = await createRes.json();
// Update the lead
await fetch(`${BASE_URL}/leads/${lead.id}/`, {
method: "PATCH",
headers: HEADERS,
body: JSON.stringify({
status_name: "Proposal Sent",
custom_fields: { crm_id: "SF-00456" },
}),
});
// Soft-delete the lead
await fetch(`${BASE_URL}/leads/${lead.id}/`, {
method: "DELETE",
headers: HEADERS,
});
Deduplication Explained
Deduplication prevents the same person from appearing as multiple leads in your CRM. This is important because the same person might:
- Fill out a Facebook form and a Google Ads form.
- Submit a website contact form, then come in for a walk-in tour.
- Be imported from a Google Sheet that's synced periodically (sending the same row again).
How It Works
When deduplicate is true (the default):
- The API checks if a lead with the same email address already exists on the same campus.
- If no match is found: A new lead is created normally. Response:
201 Createdwithis_new: true. - If a match is found: The existing lead is updated:
- Blank fields are filled in: if the existing lead has no
company_namebut the new request includes one, it's added. Existing values are never overwritten. - Tags are appended: new tags are added alongside existing ones.
- Notes are appended: new notes are added below existing notes with a timestamp separator.
- Custom fields are upserted: provided keys are written or updated; keys not in the request are left as-is.
- Response:
200 OKwithis_new: false.
- Blank fields are filled in: if the existing lead has no
When to Disable Deduplication
Set "deduplicate": false if you intentionally want to create separate lead records for every submission, even if the email matches. This might be useful for:
- Event registrations where each submission is a separate enquiry.
- Forms where the same person might be enquiring on behalf of different companies.
Dedup vs. PATCH
POST /leads/ (with dedup) | PATCH /leads/<id>/ | |
|---|---|---|
| How lead is identified | By email + campus | By lead ID |
| What gets updated | Only blank/empty fields | All fields you include |
| Tags behaviour | Appended | Replaced entirely |
| Notes behaviour | Appended with timestamp | Replaced entirely |
| Custom fields | Upserted | Upserted (same keys only) |
| Use when | Pushing from external sources | Explicitly syncing a known record |
Rate Limiting
Each API key has a configurable daily limit (default: 1,000 requests per day). The counter resets at midnight UTC.
| Field | Description |
|---|---|
| Daily limit | Configured per API key (visible in the /health/ endpoint) |
| Current usage | Check via the /health/ endpoint (requests_today field) |
| Exceeded response | 429 Too Many Requests |
If you're running high-volume campaigns and need a higher limit, contact the DeskOS team.
Best Practices
1. Always Send an Email When Available
Email is the primary deduplication key. Without it, the system can't detect if a lead already exists and may create duplicates.
2. Set Your Sources Up First, Then Match Them Exactly
Create the sources you plan to use in DeskOS (Leads → Settings → Sources) before you go live, then send those exact names in source_name. Specific names like "Facebook Lead Ad - Q1 Campaign" or "Google Ads - Coworking" make your pipeline analytics far more useful than a generic "Online".
A name that doesn't match an existing source is silently filed under "Unknown Source" - no error is returned - so a typo will quietly cost you attribution. If you see leads piling up under "Unknown Source", check your integration's spelling against the source list.
3. Capture Campaign Context in Notes
For ad-driven leads, include campaign and ad details in the notes field:
{
"notes": "Campaign: Summer Coworking Promo | Ad Set: Bangalore IT | Ad: 10-Seater Cabin"
}
4. Use Custom Fields for Structured Data
Instead of cramming extra data into notes, map it to custom fields for structured filtering and reporting in DeskOS:
{
"custom_fields": {
"fb_campaign": "Summer Promo",
"budget_range": "1L-2L",
"referral_channel": "Instagram"
}
}
5. Use External IDs for Traceability
Always pass external_id when your source system provides one (Facebook lead ID, Google form response ID, etc.). This lets you trace any DeskOS lead back to its original source, and lets you use PATCH to update it later using the returned lead.id.
6. Test on Staging First
Before going live, send a few test leads to your staging environment to verify:
- The API key works (hit the
/health/endpoint first). - Fields are mapping correctly.
- Custom field slugs are correct and values appear in the CRM.
- Leads appear with the right status, source, and assignment.
7. Handle Errors Gracefully
In Zapier/Pabbly, enable error notifications so you're alerted if leads fail to sync. Common issues:
- 401 errors: API key is wrong or deactivated.
- 400 errors: A required field is missing or a field value is invalid.
- 404 errors: Lead ID not found (for update/delete).
- 429 errors: Daily limit reached, so leads will fail until the next day.
8. Use Bulk Endpoint for Batch Imports
If you're migrating historical leads or syncing a large Google Sheet, use the bulk endpoint instead of hitting the single endpoint in a loop. It's faster and counts as fewer requests against your rate limit.
Frequently Asked Questions
Q: What happens if I send a lead without an email?
A: The lead is created normally, but deduplication won't work for it (since email is the dedup key). If the same person submits again without an email, a duplicate may be created.
Q: Can I update an existing lead's name or phone via the API?
A: Yes, use the PATCH /leads/<id>/ endpoint. When deduplication matches an existing lead via POST /leads/, only blank fields are filled in. But PATCH explicitly overwrites whatever fields you send.
Q: What if my organization has multiple campuses?
A: You have three options:
- Set a
default_campuson your API key (easiest: all leads go to one campus). - Pass
campus_idin each request to route leads to specific campuses. - Create separate API keys for each campus, each with its own default campus.
Q: Can I delete a lead via the API?
A: Yes, use DELETE /leads/<id>/. This performs a soft delete: the lead is marked inactive and hidden from the CRM pipeline but the record is preserved in the database. For permanent deletion (e.g., GDPR erasure), use the DeskOS CRM interface.
Q: Can I set a lead's status when creating it?
A: Not on creation. New leads are assigned your campus's default status automatically. Use PATCH /leads/<id>/ afterward with status_name to move the lead to a different stage. Every status change is automatically recorded in the history, retrievable via GET /leads/<id>/history/.
Q: What custom field types are supported?
A: DeskOS supports text, textarea, number, date, select (dropdown), multi-select, checkbox, URL, phone, and email custom fields. All values are passed as strings via the API, and DeskOS stores them as provided.
Q: Is there a webhook to get notified when a lead's status changes?
A: Yes. Register a webhook subscription for the lead.status_changed event and DeskOS will POST to your URL every time a lead moves pipeline stage, with the old and new status included. See Webhooks (Outbound Events). You can also subscribe to lead creation, updates, deletion, and conversion.
Q: How do I know a webhook really came from DeskOS?
A: Every delivery is signed with X-DeskOS-Signature: sha256=<hmac>, computed from your subscription's secret and the raw request body. Verify it before trusting the payload - there are copy-paste examples for Python and Node in the signature verification section. The one gotcha: hash the raw body bytes, not a re-serialized copy of the parsed JSON.
Q: My webhook endpoint stopped receiving events. What happened?
A: A subscription is automatically deactivated after 15 consecutive failed events, so an endpoint that was down or erroring for a while may have been disabled. Ask your DeskOS admin to check Leads → Settings → Integrations for the last error message, then re-register the subscription once your endpoint is healthy again.
Q: What timezone are dates in?
A: All datetime fields in responses are in UTC (ISO 8601 format). Date-only fields like move_in_date are timezone-naive (YYYY-MM-DD).
Q: I lost my API key. Can the DeskOS team retrieve it?
A: No. API keys are hashed and cannot be retrieved. The DeskOS team will generate a new key for you and deactivate the old one.
Q: Can I use this API from a frontend/browser?
A: We strongly advise against it. Your API key would be exposed in browser network traffic. Always call this API from a backend server or through integration tools like Zapier/Pabbly.
Quick Reference
| Action | Method | Endpoint |
|---|---|---|
| Create a lead | POST | /api/v1/leads-management/external/leads/ |
| Bulk create leads | POST | /api/v1/leads-management/external/leads/bulk/ |
| Retrieve a lead | GET | /api/v1/leads-management/external/leads/<id>/ |
| Update a lead | PATCH | /api/v1/leads-management/external/leads/<id>/ |
| Delete a lead (soft) | DELETE | /api/v1/leads-management/external/leads/<id>/ |
| Add an activity / note | POST | /api/v1/leads-management/external/leads/<id>/activities/ |
| Retrieve activities | GET | /api/v1/leads-management/external/leads/<id>/activities/ |
| Retrieve status history | GET | /api/v1/leads-management/external/leads/<id>/history/ |
| Validate API key | GET | /api/v1/leads-management/external/health/ |
| Register a webhook | POST | /api/v1/leads-management/external/webhooks/ |
| List webhooks | GET | /api/v1/leads-management/external/webhooks/ |
| Delete a webhook | DELETE | /api/v1/leads-management/external/webhooks/<id>/ |
| Webhook Event | Fires when |
|---|---|
lead.created | A new lead is created |
lead.updated | Any non-status field changes (chatty - see the caveat above) |
lead.status_changed | The lead moves pipeline stage |
lead.deleted | The lead is soft- or hard-deleted |
lead.converted | The lead becomes a member/company |
| Auth Method | How |
|---|---|
| Header (recommended) | Authorization: Api-Key pk_live_xxx |
| Header (alternative) | X-API-Key: pk_live_xxx |
| Query param (testing) | ?api_key=pk_live_xxx |
| Status Code | Meaning |
|---|---|
200 | Lead updated (dedup match, PATCH, or bulk response) |
201 | New lead created |
204 | Lead deleted successfully (no body) |
400 | Validation error: check the errors object |
401 | Invalid or deactivated API key |
404 | Lead not found or not accessible |
429 | Daily rate limit exceeded |
Getting Help
If you run into issues or need changes to your API key configuration (rate limits, default campus, allowed campuses, custom field setup, etc.), reach out to the DeskOS team through your usual support channel. They can also help you set up and test your Zapier/Pabbly integration.
- Email: sales@deskos.net
- Phone: +91 742810 9069