Registers an endpoint to receive lead-activity payloads. Everything below
describes how those deliveries behave.
Verifying a delivery
A webhook with a secret configured receives three headers:
x-lp-signature— HMAC-SHA256 hex digest (64 hex characters)x-lp-timestamp— Unix ms timestamp used in the signaturex-lp-signature-version— currently alwaysv1
Each delivery is signed with that webhook's own secret, not a shared key.
A webhook registered without a secret is delivered unsigned, with none of
these headers, and nothing else on the request authenticates it — establish
trust another way (IP allow-listing, mTLS, an unguessable path) or configure a
secret.
Signing recipe:
stringToSign = timestamp + url + JSON.stringify(payload)
signature = HMAC_SHA256(stringToSign, secret).hex()
Where url is the destination exactly as registered and payload is the
delivered body. Hash the raw bytes you received — re-serializing a parsed
object can reorder keys and break the signature even when the data is
identical. Compare with a timing-safe comparison, and validate the digest is
64 hex characters before decoding.
Custom headers you register are merged with the LP-managed ones. Any header
using the x-lp prefix, or colliding with the signature headers,
x-lp-delivery-id or Content-Type, is dropped — a custom header can never
spoof these.
Duplicate deliveries
Delivery is at-least-once. The same payload can arrive more than once, both
from retries and because a failure to reach any one of your webhooks re-queues
the whole event for every endpoint.
Every delivery carries x-lp-delivery-id, a 64-character lowercase hex string
that is stable across retries and redeliveries of the same event to the
same webhook, and distinct per webhook and per payload. Use it as an
idempotency key: record ids you have finished processing, and return 2xx
without re-doing the work when one repeats.
Track claimed and finished separately. Marking an id handled before the work
succeeds means a crash mid-processing looks like a duplicate on retry — you ack,
we stop retrying, and the event is lost silently. Give the claim a lease so a
crashed attempt is retried rather than stranded.
Do not use the signature as an idempotency key: x-lp-timestamp is
regenerated per attempt and is part of the signed string, so two deliveries of
one event carry different signatures. x-lp-delivery-id is not part of the
signed string and is never proof of origin — treat it strictly as a
deduplication hint.
Two payloads that are byte-for-byte identical, delivered to the same webhook,
share one id. Nothing distinguishes them, so treating the second as a duplicate
is intended.
Field presence
Only activityType and activitySourceUrl are guaranteed. Every other
field is optional and may be absent on any given delivery — fields are
omitted, never sent empty, so treat absent and empty as different and do not
key your handler on a field always being present.
This applies in particular to activityAction, platform, leadState,
isNewLead, newPhoneNumber and formCustomFields. Absence carries no
meaning: a missing isNewLead or newPhoneNumber is not equivalent to
false.
Some fields describe the lead record rather than the activity —
leadFirstName, leadLastName, leadPhoneNumber, leadSource,
statusType, companyName, leadTags, assignedAgents and
assignedAgent. These reflect the lead record at the time of delivery
rather than as of the activity. If a lead's status or assigned agents changed
in between, you receive the newer values. Do not use them to reconstruct what
the lead looked like when the activity happened.
leadEmail and leadId are present only for activity that identifies a
lead. Anonymous browsing — VIEW_LISTING above all — arrives with neither.
Which surface the activity came from
When present, platform is one of mobile, web or unknown.
Only mobile is exact: the activity originated in the Luxury Presence mobile
app. web is the default for activity received through services that
serve both surfaces, so it is not a guarantee of browser origin — some web
activity originates on mobile. unknown means the surface could not be
determined.
Segment on platform: "mobile" where certainty is required. web is not
its complement, and a missing platform must not be read as web.
MLS and address enrichment (activityListingMlsId, activityListingAddress,
activityListingCity, activityListingState, activityListingZip and
meta.property) is populated only for CONTACT_INQUIRY submitted through a
contact form, and activityListingMlsId only where that listing carries an
MLS key. Other listing-scoped activity carries activityListingId alone.
Which activity types you receive
Filtering on the events array is best-effort, so a webhook can receive
activity types it did not subscribe to — VIEW_LISTING most of all, which is
by a wide margin the highest-volume type. Filter on activityType in your
handler rather than relying on the subscription.
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||
400Invalid webhook data
