Webhook Signature Verification

This explains how to verify webhooks requests being sent from Luxury Presence.

Overview

This guide explains how to use the Luxury Presence Webhooks API. The Webhooks API can be used to subscribe to receive real-time Lead Activity events originating from actions taken on your Luxury Presence Websites.

Setup

See our documentation Webhooks API reference for listing, creating and deleting webhook subscriptions.

Receiving requests

Webhook requests from Luxury Presence are signed with HMAC-SHA256 so you can verify their authenticity and integrity. Verify the signature before processing a payload.

Each webhook is signed with the secret you configured on that webhook, not a secret shared across customers or across your own webhooks. A webhook registered without a secret is delivered unsigned, with none of the signature headers below, and nothing else on the request authenticates it — establish trust another way or configure a secret.

Signature Headers

Each webhook request includes the following headers:

  • x-lp-signature: The HMAC-SHA256 signature in hexadecimal format
  • x-lp-timestamp: Unix timestamp (in milliseconds) when the signature was generated
  • x-lp-signature-version: Signature algorithm version (currently "v1")

Signature Generation

The signature is generated using the following process:

  1. Create a string to sign by concatenating: timestamp + url + JSON.stringify(payload)
  2. Generate the HMAC-SHA256 signature using that webhook's own secret
  3. Convert the signature to hexadecimal format

Payload format

Currently, we only support Lead activity sync through webhooks. See Webhook Events for more information on what action triggers each activity type. Payload follows the example structure:

{  
    "eventName": "leads",  
    "companyId": "17g498eh-5b11-4d1c-8448-cb06ef213730",  
    "data": {
        "leadId": "1d53e414-c8ad-49b8-9e23-2046f846d3d9",
        "leadEmail": "[email protected]",
        "leadFirstName": "Fluffy",
        "leadLastName": "Trang",
        "leadPhoneNumber": "(718) 555-5555",
        "leadSource": "HOME_SEARCH",
        "leadOrigin": "HOME_SEARCH",
        "activityType": "ADD_FAVORITE",
        "activityListingId": "b8fb82d6-03f4-42f3-ab57-526493b60949",
        "activityAction": "ADD_FAVORITE",
        "activityMessage": "",
        "activityListingMlsId": "RLS30036941",
        "activityListingAddress": "52 JANE Street 20",
        "activityListingCity": "New York City",
        "activityListingState": "NY",
        "activityListingZip": "10014",
        "activitySourceUrl": "p-110e844c-6254-4378-a176-f04ed117be61.staging.presencepreview.site/home-search/listings/b8fb82d6-03f4-42f3-ab57-526493b60949",
        "assignedAgents": [
            \{
                "firstName": "",
                "lastName": "",
                "email": "[email protected]",
                "mlsAgentId": ""
            }
        ],
        "assignedAgent": {
            "firstName": "",
            "lastName": "",
            "email": "[email protected]",
            "mlsAgentId": ""
        },
        "isNewLead": false,
        "newPhoneNumber": false,
        "companyName": "The Aster Group",
    }
}


Verification Example (Node.js)

const crypto = require('crypto');

function verifyWebhookSignature(url, timestamp, signature, payload, secret) {
  // Recreate the string that was signed
  const stringToSign = timestamp + url + JSON.stringify(payload);

  // Generate expected signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(stringToSign)
    .digest('hex');

  // Compare signatures using timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
}

Did this page help you?