Add an endpoint in Settings → Integrations → Webhooks and choose events. PropZolo POSTs a signed JSON event to it as things happen — new leads, status changes, site visits, deals, payments.
Events are thin: the event’s ids and changed fields under data, never a whole record. Fetch what you need through the API with a scoped key. Ignore fields you do not know — new ones are added without notice.
{
"id": "evt_918273",
"type": "lead.created",
"api_version": "2026-10-01",
"created_at": "2026-10-06T04:30:02.000Z",
"company_id": "cmcomp00000000000000000001",
"data": {
"lead_id": "cmlead00000000000000000001",
"lead_ref": "L-1042",
"contact_id": "cmcont00000000000000000001",
"channel": "portal",
"lead_type": "buy",
"status_key": "new",
"source_id": "cmsrc000000000000000000001",
"owner_id": "cmmemb00000000000000000001",
"duplicate_kind": null,
"object": "lead",
"id": "cmlead00000000000000000001"
}
}Every event carries api_version, the dated payload version its endpoint is pinned to. A new endpoint is pinned to the current version, 2026-10-01.
api_version field on the endpoint).X-PropZolo-EventX-PropZolo-Event-IdX-PropZolo-DeliveryX-PropZolo-TimestampX-PropZolo-Api-VersionX-PropZolo-SignatureX-PropZolo-Timestamp is more than 5 minutes away from your clock.<timestamp>.<raw body> with your endpoint’s secret (hex).v1= value of X-PropZolo-Signature, compared in constant time.import { createHmac, timingSafeEqual } from 'node:crypto'
// rawBody: the request body exactly as received (a string), before any JSON parsing
export function verifyPropZolo(rawBody, headers, secret) {
const timestamp = headers['x-propzolo-timestamp']
if (!/^\d+$/.test(timestamp ?? '') || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest()
return (headers['x-propzolo-signature'] ?? '')
.split(',')
.map((part) => part.trim())
.filter((part) => part.startsWith('v1='))
.some((part) => {
const given = Buffer.from(part.slice(3), 'hex')
return given.length === expected.length && timingSafeEqual(given, expected)
})
}import hashlib
import hmac
import time
def verify_propzolo(raw_body: bytes, headers, secret: str) -> bool:
timestamp = headers.get("X-PropZolo-Timestamp", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
parts = [p.strip() for p in headers.get("X-PropZolo-Signature", "").split(",")]
return any(hmac.compare_digest(p[3:], expected) for p in parts if p.startswith("v1="))function verify_propzolo(string $rawBody, array $headers, string $secret): bool {
$timestamp = $headers['x-propzolo-timestamp'] ?? '';
if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
return false;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
foreach (explode(',', $headers['x-propzolo-signature'] ?? '') as $part) {
$part = trim($part);
if (str_starts_with($part, 'v1=') && hash_equals($expected, substr($part, 3))) {
return true;
}
}
return false;
}X-PropZolo-Event-Id."test": true.lead.created — A new lead arrived (form, portal, ad, Lead Push API, import or typed in).
{
"lead_id": "cmlead00000000000000000001",
"lead_ref": "L-1042",
"contact_id": "cmcont00000000000000000001",
"channel": "portal",
"lead_type": "buy",
"status_key": "new",
"source_id": "cmsrc000000000000000000001",
"owner_id": "cmmemb00000000000000000001",
"duplicate_kind": null
}lead.reenquired — An existing lead enquired again (same phone number).
{
"lead_id": "cmlead00000000000000000001",
"lead_ref": "L-1042",
"channel": "whatsapp"
}lead.updated — Fields of a lead changed.
{
"lead_id": "cmlead00000000000000000001",
"fields": [
"temperature",
"next_follow_up_at"
]
}lead.status_changed — A lead moved to another status or sub-status.
{
"lead_id": "cmlead00000000000000000001",
"from_status_key": "new",
"to_status_key": "site_visit_scheduled",
"from_category": "open",
"to_category": "open"
}lead.assigned — A lead got a new owner.
{
"owner_id": "cmmemb00000000000000000001",
"previous_owner_id": null,
"lead_ref": "L-1042"
}lead.deleted — A lead was deleted.
{
"lead_id": "cmlead00000000000000000001"
}contact.created — A new person was added (contacts are unique by phone number).
{
"contact_id": "cmcont00000000000000000001"
}contact.updated — A contact’s details changed.
{
"contact_id": "cmcont00000000000000000001",
"fields": [
"email"
]
}task.created — A follow-up, call or other task was created.
{
"task_id": "cmtask00000000000000000001",
"lead_id": "cmlead00000000000000000001",
"assignee_id": "cmmemb00000000000000000001"
}task.completed — A task was marked done.
{
"task_id": "cmtask00000000000000000001",
"lead_id": "cmlead00000000000000000001"
}visit.scheduled — A site visit was scheduled.
{
"lead_id": "cmlead00000000000000000001",
"agent_id": "cmmemb00000000000000000001",
"scheduled_at": "2026-10-10T05:30:00.000Z"
}visit.rescheduled — A site visit moved to another time.
{
"lead_id": "cmlead00000000000000000001",
"scheduled_at": "2026-10-11T06:00:00.000Z"
}visit.completed — A site visit was done (checked out or verified).
{
"lead_id": "cmlead00000000000000000001",
"visit_id": "cmvisi00000000000000000001"
}visit.cancelled — A site visit was cancelled.
{
"lead_id": "cmlead00000000000000000001",
"visit_id": "cmvisi00000000000000000001"
}visit.no_show — The client did not turn up for a site visit.
{
"lead_id": "cmlead00000000000000000001",
"visit_id": "cmvisi00000000000000000001"
}call.outcome_logged — An agent logged the outcome of a call.
{
"call_id": "cmcall00000000000000000001",
"lead_id": "cmlead00000000000000000001",
"outcome": "interested"
}message.received — A WhatsApp, SMS or email message came in.
{
"conversation_id": "cmconv00000000000000000001",
"channel": "whatsapp_cloud",
"contact_id": "cmcont00000000000000000001"
}email.received — Email sync found a lead’s email in a member’s own Gmail or Outlook mailbox.
{
"email_id": "cmmail00000000000000000001",
"lead_id": "cmlead00000000000000000001",
"contact_id": "cmcont00000000000000000001",
"member_id": "cmmemb00000000000000000001",
"provider": "google",
"has_attachments": false,
"sent_at": "2026-10-06T05:00:00.000Z"
}email.sent — Email sync found a member’s email to a lead in their own Gmail or Outlook mailbox.
{
"email_id": "cmmail00000000000000000002",
"lead_id": "cmlead00000000000000000001",
"contact_id": "cmcont00000000000000000001",
"member_id": "cmmemb00000000000000000001",
"provider": "microsoft",
"has_attachments": true,
"sent_at": "2026-10-06T05:10:00.000Z"
}property.created — A resale or rental listing was added.
{
"ref_code": "P-2031"
}property.status_changed — A listing was published, put on hold, sold, rented …
{
"ref_code": "P-2031",
"from": "active",
"to": "under_offer",
"listing_type": "sale"
}property.price_changed — The asking price or rent of a listing changed (paise).
{
"ref_code": "P-2031",
"field": "price",
"from": 950000000,
"to": 925000000,
"change_pct": -2.63
}property.stale — The owner has not confirmed availability for the configured number of days.
{
"property_id": "cmprop00000000000000000001",
"ref_code": "P-2031",
"days_since_confirmed": 21
}property.reconfirmed — The owner confirmed availability, changed the price or said it is no longer available.
{
"property_id": "cmprop00000000000000000001",
"ref_code": "P-2031",
"answer": "confirm"
}listing.verification_granted — A reviewer granted the Verified badge after an agent’s visit and a document check.
{
"property_id": "cmprop00000000000000000001",
"ref_code": "P-2031",
"verification_id": "cmverif000000000000000001",
"method": "visit_documents",
"verified_at": "2026-10-07T05:30:00.000Z",
"expires_at": "2027-04-04T18:30:00.000Z"
}listing.verification_expiring — A listing’s Verified badge lapses soon (the company’s reminder window).
{
"property_id": "cmprop00000000000000000001",
"ref_code": "P-2031",
"verification_id": "cmverif000000000000000001",
"expires_at": "2027-04-04T18:30:00.000Z"
}listing.verification_expired — A listing’s Verified badge reached the end of its validity.
{
"property_id": "cmprop00000000000000000001",
"ref_code": "P-2031",
"verification_id": "cmverif000000000000000001",
"reason": "expired"
}listing.verification_invalidated — A new owner, a price beyond the allowed change or a location edit ended the Verified badge.
{
"property_id": "cmprop00000000000000000001",
"ref_code": "P-2031",
"verification_id": "cmverif000000000000000001",
"reason": "price_changed",
"reasons": [
"price_changed"
]
}listing.verification_revoked — A reviewer revoked a listing’s Verified badge (the reason stays on the listing’s timeline).
{
"property_id": "cmprop00000000000000000001",
"ref_code": "P-2031",
"verification_id": "cmverif000000000000000001",
"reason": "revoked"
}deal.created — A deal was opened.
{
"deal_id": "cmdeal00000000000000000001",
"ref": "D-1024",
"lead_id": "cmlead00000000000000000001"
}deal.stage_changed — A deal moved stage (token, agreement, registration, won, lost).
{
"ref": "D-1024",
"lead_id": "cmlead00000000000000000001",
"from_stage": "agreement",
"to_stage": "closed_won"
}receipt.recorded — Money was received against a deal (commission or a token collected for the owner).
{
"deal_id": "cmdeal00000000000000000001",
"number": "RCPT/2026-27/0007",
"amount": 5000000,
"mode": "gateway",
"purpose": "commission"
}payment_link.paid — A payment link was paid in full.
{
"deal_id": "cmdeal00000000000000000001",
"payment_link_id": "cmplnk00000000000000000001",
"amount": 5000000,
"purpose": "token"
}payment_link.partially_paid — Part of a payment link’s amount was paid.
{
"deal_id": "cmdeal00000000000000000001",
"payment_link_id": "cmplnk00000000000000000001",
"amount_paid": 2000000
}payment.refunded — A payment taken through a payment link was refunded (or a dispute on it was lost).
{
"deal_id": "cmdeal00000000000000000001",
"booking_id": null,
"payment_link_id": "cmplnk00000000000000000001",
"provider": "razorpay",
"provider_refund_id": "rfnd_00000000000001",
"source": "refund",
"amount": 1000000,
"outcome": "reversed"
}payment.disputed — The payer disputed a payment taken through a payment link (a chargeback was raised).
{
"deal_id": "cmdeal00000000000000000001",
"booking_id": null,
"payment_link_id": "cmplnk00000000000000000001",
"provider": "razorpay",
"provider_dispute_id": "disp_00000000000001",
"amount": 5000000,
"respond_by": "2026-10-20T18:29:59.000Z"
}payment.dispute_resolved — A dispute on a payment-link payment was won, lost or closed.
{
"deal_id": "cmdeal00000000000000000001",
"payment_link_id": "cmplnk00000000000000000001",
"provider_dispute_id": "disp_00000000000001",
"outcome": "won"
}integration.error — A connection broke (revoked access, refused credentials or errors in a row); its admins were told to reconnect.
{
"provider": "meta_lead_ads",
"error": "The access token was revoked. Reconnect Facebook.",
"auth": true
}integration.recovered — A broken connection worked again.
{
"provider": "meta_lead_ads"
}ingest.failed — An incoming lead failed every attempt; it waits in its source’s logs for a replay.
{
"source_id": "cmsrc000000000000000000001",
"provider": "99acres",
"error": "The portal sent no phone number.",
"attempts": 8
}