Webhooks and Events
Instead of polling every minute to see if something happened: register a URL and get notified.
Instead of polling every minute to see if something happened: register a URL and get notified.
curl -X POST https://api.realestateagency.example/v1/webhooks \
-H "apikey: $REAL_ESTATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://crm.your-company.com/hooks/real-estate",
"events": ["listing.completed", "inquiry.received", "appointment.confirmed"],
"description": "Production CRM integration"
}'
The response contains a signing_secret. It's only shown once and is used to verify incoming deliveries.
| Event | Triggered by |
|---|---|
property.created | A new property was saved |
property.updated | Master data, price, or media changed |
property.archived | Property was taken off the market |
listing.completed | Generation finished, PDF is ready |
listing.published | Listing is live on at least one channel |
inquiry.received | A lead reached out about a property |
inquiry.scored | Matching scored an inquiry |
appointment.confirmed | A viewing appointment was confirmed |
appointment.cancelled | An appointment was cancelled by either side |
settlement.created | A commission settlement was generated |
{
"id": "evt_0d41c8",
"type": "listing.completed",
"created_at": "2026-08-20T09:41:20Z",
"data": {
"listing_id": "lst_4d9b2e",
"property_id": "prop_8f2c1a",
"status": "completed",
"pdf_url": "https://api.realestateagency.example/v1/listings/lst_4d9b2e/pdf"
}
}
Headers on every delivery:
| Header | Content |
|---|---|
X-Real-Estate-Event | Event type, e.g. listing.completed |
X-Real-Estate-Delivery | Unique ID of this delivery |
X-Real-Estate-Signature | t=<unix_time>,v1=<hex> |
The signed payload is "<unix_time>.<raw_body>", via HMAC-SHA256 with your signing_secret.
import hashlib, hmac, time
def is_signature_valid(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
timestamp, signature = parts["t"], parts["v1"]
if abs(time.time() - int(timestamp)) > tolerance:
return False # too old - protects against replay
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
Verify against the raw request body. Parsing to JSON and re-serializing first changes whitespace and key order - the signature then no longer matches.
We expect a 2xx status within five seconds. If none arrives, we retry with increasing backoff: after 1 min, 5 min, 30 min, 2 h and 6 h. After that, the delivery is considered failed, and the endpoint is automatically paused after 24 hours without success.
Deliveries can arrive more than once. Keep the processed id values around for at least seven days and discard repeats. Respond immediately with 202 and keep working asynchronously - slow processing in the request handler is the most common cause of unnecessary retries.