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 |
|---|---|
lead | someone 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" }'
| Field | Required | What it is |
|---|---|---|
url | yes | your 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 field | What it is |
|---|---|
event | what happened. Today always lead |
portal | which portal it came from — the same name you use to publish and in your scope |
clientRef | which 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…
tis the moment of the delivery, in seconds.v1isHMAC-SHA256of<t>.<raw body>with yoursecret, 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 answer | What we do |
|---|---|
2xx | done, we will not send it to you again |
| anything else, or no answer | we 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
| Code | error | What happened |
|---|---|---|
400 | url is required · url must use https · url must be a valid absolute URL · url must be at most 512 chars | check the url field |
400 | Invalid JSON body | the body of the PUT is not valid JSON |
400 | event must be one of: lead | the {event} in the path is not one of those above |
403 | Missing scope: property-api-webhooks | your token does not have the scope. Write to us |
404 | webhook_not_found | you 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