Tema
Webhookovi
Veza „Website webhook“ šalje potpisan JSON na vaš URL svaki put kada se oglas objavi, izmeni ili skine. Koristite je da odmah osvežite stranicu na sajtu (Next.js ISR, keš, WordPress), ili da održavate kopiju oglasa u sopstvenom sistemu.
Podešavanje
Vezu povezuje Propertio tim, ili agencija sama u režimu za programere (Portali i sajt → dodaj „Website webhook“). Podešavanja veze:
| Polje | Podrazumevano | Značenje |
|---|---|---|
url | (obavezno) | vaš endpoint; javno dostupan HTTPS URL |
secret | (obavezno) | tajna za potpis; dugačak slučajan niz (npr. openssl rand -hex 32) |
includeImages | true | šalje i fotografije oglasa (potpisani linkovi koji važe 30 dana) |
includeInternal | false | šalje i interna polja oglasa (beleške, vlasnik); samo za sopstvene, zaštićene sisteme |
Kao i za portale, veza ima automatsko objavljivanje: kada je uključeno, svaki nov ili izmenjen aktivan oglas ide na webhook sam; kada nije, agencija bira oglase. Veza vezana za filijalu šalje samo oglase te filijale. „Proveri vezu“ u aplikaciji šalje događaj ping.
Događaji
x-propertio-event | Kada |
|---|---|
property.upserted | oglas je objavljen na ovu vezu, ili mu se promenio sadržaj (tekst, cena, površina, lokacija, osobine, fotografije i njihov redosled) |
property.removed | oglas više nije aktivan (rezervisan, prodat, arhiviran), obrisan je, skinut sa ove veze, ili premešten u drugu filijalu |
ping | provera veze iz aplikacije |
Svaki zahtev nosi jedan događaj za jedan oglas. Ako se oglas nije promenio, ponovna objava ga ne šalje.
Zahtev
http
POST /propertio-webhook HTTP/1.1
Host: www.agencija.rs
content-type: application/json
user-agent: Propertio-Webhook/1.0
x-propertio-event: property.upserted
x-propertio-timestamp: 1792483200000
x-propertio-signature: sha256=3f1c9a…
{
"event": "property.upserted",
"sentAt": "2026-10-20T10:00:00.000Z",
"channelId": "6f1c2b8e-…",
"agencyId": "0b7e41d2-…",
"property": {
"id": "5b1f0c7e-…", "reference": "P-1042", "slug": "trosoban-stan-vracar-p-1042",
"title": "Trosoban stan, Vračar", "status": "active", "type": "apartment", "dealType": "sale",
"price": 245000, "currency": "EUR", "areaSqm": 70, "features": ["Lift", "Terasa"],
"location": { "country": "RS", "city": "Beograd", "municipality": "Vračar" },
"agentId": "a7c2…", "branchId": null, "attributes": {},
"images": [ { "id": "c0de…", "isCover": true, "position": 0, "status": "ready", "variants": [ { "name": "large", "url": "https://app.propertio.rs/media/…?md5=…&expires=…" } ] } ],
"createdAt": "…", "updatedAt": "2026-10-20T09:59:58.000Z", "publishedAt": "…"
}
}json
{ "event": "property.removed", "sentAt": "2026-10-20T10:05:00.000Z", "channelId": "6f1c…", "agencyId": "0b7e…", "externalId": "5b1f0c7e-…" }property(samo kodproperty.upserted) ima ista polja kao oglas u javnom API-ju, bez objektaagent(imaagentId) i sa poljemattributes(dodatna polja za portale). BezincludeImagesnemaimages.externalId(samo kodproperty.removed) jeidoglasa.- Šema:
WebhookEventu OpenAPI dokumentu.
Provera potpisa
x-propertio-signature je sha256= + heksadecimalni HMAC-SHA256 sirovog tela zahteva, sa secret-om veze kao ključem. Računajte ga nad bajtovima tela pre parsiranja JSON-a (posle parsiranja i ponovnog serijalizovanja potpis se ne poklapa), i poredite u konstantnom vremenu.
Vreme u zaglavlju nije potpisano
x-propertio-timestamp nije deo potpisa. Za zaštitu od ponovljenih zahteva koristite polje sentAt iz tela, koje jeste potpisano: odbijte događaj stariji od 5 minuta.
ts
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.PROPERTIO_WEBHOOK_SECRET!;
export function verifyPropertio(raw: Buffer, header: string | undefined): boolean {
if (!header) return false;
const expected = Buffer.from(`sha256=${createHmac("sha256", SECRET).update(raw).digest("hex")}`);
const given = Buffer.from(header);
return expected.length === given.length && timingSafeEqual(expected, given);
}
const app = express();
// sirovo telo samo za ovu rutu
app.post("/propertio-webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyPropertio(req.body, req.get("x-propertio-signature"))) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
if (Date.now() - Date.parse(event.sentAt) > 5 * 60_000) return res.sendStatus(400); // star ili ponovljen
res.sendStatus(204); // odgovorite odmah, obradite posle
queueMicrotask(() => handle(event));
});ts
// app/api/propertio-webhook/route.ts
import { createHmac, timingSafeEqual } from "node:crypto";
import { revalidatePath } from "next/cache";
export async function POST(req: Request) {
const raw = await req.text();
const expected = Buffer.from(`sha256=${createHmac("sha256", process.env.PROPERTIO_WEBHOOK_SECRET!).update(raw).digest("hex")}`);
const given = Buffer.from(req.headers.get("x-propertio-signature") ?? "");
if (expected.length !== given.length || !timingSafeEqual(expected, given)) return new Response("bad signature", { status: 401 });
const event = JSON.parse(raw);
if (Date.now() - Date.parse(event.sentAt) > 5 * 60_000) return new Response("stale", { status: 400 });
if (event.event === "property.upserted") revalidatePath(`/nekretnine/${event.property.slug}`);
revalidatePath("/nekretnine");
return new Response(null, { status: 204 });
}php
<?php
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, getenv('PROPERTIO_WEBHOOK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_PROPERTIO_SIGNATURE'] ?? '')) { http_response_code(401); exit; }
$event = json_decode($raw, true);
if (time() - strtotime($event['sentAt']) > 300) { http_response_code(400); exit; }
http_response_code(204);
// obrada: $event['event'], $event['property'] / $event['externalId']python
import hashlib, hmac, os
def verify(raw: bytes, header: str | None) -> bool:
expected = "sha256=" + hmac.new(os.environ["PROPERTIO_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")Primer: Next.js revalidacija
Kod iznad (kartica „Next.js“) je ceo primer: na property.upserted osvežava stranicu oglasa i listu, na property.removed listu (stranica uklonjenog oglasa će pri sledećem prikazu dobiti 404 od API-ja). Ako je slug promenjen, stara adresa takođe vraća 404; vidi slug se menja.
Odgovor, ponovni pokušaji i redosled
- Uspeh je svaki
2xxodgovor u roku od 20 sekundi. Sve ostalo (drugi status, timeout, greška veze) je neuspeh. Odgovorite odmah, a težu obradu radite u pozadini. - Neuspeli zahtevi se ponavljaju sami, i za
property.upsertedi zaproperty.removed: posle 1 minuta, pa 5 i 30 minuta, 2 h, 6 h i 24 h (šest pokušaja). Ako ni poslednji ne uspe, oglas ostaje u aplikaciji označen kao greška na toj vezi i šalje se ponovo pri sledećoj izmeni ili kada agencija ponovo pokrene objavu. - Ako održavate kopiju oglasa, ipak povremeno (npr. jednom dnevno) uporedite je sa javnim API-jem (
properties.all()) i uklonite oglase kojih tamo više nema. - Redosled nije garantovan i isti događaj može stići više puta. Obrada neka bude idempotentna: ključ je
idoglasa, a stariju verziju (manjiproperty.updatedAtod onog koji već imate) zanemarite. Ponovljeni pokušaj ima novisentAt, pa ga provera starosti ne odbija. - Linkovi slika u webhooku važe 30 dana, a oglas koji se ne menja ne šalje nove. Ne čuvajte ih kao trajne: za prikaz tražite oglas od API-ja, ili osvežavajte kopiju (vidi gore). Vidi Slike.
Bezbednost
- Endpoint prihvata samo zahteve sa ispravnim potpisom;
secretčuvajte kao lozinku i menjajte ga ako procuri (novisecretu podešavanjima veze važi odmah). - Koristite HTTPS. Ne logujte ceo payload ako sadrži interna polja (
includeInternal). - Webhook ne nosi ključ za API i ne daje pristup ničemu; potpis samo potvrđuje da je poruka od Propertija.