Otkucaj API v1
REST API za izdavanje fiskalnih računa, refundacije i kopije, slanje računa e-poštom, izveštaje o prometu i rad sa artiklima. Sve što radi kasa u pregledaču može i vaš softver: veb prodavnica, ERP, naplatni sistem.
Sve što ovaj dokument kaže o propisima je naše razumevanje, a ne pravni ni poreski savet; pre odluke proverite kod knjigovođe ili u Poreskoj upravi.
Za AI agente i jezičke modele
Mašinski čitljivi opisi ovog API-ja postoje i održavaju se uz ovu stranicu.
Uvod i bazna adresa
Bazna adresa svih poziva:
https://otkucaj.com/api/v1
Svi zahtevi i odgovori su JSON u UTF-8 kodiranju, osim žurnala koji se vraća kao čist tekst (text/plain). Komunikacija ide isključivo preko HTTPS-a. Iznosi u JSON-u koriste tačku kao decimalni znak (2400.00), jer je to JSON standard; na samom računu i u žurnalu iznosi su u srpskom formatu (2.400,00).
Versionisanje. Verzija API-ja je deo putanje: /api/v1. Postojeća polja i ponašanje se unutar v1 ne menjaju unazad nekompatibilno; nove mogućnosti se dodaju kao nova, opciona polja. Ako ikada bude potreban prekid ugovora, on će živeti na putanji /api/v2, a v1 će nastaviti da radi.
Završna kosa crta se ignoriše: /api/v1/items/ i /api/v1/items su ista putanja. Nepoznata putanja vraća 404 sa kodom not_found.
Brzi početak
Od ključa do prvog računa u tri koraka.
-
Proverite vezu i stanje PFR-a.
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/status
-
Izdajte prvi račun.
curl -X POST https://otkucaj.com/api/v1/invoices \ -H "Authorization: Bearer kasir_vas_kljuc" \ -H "Content-Type: application/json" \ -d '{ "invoiceType": "Normal", "transactionType": "Sale", "cashier": "Веб продавница", "payments": [{ "paymentType": "Card", "amount": 2400.00 }], "items": [ { "name": "Мајица", "quantity": 2, "unitPrice": 1200.00, "label": "Ђ" } ] }'Odgovor je
201sa celim računom: zapamtiteuuidipfr.invoiceNumber. Pun oblik odgovora je kod POST /invoices. -
Preuzmite žurnal (tekst računa spreman za štampu na 40 kolona):
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/journal
/api/v1/status
Stanje aplikacije i veze sa PFR-om. Upotrebite za proveru konfiguracije i za nadzor.
Zahtev
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/status
Odgovor
{
"app": "Otkucaj",
"version": "1.2.0",
"esirNumber": "1667/1.3.6",
"pfr": {
"ok": true,
"mode": "demo",
"uid": "DMX7K2QP",
"message": "Демо ПФР симулатор: активан"
},
"time": "2026-08-19T14:00:00.000+02:00"
}
pfr.mode je demo, lpfr ili vpfr, u zavisnosti od podešavanja instalacije. Kada je režim L-PFR ili V-PFR, objekat pfr prenosi i podatke koje vrati sam procesor fiskalnih računa; ok: false sa porukom znači da PFR trenutno nije dostupan. esirNumber se sklapa od broja i verzije ESIR-a iz Podešavanja.
- 401 unauthorized
- 429 rate_limited
Autentifikacija
Svaki zahtev mora da nosi zaglavlje:
Authorization: Bearer <api-kljuc>
Ključeve generiše administrator u aplikaciji, u odeljku Podešavanja · API ključevi. Ključ se prikazuje samo jednom, u trenutku generisanja: u bazi se čuva isključivo njegov SHA-256 otisak, pa izgubljen ključ nije moguće ponovo pročitati, već se opoziva i generiše nov. Opoziv je trenutan: svaki sledeći zahtev sa opozvanim ključem dobija 401.
Ključevi nisu pojedinačno ograničeni po pravima: svaki aktivan ključ može da pozove svaku putanju. Ako želite da razdvojite sisteme, generišite zaseban ključ po sistemu i opozovite ga pojedinačno kad zatreba; to je nivo izolacije koji aplikacija zaista nudi.
/api/v1/me
Podaci o ključu kojim je zahtev potpisan: naziv, sačuvani prefiks, datumi, ograničenje broja zahteva koje se stvarno primenjuje i šta ključ sme. Korisno za proveru da integracija koristi onaj ključ koji mislite da koristi.
Zahtev
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/me
Odgovor
{
"key": {
"id": 3,
"label": "Веб продавница",
"prefix": "kasir_7f3a",
"active": true,
"createdAt": "2026-08-01 09:12:44",
"lastUsedAt": "2026-08-19 13:58:02"
},
"rateLimit": { "requests": 240, "windowSeconds": 60 },
"scope": "full",
"permissions": [
"invoices:read", "invoices:write", "items:read", "items:write",
"reports:read", "taxLabels:read", "categories:read"
],
"note": "API keys are not scoped individually: every active key may call every route."
}
prefix je onaj kratki niz koji vidite i u Podešavanjima; sam ključ se nigde ne čuva u čitljivom obliku i nikada se ne vraća.
scope je uvek "full", a permissions nabraja šta svaki aktivan ključ stvarno može. To nije po ključu podesivo: sistem prava po ključu ne postoji, pa ga ovaj odgovor ni ne glumi.
lastUsedAt je prethodni poziv, ne ovaj: vreme tekućeg poziva se upisuje tek pošto je red ključa pročitan.
- 401 unauthorized
- 429 rate_limited
Ograničenja i ponašanje
| Ograničenje | Vrednost | Šta se dešava pri prekoračenju |
|---|---|---|
| Broj zahteva po ključu | 240 u 60 sekundi | 429 rate_limited |
| Slanje računa e-poštom po ključu | 30 poruka na sat | 429 rate_limited, posebna poruka |
| Stavki po računu | 1 do 500 | 422 validation |
Računa po strani (GET /invoices) | 100 | sledeća strana kroz page |
Artikala po pozivu (GET /items) | 1.000 | višak se ne vraća, straničenja nema |
| Dužina naziva stavke i artikla | 200 znakova | 422 validation |
Dužina buyerId | 20 znakova, oblik ознака:вредност | 422 validation |
Dužina adText | 500 znakova | 422 validation |
Prozor ograničenja je fiksan, ne klizni: prvi zahtev otvara prozor od 60 sekundi i broji do 240; kad prozor istekne, brojač kreće ispočetka. Prekoračenje vraća HTTP 429:
{ "error": { "code": "rate_limited", "message": "Rate limit exceeded (240 requests/minute)." } }
Kada dobijete 429, sačekajte do isteka tekućeg prozora pa nastavite. Za uobičajen rad prodavnice (jedan račun po porudžbini) ovo ograničenje se u praksi ne doseže. Ograničenje koje se primenjuje i ono koje prijavljuje GET /me dolaze iz istog mesta u kodu, pa ne mogu da se raziđu.
Okruženja: test i produkcija
Svaka instalacija Otkucaja ima dva odvojena okruženja, i u svakom trenutku jedno je aktivno. API uvek radi u aktivnom okruženju, a koje je to vidi se u odgovoru GET /status i u svakoj listi i izveštaju.
| Okruženje | Šta je | Da li su računi fiskalni |
|---|---|---|
sandbox | Test sistem Poreske uprave (*.sandbox.suf.purs.gov.rs). Služi za tehničku proveru pre odobrenja i za obuku. | Ne. Računi ne ulaze u promet ni u poslovne knjige. |
production | Stvarni sistem Poreske uprave. | Da. Svaki račun vrste Promet i Avans je fiskalni račun; Kopija, Predračun i Obuka nisu. |
Dva okruženja imaju različit bezbednosni element, različit PAK i različit ESIR broj. Aplikacija odbija da u oba postavi isti sertifikat i odbija da produkciju usmeri na test adresu Poreske uprave (i obrnuto).
Podaci su potpuno razdvojeni. Račun nosi oznaku okruženja u kome je izdat, pa GET /invoices, GET /reports/summary i GET /reports/daily vraćaju samo ono što pripada aktivnom okruženju. Test račun ne može da uđe u produkcijski promet.
GET /api/v1/status
{
"app": "Otkucaj",
"version": "1.3.6",
"esirNumber": "1667/1.3.6",
"environment": "sandbox", // sandbox | production
"fiscal": false, // true само у продукцији уз прави ПФР
"ready": true, // да ли се уопште може издати рачун
"problems": [], // ако није спремно, разлози редом
"pfr": { "ok": true, "mode": "demo", "env": "sandbox" },
"company": { "name": "…", "tin": "…", "location": "…" },
"time": "2026-08-20T14:00:00.000+02:00"
}
Pre nego što počnete da šaljete stvarne račune, proverite environment i fiscal. Ako je fiscal: false, ono što izdajete nije fiskalni račun, ma šta pisalo na žurnalu.
Svaka izdata stavka nosi istu informaciju:
"pfr": {
"mode": "vpfr",
"environment": "production",
"fiscal": true,
"invoiceNumber": "…",
"verificationUrl": "https://suf.purs.gov.rs/v/?vl=…",
"verificationQRCode": "R0lGODlh…" // base64 GIF који је нацртао сам ПФР
}
Kada verificationQRCode postoji, to je QR koji pripada računu: crta ga PFR, po parametrima Poreske uprave. Ne crtajte svoj preko njega. U demo režimu tog polja nema, pa se QR pravi iz verificationUrl.
Idempotentnost: kako da ne izdate dva puta isti račun
Ako veza pukne pošto je zahtev stigao, a pre nego što je odgovor došao nazad, izgleda isto kao da zahtev nikada nije ni stigao. Ponavljanje tada stvara dva fiskalna računa za jednu prodaju, što se ne može obrisati nego samo refundirati.
Zato svaki POST /invoices sme da nosi ključ zahteva. Pošaljite ga na jedan od dva načina:
POST /api/v1/invoices
Authorization: Bearer <api-kljuc>
Idempotency-Key: order-2026-08-20-4711
Content-Type: application/json
{ "items": [ … ] }
ili u samom telu:
{ "clientRequestId": "order-2026-08-20-4711", "items": [ … ] }
Pravila su jednostavna:
- Ključ sme imati 8 do 64 znaka: slova, brojevi i
. _ : -. Sve drugo vraća422. - Ključ se rezerviše pre nego što se pozove PFR, pa dva istovremena ponavljanja ne mogu oba da prođu.
- Ponavljanje sa istim ključem vraća
201i isti račun, sa istimuuid-om i istim PFR brojem. Novi se ne izdaje. - Ako PFR odbije zahtev, ključ se oslobađa, pa pravi pokušaj ponovo radi.
Najbolje je uzeti nešto što već imate i što je jedinstveno, na primer broj porudžbine iz svog sistema. Tako i mrežni prekid i vaš sopstveni retraj vode istom računu.
Pozivanje iz pregledača (CORS)
API dozvoljava pozive sa bilo kog porekla (Access-Control-Allow-Origin: *) i odgovara na OPTIONS proveru. Autentifikacija ide isključivo zaglavljem Authorization, nikada kolačićem, a Access-Control-Allow-Credentials se namerno ne šalje.
To znači: strani sajt bez vašeg ključa ne može ništa. Ali to znači i da ključ ne sme da stoji u JavaScript-u javne stranice, jer je tamo vidljiv svakome. Iz pregledača zovite svoj server, a svoj server neka zove Otkucaj.
Računi
Sedam putanja oko računa: izdavanje, lista, detalj, žurnal, refundacija, kopija i slanje e-poštom.
/api/v1/invoices
Izdavanje računa. Zahtev se validira, prosleđuje PFR-u, a račun se upisuje u bazu tek kada ga PFR potpiše. Odgovor 201 vraća ceo račun, uključujući žurnal i link za proveru.
| Polje | Tip | Obavezno | Opis |
|---|---|---|---|
invoiceType | string | ne (podrazumevano Normal) | Normal (promet), Advance (avans), ProForma (predračun), Training (obuka), Copy (kopija) |
transactionType | string | ne (podrazumevano Sale) | Sale (prodaja) ili Refund (refundacija) |
items[] | array | da | 1 do 500 stavki, zbir mora biti veći od 0 |
items[].name | string | da | Naziv stavke, do 200 znakova |
items[].unit | string | ne | Jedinica mere, podrazumevano ком |
items[].quantity | number | da | Veća od 0 i najviše 999.999; zaokružuje se na 3 decimale (npr. 0.375) |
items[].unitPrice | number | da | Jedinična cena sa PDV, 0 ili više, zaokružuje se na 2 decimale |
items[].totalAmount | number | ne | Iznos reda. Server ga sam izračunava kao količina × cena, poslata vrednost se ne koristi |
items[].label | string | da | Poreska oznaka iz šifarnika u važnosti, vidi GET /tax-labels. Stope određuje isključivo PFR |
items[].gtin | string | ne | GTIN ili bar-kod, 8 do 14 cifara |
items[].itemId | number | ne | ID artikla iz šifarnika artikala. Samo se pamti uz red računa kao veza ka artiklu; cena i naziv se uvek uzimaju iz samog zahteva |
payments[] | array | ne | Načini plaćanja. Ako izostane ili je prazan, ceo iznos ide kao Cash |
payments[].paymentType | string | da | Cash (gotovina), Card (platna kartica), Check (ček), WireTransfer (prenos na račun), Voucher (vaučer), MobileMoney (instant plaćanje), Other (drugo bezgotovinsko) |
payments[].amount | number | da | Iznos. Stavke sa iznosom 0 ili manjim se odbacuju. Ako posle toga ostane tačno jedno plaćanje, njegov iznos se sam postavlja na ukupan iznos računa; ako ih je više, zbir mora da odgovara zbiru stavki (tolerancija 0,01) |
cashier | string | ne | Ime kasira na računu, najviše 50 znakova, koliko prima PFR: duži naziv se odbija (422) i ništa se ne skraćuje; za API pozive podrazumevano API |
buyerId | string | uslovno | ID kupca, do 64 znaka, oblik шифра:вредност. Šifarnik: 10 PIB, 11 JMBG, 12 PIB:JBKJS, 20 lična karta, 23 pasoš, 30 strani pasoš, 40 strani TIN. Obavezan za svaku refundaciju |
buyerCostCenterId | string | ne | Opciono polje kupca (mesto troška) |
referentDocumentNumber | string | uslovno | PFR broj originalnog računa. Obavezan za Refund i za Copy |
referentDocumentDT | string | ne | Vreme originalnog računa, YYYY-MM-DD HH:MM:SS |
dateAndTimeOfIssue | string | ne | ESIR vreme, YYYY-MM-DD HH:MM:SS. Koristi se za avans kada je uplata stigla ranije nego što se račun izdaje (npr. prenos na račun proknjižen sutradan); štampa se na računu kao „ESIR vreme“. Račun Avans Prodaja se tada izdaje najkasnije narednog radnog dana od prijema uplate (čl. 11 st. 5 Pravilnika o vrstama fiskalnih računa) |
adText | string | ne | Reklamni tekst, do 500 znakova, štampa se ispod kraja računa. Pri zatvaranju avansa ovde ide broj i datum poslednjeg avansnog računa |
Zahtev
curl -X POST https://otkucaj.com/api/v1/invoices \
-H "Authorization: Bearer kasir_vas_kljuc" \
-H "Content-Type: application/json" \
-d '{
"invoiceType": "Normal",
"transactionType": "Sale",
"cashier": "Веб продавница",
"payments": [{ "paymentType": "Card", "amount": 2400.00 }],
"items": [
{ "name": "Мајица", "unit": "ком", "quantity": 2, "unitPrice": 1200.00,
"label": "Ђ", "gtin": "8600000000000" }
]
}'
Odgovor 201
{
"uuid": "9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30",
"invoiceType": "Normal",
"transactionType": "Sale",
"cashier": "Веб продавница",
"total": 2400.0,
"payments": [
{ "paymentType": "Card", "amount": 2400.0 }
],
"items": [
{
"name": "Мајица",
"unit": "ком",
"quantity": 2.0,
"unitPrice": 1200.0,
"totalAmount": 2400.0,
"label": "Ђ",
"gtin": "8600000000000"
}
],
"taxItems": [
{ "label": "Ђ", "categoryName": "О-ПДВ", "rate": 20.0, "amount": 400.0, "total": 2400.0 }
],
"buyerId": null,
"referentDocumentNumber": null,
"referentDocumentDT": null,
"pfr": {
"mode": "demo",
"demo": true,
"requestedBy": "DMX7K2QP",
"signedBy": "DMX7K2QP",
"invoiceNumber": "DMX7K2QP-DMX7K2QP-302",
"invoiceCounter": "143/302ПП",
"sdcDateTime": "2026-08-19 14:02:33",
"verificationUrl": "https://otkucaj.com/verify?id=9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30"
},
"journal": "======== ФИСКАЛНИ РАЧУН =======\n...",
"createdAt": "2026-08-19 14:02:33"
}
Ovaj oblik vraćaju i GET /invoices/{uuid}, svaki element liste računa, i putanje za refundaciju i kopiju. Značenje PFR polja: invoiceNumber je PFR broj računa (oblik JID-JID-N), invoiceCounter je brojač (npr. 143/302ПП: 143. račun vrste Promet-Prodaja od ukupno 302), sdcDateTime je PFR vreme, verificationUrl je link ka stranici za proveru računa, demo kaže da li je račun izdat u demo režimu. taxItems je poreska rekapitulacija po oznakama, sa stopom, iznosom poreza i osnovicom; stope dolaze isključivo od PFR-a, nikada iz vašeg zahteva.
- 400 bad_request
- 401 unauthorized
- 422 validation
- 429 rate_limited
- 409 pfr_error
Na 409 račun nije izdat i ništa nije upisano u bazu.
/api/v1/invoices
Lista računa, najnoviji prvi, do 100 po strani. Svaki element je ceo račun, u istom obliku kao odgovor na POST /invoices, zajedno sa žurnalom.
| Parametar | Tip | Opis |
|---|---|---|
from | string | Datum od, YYYY-MM-DD (uključuje ceo dan od 00:00:00) |
to | string | Datum do, YYYY-MM-DD (uključuje ceo dan do 23:59:59) |
type | string | Vrsta računa: Normal, Advance, ProForma, Training, Copy |
page | number | Strana, od 1 (podrazumevano 1), po 100 računa |
from i to se ovde ne proveravaju formatom: neispravan datum neće vratiti 422, nego praznu ili neočekivanu listu. Nepoznata vrednost u type se tiho ignoriše i filter se ne primenjuje. Šaljite datume u obliku YYYY-MM-DD i vrstu iz gornjeg spiska.Zahtev
curl -H "Authorization: Bearer kasir_vas_kljuc" \ "https://otkucaj.com/api/v1/invoices?from=2026-08-01&to=2026-08-19&type=Normal&page=1"
Odgovor
{
"page": 1,
"perPage": 100,
"invoices": [
{ "uuid": "9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30", "invoiceType": "Normal", "...": "..." }
]
}
Prazna lista znači da za zadate filtere nema računa. Ukupan broj računa se ne vraća: straničite dok ne dobijete manje od 100 elemenata.
- 401 unauthorized
- 429 rate_limited
/api/v1/invoices/{uuid}
Jedan račun po UUID-u, u punom obliku opisanom kod POST /invoices.
Zahtev
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30
Odgovor
{
"uuid": "9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30",
"invoiceType": "Normal",
"transactionType": "Sale",
"total": 2400.0,
"pfr": { "invoiceNumber": "DMX7K2QP-DMX7K2QP-302", "...": "..." },
"journal": "======== ФИСКАЛНИ РАЧУН =======\n...",
"createdAt": "2026-08-19 14:02:33"
}
UUID koji ne postoji vraća 404 sa kodom not_found. UUID ispisan velikim slovima ne odgovara obliku putanje i završava kao nepoznata putanja, takođe 404: šaljite ga tačno onako kako vam ga je API vratio.
- 401 unauthorized
- 404 not_found
- 429 rate_limited
/api/v1/invoices/{uuid}/journal
Žurnal računa kao čist tekst (text/plain; charset=utf-8), 40 kolona, ćirilicom, spreman za termalni štampač od 58 ili 80 mm. Identičan žurnalu koji aplikacija sama štampa. Ovo je jedini odgovor koji nije JSON.
Zahtev
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/journal
Odgovor, skraćeno
======== ФИСКАЛНИ РАЧУН =======
111111111
Пример д.о.о.
Продавница 1
Булевар ослобођења 1
Београд
Касир: Веб продавница
ЕСИР број: 1667/1.3.6
------ ПРОМЕТ - ПРОДАЈА -------
Артикли
========================================
Назив Цена Кол. Укупно
Мајица /ком (Ђ)
1.200,00 2 2.400,00
----------------------------------------
За уплату: 2.400,00
Платна картица: 2.400,00
========================================
Ознака Име Стопа Порез
Ђ О-ПДВ 20,00% 400,00
----------------------------------------
Укупан износ пореза: 400,00
========================================
ПФР време: 19.08.2026. 14:02:33
ПФР број рачуна: DMX7K2QP-DMX7K2QP-302
Бројач рачуна: 143/302ПП
======== КРАЈ ФИСКАЛНОГ РАЧУНА ========
Jedan račun je jedan žurnal, i štampa se sam. Svaka izmena računa — refundacija, poništavanje, kopija — jeste nov fiskalni dokument sa svojim brojem, svojim žurnalom i svojim QR kodom. Nikada ne spajajte dva žurnala u jedan zapis i ne dodajte između njih nijedan svoj red (ni „===== PONIŠTAVANJE =====“, ni broj porudžbine, ni datum): tako na jednom listu završe dva fiskalna dokumenta sa vašim tekstom unutar okvira, što se, kako mi razumemo, ne slaže sa pravilom da se račun izdaje u jednom primerku (čl. 13 st. 5 Pravilnika) ni sa 16.P13, po kome ništa ispod završne linije nije deo računa, a na A4 se račun i prelomi na više strana. Čuvajte svaki žurnal u svom polju i štampajte po jedan dokument na jednom listu. Onaj koji ispravlja pokazuje na izvorni preko Реф. број linije koju PFR sam upisuje.
A4 nije isti dokument kao isečak sa kase. Prijava IB 1651 (13.P3) odobrava oba oblika: i žurnal na A4, i prikaz računa kao A4 dokumenta sa zaglavljem prodavca, tabelom stavki, poreskim pregledom, QR kodom i fiskalnim isečkom doslovno. Za kancelarijski štampač uzmite drugi oblik — stavite fiskalni isečak doslovno u dokument, a adresu za proveru kao pravi link izvan okvira računa. Najbolje ga uopšte ne crtajte: pozovite GET /api/v1/invoices/{uuid}/document svojim API ključem i odštampajte ono što stigne, znak po znak. To je oblik koji Otkucaj štampa i koji je odobren, i jedini način da ga dobijete preko API-ja — stranica /racun/dokument traži prijavu i API ključ je ne otvara. Uz ?base=https://vasa-prodavnica.rs stilovi i fontovi se učitavaju i kada dokument prikazujete na svom sajtu, a uz &print=1 odmah se otvara dijalog za štampu.
Ako sami iscrtavate račun iz polja journal: QR kod ide unutar okvira računa — između poslednje linije ==== i linije КРАЈ ФИСКАЛНОГ РАЧУНА, nikada ispod nje. Sve odštampano ispod te linije ne smatra se delom fiskalnog računa (Tehničko uputstvo 16.P13), pa tamo sme samo polje za reklamu: ni QR, ni link za proveru, ni vaš broj porudžbine uz njih. Odštampan QR mora da bude kvadrat od 40 do 50 mm (16.P12) i ne sme se smanjivati CSS-om, jer skaliranje briše kolone modula i kod prestaje da se čita. Ovo je isti raspored koji Otkucaj štampa sam i koji je odobren u prijavi IB 1651.
Nefiskalne vrste (Kopija, Predračun, Obuka) umesto zaglavlja i kraja fiskalnog računa nose liniju „OVO NIJE FISKALNI RAČUN“, koju aplikacija pri štampi uvećava na dvostruku veličinu fonta. Kopija-Refundacija dodatno štampa prostor „Potpis kupca“ za potpis pri vraćanju novca.
- 401 unauthorized
- 404 not_found
- 429 rate_limited
/api/v1/invoices/{uuid}/document
Račun kao gotov dokument za štampu, onako kako ga štampa sama aplikacija, da integrator nikada ne crta svoj račun. Parametar paper bira list: a4 (podrazumevano) je A4 dokument odobren prijavom IB 1651 (13.P3): zaglavlje prodavca, tabela stavki, poreski pregled, način plaćanja, QR kod i fiskalni isečak doslovno; a5 je fiskalni isečak na listu A5 (13.P4), oblik koji kurir ili centar za isporuku štampa i pakuje uz pošiljku; a4slip je fiskalni isečak na listu A4. Na svakom listu QR kod stoji unutar okvira računa, između PFR bloka i završne linije, veličine 42 do 45 mm, a ispod „KRAJ FISKALNOG RAČUNA“ ne štampa se ništa naše. Uz format=pdf isti list stiže kao PDF, iscrtan na serveru istim pregledačem i istim @page pravilom (izmereno: strana A5 148 × 210 mm, QR 45 mm, čita se). GET /status u polju documents.pdf kaže da li instalacija ume da iscrta PDF; ako ne ume, tražite HTML i odštampajte ga. Ne crtajte svoj A4 ni A5 - uzmite ovaj. Odgovor nije JSON.
Zahtev
curl -H "Authorization: Bearer kasir_vas_kljuc" "https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/document?base=https://prodavnica.rs&print=1" # исечак на А5 као PDF, за курира curl -H "Authorization: Bearer kasir_vas_kljuc" "https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/document?paper=a5&format=pdf" -o racun.pdf
Kada list preuzima partner koji čeka. Centar za isporuku koji štampa račun uz pošiljku otvara link dok radnik stoji nad kutijom, pa to mora da bude gotovo za sekundu dve. Zato POST /invoices i POST /invoices/{uuid}/close-advance primaju polje "prefetchDocument": "a5": Otkucaj odgovori odmah, a list iscrta čim je odgovor otišao, pa preuzimanje koje stigne posle toga zatiče gotov fajl. Izmereno 02.09.2026: preuzimanje bez pripreme 0,60 s, sa pripremom 0,13 s. Isti list se posle toga služi iz keša, pa svako sledeće preuzimanje traje koliko i prenos samih bajtova.
Sam list je označen komentarima <!--sheet--> i <!--/sheet-->, ako vam treba samo on bez ostatka strane. Proverite da odgovor sadrži <!--sheet--> pre nego što ga prikažete: tako razlikujete dokument od strane sa greškom.
- 401 unauthorized
- 404 not_found
- 422 validation -
paper,formatilibasenisu ispravni; configuration - instalacija ne ume da iscrta PDF (tražite HTML)
/api/v1/invoices/{uuid}/refund
Refundacija celog postojećeg računa, jednim pozivom. Stavke se preuzimaju sa originala, referentni broj i vreme pokazuju na original, a nov račun se izdaje kroz isti put kao i svaki drugi. Ovo je isto što radi dugme Refundacija u aplikaciji.
Normal ili Advance, vrste transakcije Sale i u statusu izdat. Kopija, predračun, obuka i već izdata refundacija se odbijaju sa 422. Ovo su isti uslovi pod kojima se dugme prikazuje u aplikaciji.| Polje tela | Tip | Podrazumevano | Opis |
|---|---|---|---|
buyerId | string | kupac sa originala | Identifikacija kupca. Obavezna je za refundaciju: ako je nema ni u telu ni na originalu, odgovor je 422 sa porukom koja objašnjava da za poništavanje sopstvenog računa treba uneti svoj PIB |
buyerCostCenterId | string | sa originala | Mesto troška kupca |
payments | array | plaćanja sa originala | Načini vraćanja novca. Zbir mora da odgovara ukupnom iznosu računa |
cashier | string | API | Ime kasira na računu refundacije, najviše 50 znakova |
adText | string | prazno | Reklamni tekst, do 500 znakova |
Stavke se uvek uzimaju sa originalnog računa, u celosti, i ne mogu se zadati telom zahteva. Za delimičnu refundaciju (vraća se samo deo stavki ili manja količina) koristite POST /invoices sa transactionType: "Refund", gde sami navodite šta se vraća.
Zahtev
curl -X POST \
https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/refund \
-H "Authorization: Bearer kasir_vas_kljuc" \
-H "Content-Type: application/json" \
-d '{ "buyerId": "10:123456789", "cashier": "Веб продавница" }'
Odgovor 201
{
"uuid": "1c77a0d5-9b12-4f8e-b3aa-0d5e7c9f4411",
"invoiceType": "Normal",
"transactionType": "Refund",
"cashier": "Веб продавница",
"total": 2400.0,
"buyerId": "10:123456789",
"referentDocumentNumber": "DMX7K2QP-DMX7K2QP-302",
"referentDocumentDT": "2026-08-19 14:02:33",
"pfr": {
"invoiceNumber": "DMX7K2QP-DMX7K2QP-318",
"invoiceCounter": "12/318РП",
"...": "..."
},
"journal": "======== ФИСКАЛНИ РАЧУН =======\n...",
"createdAt": "2026-08-19 16:41:02"
}
- 400 bad_request
- 401 unauthorized
- 404 not_found
- 422 validation
- 429 rate_limited
- 409 pfr_error
/api/v1/invoices/{uuid}/close-advance
Zatvaranje avansa jednim pozivom. Za izdati avansni račun prodaje (Avans Prodaja) aplikacija redom izdaje Avans Refundaciju na ceo iznos avansa (referenca = avansni račun) i odmah zatim konačni Promet Prodaja sa stavkama isporuke, referencom na tu refundaciju i reklamnim poljem u kom Tehničko uputstvo §9.1.1 kao obavezne propisuje broj i datum poslednjeg avansnog računa; aplikacija uz njih upisuje i iznos plaćen avansno i broj avans refundacije. To je isto što radi dugme „Zatvori avans“ u aplikaciji, od početka do kraja, u obliku odobrenih uzoraka P9, P10 i P15 prijave IB 1651.
Zahtev
curl -X POST \
https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/close-advance \
-H "Authorization: Bearer kasir_vas_kljuc" \
-H "Content-Type: application/json" \
-d '{
"items": [{ "name": "Ормар по мери", "quantity": 1, "unitPrice": 8000.00, "label": "Ђ" }],
"payments": [{ "paymentType": "WireTransfer", "amount": 8000.00 }],
"adText": "Поруџбина #1042",
"clientRequestId": "order-1042-final"
}'
Poziv se sme ponoviti. Ako je refundacija izdata, a konačni račun nije (PFR je odbio drugi zahtev, ili je veza pukla između dva), ponovljeni poziv nalazi refundaciju po njenom referentnom broju i izdaje samo konačni račun. Refundacija koja je jednom izdata ne izdaje se dvaput.
- 401 unauthorized
- 404 not_found
- 422 validation - račun nije izdati avansni račun prodaje, ili stavke i plaćanja konačnog računa ne prolaze proveru; poruka kaže koji dokument
- 409 pfr_error - PFR je odbio ili nije dostupan; poruka kaže koji dokument, a već izdata refundacija ostaje
/api/v1/invoices/{uuid}/copy
Izdavanje kopije postojećeg računa, kroz isti put koji koristi dugme Kopija u aplikaciji. Kopija nosi sve stavke i plaćanja originala, referentni broj i vreme originala, i njegovu identifikaciju kupca.
422, isto kao u aplikaciji.Zahtev
curl -X POST \ https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/copy \ -H "Authorization: Bearer kasir_vas_kljuc"
Odgovor 201
{
"uuid": "6d4b1f92-2a70-4c31-8f5e-7b1c0a93de84",
"invoiceType": "Copy",
"transactionType": "Sale",
"total": 2400.0,
"referentDocumentNumber": "DMX7K2QP-DMX7K2QP-302",
"referentDocumentDT": "2026-08-19 14:02:33",
"pfr": { "invoiceNumber": "DMX7K2QP-DMX7K2QP-319", "...": "..." },
"journal": "ОВО НИЈЕ ФИСКАЛНИ РАЧУН\n...",
"createdAt": "2026-08-19 16:43:20"
}
Kopija nije fiskalni račun: u žurnalu nema zaglavlja fiskalnog računa, nego linija „OVO NIJE FISKALNI RAČUN“. Kopija refundacije dodatno nosi prostor za potpis kupca.
- 401 unauthorized
- 404 not_found
- 422 validation
- 429 rate_limited
- 409 pfr_error
/api/v1/invoices/{uuid}/email
Slanje računa kupcu e-poštom. Poruka sadrži žurnal i link za proveru računa, i potpuno je ista kao ona koju šalje dugme u aplikaciji, jer obe idu kroz isti kod. Slanje se upisuje u revizioni trag kao invoice.email.
| Polje tela | Tip | Obavezno | Opis |
|---|---|---|---|
to | string | da | Adresa primaoca, ispravna e-adresa, najviše 190 znakova |
Zahtev
curl -X POST \
https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/email \
-H "Authorization: Bearer kasir_vas_kljuc" \
-H "Content-Type: application/json" \
-d '{ "to": "[email protected]" }'
Odgovor
{
"ok": true,
"uuid": "9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30",
"to": "[email protected]"
}
Ograničenje slanja je 30 poruka na sat po ključu, nezavisno od opšteg ograničenja od 240 zahteva u minutu. Prekoračenje vraća 429 sa porukom o ograničenju pošte. Ako server pošte odbije poruku, odgovor je 409 sa kodom mail_error: to nije ni greška PFR-a ni greška u zahtevu, nego neuspelo slanje, i zahtev se sme ponoviti kasnije.
- 400 bad_request
- 401 unauthorized
- 404 not_found
- 422 validation
- 429 rate_limited
- 409 mail_error
Tokovi: prodaja, refundacija, kopija, predračun
Svaki zakonom propisani tok, sa punim telom zahteva. Odgovor je uvek isti oblik računa kao kod POST /invoices.
Promet-prodaja, sa podeljenim plaćanjem
Najčešći račun. Na jednom računu sme više načina plaćanja; zbir mora odgovarati zbiru stavki.
{
"invoiceType": "Normal",
"transactionType": "Sale",
"cashier": "Веб продавница",
"payments": [
{ "paymentType": "Cash", "amount": 1000.00 },
{ "paymentType": "Card", "amount": 1400.00 }
],
"items": [
{ "name": "Мајица", "quantity": 2, "unitPrice": 1200.00, "label": "Ђ" }
]
}
Refundacija
Ako vraćate ceo račun, najjednostavnije je pozvati POST /invoices/{uuid}/refund. Ručna refundacija kroz POST /invoices mora da nosi referentDocumentNumber (PFR broj originalnog računa) i buyerId (identifikaciju kupca). Bez bilo kog od ta dva polja zahtev se odbija sa 422. Stavke i iznosi su oni koji se vraćaju, dakle mogu biti i deo originalnog računa.
{
"invoiceType": "Normal",
"transactionType": "Refund",
"cashier": "Веб продавница",
"buyerId": "10:123456789",
"referentDocumentNumber": "DMX7K2QP-DMX7K2QP-302",
"referentDocumentDT": "2026-08-19 14:02:33",
"payments": [{ "paymentType": "Card", "amount": 1200.00 }],
"items": [
{ "name": "Мајица", "quantity": 1, "unitPrice": 1200.00, "label": "Ђ" }
]
}
Refundacije umanjuju promet u izveštajima. Kupcu koji prima gotovinu izdaje se i Kopija-Refundacija sa prostorom za potpis.
Poništavanje sopstvenog računa
Pogrešno izdat račun se poništava refundacijom celog iznosa, pri čemu prodavac kao identifikaciju kupca unosi svoj PIB, u obliku 10:СВОЈПИБ.
{
"invoiceType": "Normal",
"transactionType": "Refund",
"buyerId": "10:111111111",
"referentDocumentNumber": "DMX7K2QP-DMX7K2QP-302",
"referentDocumentDT": "2026-08-19 14:02:33",
"payments": [{ "paymentType": "Card", "amount": 2400.00 }],
"items": [{ "name": "Мајица", "quantity": 2, "unitPrice": 1200.00, "label": "Ђ" }]
}
Kopija
Kopija postojećeg računa se najlakše izdaje kroz POST /invoices/{uuid}/copy. Ručno, kroz POST /invoices, kopija mora da nosi referentDocumentNumber, inače 422.
{
"invoiceType": "Copy",
"transactionType": "Sale",
"referentDocumentNumber": "DMX7K2QP-DMX7K2QP-302",
"referentDocumentDT": "2026-08-19 14:02:33",
"payments": [{ "paymentType": "Card", "amount": 2400.00 }],
"items": [{ "name": "Мајица", "quantity": 2, "unitPrice": 1200.00, "label": "Ђ" }]
}
Predračun, pa fiskalni račun
Predračun (ProForma) nije fiskalni račun: služi kao ponuda. Kada kupac plati, izdaje se pravi Promet-Prodaja sa referencom na predračun.
Корак 1, предрачун:
{
"invoiceType": "ProForma",
"transactionType": "Sale",
"payments": [{ "paymentType": "WireTransfer", "amount": 12000.00 }],
"items": [{ "name": "Услуга одржавања", "unit": "мес", "quantity": 1, "unitPrice": 12000.00, "label": "Ђ" }]
}
Корак 2, по уплати, фискални рачун са референцом на предрачун:
{
"invoiceType": "Normal",
"transactionType": "Sale",
"referentDocumentNumber": "DMX7K2QP-DMX7K2QP-310",
"referentDocumentDT": "2026-08-19 09:15:00",
"payments": [{ "paymentType": "WireTransfer", "amount": 12000.00 }],
"items": [{ "name": "Услуга одржавања", "unit": "мес", "quantity": 1, "unitPrice": 12000.00, "label": "Ђ" }]
}
U samoj aplikaciji dugme „Izdaj fiskalni račun“ na predračunu radi tačno ovo: prepuni kasu i sama postavi referencu.
Avansni tok, od prve uplate do konačnog računa
Avansna stavka nosi propisan naziv po poreskoj oznaci: 10: Аванс (Ђ), 11: Аванс (Е), 12: Аванс (Г) ili 13: Аванс (А). Količina je uvek 1, a cena je iznos primljenog avansa. Tok ima tri vrste dokumenata: avansni račun (AP) za svaku uplatu, avans-refundacija (AR) koja zatvara zbir svih avansa, i konačni Promet-Prodaja.
-
Prvi avansni račun (AP). Uplata od 5.000,00 stigla prenosom na račun 18.08, a knjiži se 19.08: zato
dateAndTimeOfIssuenosi stvarno vreme uplate (štampa se kao „ESIR vreme“).{ "invoiceType": "Advance", "transactionType": "Sale", "dateAndTimeOfIssue": "2026-08-18 11:30:00", "payments": [{ "paymentType": "WireTransfer", "amount": 5000.00 }], "items": [{ "name": "10: Аванс (Ђ)", "quantity": 1, "unitPrice": 5000.00, "label": "Ђ" }] }PFR vrati npr.
invoiceNumber: "DMX7K2QP-DMX7K2QP-311". -
Dodatni avansni računi za svaku sledeću uplatu, svaki sa referencom na prethodni AP:
{ "invoiceType": "Advance", "transactionType": "Sale", "referentDocumentNumber": "DMX7K2QP-DMX7K2QP-311", "referentDocumentDT": "2026-08-18 11:30:00", "payments": [{ "paymentType": "WireTransfer", "amount": 3000.00 }], "items": [{ "name": "10: Аванс (Ђ)", "quantity": 1, "unitPrice": 3000.00, "label": "Ђ" }] }PFR vrati npr.
invoiceNumber: "DMX7K2QP-DMX7K2QP-312". -
Zatvaranje: avans-refundacija (AR) na ceo primljeni iznos, sa referencom na poslednji AP i sa
buyerId, koji Otkucaj traži na svakoj refundaciji:{ "invoiceType": "Advance", "transactionType": "Refund", "buyerId": "10:123456789", "referentDocumentNumber": "DMX7K2QP-DMX7K2QP-312", "referentDocumentDT": "2026-08-19 10:00:00", "payments": [{ "paymentType": "WireTransfer", "amount": 8000.00 }], "items": [{ "name": "10: Аванс (Ђ)", "quantity": 1, "unitPrice": 8000.00, "label": "Ђ" }] }PFR vrati npr.
invoiceNumber: "DMX7K2QP-DMX7K2QP-313". -
Konačni Promet-Prodaja sa stvarnim stavkama isporuke, referencom na AR i reklamnim tekstom koji nosi broj i datum poslednjeg avansnog računa:
{ "invoiceType": "Normal", "transactionType": "Sale", "referentDocumentNumber": "DMX7K2QP-DMX7K2QP-313", "referentDocumentDT": "2026-08-19 10:05:00", "adText": "Аванс: DMX7K2QP-DMX7K2QP-312 од 19.08.2026.", "payments": [{ "paymentType": "WireTransfer", "amount": 8000.00 }], "items": [{ "name": "Ормар по мери", "quantity": 1, "unitPrice": 8000.00, "label": "Ђ" }] }
U aplikaciji dugme „Zatvori avans“ na avansnom računu samo odradi korake 3 i 4: izdaje AR na ceo iznos i prepuni kasu za konačni račun sa postavljenom referencom i reklamnim tekstom. Preko API-ja korake 3 i 4 radi jedan poziv, POST /invoices/{uuid}/close-advance: pošaljite stavke isporuke, a refundaciju, reference i obavezne redove reklamnog polja aplikacija sastavlja sama. Avansna stavka se šalje kao 10: Аванс bez jedinice mere: oznaku u zagradi dodaje PFR.
Režim po čl. 6 st. 2 Pravilnika
Instalacija može da radi u režimu u kom su načini plaćanja Card, Check i MobileMoney isključeni, a takve naplate se evidentiraju kao gotovina. Jedna instalacija radi u tačno jednom režimu, koji bira administrator u Podešavanjima. Kada je režim uključen, zahtev sa nekim od isključenih načina vraća:
422 { "error": { "code": "validation",
"message": "Начин плаћања „Card“ је искључен подешавањем (режим по чл. 6 ст. 2 Правилника). Унесите као готовину." } }
Integracija tada šalje {"paymentType": "Cash"} za te naplate.
Artikli
Šifarnik artikala koji kasa nudi na dodir. Artikli nisu uslov za izdavanje računa: POST /invoices prima stavke slobodno, sa nazivom i cenom iz vašeg sistema.
/api/v1/items
Lista artikala, sortirano po nazivu, do 1.000 komada. Polje active razlikuje aktivne od deaktiviranih; deaktivirani su takođe u odgovoru.
Zahtev
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/items
Odgovor
{
"items": [
{
"id": 12,
"plu": "1001",
"name": "Мајица",
"unit": "ком",
"price": 1200.0,
"label": "Ђ",
"gtin": "8600000000000",
"active": true
}
]
}
- 401 unauthorized
- 429 rate_limited
/api/v1/items
Dodavanje artikla u šifarnik.
| Polje | Tip | Obavezno | Opis |
|---|---|---|---|
name | string | da | Naziv, do 200 znakova |
label | string | da | Poreska oznaka iz šifarnika u važnosti (GET /tax-labels). Nepoznata oznaka vraća 422 sa spiskom dozvoljenih |
price | number | ne (podrazumevano 0) | Cena sa PDV, 0 ili više, zaokružuje se na 2 decimale |
plu | string | ne | Šifra artikla za brzo kucanje |
unit | string | ne | Jedinica mere, podrazumevano ком |
gtin | string | ne | GTIN ili bar-kod, 8 do 14 cifara |
Nov artikal se uvek upisuje kao aktivan. Polje category se ovim pozivom ne postavlja.
Zahtev
curl -X POST https://otkucaj.com/api/v1/items \
-H "Authorization: Bearer kasir_vas_kljuc" \
-H "Content-Type: application/json" \
-d '{ "name": "Мајица", "price": 1200.00, "label": "Ђ",
"plu": "1001", "gtin": "8600000000000", "unit": "ком" }'
Odgovor 201
{ "id": 12, "ok": true }
- 400 bad_request
- 401 unauthorized
- 422 validation
- 429 rate_limited
/api/v1/items/{id}
Delimična izmena artikla: šaljete samo polja koja menjate, ostala ostaju netaknuta. Oba metoda, PUT i PATCH, rade isto.
label upisuje onakva kakva je poslata, bez provere da li takva poreska oznaka postoji, za razliku od dodavanja artikla gde se proverava. Artikal sa oznakom koje nema u šifarniku će pri izdavanju računa proizvesti 422. Šaljite samo oznake koje vraća GET /tax-labels.Zahtev
curl -X PUT https://otkucaj.com/api/v1/items/12 \
-H "Authorization: Bearer kasir_vas_kljuc" \
-H "Content-Type: application/json" \
-d '{ "price": 1350.00, "active": true }'
Odgovor
{ "ok": true }
- 400 bad_request
- 401 unauthorized
- 404 not_found
- 429 rate_limited
/api/v1/items/{id}
Meka deaktivacija: artikal se ne briše iz baze, jer ga postojeći računi i dalje referenciraju. Dobija active: false i nestaje sa kase. Ponovno aktiviranje je PUT sa {"active": true}.
Zahtev
curl -X DELETE https://otkucaj.com/api/v1/items/12 \ -H "Authorization: Bearer kasir_vas_kljuc"
Odgovor
{ "ok": true }
{"ok": true}, jer se ništa ne menja i ništa ne puca. Ako vam je važno da li artikal postoji, proverite ga prethodno kroz GET /items.- 401 unauthorized
- 429 rate_limited
Izveštaji
Oba izveštaja računaju promet po istom pravilu i istim upitima kao stranica Izveštaji u aplikaciji, pa se brojke ne mogu razići.
Normal i Advance u statusu izdat. Refundacije se oduzimaju. Predračun, kopija i obuka ne ulaze ni u jedan zbir. Isto pravilo vraća i sam odgovor, u polju rule./api/v1/reports/summary
Promet za period, sa razradom po poreskoj oznaci, načinu plaćanja i kasiru.
| Parametar | Tip | Podrazumevano | Opis |
|---|---|---|---|
from | string | danas | Datum od, strogo YYYY-MM-DD, uključuje ceo dan |
to | string | danas | Datum do, strogo YYYY-MM-DD, uključuje ceo dan. Ne sme biti raniji od from |
Neispravan oblik datuma ili period u kom je from kasnije od to vraćaju 422, za razliku od liste računa gde se datumi ne proveravaju.
Zahtev
curl -H "Authorization: Bearer kasir_vas_kljuc" \ "https://otkucaj.com/api/v1/reports/summary?from=2026-08-01&to=2026-08-19"
Odgovor
{
"from": "2026-08-01",
"to": "2026-08-19",
"totals": {
"invoices": 142,
"turnover": 386400.0,
"refunds": { "count": 3, "amount": 7200.0 }
},
"byTaxLabel": [
{ "label": "Ђ", "name": "О-ПДВ", "rate": 20.0, "turnover": 351200.0 },
{ "label": "Е", "name": "П-ПДВ", "rate": 10.0, "turnover": 35200.0 }
],
"byPaymentType": [
{ "paymentType": "Card", "amount": 244800.0 },
{ "paymentType": "Cash", "amount": 141600.0 }
],
"byCashier": [
{ "cashier": "Веб продавница", "invoices": 118, "turnover": 302400.0 },
{ "cashier": "Марија", "invoices": 24, "turnover": 84000.0 }
],
"rule": "Turnover counts issued Normal and Advance receipts only; refunds subtract; ProForma, Copy and Training are excluded."
}
Značenje polja: totals.invoices je broj računa u periodu, uključujući i refundacije; totals.turnover je promet umanjen za refundacije; totals.refunds posebno prikazuje broj i ukupan iznos refundacija, kao pozitivan broj. U byTaxLabel su name i rate vrednosti null ako oznaka više ne postoji u šifarniku, a računi sa njom su ostali u bazi. Načini plaćanja se sabiraju iz samog računa, pa za podeljena plaćanja svaki deo ulazi u svoj red.
- 401 unauthorized
- 422 validation
- 429 rate_limited
/api/v1/reports/daily
Promet i broj računa po danima, za isti period i po istom pravilu kao sabirni izveštaj.
Zahtev
curl -H "Authorization: Bearer kasir_vas_kljuc" \ "https://otkucaj.com/api/v1/reports/daily?from=2026-08-01&to=2026-08-05"
Odgovor
{
"from": "2026-08-01",
"to": "2026-08-05",
"days": [
{ "date": "2026-08-01", "invoices": 12, "turnover": 28800.0 },
{ "date": "2026-08-02", "invoices": 9, "turnover": 19400.0 },
{ "date": "2026-08-05", "invoices": 15, "turnover": 41250.0 }
],
"rule": "Turnover counts issued Normal and Advance receipts only; refunds subtract; ProForma, Copy and Training are excluded."
}
Dani bez prometa se izostavljaju, tačno kao u tabeli u aplikaciji. Ako vam treba neprekidan niz datuma, popunite praznine na svojoj strani.
- 401 unauthorized
- 422 validation
- 429 rate_limited
Šifarnici
Dve liste koje integraciji trebaju da bi slala ispravne vrednosti, obe iz istog izvora koji aplikacija i sama koristi.
/api/v1/tax-labels
Poreske oznake i stope koje su trenutno na snazi, iz istog izvora koji koristi i validator računa (samo aktivni redovi). Oznaka koju vrati ovaj poziv je oznaka koju će POST /invoices prihvatiti.
Zahtev
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/tax-labels
Odgovor
{
"taxLabels": [
{ "label": "Ђ", "name": "О-ПДВ", "rate": 20.0 },
{ "label": "Е", "name": "П-ПДВ", "rate": 10.0 },
{ "label": "Г", "name": "Без ПДВ", "rate": 0.0 }
]
}
POST /invoices podrazumevano ne prihvata. Oznake i stope se menjaju u Podešavanjima, a u režimu L-PFR ili V-PFR merodavan je spisak koji daje sam PFR. Zato čitajte ovaj poziv, a ne prepisujte oznake u svoj kod.- 401 unauthorized
- 429 rate_limited
/api/v1/categories
Kategorije aktivnih artikala, bez praznih vrednosti, sortirane azbučno. Isti upit koji kasa koristi za dugmad kategorija.
Zahtev
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/categories
Odgovor
{ "categories": ["Пиће", "Слаткиши", "Храна"] }
Kategorije se unose uz artikal u aplikaciji. Artikli bez kategorije ne prave prazan unos u ovoj listi.
- 401 unauthorized
- 429 rate_limited
Greške
Svaka greška ima isti JSON oblik, bez obzira na putanju:
{ "error": { "code": "validation", "message": "Рефундација мора имати референтни број оригиналног рачуна (referentDocumentNumber)." } }
| HTTP | code | Uzrok | Šta uraditi |
|---|---|---|---|
| 400 | bad_request | Telo zahteva nije ispravan JSON | Proverite serijalizaciju i Content-Type: application/json |
| 401 | unauthorized | Nema Authorization zaglavlja, ključ je pogrešan ili opozvan | Proverite ključ; po potrebi generišite nov u Podešavanjima |
| 404 | not_found | Nepoznata putanja, nepostojeći UUID računa ili ID artikla pri izmeni | Proverite putanju i identifikator |
| 422 | validation | Zahtev ne prolazi validaciju; poruka precizno kaže koje polje i zašto | Ispravite zahtev. Ne ponavljajte isti zahtev nepromenjen: rezultat će uvek biti isti |
| 429 | rate_limited | Više od 240 zahteva u minutu po ključu, ili više od 30 poslatih poruka na sat | Sačekajte do isteka prozora, pa nastavite smanjenim tempom |
| 409 | pfr_error | PFR je odbio zahtev ili nije dostupan | Račun NIJE izdat, ništa nije upisano. Proverite GET /status, pa ponovite isti zahtev |
| 409 | mail_error | Poruka sa računom nije mogla da bude poslata | Račun postoji i nije dirnut. Slanje se sme ponoviti kasnije |
WooCommerce i integracije
Gotov dodatak povezuje WooCommerce prodavnicu bez ijedne linije koda: otkucaj-fiskalizacija.zip.
- U WordPress administraciji otvorite Dodaci · Dodaj novi · Otpremi dodatak, izaberite preuzeti zip i aktivirajte dodatak.
- U Otkucaju (Podešavanja · API ključevi) generišite ključ i odmah ga iskopirajte: prikazuje se samo jednom.
- Otvorite Podešavanja · Otkucaj fiskalizacija u WordPress-u i unesite: adresu (
https://otkucaj.com), API ključ, status porudžbine koji okida izdavanje računa (onaj u kom, kako mi razumemo čl. 6 st. 1 Zakona o fiskalizaciji, nastaje promet ili prijem avansa; odredite ga sa knjigovođom) i mapiranje WooCommerce načina plaćanja na fiskalne (npr. kartice naCard, pouzeće naCash, virman naWireTransfer). - Gotovo. Kada porudžbina pređe u izabrani status, dodatak izdaje račun, tačno jednom po porudžbini, i u samu porudžbinu upisuje PFR broj računa i link za proveru, vidljive i vama i kupcu. Račun kupcu i dalje treba dostaviti, na primer e-poštom (POST /invoices/{uuid}/email).
Poreska oznaka po proizvodu: dodatku se za pojedinačni proizvod može zadati meta polje otkucaj_label (vrednost iz GET /tax-labels); proizvodi bez njega koriste podrazumevanu oznaku iz podešavanja dodatka.
Primer: čist PHP
function otkucaj_izdaj_racun(string $apiKey, array $body): array
{
$ch = curl_init('https://otkucaj.com/api/v1/invoices');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json; charset=utf-8',
],
CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE),
]);
$raw = curl_exec($ch);
$err = curl_error($ch);
$http = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($raw === false) {
throw new RuntimeException("Мрежна грешка: $err"); // рачун можда јесте издат, проверити пре понављања
}
$json = json_decode($raw, true);
if ($http === 201) {
return $json; // цео рачун: uuid, pfr, journal...
}
$code = $json['error']['code'] ?? 'unknown';
$msg = $json['error']['message'] ?? $raw;
if ($http === 422) {
throw new InvalidArgumentException("Неисправан захтев ($code): $msg"); // не понављати
}
throw new RuntimeException("Откуцај HTTP $http ($code): $msg"); // 429/409: поновити касније
}
$racun = otkucaj_izdaj_racun($apiKey, [
'invoiceType' => 'Normal',
'transactionType' => 'Sale',
'cashier' => 'Веб продавница',
'payments' => [['paymentType' => 'Card', 'amount' => 2400.00]],
'items' => [['name' => 'Мајица', 'quantity' => 2, 'unitPrice' => 1200.00, 'label' => 'Ђ']],
]);
// сачувати уз поруџбину:
// $racun['uuid'], $racun['pfr']['invoiceNumber'], $racun['pfr']['verificationUrl']
Primer: Node.js
async function izdajRacun(apiKey, body) {
const res = await fetch('https://otkucaj.com/api/v1/invoices', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json; charset=utf-8',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000),
});
const json = await res.json();
if (res.status === 201) return json;
const { code, message } = json.error ?? {};
const e = new Error(`Откуцај HTTP ${res.status} (${code}): ${message}`);
e.retryable = res.status === 429 || res.status === 409; // 422 се никад не понавља
throw e;
}
const racun = await izdajRacun(process.env.OTKUCAJ_KEY, {
invoiceType: 'Normal',
transactionType: 'Sale',
cashier: 'Веб продавница',
payments: [{ paymentType: 'Card', amount: 2400.00 }],
items: [{ name: 'Мајица', quantity: 2, unitPrice: 1200.00, label: 'Ђ' }],
});
console.log(racun.pfr.invoiceNumber, racun.pfr.verificationUrl);
Primer: Python
import requests
def izdaj_racun(api_key: str, body: dict) -> dict:
r = requests.post(
'https://otkucaj.com/api/v1/invoices',
json=body,
headers={'Authorization': f'Bearer {api_key}'},
timeout=30,
)
if r.status_code == 201:
return r.json()
err = r.json().get('error', {})
if r.status_code == 422:
raise ValueError(f"Неисправан захтев: {err.get('message')}") # не понављати
raise RuntimeError(f"Откуцај HTTP {r.status_code} ({err.get('code')}): {err.get('message')}")
racun = izdaj_racun(API_KEY, {
'invoiceType': 'Normal',
'transactionType': 'Sale',
'cashier': 'Веб продавница',
'payments': [{'paymentType': 'Card', 'amount': 2400.00}],
'items': [{'name': 'Мајица', 'quantity': 2, 'unitPrice': 1200.00, 'label': 'Ђ'}],
})
print(racun['pfr']['invoiceNumber'], racun['pfr']['verificationUrl'])
Dobra praksa
- Izdajte račun u trenutku prometa ili prijema avansa. Kako mi razumemo čl. 6 st. 1 Zakona o fiskalizaciji, račun se izdaje kada se roba isporuči, odnosno usluga pruži, ili kada se primi uplata unapred (avansni račun), bez obzira na to kada i kako se plaća. Naplata karticom pri porudžbini, pre otpreme, je avans.
- Čuvajte
uuidipfr.invoiceNumberuz porudžbinu. Bez njih ne možete kasnije da izdate kopiju, refundaciju, da pošaljete račun e-poštom niti da preuzmete žurnal. - Čitajte šifarnike, ne prepisujte ih. Poreske oznake uzmite sa GET /tax-labels pri podešavanju integracije, jer se spisak razlikuje od instalacije do instalacije.
- Postavite vremensko ograničenje od 30 sekundi, da poziv ka API-ju ne blokira vašu aplikaciju unedogled.
- Nikada ne pozivajte API iz pregledača kupca ni iz mobilne aplikacije: ključ bi bio javan. Sav saobraćaj ide sa vašeg servera.
- Ne šaljite duple zahteve. Uz svaku porudžbinu čuvajte svoj idempotentni ključ (npr. broj porudžbine) i pre slanja proverite da li je za nju račun već izdat. Mrežni prekid posle uspešnog izdavanja je najčešći uzrok duplog računa: pre ponavljanja proverite GET /invoices za taj dan.
- Razvijte i testirajte u demo režimu. Prelazak na L-PFR ili V-PFR je izmena podešavanja u Otkucaju, bez ijedne izmene u vašem kodu.
Promene
| Verzija | Novine |
|---|---|
| 1.3.6 | Račun kao dokument i na listu A5 (fiskalni isečak, oblik za kurira uz pošiljku) i kao PDF iscrtan na serveru: GET /invoices/{uuid}/document?paper=a5&format=pdf. Novo POST /invoices/{uuid}/close-advance zatvara avans jednim pozivom (Avans Refundacija + konačni Promet Prodaja sa referencom i obaveznim reklamnim poljem), uz bezbedno ponavljanje. GET /status prijavljuje documents. Dodatak za WooCommerce 1.1.0: avansni račun pri naplati karticom, konačni pri otpremi, javni PDF link za kurira, refundacije iz panela. Novo prefetchDocument uz izdavanje: list se iscrta čim je odgovor otišao, za partnera koji ga preuzima dok pakuje. Dodatak 1.2.0: lokalna kopija preuzetog PDF-a i slanje pošte tek pošto je dokument otišao. |
| 1.3.5 | Cela lista za samoprocenu (Tehničko uputstvo §7.4.2, odeljci 9 do 16) proverena je stavku po stavku. Najvažnije: demo simulator više ne štampa dokument sa naslovom „FISKALNI RAČUN“, nego nosi poruku da nije fiskalni račun, a šifra avansnog artikla se izvodi iz stope, ne iz slova oznake. Dodati su: podrazumevani reklamni tekst, prikaz PFR rukovanja na Kasi, pregled poreskih stopa za kasira, GTIN na računu i prelamanje dugačkih naziva artikala. Uputstvo je dobilo poglavlja 6.1, 18.2.1, 18.6.1, 25 i 26. |
| 1.3.4 | Račun se sada može dati kupcu i kao dokument formata A4 (dugme „Dokument“ na stranici računa), a poruka e-pošte je prerađena u isti taj oblik, sa QR kodom u samoj poruci. Fiskalni isečak ostaje u oba oblika, znak po znak, onako kako ga je PFR potpisao. Ispravljene i tri greške: dugačak naziv firme je izlazio iz okvira računa (PFR ne prelama redove, a termalni štampač prelama), štampa na rolni nije radila jer je pravilo @page bilo neispravno, i html deo poruke e-pošte je stizao sa pokvarenim ćiriličnim znacima. |
| 1.3.3 | Prva verzija koja je zaista govorila sa pravim PFR-om. Uz testni bezbednosni element Poreske uprave i V-PFR test sistem izašle su tri greške koje se u demo režimu nisu mogle videti: zaglavlje RequestId mora biti broj (sa GUID-om je svaki zahtev vraćao 400), žurnal stiže sa CRLF prelomom reda pa se obavezna poruka „OVO NIJE FISKALNI RAČUN“ na Kopiji, Predračunu i Obuci štampala normalnom veličinom, a pravi PFR ne štampa red „Povraćaj“ pa ga sada ESIR dodaje ispod fiskalnog bloka. Poreske stope se uzimaju iz currentTaxRates, a poruka o grešci PFR-a sada se čita i kada stigne kao običan tekst ili kao modelState. |
| 1.3.2 | Plaćanje sada prima stvarno predati iznos. Zbir plaćanja sme biti veći od iznosa računa: razlika je povraćaj, prikazuje se na Kasi, štampa se u redu „Povraćaj“ i vraća se u odgovoru kao change. Manji iznos od ukupnog se i dalje odbija, a kod refundacije povraćaja nema. Izveštaj po načinima plaćanja odbija povraćaj od gotovine, da predati višak ne bi uvećao promet. Račun poslat e-poštom sada nosi i HTML verziju, u kojoj je adresa za proveru pravi hiperlink, a poruka „OVO NIJE FISKALNI RAČUN“ zadržava dvostruku veličinu. |
| 1.3.1 | Novo polje buyerCostCenterId (Opciono polje kupca) na Kasi i u odgovoru API-ja; kopija računa ga preuzima sa originala. Registar računa je dobio pretragu određenog računa po PFR broju, brojaču, referentnom broju, kupcu, iznosu i artiklu, kroz ceo žurnal. Poruka „OVO NIJE FISKALNI RAČUN“ se prikazuje u dvostrukoj veličini i kada je PFR ispiše unutar naslovne linije. Žurnal računa je uvek na ćirilici, bez obzira na jezik interfejsa. Podešavanja prikazuju serijski broj instalacije. |
| 1.3 | Odvojena okruženja: test i produkcija, svako sa svojim bezbednosnim elementom, PAK-om i ESIR brojem. Račun nosi oznaku okruženja, a liste i izveštaji prikazuju samo aktivno. Nova polja: environment i fiscal u GET /status i u svakom računu, verificationQRCode (QR koji je nacrtao sam PFR), clientRequestId. Idempotentnost: zaglavlje Idempotency-Key ili polje clientRequestId sprečava dvostruko izdavanje posle prekida veze. CORS je uključen, pa se API može zvati i iz pregledača. buyerId se proverava po obliku ознака:вредност, najviše 20 znakova. |
| 1.2 | Proširen API: GET /me, GET /tax-labels, GET /categories, POST /invoices/{uuid}/refund, POST /invoices/{uuid}/copy, POST /invoices/{uuid}/email, GET /reports/summary i GET /reports/daily. U samoj aplikaciji: registracija naloga sa odobravanjem, Cloudflare Turnstile zaštita prijave, Telegram obaveštenja o izdatim računima, nova kontrolna tabla, kolapsibilni sajdbar, kategorije artikala, popusti po stavci i na ceo račun, konverzija predračuna u fiskalni račun, automatsko zatvaranje avansa, slanje računa e-poštom. |
| 1.1 | Oflajn red na kasi (račun čeka u lokalnom redu i izdaje se po povratku veze), podeljena plaćanja na jednom računu, WooCommerce dodatak. |
| 1.0 | Početna verzija: izdavanje računa, lista i detalj računa, žurnal, artikli, API ključevi. |
Ugovor API-ja v1 je stabilan kroz sve navedene verzije aplikacije: dodavane su nove mogućnosti, postojeća polja se nisu menjala.
Pitanja i pomoć pri integraciji: obrazac za kontakt. Uputstvo za rad u samoj aplikaciji: Uputstvo (PDF).