Preskoči na sadržaj

@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/sdk

Paket 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!,
});
OpcijaTipZnačenje
baseUrlstring, obaveznohttps://app.propertio.rs/api (test: https://app.test.propertio.rs/api); / na kraju se uklanja
apiKeystring, obaveznoključ agencije; mora počinjati sa pk_
timeoutMsbrojnajduže trajanje jednog pokušaja u ms, sa čitanjem odgovora (podrazumevano 15.000)
maxRetriesbrojponovni pokušaji GET zahteva (podrazumevano 2; 0 ih isključuje), vidi ispod
requestInitRequestInitdodaje se svakom zahtevu, npr. Next.js { next: { revalidate: 300 } } ili { cache: "no-store" }; zaglavlja se spajaju
fetchtypeof fetchsopstveni 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 px

Varijante 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:

PoljeZnačenje
statusHTTP status; 0 kada odgovora nije bilo (timeout, prekid, mreža)
codekod API-ja (VALIDATION, BAD_REQUEST, UNAUTHORIZED, LICENCE, FORBIDDEN, NOT_FOUND, RATE_LIMITED, AI_QUOTA, VOICE_QUOTA, INTERNAL …) ili SDK-a (tabela ispod)
messageopis na engleskom, za log (ne za posetioca)
detailsdetalji, npr. problemi po poljima kod VALIDATION
requestIdid zahteva (zaglavlje x-request-id); pošaljite ga uz prijavu problema
retryAftersekunde do novog pokušaja (Retry-After), kod 429
causeoriginalna greška kod NETWORK, TIMEOUT i ABORTED
Situacijastatuscode
API vrati greškuHTTP statuspolje error iz odgovora
Previše zahteva429RATE_LIMITED (vidi retryAfter)
Greška bez JSON tela (npr. stranica proxy-ja)HTTP statusHTTP_ERROR, message: "HTTP <status>"
Uspešan status, ali odgovor nije JSONHTTP statusINVALID_RESPONSE
Istekao timeoutMs0TIMEOUT
Prekinut signal0ABORTED
Mreža nedostupna, DNS, CORS u browseru0NETWORK
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"
  • timeoutMs važi za svaki pokušaj posebno.
  • GET zahtevi se ponavljaju posle greške mreže, timeouta, 429 i 500/502/503/504, sa eksponencijalnim čekanjem (≈0,5 s, 1 s, …). Retry-After do 10 s se poštuje; duži se ne čeka, nego se greška baca odmah (sa retryAfter). 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.