Skip to main content

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:

  1. The DeskOS team generates a secure API key for your organization.
  2. 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.
  3. Every lead flows into DeskOS in real time: auto-assigned, auto-tagged, with follow-ups created automatically.
  4. 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:

EnvironmentPurposeServer URL
Test / StagingDevelopment and testingProvided by the DeskOS team
ProductionLive lead dataProvided by the DeskOS team
note

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):

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
tip

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_id with every request.
  • Default source: Your key can have a default lead source (e.g., "Website") so leads are auto-tagged even when source_name isn'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​
FieldTypeDescription
first_namestring (max 100)Lead's first name
Contact Information (Optional)​
FieldTypeDefaultDescription
last_namestring (max 100)""Lead's last name
emailemailnullEmail 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.
phonestring (max 20)""Primary phone number. Any format accepted (e.g., +919876543210, 098765 43210).
alternate_phonestring (max 20)nullSecondary phone number
Company & Professional (Optional)​
FieldTypeDefaultDescription
company_namestring (max 200)nullCompany or organization name
job_titlestring (max 100)nullJob title or designation
linkedin_urlURLnullLinkedIn profile URL
Lead Classification (Optional)​
FieldTypeDefaultDescription
lead_typeenum"individual"Type of lead. Options: individual, company, visitor
source_namestring (max 100)nullName 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_namestring (max 100)nullIndustry the lead belongs to (e.g., "Technology", "Healthcare"). Automatically created if it doesn't exist.
tagsstring (max 255)nullComma-separated tags for categorization. The system automatically appends api-lead to all leads created via this API.
Space Requirements (Optional)​
FieldTypeDefaultDescription
space_preferencestring (max 100)nullType of space the lead is looking for (e.g., "Private Cabin", "Hot Desk", "Dedicated Desk", "Virtual Office")
required_seatsinteger (min: 1)nullNumber of seats or desks needed
move_in_datedate (YYYY-MM-DD)nullWhen the lead wants to start
Budget & Deal (Optional)​
FieldTypeDefaultDescription
estimated_valuedecimalnullEstimated total deal value
monthly_budgetdecimalnullMonthly budget the lead has indicated
Notes & Details (Optional)​
FieldTypeDefaultDescription
notestextnullFree-text notes about the lead
requirementstextnullDetailed space or service requirements
pain_pointstextnullChallenges or problems the lead is facing
referral_sourcestring (max 255)nullWho or what referred this lead
Custom Fields (Optional)​
FieldTypeDefaultDescription
custom_fieldsobject{}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"
}
Getting your custom field slugs

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)​
FieldTypeDefaultDescription
campus_idintegernullID 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.
deduplicatebooleantrueWhen 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_idstring (max 255)nullYour 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."]
}
}
]
}
note

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​

FieldTypeDescription
first_namestring (max 100)Lead's first name
last_namestring (max 100)Lead's last name
emailemailLead's email address
phonestring (max 20)Primary phone number
alternate_phonestring (max 20)Secondary phone number
company_namestring (max 200)Company or organization name
job_titlestring (max 100)Job title or designation
linkedin_urlURLLinkedIn profile URL
lead_typeenumindividual, company, or visitor
source_namestring (max 100)Lead source name. Must match an existing source in DeskOS. Unrecognized names are ignored.
industry_namestring (max 100)Industry name. Auto-created if it doesn't exist.
status_namestring (max 100)Pipeline status name. Must match an existing status for this campus. Unrecognized names are ignored.
tagsstring (max 255)Comma-separated tags. Replaces the existing tags value entirely.
space_preferencestring (max 100)Type of space preferred
required_seatsinteger (min: 1)Number of seats needed
move_in_datedate (YYYY-MM-DD)Desired move-in date
estimated_valuedecimalEstimated total deal value
monthly_budgetdecimalMonthly budget
notestextFree-text notes. Replaces the existing notes value entirely.
requirementstextSpace/service requirements
pain_pointstextChallenges the lead is facing
referral_sourcestring (max 255)Referral information
custom_fieldsobjectKey-value pairs of custom field slugs → values. Only the keys you include are updated; other custom fields are untouched.
Difference from deduplication update

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"
Soft delete, not permanent removal

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​

FieldTypeRequiredDescription
activity_typeenumYesType of activity. See values below.
subjectstring (max 255)YesShort summary of the activity
descriptiontextNoFull details / body of the note or activity
performed_atdatetime (ISO 8601)NoWhen the activity happened. Defaults to now.
durationintegerNoDuration in minutes (useful for calls and meetings)

activity_type values:

ValueMeaning
noteInternal note
callPhone call
emailEmail sent or received
meetingIn-person or video meeting
whatsappWhatsApp message
visitPhysical visit to the campus
taskTask or to-do item
otherAnything 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"
}
}
note

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​

ParameterDefaultDescription
page1Page number
page_size20Results 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
}
FieldDescription
statusAlways "ok" if the key is valid
key_nameLabel assigned to this API key
organizationYour organization name in DeskOS
default_campusThe campus leads go to if campus_id isn't specified
allowed_campusesList of campus names this key can target, or "all"
default_sourceDefault lead source applied when source_name isn't sent
daily_limitMaximum leads this key can create per day
requests_todayHow 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​

  1. You register a target URL with DeskOS, choosing which events you care about.
  2. DeskOS returns a signing secret (shown once, at registration).
  3. Whenever a matching event happens, DeskOS POSTs a JSON payload to your URL, signed with that secret.
  4. 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​

FieldTypeRequiredDescription
target_urlURL (max 500)YesWhere DeskOS will POST events. Must be publicly reachable over HTTPS.
eventsarray of stringsNoWhich 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"
}
}
Save the secret

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.

Registering the same URL twice creates two subscriptions

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"
}
]
}
Recovering a lost secret

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​

EventFires when
lead.createdA new lead is created - through this API, the DeskOS CRM, a web form, or an ad integration.
lead.updatedAny field on an existing lead changes (other than a status change). See the caveat below.
lead.status_changedThe lead moves from one pipeline stage to another. Carries the old and new status in changes.
lead.deletedThe lead is soft-deleted (marked inactive) or hard-deleted from the database.
lead.convertedThe lead is converted into a member/company. Carries the resulting company in changes.
lead.updated is chattier than you'd expect

A 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​

FieldDescription
eventOne of the five event names above
occurred_atISO 8601 timestamp of when the event fired
organization_idYour DeskOS organization ID
leadSnapshot of the lead after the change
changesEvent-specific context. {} for events that don't carry any.

The lead Object​

FieldTypeNotes
idintegerDeskOS lead ID - use this with PATCH /leads/<id>/ to write back
lead_typestringindividual, company, or visitor
first_name, last_namestring
email, phonestring / null
company_name, job_titlestring / null
statusstring / nullPipeline stage name
sourcestring / nullLead source name
prioritystringlow, medium, high, or urgent
estimated_valuestring / nullDecimal serialized as a string
expected_close_datedate / nullYYYY-MM-DD
tagsstringComma-separated
campus_id, campus_nameinteger / stringWhich campus the lead belongs to
assigned_tostring / nullFull name of the assigned team member
is_activebooleanfalse after a soft delete
created_at, updated_atdatetimeISO 8601, UTC
Key names differ slightly from the REST responses

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:

HeaderExamplePurpose
Content-Typeapplication/json
User-AgentDeskOS-Webhooks/1.0Identifies DeskOS traffic in your logs
X-DeskOS-Eventlead.status_changedRoute without parsing the body
X-DeskOS-Deliverya4f1c8e2-...Unique per delivery attempt - use this for idempotency
X-DeskOS-Signaturesha256=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=.

Verify against the raw body bytes

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​

BehaviourDetail
Timeout10 seconds. Respond quickly - queue slow work rather than doing it inline.
SuccessAny 2xx response. Anything else (or a timeout) counts as a failure.
RetriesUp to 5, with exponential backoff: 10s, 20s, 40s, 80s, 160s.
Delivery guaranteeAt-least-once. A slow-but-successful response can still be retried, so you may receive the same event twice.
Auto-disableAfter 15 consecutive failed events (all retries exhausted), the subscription is deactivated and deliveries stop until it's re-registered.
OrderingNot 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 safetyEvents are only sent after the database change commits. A rolled-back save never produces a webhook.
Make your handler idempotent

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_id in 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 - check lead.is_active if you need to distinguish.
  • Target URLs must be publicly reachable. DeskOS calls out from its own servers, so localhost and 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:

AutomationWhat Happens
Default statusThe lead is assigned your campus's default status (e.g., "New").
Source taggingThe lead source is resolved from source_name, the API key's default, or auto-created as "Public API".
Auto-assignmentIf enabled in your campus's lead settings, the lead is automatically assigned to the team member with the fewest active leads.
Follow-up creationIf enabled, a follow-up task is created and assigned to the team member, due in X days (configurable in lead settings).
Assignment alertThe assigned team member receives an alert about the new lead.
Activity loggingAn activity entry is created recording that the lead was submitted via the public API. Updates and deletes are also logged.
DeduplicationIf a lead with the same email exists on the campus, the existing record is updated instead of creating a duplicate.
TagsAll API-created leads automatically receive the api-lead tag. If you pass external_id, it's stored as ext:<your_id>.
Configuration

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:

SettingValue
MethodPOST
URLhttps://<your-server-url>/api/v1/leads-management/external/leads/
Payload Typejson
HeadersContent-Type: application/json
HeadersX-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}}"
}
}
tip

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:

SettingValue
MethodPOST
URLhttps://<your-server-url>/api/v1/leads-management/external/leads/
HeadersContent-Type: application/json, X-API-Key: pk_live_your_key_here
BodyJSON (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:

SettingValue
URLhttps://<your-server-url>/api/v1/leads-management/external/leads/
MethodPOST
Body TypeRaw
Content TypeJSON (application/json)
HeadersX-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}}"
}
tip

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):

  1. The API checks if a lead with the same email address already exists on the same campus.
  2. If no match is found: A new lead is created normally. Response: 201 Created with is_new: true.
  3. If a match is found: The existing lead is updated:
    • Blank fields are filled in: if the existing lead has no company_name but 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 OK with is_new: false.

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 identifiedBy email + campusBy lead ID
What gets updatedOnly blank/empty fieldsAll fields you include
Tags behaviourAppendedReplaced entirely
Notes behaviourAppended with timestampReplaced entirely
Custom fieldsUpsertedUpserted (same keys only)
Use whenPushing from external sourcesExplicitly 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.

FieldDescription
Daily limitConfigured per API key (visible in the /health/ endpoint)
Current usageCheck via the /health/ endpoint (requests_today field)
Exceeded response429 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:

  1. Set a default_campus on your API key (easiest: all leads go to one campus).
  2. Pass campus_id in each request to route leads to specific campuses.
  3. 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​

ActionMethodEndpoint
Create a leadPOST/api/v1/leads-management/external/leads/
Bulk create leadsPOST/api/v1/leads-management/external/leads/bulk/
Retrieve a leadGET/api/v1/leads-management/external/leads/<id>/
Update a leadPATCH/api/v1/leads-management/external/leads/<id>/
Delete a lead (soft)DELETE/api/v1/leads-management/external/leads/<id>/
Add an activity / notePOST/api/v1/leads-management/external/leads/<id>/activities/
Retrieve activitiesGET/api/v1/leads-management/external/leads/<id>/activities/
Retrieve status historyGET/api/v1/leads-management/external/leads/<id>/history/
Validate API keyGET/api/v1/leads-management/external/health/
Register a webhookPOST/api/v1/leads-management/external/webhooks/
List webhooksGET/api/v1/leads-management/external/webhooks/
Delete a webhookDELETE/api/v1/leads-management/external/webhooks/<id>/
Webhook EventFires when
lead.createdA new lead is created
lead.updatedAny non-status field changes (chatty - see the caveat above)
lead.status_changedThe lead moves pipeline stage
lead.deletedThe lead is soft- or hard-deleted
lead.convertedThe lead becomes a member/company
Auth MethodHow
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 CodeMeaning
200Lead updated (dedup match, PATCH, or bulk response)
201New lead created
204Lead deleted successfully (no body)
400Validation error: check the errors object
401Invalid or deactivated API key
404Lead not found or not accessible
429Daily 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.