Overview
Public CRM API provides secure read-only access for analytics systems, BI/reporting tools,
partner integrations, and data pipelines.
All responses use a standard JSON envelope:
{
"data": [...],
"count": 123,
"page": { "limit": 50, "cursor": "<token>" },
"meta": { "from": "...", "to": "...", "sortBy": "...", "sortDir": "...", "filters": {} }
}Authentication
All requests require an API key in the request header:
x-api-key: <YOUR_API_KEY>| Scenario | HTTP Status | Error Code |
|---|---|---|
| Missing header | 401 | MISSING_API_KEY |
| Invalid key | 401 | INVALID_API_KEY |
| Disabled key / missing permission | 403 | FORBIDDEN |
Base URLs
Each resource is served by a dedicated base endpoint URL:
| Resource | Base URL |
|---|---|
| Contacts | https://us-central1-kendal-17416.cloudfunctions.net/contacts |
| Listings | https://us-central1-kendal-17416.cloudfunctions.net/listings |
| Inquiries | https://us-central1-kendal-17416.cloudfunctions.net/inquiries |
| Deals | https://us-central1-kendal-17416.cloudfunctions.net/deals |
| Appointments | https://us-central1-kendal-17416.cloudfunctions.net/appointments |
Pagination
All list endpoints use cursor-based pagination:
- Call the endpoint without a
cursorparameter - Read
page.cursorfrom the response - Call the same endpoint with
cursor=<value>to get the next page - Stop when
cursorisnull
Never construct cursors manually — they are opaque tokens signed by the server.
Rate Limits
Rate limits are applied per API key using a sliding window.
| Header | Description |
|---|---|
x-ratelimit-remaining | Requests remaining in current window |
x-ratelimit-reset | Unix timestamp when the window resets |
Exceeding the limit returns 429 RATE_LIMIT_EXCEEDED.
Best practices:
- Use
/countendpoints when you only need totals - Retry
UPSTREAM_SEARCH_ERRORwith exponential backoff - Use explicit
from/tofor reproducible reports - Log the
x-request-idresponse header for debugging
Errors
All errors use a consistent envelope and are accompanied by an HTTP status code.
| Code | HTTP | Meaning |
|---|---|---|
| MISSING_API_KEY | 401 | Missing x-api-key header |
| INVALID_API_KEY | 401 | Key not recognized |
| FORBIDDEN | 403 | Missing endpoint permission |
| INVALID_QUERY | 400 | Invalid query parameter value |
| INVALID_CURSOR | 400 | Cursor invalid or tampered |
| RATE_LIMIT_EXCEEDED | 429 | Request rate exceeded for this API key |
| UPSTREAM_SEARCH_ERROR | 502 | Elasticsearch temporary issue — retry with backoff |
| INTERNAL_ERROR | 500 | Unexpected server error |
Contacts
Read-only access to CRM contact records. Backed by Elasticsearch index contacts_v2.
/api/v1/contactsList Contacts
contacts:dataReturns a paginated list of contacts. Supports filtering by status, source, agent, tags, and full-text search.
limitintegerRange 1-200. Number of records per page.
cursorstringOpaque pagination token from previous page.cursor. Never construct manually.
fromISO datetimeInclusive start of date range on lastEditedOn/updatedAt.
toISO datetimeInclusive end of date range.
sortBystringAccepted: lastEditedOn, createdOn, updatedAt
sortDirstringAccepted: asc, desc
createdBystring (CSV)Filter by creator email. Accepts comma-separated values to match multiple creators (e.g., createdBy=user1@example.com,user2@example.com).
leadAgentstring (CSV)Filter by lead agent email. Accepts comma-separated values to match multiple agents (e.g., leadAgent=agent1@example.com,agent2@example.com).
assignedAgentstring (CSV)Filter by assigned agent email. Accepts comma-separated values to match multiple agents. Alias for leadAgent. Also accepts: agentAssigned.
statusstring (CSV)Filter by contact status. Accepts comma-separated values to match multiple statuses (e.g., status=new,active).
sourcestring (CSV)Filter by contact source. Accepts comma-separated values to match multiple sources (e.g., source=Imported Contact,Website). Example values: "Imported Contact".
tagsstring (CSV)Filter by contact tags. Accepts comma-separated values to match multiple tags. Possible values: "preLead", "prospect", "client". Alias: tag.
hasEmailbooleanOnly returns contacts with a non-empty email field.
hasPhonebooleanOnly returns contacts with a non-empty phone or phoneNumber field.
qstringSearch contacts by name, email, phone number, source, or status. Supports partial matches (e.g., 'john' finds 'John Smith'). Combine with other filters to narrow results.
dataarrayArray of contact objects
contactIdstringUnique contact identifier
firstNamestringContact name
phonestringPhone number
emailstringEmail address
organisationIdstringTenant identifier
sourcestringContact source label
countintegerTotal matching contacts across all pages
pageobjectPagination info
limitintegerRecords per page
cursorstring or nullToken for next page. null when exhausted.
metaobjectApplied filters and sort info
fromstringISO datetime
tostringISO datetime
sortBystringField used for sorting (e.g., lastEditedOn, createdOn, updatedAt)
sortDirstringSort direction: 'asc' (ascending) or 'desc' (descending)
filtersobjectAll filters applied to this query (status, source, tags, etc.)
/api/v1/contacts/countCount Contacts
contacts:countReturns only the total count and applied filter metadata. Use when you need totals for dashboards and do not need actual records — cheaper on rate limits.
statusstring (CSV)Filter by contact status. Accepts comma-separated values to match multiple statuses (e.g., status=new,active).
sourcestring (CSV)Filter by contact source. Accepts comma-separated values to match multiple sources (e.g., source=Imported Contact,Website). Example values: "Imported Contact".
tagsstring (CSV)Filter by contact tags. Accepts comma-separated values to match multiple tags. Possible values: "preLead", "prospect", "client". Alias: tag.
hasEmailbooleanOnly returns contacts with a non-empty email field.
hasPhonebooleanOnly returns contacts with a non-empty phone or phoneNumber field.
createdBystring (CSV)Filter by creator email. Accepts comma-separated values to match multiple creators (e.g., createdBy=user1@example.com,user2@example.com).
leadAgentstring (CSV)Filter by lead agent email. Accepts comma-separated values to match multiple agents (e.g., leadAgent=agent1@example.com,agent2@example.com).
assignedAgentstring (CSV)Filter by assigned agent email. Accepts comma-separated values to match multiple agents. Alias for leadAgent. Also accepts: agentAssigned.
qstringSearch contacts by name, email, phone number, source, or status. Supports partial matches (e.g., 'john' finds 'John Smith'). Combine with other filters to narrow results.
countintegerTotal number of matching contacts
metaobjectFilter echo
fromstringISO datetime
tostringISO datetime
filtersobjectAll filters applied to this query (status, source, tags, etc.)
Listings
Read-only access to property listing records. Backed by Elasticsearch index listings_v1.
/api/v1/listingsList Listings
listings:dataReturns a paginated list of property listings. Supports filtering by status, portal, property type, location, price range, and bedrooms.
limitintegerRange 1-200
cursorstringOpaque pagination token
fromISO datetimeInclusive, filters on updatedAt
toISO datetimeInclusive
sortBystringAccepted: updatedAt, createdAt
sortDirstringasc / desc
statusstring (CSV)Filter by listing status. Accepts comma-separated values to match multiple statuses. Possible values: "onMarket", "offTheMarket", "tempOffMarket".
portalstring (CSV)Filter by portal publication status. Accepts one or more portal names (bayut, dubizzle, propertyFinder, externalWebsite) as comma-separated values (e.g., portal=bayut or portal=bayut,dubizzle).
propertyTypestring (CSV)Filter by property type. Accepts comma-separated values to match multiple types. Possible values: "Apartment / Flat", "Villa / House", "Bungalow", "Duplex", "Penthouse", "Townhouse", "Bulk Unit", "Full Residential Floor", "Whole Building", "Hotel Apartment", "Loft Apartment", "Residential Land / Plot", "Office", "Compound", "Factory", "Farm", "Labor Camp", "Business Centre", "Retail", "Restaurant", "Storage", "Shop", "Showroom", "Co-working Space", "Warehouse", "Half Floor", "Staff Accommodation".
citystring (CSV)Filter by city. Accepts comma-separated values to match multiple cities (e.g., city=Dubai,Abu Dhabi). Searches across listing location fields.
locationstring (CSV)Filter by location. Accepts comma-separated values to match multiple locations (e.g., location=Downtown,Marina). Searches across listing location fields.
agentEmailstring (CSV)Filter by assigned agent email. Accepts comma-separated values to match multiple agents (e.g., agentEmail=agent1@example.com,agent2@example.com).
referenceIdstring (CSV)Filter by listing reference ID. Accepts comma-separated values to match multiple listings (e.g., referenceId=KD-1074,KD-1075).
minPricenumberMinimum price filter. For rentals, filters by rent amount; for sales, filters by asking price.
maxPricenumberMaximum price filter. For rentals, filters by rent amount; for sales, filters by asking price.
minBedroomsnumberMinimum number of bedrooms.
maxBedroomsnumberMaximum number of bedrooms.
dataarrayArray of listing objects
listingIdstringReference number e.g. KD-1074
organisationIdstringTenant identifier
titlestringListing title
propertyTypestringType of property
citystringCity
listingStatusstringCurrent listing status
createdAtstringISO datetime of creation
countintegerTotal matching listings
pageobjectPagination
limitintegerNo description provided.
cursorstring or nullNo description provided.
metaobjectFilter/sort echo
/api/v1/listings/countCount Listings
listings:countReturns only the total count and filter metadata. Ideal for dashboards showing portfolio size or inventory analytics without fetching full records.
statusstring (CSV)Filter by listing status. Accepts comma-separated values to match multiple statuses. Possible values: "onMarket", "offTheMarket", "tempOffMarket".
portalstring (CSV)Filter by portal publication status. Accepts one or more portal names (bayut, dubizzle, propertyFinder, externalWebsite) as comma-separated values (e.g., portal=bayut or portal=bayut,dubizzle).
propertyTypestring (CSV)Filter by property type. Accepts comma-separated values to match multiple types. Possible values: "Apartment / Flat", "Villa / House", "Bungalow", "Duplex", "Penthouse", "Townhouse", "Bulk Unit", "Full Residential Floor", "Whole Building", "Hotel Apartment", "Loft Apartment", "Residential Land / Plot", "Office", "Compound", "Factory", "Farm", "Labor Camp", "Business Centre", "Retail", "Restaurant", "Storage", "Shop", "Showroom", "Co-working Space", "Warehouse", "Half Floor", "Staff Accommodation".
citystring (CSV)Filter by city. Accepts comma-separated values to match multiple cities (e.g., city=Dubai,Abu Dhabi). Searches across listing location fields.
locationstring (CSV)Filter by location. Accepts comma-separated values to match multiple locations (e.g., location=Downtown,Marina). Searches across listing location fields.
agentEmailstring (CSV)Filter by assigned agent email. Accepts comma-separated values to match multiple agents (e.g., agentEmail=agent1@example.com,agent2@example.com).
referenceIdstring (CSV)Filter by listing reference ID. Accepts comma-separated values to match multiple listings (e.g., referenceId=KD-1074,KD-1075).
minPricenumberMinimum price filter. For rentals, filters by rent amount; for sales, filters by asking price.
maxPricenumberMaximum price filter. For rentals, filters by rent amount; for sales, filters by asking price.
minBedroomsnumberMinimum number of bedrooms.
maxBedroomsnumberMaximum number of bedrooms.
countintegerTotal matching listings
metaobjectFilter echo