Browse the documentation

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"
  }
}
Tap or hover a field to see what it means.
  • Use the event id to avoid processing the same event twice.
  • Writes made through the API also generate events. Your system can recognize its own change by changedFields and updatedAt.
  • 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, ARCHIVED or DELETED (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).
kind
CLIENT_FOR_PROPERTY or PROPERTY_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) or API.

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

  1. On the first read, send since with the date to start from (up to 30 days back).
  2. Keep nextCursor and send it back as cursor next time. It ALWAYS comes, even when there is nothing new.
  3. With hasMore: true, read again right away. With false, wait a few minutes.
  4. For only some types, use types (comma-separated).
Follow the cursor, never the clock. An event takes from seconds to over a minute to show up. Asking “what changed since 10:00?” misses a late event; the cursor does not.

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.maxWebhooks in GET /v1/me.
  • Each delivery is a POST with the body {type, timestamp, data} and three headers: webhook-id (the event id), webhook-timestamp (seconds since 1970) and webhook-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 updatedAt or read the resource.
  • webhook-id is 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 410 disables 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.