Tema
@propertio/sdk
Zvanični TypeScript klijent za javni API. Bez zavisnosti, sa tipovima za sve odgovore i ulaze, radi u Node.js 18+, Next.js, Astro, edge okruženjima (Vercel, Cloudflare Workers, Deno, Bun) i u browseru. Ima timeout, prekid (AbortSignal), ponovne pokušaje za čitanje i jednu klasu greške.
bash
npm i @propertio/sdk
# ili: pnpm add @propertio/sdk · yarn add @propertio/sdkPaket ima ESM i CommonJS izdanje: radi i import { PropertioClient } from "@propertio/sdk" i const { PropertioClient } = require("@propertio/sdk").
Verzije
Trenutna verzija je na npmjs.com/package/@propertio/sdk, a izmene po verzijama u CHANGELOG.md paketa (packages/sdk/CHANGELOG.md u repozitorijumu) i na strani Izmene. SDK prati semver: dok je verzija 0.x, nova „minor“ verzija sme da promeni API, pa u package.json pišite ~0.1.0 ili tačnu verziju.
Klijent
ts
import { PropertioClient } from "@propertio/sdk";
export const propertio = new PropertioClient({
baseUrl: "https://app.propertio.rs/api",
apiKey: process.env.PROPERTIO_API_KEY!,
});| Opcija | Tip | Značenje |
|---|---|---|
baseUrl | string, obavezno | https://app.propertio.rs/api (test: https://app.test.propertio.rs/api); / na kraju se uklanja |
apiKey | string, obavezno | ključ agencije; mora počinjati sa pk_ |
timeoutMs | broj | najduže trajanje jednog pokušaja u ms, sa čitanjem odgovora (podrazumevano 15.000) |
maxRetries | broj | ponovni pokušaji GET zahteva (podrazumevano 2; 0 ih isključuje), vidi ispod |
requestInit | RequestInit | dodaje se svakom zahtevu, npr. Next.js { next: { revalidate: 300 } } ili { cache: "no-store" }; zaglavlja se spajaju |
fetch | typeof fetch | sopstveni fetch (testovi, proxy, merenje); podrazumevano globalni |
Pogrešan baseUrl (nije apsolutni http(s) URL), ključ koji ne počinje sa pk_ ili okruženje bez fetch-a bacaju TypeError odmah pri new PropertioClient(…).
Svaki zahtev ide na {baseUrl}/v1/… sa zaglavljima x-api-key i accept. SDK ključ nigde ne loguje i ne drži ga kao polje objekta (console.log(propertio) ga ne prikazuje).
Ključ: server ili browser
Ključ ima opsege public:read (oglasi, agencija, agenti, asistent, glas) i/ili public:inquiry (upiti, zahtevi za pokazivanje i zakazivanje preko asistenta).
- Na serveru (preporučeno): ključ u promenljivoj okruženja, pozivi iz serverskog koda (Next.js serverske komponente i akcije, Astro, API ruta). Posetilac ključ ne vidi, a odgovore možete keširati.
- U browseru: ključ u kodu stranice je javan. To je prihvatljivo samo ako je agencija podesila dozvoljene sajtove, pa API odbija pozive sa drugih sajtova. Koristite poseban ključ sajta sa najmanje potrebnih opsega i opozovite ga ako se zloupotrebi.
Metode
Svaka metoda vraća Promise, pri grešci baca PropertioError i kao poslednji argument prima opcije zahteva (RequestOptions): { signal, timeoutMs, maxRetries, requestInit }.
properties.search(params?, options?)
ts
const page = await propertio.properties.search({
dealType: "rent", city: "Novi Sad", roomsMin: 2, priceMax: 600, features: ["Parking", "Klima"],
sort: "priceAsc", page: 1, pageSize: 12,
});
// page: { items: Property[], page, pageSize, total }Svi parametri pretrage (SearchParams): q, type, dealType, city, municipality, priceMin/priceMax, areaMin/areaMax, roomsMin/roomsMax, features (niz ili tekst odvojen zarezom; oglas mora imati sve), bbox ([minLng, minLat, maxLng, maxLat] ili tekst), sort, page, pageSize (1–100, podrazumevano 20). Prazne vrednosti (undefined, null, "", NaN, []) se ne šalju, pa vrednosti iz forme možete proslediti direktno. Agenciju i filijalu određuje ključ. U rezultatima pretrage images ima samo naslovnu fotografiju, a imageCount ukupan broj.
properties.get(idOrSlug, options?)
ts
const p: Property = await propertio.properties.get("trosoban-stan-vracar-p-1042");Oglas sa svim fotografijama, po id-u ili slug-u. Baca PropertioError sa status: 404 kada oglas ne postoji ili nije na sajtu.
slug se menja sa naslovom, ali se završava stalnom šifrom (…-p-1042), pa i stari link pronalazi oglas. Odgovor uvek nosi trenutni slug: kada se razlikuje od onog iz adrese, preusmerite (301) na novu adresu.
ts
const p = await propertio.properties.get(params.slug);
if (p.slug !== params.slug) redirect(`/nekretnine/${p.slug}`); // Next.js: permanentRedirect(...)properties.all(params?, options?)
Asinhroni iterator kroz sve oglase (sve strane), za sitemap i statičko generisanje. U options je i pageSize (podrazumevano 100). Oglas koji se tokom prolaza pomeri na drugu stranu vraća se samo jednom.
ts
for await (const p of propertio.properties.all({ dealType: "sale" })) {
console.log(p.reference, p.slug);
}agency.get(options?)
ts
const agency: Agency = await propertio.agency.get();
// { name, slug, email, phone, website, address, logoUrl, defaultCurrency, branch }GET /v1/agency. Sa ključem filijale kontakt podaci su filijalini, a branch je { id, name, slug, accent }; inače null.
agents.list(options?)
ts
const agents: Agent[] = await propertio.agents.list();
const team = agents.filter((a) => a.activeListings > 0);GET /v1/agents. Nije paginirano.
inquiries.create(input, options?)
ts
const { id } = await propertio.inquiries.create({
propertyId: p.id, name: "Ana Anić", email: "ana@example.com", phone: "+38163111222",
message: "Zanima me stan.", consent: true, website: form.get("website") as string,
});POST /v1/inquiries. Ključ mora imati public:inquiry. website je honeypot polje iz forme, prosleđeno netaknuto.
viewings.request(input, options?)
ts
const { id, status } = await propertio.viewings.request({
propertyId: p.id, name: "Ana Anić", phone: "+38163111222",
preferredAt: new Date("2026-11-03T10:00").toISOString(), consent: true, website: "",
});
// status === "requested"POST /v1/viewing-requests. Termin mora biti u budućnosti.
assistant.chat(messages, chatOptions?, options?)
ts
const history = [{ role: "user" as const, content: "Tražim kuću sa dvorištem u Zemunu." }];
const r = await propertio.assistant.chat(history, { language: "sr", visitor: { name: "Ana", phone: "+38163111222" } });
history.push({ role: "assistant", content: r.reply });
for (const l of r.listings) prikaziKarticu({ href: `/nekretnine/${l.slug}`, naslov: l.title, sifra: l.reference });Vraća { reply, speechToken, actions, listings }. Odgovor (reply) je običan tekst i oglase pominje po šifri; listings su oglasi koje je asistent pogledao u tom koraku (reference, slug, title, price, currency), pa kartice i linkove pravite sami. actions su zakazana pokazivanja (request_viewing) i poslati upiti (send_inquiry). Šaljite ceo razgovor svaki put (1–30 poruka). Potrebna je opcija plana ai (inače 402 LICENCE). Vidi Asistent i glas.
voice.speak({ text, token, language? }, options?)
ts
const mp3: Blob = await propertio.voice.speak({ text: r.reply, token: r.speechToken, language: "sr" });
new Audio(URL.createObjectURL(mp3)).play();Izgovara samo odgovore asistenta: uz tekst ide njegov speechToken (inače 403). Tekst duži od 600 znakova se skraćuje na 600.
voice.confirmation(language = "sr", options?)
ts
const mp3 = await propertio.voice.confirmation("sr"); // „Hvala vam! Vaš zahtev je primljen…“Obe glasovne metode traže opciju plana ai i vraćaju audio/mpeg kao Blob.
Pomoćne funkcije
imageUrl(p, variant = "card", index = 0)
URL fotografije: naslovna prva, pa po redosledu. Ako tražene veličine nema, vraća prvu veću, pa najveću dostupnu; bez te fotografije vraća null.
ts
import { imageUrl } from "@propertio/sdk";
const cover = imageUrl(p, "card"); // naslovna, 800 px
const second = imageUrl(p, "large", 1); // druga fotografija, 1600 pxVarijante su thumb (320 px), card (800 px) i large (1600 px).
imageSrcSet(p, index = 0)
ts
<img src={imageUrl(p, "card")!} srcSet={imageSrcSet(p)} sizes="(min-width: 960px) 33vw, 100vw" alt="" />
// srcSet: "…thumb.webp 320w, …card.webp 800w, …large.webp 1600w"Linkovi slika ističu
Linkovi su potpisani i važe 24 h od odgovora. Koristite ih za prikaz, ali ih ne čuvajte u bazi, CMS-u ili dugom kešu; keš odgovora neka bude mnogo kraći od 24 h. Vidi Slike.
formatPrice(p, locale = "sr-RS")
ts
formatPrice({ price: 245000, currency: "EUR", dealType: "sale" }); // "245.000 €"
formatPrice({ price: 650, currency: "EUR", dealType: "rent" }); // "650 €/mes"
formatPrice({ price: 650, currency: "EUR", dealType: "rent" }, "en-GB"); // "€650/month"formatLocation(p)
ts
formatLocation(p); // "Crveni krst, Beograd" (naselje ili opština, pa grad)Greške
Sve metode bacaju samo PropertioError:
| Polje | Značenje |
|---|---|
status | HTTP status; 0 kada odgovora nije bilo (timeout, prekid, mreža) |
code | kod API-ja (VALIDATION, BAD_REQUEST, UNAUTHORIZED, LICENCE, FORBIDDEN, NOT_FOUND, RATE_LIMITED, AI_QUOTA, VOICE_QUOTA, INTERNAL …) ili SDK-a (tabela ispod) |
message | opis na engleskom, za log (ne za posetioca) |
details | detalji, npr. problemi po poljima kod VALIDATION |
requestId | id zahteva (zaglavlje x-request-id); pošaljite ga uz prijavu problema |
retryAfter | sekunde do novog pokušaja (Retry-After), kod 429 |
cause | originalna greška kod NETWORK, TIMEOUT i ABORTED |
| Situacija | status | code |
|---|---|---|
| API vrati grešku | HTTP status | polje error iz odgovora |
| Previše zahteva | 429 | RATE_LIMITED (vidi retryAfter) |
| Greška bez JSON tela (npr. stranica proxy-ja) | HTTP status | HTTP_ERROR, message: "HTTP <status>" |
| Uspešan status, ali odgovor nije JSON | HTTP status | INVALID_RESPONSE |
Istekao timeoutMs | 0 | TIMEOUT |
Prekinut signal | 0 | ABORTED |
| Mreža nedostupna, DNS, CORS u browseru | 0 | NETWORK |
ts
import { PropertioError } from "@propertio/sdk";
try {
await propertio.inquiries.create(input);
} catch (e) {
if (e instanceof PropertioError) {
if (e.code === "VALIDATION") return { error: "Proverite podatke u formi." };
if (e.code === "RATE_LIMITED") return { error: "Previše pokušaja, pokušajte za minut." };
console.error(e.code, e.message, e.requestId);
}
return { error: "Slanje nije uspelo, pokušajte ponovo ili nas pozovite." };
}instanceof PropertioError radi i kada se učitaju oba izdanja paketa (ESM i CJS); isPropertioError(e) je ista provera kao funkcija.
Timeout, ponovni pokušaji i prekid
ts
const ac = new AbortController();
const page = await propertio.properties.search({ q }, { signal: ac.signal, timeoutMs: 5_000, maxRetries: 0 });
// ac.abort() prekida zahtev i čekanje između pokušaja → PropertioError "ABORTED"timeoutMsvaži za svaki pokušaj posebno.- GET zahtevi se ponavljaju posle greške mreže, timeouta,
429i500/502/503/504, sa eksponencijalnim čekanjem (≈0,5 s, 1 s, …).Retry-Afterdo 10 s se poštuje; duži se ne čeka, nego se greška baca odmah (saretryAfter). Greške limita plana (AI_QUOTA,VOICE_QUOTA,LICENCE…) se ne ponavljaju. - POST zahtevi (upiti, pokazivanja, asistent, glas) se nikad ne ponavljaju, pa se ništa ne pošalje dvaput.
- Next.js keš po pozivu:
propertio.properties.get(slug, { requestInit: { next: { tags: [oglas:${slug}] } } }).
U browseru API ne izlaže zaglavlja Retry-After i x-request-id, pa retryAfter i requestId tamo mogu izostati.
Tipovi
Paket izvozi tipove Property, PropertyImage, ImageVariant, ImageVariantName, Location, PropertyAgent, Agent, Agency, AgencyBranch, Paginated<T>, SearchParams, SortOrder, InquiryInput, ViewingRequestInput, ChatMessage, ChatOptions, ChatResponse, AssistantAction, AssistantListing, SpeakInput, PropertyType, DealType, Currency, HeatingType, Furnished, Language, RequestOptions, ListAllOptions, PropertioClientOptions i PropertioSdkErrorCode. Tipovi se u testovima paketa porede sa šemama servera, pa prate javni API; polja koja API doda kasnije stižu i pre nego što ih SDK opiše (ignorišite nepoznata polja).
Primer sajta
U repozitorijumu, apps/demo-site je ceo mali sajt agencije na SDK-u. Vidi Primeri.