POST every time something happens with one of your listings: it’s published, rejected, edited in the portal. You no longer need to ask periodically.
Webhooks follow the open specification Standard Webhooks, so any compatible library will verify your signature.
Configuration
From your account, at Settings → API, in the Webhooks section:1
Add address
A public
https:// address, for example https://crm.exemplu.ro/hooks/housera. You choose the events you want to receive.2
Keep secret
Upon creation, you receive the webhook secret, in the form
whsec_…. It’s shown only once. You use it to verify the signature of every delivery.3
Send a test
The “Send a test” button delivers an event
webhook.test, signed like any other, solely to that webhook.The events
State changes produce events regardless of where they originated: through API, from the portal, or from moderation. Events of a listing reach the account that owns the listing only if that account has at least one API key.
The format of an event
The body of each delivery is a JSON object:string
The unique ID of the event. It’s the same as the header
webhook-id. You use it so you don’t process the same event twice.string
The type of the event, from the table above.
string
The moment of the event, ISO 8601.
object
For listing events:
code, external_ref (or null), status (the state of the listing at the time of the event), url, plus fields specific to the type.listing.rejected
listing.rejected
listing.withdrawn
listing.withdrawn
by is owner and appears "deleted": true.listing.edited_in_portal
listing.edited_in_portal
GET /account/listings/{code} and decide what to keep. See For CRMs.media.failed
media.failed
webhook.test
webhook.test
Delivery
Each delivery is aPOST with the JSON body and headers:
- Respond with a status
2xxwithin 10 seconds at most. Any other status, a redirect, or a timeout is a failed delivery. - Respond quickly, process later. Put the event in your own queue and confirm immediately; do not do all the synchronization while Housera waits.
- Redirects are not followed. Configure the final address.
Retries
A failed delivery is retried automatically 6 more times, after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours, and 6 hours. In total, Housera tries for about 9 hours. Eventswebhook.test are not retried.
Because a delivery can arrive multiple times (for example, you processed it, but your response got lost) and because the order is not guaranteed:
- remember the processed IDs (
webhook-id) and ignore an already seen event; - do not rely on the order of arrival: for the current state of a listing, the source of truth remains
GET /account/listings/{code}.
Signature verification
Any server can send aPOST to your address. The signature proves that the delivery comes from Housera and has not been altered.
The signature is an HMAC-SHA256 in base64, calculated over the string {webhook-id}.{webhook-timestamp}.{corp}, where {corp} are the exact bytes of the received body. The HMAC key is the part after whsec_ in the secret, decoded from base64.
Steps:
- Read the raw body, before any JSON parsing. A re-serialized body will not yield the same signature.
- Check
webhook-timestamp: reject deliveries older or newer than 5 minutes compared to your time. This way, an intercepted delivery cannot be resent later. - Calculate the expected signature and compare it in constant time with each signature in
webhook-signature(the header may contain multiple, separated by space, each prefixed withv1,).
Event retrieval
A webhook that your server did not receive (was down for more than 9 hours, or the webhook was off) is not lost. Each event is kept for 35 days and you read it withGET /events (the right listings:read):
- Events come in order, from the oldest to the newest, in the same form as in the webhook.
afteris the ID of the last event you processed. Withoutafter, you start from the oldest stored event.limitranges from 1 to 500 (default 100).- As long as
meta.nextis notnull, there are more events: you request the next page withafterequal tometa.next. - When
meta.nextisnull, you have reached the end. Keep the ID of the last event fromdataand start from it next time.
GET /events?after=<ultimul id> to catch anything missed. The IDs tell you what you’ve already processed.