trgovo.

Trgovo API — dokumentacija za integratore

Sve što treba da tvoj lager sam živi na Trgovu: REST API, feed koji se sam povlači i webhooks koji te zovu kad padne porudžbina. Bez SDK-a, bez OAuth plesa — jedan ključ i curl.

Baza: https://trgovo.rs/api/v1 · Ključ napraviš u Nalog → Alati.

Brzi start (3 minuta)

Tri poziva pokrivaju 90% integracije: napravi oglas, promeni mu lager, preuzmi porudžbine.

# 1) šta imam objavljeno
curl -s "https://trgovo.rs/api/v1/moji-oglasi" \
  -H "Authorization: Bearer trg_live_..."

# 2) spusti lager na 3 i cenu na 1.290 din (id sme biti i tvoj sopstveni šifarnik)
curl -s -X PATCH "https://trgovo.rs/api/v1/listings/A1K74/lager" \
  -H "Authorization: Bearer trg_live_..." \
  -H "Content-Type: application/json" \
  -d '{"kolicina":3,"cena":1290}'

# 3) nove porudžbine
curl -s "https://trgovo.rs/api/v1/orders" -H "Authorization: Bearer trg_live_..."

Ne moraš da pišeš ništa od ovoga ako ti je dovoljan feed: zakači CSV ili XML sa svog sajta i Trgovo sam povlači izmene — vidi Feed.

Ključ i autorizacija

Ključ se pravi u Nalog → Alati i prikazuje se tačno jednom — čuvamo samo njegov otisak, pa ga ne možemo ponovo pokazati. Izgubljen ključ se opoziva i pravi nov.

Authorization: Bearer trg_live_9f2c…
  • scope write — sve operacije. scope read — samo GET; svaki drugi metod vraća 403 scope_read.
  • Ključ radi u ime tvog naloga i vidi isključivo tvoje oglase i porudžbine. Nema pristupa administraciji ni tuđim podacima.
  • Opoziv deluje odmah — sledeći poziv dobija 401 kljuc_neispravan.
  • Ključ ide u header, nikad u URL: URL-ovi završe u logovima, istoriji pregledača i Referer zaglavlju.

Oblik odgovora i greške

Svaki odgovor je jedan od dva oblika. Nema trećeg, nema HTML stranice greške na /api/v1.

{ "data": … , "cursor": "…" }          // uspeh (cursor samo na listama)
{ "error": { "code": "…", "message": "…" } }   // greška, uz odgovarajući HTTP status

Uvek granaj po error.code, nikad po tekstu poruke — poruke pišemo za ljude i menjaju se; kodovi ne.

HTTPcodeŠta da uradiš
400cena_neispravna, nema_izmena…Podaci nisu ispravni — retry neće pomoći.
401nije_prijavljen, kljuc_neispravanKljuč fali, pogrešan je ili opozvan.
403scope_read, los_originKljuč nema pravo na taj metod.
404nije_vlasnik, kategorija_ne_postojiNe postoji ili nije tvoje — namerno se ne razlikuje.
429previse_zahtevaSačekaj koliko kaže Retry-After pa ponovi.
5xx—Naša greška. Ponovi sa exponential backoff-om; POST šalji sa Idempotency-Key.

Straničenje (kursor)

Liste vraćaju cursor. Šalji ga nazad kao ?cursor=… dok ne dobiješ null. Kursor je keyset, ne offset: dok listaš, novi oglasi ne pomeraju stranice pa ništa ne preskačeš i ništa ne vidiš dvaput.

Idempotencija (POST)

Šalji Idempotency-Key na svaki POST. Isti ključ u naredna 24 h vraća isti sačuvani odgovor(uz Idempotency-Replay: true) umesto da napravi drugi oglas. Bez toga, jedan timeout tvoje skripte = duplikat kod nas.

curl -X POST "https://trgovo.rs/api/v1/listings" \
  -H "Authorization: Bearer trg_live_..." \
  -H "Idempotency-Key: lager-2026-08-25-A1K74" \
  -H "Content-Type: application/json" -d '{"categoryId":42}'

Ograničenje broja poziva

Po ključu: 600 čitanja/min i 120 upisa/min. Prozor je fiksna minuta, brojači su odvojeni.

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1786742400     # unix sekunde kad se brojač nulira
Retry-After: 37                   # samo uz 429

Ako sinhronizuješ hiljade artikala, ne mlati API u petlji — feed je za to napravljen i prolazi bez limita.

Oglasi

RutaŠta radi
GET /api/v1/listingsJavna pretraga: ?q=, ?kategorija=slug|id (sa podkategorijama), ?prodavac=slug|id, ?a_brend=Bosch, ?cursor=, ?limit=
GET /api/v1/listings/{id}Jedan oglas sa slikama i atributima
POST /api/v1/listingsNacrt: { categoryId }. Vraća id koji dalje popunjavaš
PATCH /api/v1/listings/{id}Naslov, opis, cena, količina, isporuka, atributi
POST /api/v1/listings/{id}/objaviNacrt → aktivan (proverava obavezna polja)
PATCH /api/v1/listings/{id}/lagerSamo cena i količina — vidi ispod
GET /api/v1/moji-oglasiTvoji oglasi u svim statusima
GET /api/v1/kategorije, /kategorije/{id}/atributiStablo kategorija i polja koja kategorija očekuje
GET /api/v1/sellers/{id|slug}Prodavnica: naziv, mesto, ocena, skala isporuke, kuriri, broj oglasa

Cene su uvek u parama (cenaPara: 129000 = 1.290 din) — celobrojno, bez zaokruživanja u pokretnom zarezu. Gde je zgodnije, primamo i cena u dinarima pa sami množimo.

Lager i cena — poziv koji ćeš zvati najčešće

Jedan poziv, jedno-dva polja. {id} sme da bude i tvoj sopstveni šifarnik (ono što si poslao kao id u feedu), pa ne moraš da vodiš mapu naših id-jeva.

curl -X PATCH "https://trgovo.rs/api/v1/listings/A1K74/lager" \
  -H "Authorization: Bearer trg_live_..." -H "Content-Type: application/json" \
  -d '{"kolicina":0}'

# → {"data":{"id":8412,"cenaPara":129000,"kolicina":0,"status":"pauziran"}}
  • Količina 0 pauzira oglas, ne briše ga. URL, slike, pregledi i pozicija u pretrazi ostaju; dopuna lagera ga vraća u aktivan.
  • Prodat oglas se ne menja — dobićeš 400 nije_izmenjiv.

Porudžbine

Kupac može u jednoj korpi da kupi od više prodavaca — zato je jedinica koju ti dobijaš „tvoj deo porudžbine"(broj porudžbine + tvoje stavke), a ne cela korpa. Tebe se tiče samo tvoj deo.

GET  /api/v1/orders            # tvoje prodaje
GET  /api/v1/orders/{broj}     # jedna porudžbina
POST /api/v1/prodaje/{id}/status   { "status": "poslato", "posiljkaBroj": "…" }

Statusi idu redom kreirano → prihvaceno → poslato → isporuceno → potvrdjeno; otkazano i vraceno su izlazi. Unazad se ne ide. Rok slanja je 3 radna dana — probijen rok upisuje penal koji se vidi na tvom profilu.

Feed: CSV i XML (bez pisanja koda)

Daš URL, mi ga povlačimo i držimo oglase u koraku sa tvojim lagerom. Format prepoznajemo po sadržaju, ne po ekstenziji. Podesi ga u Nalog → Alati, uz probni pregled pre aktivacije.

CSV

id,naslov,cena,kolicina,sku,slika,kategorija,attr_brend,attr_napon
A1K74,Rele 12V 30A,1290,7,A1K74,https://…/rele.jpg,releji,Bosch,12

XML (radi i Google Merchant feed)

<rss><channel>
  <item>
    <g:id>A1K74</g:id>
    <g:title>Rele 12V 30A</g:title>
    <g:price>1.290,00 RSD</g:price>
    <g:availability>in stock</g:availability>
    <g:image_link>https://…/rele.jpg</g:image_link>
    <attr_brend>Bosch</attr_brend>
  </item>
</channel></rss>
  • Obavezno: id, naslov, cena. Sinonime prepoznajemo sami (naziv/title/name, price/mpc, quantity/lager/stock, g: prefiks se skida).
  • id je tvoj ključ — po njemu prepoznajemo šta je izmena, a šta nov artikal. Nemoj ga menjati između sinhronizacija.
  • Svaka kolona/tag attr_nešto postaje atribut po kome se oglas filtrira. Loša vrednost je upozorenje u izveštaju — feed se zbog nje ne prekida.
  • Nestalo iz feeda ili kolicina 0 (ili out of stock) → oglas se pauzira. Vratilo se → sam se aktivira. Nikad ne brišemo.
  • Slike se skidaju jednom (dedup po sadržaju): max 5 MB, timeout 10 s, samo http(s).

Webhooks — mi zovemo tebe

Umesto da svakih 30 s pitaš „ima li nove porudžbine", daj nam URL pa te zovemo. Podešava se u Nalog → Alati, uz probni poziv i istoriju isporuka.

DogađajKad stiže
porudzbina.kreiranaNova porudžbina kod tebe (najvažniji — po njemu se pakuje).
porudzbina.statusPorudžbina promenila status (prihvaćeno, poslato, isporučeno, otkazano…).
oglas.objavljenTvoj oglas je objavljen (ručno, preko API-ja ili iz feeda).
oglas.izmenjenIzmenjena cena, količina ili sadržaj oglasa.
oglas.statusOglas pauziran, prodat ili uklonjen.
POST https://tvoj-sajt.rs/trgovo-hook
X-Trgovo-Dogadjaj: porudzbina.kreirana
X-Trgovo-Isporuka: 4471            # isti broj na svakom pokušaju → dedup kod tebe
X-Trgovo-Pokusaj: 1
X-Trgovo-Potpis: t=1786742400,v1=3f9a…

{
  "dogadjaj": "porudzbina.kreirana",
  "kreiran": "2026-08-25T12:00:00.000Z",
  "podaci": { "porudzbina": {
    "broj": "260825-Q7VLK", "deoId": 91, "status": "kreirano",
    "iznosRobaPara": 129000, "postarinaPara": 39000, "iznosUkupnoPara": 168000,
    "nacinPlacanja": "pouzece", "rokIsporukeAt": "2026-08-28T…",
    "kupac": { "ime": "…", "telefon": "+3816…", "adresa": "…", "mesto": "…", "ptt": "…" },
    "stavke": [ { "listingId": 8412, "externalId": "A1K74", "naslov": "Rele 12V 30A", "sku": "A1K74",
                  "cenaPara": 129000, "kolicina": 1, "iznosPara": 129000 } ]
  } }
}
  • externalId stavke je tvoj id artikla (iz feeda ili sa kreiranja) — po njemu skidaš lager u svom sistemu; null kad oglas nije došao od tebe.
  • Odgovori 2xx što pre (rok je 10 s). Posao radi u pozadini — ne obrađuj porudžbinu dok držiš našu konekciju.
  • Neuspeh se ponavlja 3 puta: posle 1 min, 5 min i 30 min. Telo i potpis su bajt u bajt isti u svakom pokušaju.
  • Isti događaj možeš dobiti dvaput (mreža ume da prekine posle tvog 200). Radi dedup po X-Trgovo-Isporuka.
  • Posle 15 uzastopnih neuspeha webhook se gasi i dobijaš obaveštenje — uključuje se natrag jednim klikom.

Provera potpisa (obavezno)

Tvoj URL je javan — bilo ko može da ti pošalje lažnu „porudžbinu". Potpis je jedina stvar koja to razlikuje. Računa se HMAC-SHA256 nad `${t}.${sirovo_telo}` tvojom tajnom iz Alata. Potpisuje se sirovo telo — ako ga prvo raspakuješ pa ponovo serijalizuješ, potpis neće valjati.

// Node / Next.js
import { createHmac, timingSafeEqual } from "node:crypto";

const sirovo = await req.text();                       // NE req.json()
const [t, v1] = req.headers.get("x-trgovo-potpis").split(",").map(x => x.split("=")[1]);
if (Math.abs(Date.now()/1000 - Number(t)) > 300) return new Response("stari zahtev", { status: 400 });

const ocekivan = createHmac("sha256", process.env.TRGOVO_TAJNA).update(`${t}.${sirovo}`).digest("hex");
const a = Buffer.from(ocekivan), b = Buffer.from(v1);
if (a.length !== b.length || !timingSafeEqual(a, b)) return new Response("los potpis", { status: 401 });

const dogadjaj = JSON.parse(sirovo);                   // tek sada
<?php // PHP / WooCommerce
$sirovo = file_get_contents('php://input');
preg_match('/t=(\d+),v1=([0-9a-f]+)/', $_SERVER['HTTP_X_TRGOVO_POTPIS'], $m);
if (abs(time() - (int)$m[1]) > 300) { http_response_code(400); exit; }
$ocekivan = hash_hmac('sha256', $m[1] . '.' . $sirovo, TRGOVO_TAJNA);
if (!hash_equals($ocekivan, $m[2])) { http_response_code(401); exit; }
http_response_code(200);            // prvo potvrdi, pa onda radi posao

Zamke iz prakse

  • Pare, ne dinari. cenaPara je celobrojno. 1.290,50 din = 129050.
  • Ništa se ne briše. Nema DELETE oglasa — postoji pauziranje. Tako link koji je Google već indeksirao ne postaje 404.
  • Retry bez Idempotency-Key pravi duplikate. Timeout ne znači da se posao nije desio.
  • 404 ≠ „nema ga". Tuđi resurs takođe vraća 404 — namerno, da se preko API-ja ne može mapirati šta postoji.
  • Feed umesto petlje. Za masovnu sinhronizaciju koristi feed; API je za pojedinačne izmene i porudžbine.
  • Webhook je obaveštenje, ne izvor istine. Ako propustiš poziv, stanje uvek pročitaj sa GET /api/v1/orders.

Nešto fali ili se ponaša drugačije nego što ovde piše? Javi nam — ova stranica je deo proizvoda, ne dodatak.