Events and webhooks
Learn what changed in the account without re-reading the portfolio: read events whenever you want, or receive each one at your endpoint, right away.
Two ways to learn what changed
- Read events (
GET /v1/events): your system asks whenever it wants, in order, with 30 days of history. No public endpoint needed. - Receive webhooks: ImobyFlow sends each event to your endpoint, signed, right after it happens.
- Both carry the SAME event. The safest setup uses both: webhooks to react right away and event reads to catch anything that may have been missed.
The event
Events are lean: ids, state and the NAMES of the changed fields (changedFields). Never name, phone, e-mail or address. To see the new value, read the resource — that way an event is never stale and stores no personal data.
{
"id": "evt_4f1c9a7e2b3d8c6a5e4f1b2c",
"type": "property.updated",
"createdAt": "2026-10-02T16:45:12.000Z",
"data": {
"id": "prop-3f9a1c2b7d4e",
"externalRef": "AP1234",
"status": "ACTIVE",
"changedFields": [
"salePrice"
],
"updatedAt": "2026-10-02T16:45:12.000Z"
}
}- Use the event
idto avoid processing the same event twice. - Writes made through the API also generate events. Your system can recognize its own change by
changedFieldsandupdatedAt. - Each type also requires the resource’s read permission: a key that only reads properties does not see client events.
Event types
The same list is served by GET /v1/reference/event-types. New types can arrive at any time: ignore the ones your system does not know.
property.createdproperties:read- Property created.
idexternalRefstatusupdatedAt property.updatedproperties:read- Property updated (fields in changedFields).
idexternalRefstatuschangedFieldsupdatedAt property.deletedproperties:read- Property moved to the trash.
idexternalRefstatusupdatedAt property.restoredproperties:read- Property restored.
idexternalRefstatusupdatedAt property.purgedproperties:read- Property permanently deleted.
idexternalRefstatusupdatedAt lead.createdleads:read- Client created.
idexternalRefstatestatusownerAccountIdupdatedAt lead.updatedleads:read- Client updated (fields in changedFields).
idexternalRefstatestatusownerAccountIdchangedFieldsupdatedAt lead.deletedleads:read- Client moved to the trash.
idexternalRefstatestatusownerAccountIdupdatedAt lead.restoredleads:read- Client restored.
idexternalRefstatestatusownerAccountIdupdatedAt lead.purgedleads:read- Client permanently deleted.
idexternalRefstatestatusownerAccountIdupdatedAt radar.client_summaryradar:read- New opportunities for a client, batched over a few minutes.
leadIdnewMatchestopScoresinceuntil partnership.createdpartnerships:read- Partnership created.
idstatuskind partnership.updatedpartnerships:read- Partnership stage or deadline changed.
idstatuspreviousStatuschangedFields captacao.offer_receivedcaptacao:read- An owner is now offered to the account, with the deadline.
iddeadline captacao.offer_acceptedcaptacao:read- Owner accepted by the account.
idacceptedAtvia captacao.offer_withdrawncaptacao:read- Owner no longer offered to the account (deadline passed or taken).
id
What comes in data
id- The object id — property, client, partnership or Captação owner.
externalRef- Your system's code, when the object has one.
status- The current status.
state- Where the client is:
ACTIVE,ARCHIVEDorDELETED(trash). ownerAccountId- The broker responsible for the client.
changedFields- The NAMES of the fields that changed — read the new value from the resource.
updatedAt- The last change (UTC).
kindCLIENT_FOR_PROPERTYorPROPERTY_FOR_CLIENT.previousStatus- The previous stage, when the stage is what changed.
leadId- The client.
newMatches- How many new opportunities appeared in the window.
topScore- The highest match score among them (0 to 100).
since- Window start (UTC).
until- Window end (UTC).
deadline- Until when the account can accept (UTC).
acceptedAt- When the account accepted (UTC).
via- Through where:
PANEL(the dashboard) orAPI.
radar.client_summary batches a client’s new opportunities created within a few minutes: one new recommendation can create dozens, and one event per opportunity would flood your system. Only for clients in the account’s own portfolio.
Reading events
- On the first read, send
sincewith the date to start from (up to 30 days back). - Keep
nextCursorand send it back ascursornext time. It ALWAYS comes, even when there is nothing new. - With
hasMore: true, read again right away. Withfalse, wait a few minutes. - For only some types, use
types(comma-separated).
Receiving webhooks
- Register the endpoint through the API or the dashboard (My account › API integration). HTTPS only, at a public address. The response carries the secret (
whsec_…) only once: keep it. - Without
types, the endpoint receives every type the account can read. How many endpoints the account can have:limits.maxWebhooksinGET /v1/me. - Each delivery is a
POSTwith the body{type, timestamp, data}and three headers:webhook-id(the event id),webhook-timestamp(seconds since 1970) andwebhook-signature. - Answer with a 2xx status within 10 seconds. Queue the event and process it afterwards: processing before answering blows the deadline and the delivery comes again.
- Without a 2xx, another attempt comes after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h — about 21 hours in total. Arrival order is not guaranteed: decide by
updatedAtor read the resource. webhook-idis the same on every attempt and on manual redelivery: that is how your system recognizes a repeat.- Redirects are not followed, and URLs with a username and password are refused (webhook_url_refused).
A delivery, as your server receives it
POST /imobyflow/eventos HTTP/1.1
Content-Type: application/json
User-Agent: ImobyFlow-Webhooks/1
webhook-id: evt_4f1c9a7e2b3d8c6a5e4f1b2c
webhook-timestamp: 1790959512
webhook-signature: v1,SvYNo/7lL4THMHE2HojUdVZTwMuEiUPa3y0HSpjtvtc=
{"type":"property.updated","timestamp":"2026-10-02T16:45:12.000Z","data":{"id":"prop-3f9a1c2b7d4e","externalRef":"AP1234","status":"ACTIVE","changedFields":["salePrice"],"updatedAt":"2026-10-02T16:45:12.000Z"}}With the secret whsec_pcwwPI/A9TWaDe9mPSrOkWlrkADDKDKjCKivhG986jw=, this signature is valid — use it to test your code (turn off the time window for that test only).
Verifying the signature
The signature follows the open Standard Webhooks spec — its libraries work. To verify by hand: HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the secret after whsec_, base64-decoded.
- Sign the body exactly as received, before any JSON parsing. Re-serialized JSON changes accents and spacing, and the signature no longer matches.
- The header may carry more than one signature, separated by spaces: for 24 hours after a secret rotation, both the new and the previous secret sign. Accept if any one matches.
- Compare with a constant-time function — never with plain equality.
- Reject messages more than 5 minutes off your clock: otherwise anyone who captures a delivery can replay it later.
const crypto = require('node:crypto');
// rawBody: o corpo EXATAMENTE como chegou (string), antes de qualquer JSON.parse.
// No Express: app.post('/webhook', express.raw({ type: 'application/json' }), …) e req.body.toString('utf8').
function verifyWebhook(secret, msgId, timestamp, signatureHeader, rawBody) {
const now = Math.floor(Date.now() / 1000);
if (!/^\d+$/.test(timestamp) || Math.abs(now - Number(timestamp)) > 5 * 60) return false;
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const expected = crypto
.createHmac('sha256', key)
.update(`${msgId}.${timestamp}.${rawBody}`, 'utf8')
.digest();
return signatureHeader.split(' ').some((part) => {
const [version, sig] = part.split(',');
if (version !== 'v1' || !sig) return false;
const given = Buffer.from(sig, 'base64');
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}
Each snippet is run in our tests against the real signature: valid, two signatures, altered body, wrong secret, expired timestamp and unknown version.
Rotating the secret
Generate a new secret. For 24 hours, deliveries carry both signatures — time to switch it in your system without losing events.
When an endpoint is disabled
- Answering
410disables the endpoint immediately. - With at least 5 finished deliveries in 48 hours and 50% or more of them failing after every attempt, the endpoint is disabled. A short outage on your side does not disable it: only deliveries that ran out of attempts count.
- The account owner gets an e-mail right away. While disabled, further deliveries are skipped.
- To recover: re-enable the endpoint (
status: ACTIVE), redeliver the deliveries that matter and read the events for the period — they are kept for 30 days.
Testing
The test event delivers a webhook.test now, signed, and returns what your system answered. It does not count towards disabling. Deliveries show each one’s status, attempts and last status code.