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. scoperead— samo GET; svaki drugi metod vraća 403scope_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 statusUvek granaj po error.code, nikad po tekstu poruke — poruke pišemo za ljude i menjaju se; kodovi ne.
| HTTP | code | Šta da uradiš |
|---|---|---|
| 400 | cena_neispravna, nema_izmena… | Podaci nisu ispravni — retry neće pomoći. |
| 401 | nije_prijavljen, kljuc_neispravan | Ključ fali, pogrešan je ili opozvan. |
| 403 | scope_read, los_origin | Ključ nema pravo na taj metod. |
| 404 | nije_vlasnik, kategorija_ne_postoji | Ne postoji ili nije tvoje — namerno se ne razlikuje. |
| 429 | previse_zahteva | Sač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 429Ako sinhronizuješ hiljade artikala, ne mlati API u petlji — feed je za to napravljen i prolazi bez limita.
Oglasi
| Ruta | Šta radi |
|---|---|
| GET /api/v1/listings | Javna 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/listings | Nacrt: { categoryId }. Vraća id koji dalje popunjavaš |
| PATCH /api/v1/listings/{id} | Naslov, opis, cena, količina, isporuka, atributi |
| POST /api/v1/listings/{id}/objavi | Nacrt → aktivan (proverava obavezna polja) |
| PATCH /api/v1/listings/{id}/lager | Samo cena i količina — vidi ispod |
| GET /api/v1/moji-oglasi | Tvoji oglasi u svim statusima |
| GET /api/v1/kategorije, /kategorije/{id}/atributi | Stablo 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,12XML (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). idje tvoj ključ — po njemu prepoznajemo šta je izmena, a šta nov artikal. Nemoj ga menjati između sinhronizacija.- Svaka kolona/tag
attr_neštopostaje 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(iliout 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đaj | Kad stiže |
|---|---|
| porudzbina.kreirana | Nova porudžbina kod tebe (najvažniji — po njemu se pakuje). |
| porudzbina.status | Porudžbina promenila status (prihvaćeno, poslato, isporučeno, otkazano…). |
| oglas.objavljen | Tvoj oglas je objavljen (ručno, preko API-ja ili iz feeda). |
| oglas.izmenjen | Izmenjena cena, količina ili sadržaj oglasa. |
| oglas.status | Oglas 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 } ]
} }
}externalIdstavke je tvoj id artikla (iz feeda ili sa kreiranja) — po njemu skidaš lager u svom sistemu;nullkad 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 posaoZamke iz prakse
- Pare, ne dinari.
cenaParaje 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-Keypravi 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.