Webhooks

Some events are not triggered by you: someone leaves an enquiry on one of your client's listings and the portal notifies us. For those to reach you, you declare once which URL of yours we deliver them to.

PUT     /property-v1/webhooks/{event}
GET     /property-v1/webhooks/{event}
DELETE  /property-v1/webhooks/{event}

All three require the property-api-webhooks scope.

Until you declare the URL, your clients' enquiries do not reach you — and they are not stored anywhere. There is no inbox on our side accumulating them until you register the webhook: if the event arrives and there is no declared destination, it is discarded.

And on the portals that deliver only this way, declaring it is not a preference: it is the only way to receive the enquiries. Which ones those are is told to you by Capabilities — each portal's pushFromPortal field, and right there you will also see whether you can go and fetch them yourself (pull).

The events

{event}When it reaches you
leadsomeone left an enquiry on a listing belonging to one of your clients

It is a single scope for every event, not one per event. When we add a new one you will be able to register it with the token you already have, without asking us for anything.

Declaring where we deliver

curl -X PUT "https://property-api.mapaprop.com/property-v1/webhooks/lead" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your-system.com/hooks/mapaprop-leads" }'
FieldRequiredWhat it is
urlyesyour endpoint. It must be https

The response:

{
  "event": "lead",
  "url": "https://your-system.com/hooks/mapaprop-leads",
  "scope": "app",
  "secret": "9f2c…",
  "createdAt": "2026-10-20T14:02:11.004Z",
  "updatedAt": "2026-10-20T14:02:11.004Z"
}

The secret is returned only once. Save it when you receive it: there is no method that will give it to you again. If you lose it, register the webhook again and a new one is issued — the previous one stops working.

It is what lets you verify that what reaches you is ours: we sign every delivery with it.

Registering again replaces the URL

Sending the PUT again replaces the URL. The createdAt does not change, and the secret does: that is why registering again is also how you rotate it.

The URL must be https

No exceptions, not even for testing. What we deliver is a third party's personal data — the name, email and phone number of whoever made the enquiry — and over http it would travel in the clear. To test on your machine, use a tunnel: it already gives you https.

A different destination for one specific client

By default, everything from all your clients goes to one URL. If you need one client's events to go somewhere else, send their x-client-ref:

curl -X PUT "https://property-api.mapaprop.com/property-v1/webhooks/lead" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-client-ref: inmo-42" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your-system.com/hooks/inmo-42" }'

That creates an exception for that client only ("scope": "client"): everything else still goes to the default. When an event from inmo-42 arrives we look for their exception first and, if it does not exist, we use the default.

The header is optional here, and this is the only part of the API where it is. Leaving it out is not an oversight: it means "this is the destination for all of my clients".

Checking what you have declared

curl "https://property-api.mapaprop.com/property-v1/webhooks/lead" \
  -H "Authorization: Bearer $TOKEN"
{
  "event": "lead",
  "url": "https://your-system.com/hooks/mapaprop-leads",
  "scope": "app",
  "createdAt": "2026-10-20T14:02:11.004Z",
  "updatedAt": "2026-10-20T14:02:11.004Z"
}

The secret does not come back here. If you do not have it, register the webhook again.

Add the x-client-ref to check a client's exception instead of the default.

What we deliver to you

A POST to your URL, with the event in an envelope:

{
  "event": "lead",
  "portal": "argenpropapi",
  "clientRef": "inmo-42",
  "lead": {
    "id": "77349380",
    "propertyCode": "6NN6_01M49G0SE61P525JYAKKQ0T700",
    "name": "Ana",
    "email": "ana@ejemplo.com",
    "phone": "1155556666",
    "message": "Hola, sigue disponible?",
    "date": "2026-10-20T11:30:00",
    "portalMessageId": null,
    "portalAdId": "20703571",
    "portalContactId": null,
    "portalActionId": null
  }
}
Envelope fieldWhat it is
eventwhat happened. Today always lead
portalwhich portal it came from — the same name you use to publish and in your scope
clientRefwhich of your clients it belongs to

The fields inside lead are the same ones in Leads, with the same meaning: it is the same object, so you do not have to write two readings of the same fact.

propertyCode is the code your publication wrote, so it is what you match the enquiry against the property on your side. Each portal builds it its own way — in Argenprop the agency's publication code goes in front of the paId, in Zonaprop a suffix goes behind — so do not parse it: compare it against the one the creation returned to you.

portal and clientRef travel in the envelope and not inside lead because you need them to know who what you receive belongs to: an app-scoped URL receives the leads of all your clients and of every portal.

Verifying that we are the ones who sent it

Every delivery carries the X-Mapaprop-Signature header:

X-Mapaprop-Signature: t=1792500000,v1=5f8a…
  • t is the moment of the delivery, in seconds.
  • v1 is HMAC-SHA256 of <t>.<raw body> with your secret, in hexadecimal.
const esperado = crypto.createHmac('sha256', secret)
  .update(`${t}.${cuerpoCrudo}`)
  .digest('hex');

Sign the raw body, the one that arrived. If you parse it and serialize it again, any difference in whitespace or key order gives a different signature and it will never match.

Compare with crypto.timingSafeEqual and reject if t is more than 5 minutes old: that way an old captured delivery is of no use to anyone for replaying it.

What you have to answer us

You answerWhat we do
2xxdone, we will not send it to you again
anything else, or no answerwe retry

Exactly how many times we retry

Three attempts in total — the first one plus two retries — with about 2 minutes between each one. After the three, we stop trying and that enquiry does not come back: it does not sit in a queue waiting for you to fix your endpoint, and there is no way to ask us to resend it.

That is about 4 minutes of margin. If your endpoint is down for longer than that, whatever came in during that window is lost.

That is why it is worth having your endpoint do the minimum before answering: store the raw envelope and answer 2xx; process it afterwards, with your own retries. An endpoint that validates, enriches and writes to three systems before answering is an endpoint that goes down more often than the one that only stores.

If you store the lead asynchronously, answer only after having stored it: an early 2xx tells us you already have it, and if your write fails afterwards, that lead does not come back.

Deduplicate by lead.id within the same portal: a retry of ours carries the same value.

Turning delivery off

curl -X DELETE "https://property-api.mapaprop.com/property-v1/webhooks/lead" \
  -H "Authorization: Bearer $TOKEN"
{ "deleted": true, "event": "lead" }

Deleting something that was not registered responds {"deleted": false} with 200 — it is not an error, so you can call it without checking first.

With x-client-ref you delete only that client's exception, and their events go back to your app's default.

The errors

CodeerrorWhat happened
400url is required · url must use https · url must be a valid absolute URL · url must be at most 512 charscheck the url field
400Invalid JSON bodythe body of the PUT is not valid JSON
400event must be one of: leadthe {event} in the path is not one of those above
403Missing scope: property-api-webhooksyour token does not have the scope. Write to us
404webhook_not_foundyou have nothing declared for that event (only on the GET)

The 400 responses also carry field with the name of the field, so you do not have to read the text.

What this does NOT do

  • It stores nothing. We deliver and move on: there is no inbox on our side where events wait for you to fetch them, neither before you declare the URL nor after the three attempts.
  • It is not a universal substitute for fetching them. Both roads exist, but not every portal has both: some portals only push and some only answer queries. Before choosing, look at Capabilities — it is the same data our own code uses, so nothing stays hardcoded on your side.

See also

  • Receiving enquiries — the flow, and which of the two paths suits you
  • Leads — fetching them yourself, instead of having them delivered