---
title: "Webhooks and Events"
description: "Subscribe to events, verify signatures, handle retries correctly."
url: "https://real-estate-agency.apim.eu/guides/webhooks"
image: "https://real-estate-agency.apim.eu/_og/d/c_Ocean.takumi,title_Webhooks+and+Events,description_~U3Vic2NyaWJlIHRvIGV2ZW50cywgdmVyaWZ5IHNpZ25hdHVyZXMsIGhhbmRsZSByZXRyaWVzIGNvcnJlY3RseS4,props_eyJ0aGVtZSI6eyJtb2RlIjoiZGFyayIsImNvbG9ycyI6eyJwcmltYXJ5IjoiI0Q0QTI0QyJ9fX0,p_Ii9ndWlkZXMvd2ViaG9va3Mi,s_-BJjvQ1kYnF5jvku.png"
---

## Webhooks and Events

Instead of polling every minute to see if something happened: register a URL and get notified.

## [Register an Endpoint](#register-an-endpoint)

```bash
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.

## [Available Events](#available-events)

| 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       |

## [Anatomy of a Delivery](#anatomy-of-a-delivery)

```json
{
  "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>             |

## [Verifying the Signature](#verifying-the-signature)

The signed payload is `"<unix_time>.<raw_body>"`, via HMAC-SHA256 with your `signing_secret`.

```python
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.

## [Retries and Idempotency](#retries-and-idempotency)

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.