Откуцај API v1
REST АПИ за издавање фискалних рачуна, рефундације и копије, слање рачуна е-поштом, извештаје о промету и рад са артиклима. Све што ради каса у прегледачу може и ваш софтвер: веб продавница, ERP, наплатни систем.
За АИ агенте и језичке моделе
Машински читљиви описи овог АПИ-ја постоје и одржавају се уз ову страницу.
Увод и базна адреса
Базна адреса свих позива:
https://otkucaj.com/api/v1
Сви захтеви и одговори су JSON у UTF-8 кодирању, осим журнала који се враћа као чист текст (text/plain). Комуникација иде искључиво преко HTTPS-а. Износи у JSON-у користе тачку као децимални знак (2400.00), јер је то JSON стандард; на самом рачуну и у журналу износи су у српском формату (2.400,00).
Версионисање. Верзија АПИ-ја је део путање: /api/v1. Постојећа поља и понашање се унутар v1 не мењају уназад некомпатибилно; нове могућности се додају као нова, опциона поља. Ако икада буде потребан прекид уговора, он ће живети на путањи /api/v2, а v1 ће наставити да ради.
Завршна коса црта се игнорише: /api/v1/items/ и /api/v1/items су иста путања. Непозната путања враћа 404 са кодом not_found.
Брзи почетак
Од кључа до првог рачуна у три корака.
-
Проверите везу и стање ПФР-а.
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/status
-
Издајте први рачун.
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": "Ђ" } ] }'Одговор је
201са целим рачуном: запамтитеuuidиpfr.invoiceNumber. Пун облик одговора је код POST /invoices. -
Преузмите журнал (текст рачуна спреман за штампу на 40 колона):
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/journal
/api/v1/status
Стање апликације и везе са ПФР-ом. Употребите за проверу конфигурације и за надзор.
Захтев
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/status
Одговор
{
"app": "Otkucaj",
"version": "1.2.0",
"esirNumber": "у поступку одобравања/1.0",
"pfr": {
"ok": true,
"mode": "demo",
"uid": "DMX7K2QP",
"message": "Демо ПФР симулатор: активан"
},
"time": "2026-08-19T14:00:00.000+02:00"
}
pfr.mode је demo, lpfr или vpfr, у зависности од подешавања инсталације. Када је режим Л-ПФР или В-ПФР, објекат pfr преноси и податке које врати сам процесор фискалних рачуна; ok: false са поруком значи да ПФР тренутно није доступан. esirNumber се склапа од броја и верзије ЕСИР-а из Подешавања.
- 401 unauthorized
- 429 rate_limited
Аутентификација
Сваки захтев мора да носи заглавље:
Authorization: Bearer <api-kljuc>
Кључеве генерише администратор у апликацији, у одељку Подешавања · АПИ кључеви. Кључ се приказује само једном, у тренутку генерисања: у бази се чува искључиво његов SHA-256 отисак, па изгубљен кључ није могуће поново прочитати, већ се опозива и генерише нов. Опозив је тренутан: сваки следећи захтев са опозваним кључем добија 401.
Кључеви нису појединачно ограничени по правима: сваки активан кључ може да позове сваку путању. Ако желите да раздвојите системе, генеришите засебан кључ по систему и опозовите га појединачно кад затреба; то је ниво изолације који апликација заиста нуди.
/api/v1/me
Подаци о кључу којим је захтев потписан: назив, сачувани префикс, датуми, ограничење броја захтева које се стварно примењује и шта кључ сме. Корисно за проверу да интеграција користи онај кључ који мислите да користи.
Захтев
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/me
Одговор
{
"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 је онај кратки низ који видите и у Подешавањима; сам кључ се нигде не чува у читљивом облику и никада се не враћа.
scope је увек "full", а permissions набраја шта сваки активан кључ стварно може. То није по кључу подесиво: систем права по кључу не постоји, па га овај одговор ни не глуми.
lastUsedAt је претходни позив, не овај: време текућег позива се уписује тек пошто је ред кључа прочитан.
- 401 unauthorized
- 429 rate_limited
Ограничења и понашање
| Ограничење | Вредност | Шта се дешава при прекорачењу |
|---|---|---|
| Број захтева по кључу | 240 у 60 секунди | 429 rate_limited |
| Слање рачуна е-поштом по кључу | 30 порука на сат | 429 rate_limited, посебна порука |
| Ставки по рачуну | 1 до 500 | 422 validation |
Рачуна по страни (GET /invoices) | 100 | следећа страна кроз page |
Артикала по позиву (GET /items) | 1.000 | вишак се не враћа, страничења нема |
| Дужина назива ставке и артикла | 200 знакова | 422 validation |
Дужина buyerId | 64 знака | 422 validation |
Дужина adText | 500 знакова | 422 validation |
Прозор ограничења је фиксан, не клизни: први захтев отвара прозор од 60 секунди и броји до 240; кад прозор истекне, бројач креће испочетка. Прекорачење враћа HTTP 429:
{ "error": { "code": "rate_limited", "message": "Rate limit exceeded (240 requests/minute)." } }
Када добијете 429, сачекајте до истека текућег прозора па наставите. За уобичајен рад продавнице (један рачун по поруџбини) ово ограничење се у пракси не досеже. Ограничење које се примењује и оно које пријављује GET /me долазе из истог места у коду, па не могу да се разиђу.
Рачуни
Седам путања око рачуна: издавање, листа, детаљ, журнал, рефундација, копија и слање е-поштом.
/api/v1/invoices
Издавање рачуна. Захтев се валидира, прослеђује ПФР-у, а рачун се уписује у базу тек када га ПФР потпише. Одговор 201 враћа цео рачун, укључујући журнал и линк за проверу.
| Поље | Тип | Обавезно | Опис |
|---|---|---|---|
invoiceType | string | не (подразумевано Normal) | Normal (промет), Advance (аванс), ProForma (предрачун), Training (обука), Copy (копија) |
transactionType | string | не (подразумевано Sale) | Sale (продаја) или Refund (рефундација) |
items[] | array | да | 1 до 500 ставки, збир мора бити већи од 0 |
items[].name | string | да | Назив ставке, до 200 знакова |
items[].unit | string | не | Јединица мере, подразумевано ком |
items[].quantity | number | да | Већа од 0 и највише 999.999; заокружује се на 3 децимале (нпр. 0.375) |
items[].unitPrice | number | да | Јединична цена са ПДВ, 0 или више, заокружује се на 2 децимале |
items[].totalAmount | number | не | Износ реда. Сервер га сам израчунава као количина × цена, послата вредност се не користи |
items[].label | string | да | Пореска ознака из шифарника у важности, види GET /tax-labels. Стопе одређује искључиво ПФР |
items[].gtin | string | не | ГТИН или бар-код, 8 до 14 цифара |
items[].itemId | number | не | ИД артикла из шифарника артикала. Само се памти уз ред рачуна као веза ка артиклу; цена и назив се увек узимају из самог захтева |
payments[] | array | не | Начини плаћања. Ако изостане или је празан, цео износ иде као Cash |
payments[].paymentType | string | да | Cash (готовина), Card (платна картица), Check (чек), WireTransfer (пренос на рачун), Voucher (ваучер), MobileMoney (инстант плаћање), Other (друго безготовинско) |
payments[].amount | number | да | Износ. Ставке са износом 0 или мањим се одбацују. Ако после тога остане тачно једно плаћање, његов износ се сам поставља на укупан износ рачуна; ако их је више, збир мора да одговара збиру ставки (толеранција 0,01) |
cashier | string | не | Име касира на рачуну; за АПИ позиве подразумевано API |
buyerId | string | условно | ИД купца, до 64 знака, облик шифра:вредност. Шифарник: 10 ПИБ, 11 ЈМБГ, 12 ПИБ:ЈБКЈС, 20 лична карта, 23 пасош, 30 страни пасош, 40 страни ТИН. Обавезан за сваку рефундацију |
buyerCostCenterId | string | не | Опционо поље купца (место трошка) |
referentDocumentNumber | string | условно | ПФР број оригиналног рачуна. Обавезан за Refund и за Copy |
referentDocumentDT | string | не | Време оригиналног рачуна, YYYY-MM-DD HH:MM:SS |
dateAndTimeOfIssue | string | не | ЕСИР време, YYYY-MM-DD HH:MM:SS. Користи се за аванс када је уплата стигла раније него што се рачун издаје (нпр. пренос на рачун прокњижен сутрадан); штампа се на рачуну као „ЕСИР време“ |
adText | string | не | Рекламни текст, до 500 знакова, штампа се испод краја рачуна. При затварању аванса овде иде број и датум последњег авансног рачуна |
Захтев
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" }
]
}'
Одговор 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"
}
Овај облик враћају и GET /invoices/{uuid}, сваки елемент листе рачуна, и путање за рефундацију и копију. Значење ПФР поља: invoiceNumber је ПФР број рачуна (облик JID-JID-N), invoiceCounter је бројач (нпр. 143/302ПП: 143. рачун врсте Промет-Продаја од укупно 302), sdcDateTime је ПФР време, verificationUrl је линк ка страници за проверу рачуна, demo каже да ли је рачун издат у демо режиму. taxItems је пореска рекапитулација по ознакама, са стопом, износом пореза и основицом; стопе долазе искључиво од ПФР-а, никада из вашег захтева.
- 400 bad_request
- 401 unauthorized
- 422 validation
- 429 rate_limited
- 502 pfr_error
На 502 рачун није издат и ништа није уписано у базу.
/api/v1/invoices
Листа рачуна, најновији први, до 100 по страни. Сваки елемент је цео рачун, у истом облику као одговор на POST /invoices, заједно са журналом.
| Параметар | Тип | Опис |
|---|---|---|
from | string | Датум од, YYYY-MM-DD (укључује цео дан од 00:00:00) |
to | string | Датум до, YYYY-MM-DD (укључује цео дан до 23:59:59) |
type | string | Врста рачуна: Normal, Advance, ProForma, Training, Copy |
page | number | Страна, од 1 (подразумевано 1), по 100 рачуна |
from и to се овде не проверавају форматом: неисправан датум неће вратити 422, него празну или неочекивану листу. Непозната вредност у type се тихо игнорише и филтер се не примењује. Шаљите датуме у облику YYYY-MM-DD и врсту из горњег списка.Захтев
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"
Одговор
{
"page": 1,
"perPage": 100,
"invoices": [
{ "uuid": "9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30", "invoiceType": "Normal", "...": "..." }
]
}
Празна листа значи да за задате филтере нема рачуна. Укупан број рачуна се не враћа: страничите док не добијете мање од 100 елемената.
- 401 unauthorized
- 429 rate_limited
/api/v1/invoices/{uuid}
Један рачун по UUID-у, у пуном облику описаном код POST /invoices.
Захтев
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30
Одговор
{
"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 који не постоји враћа 404 са кодом not_found. UUID исписан великим словима не одговара облику путање и завршава као непозната путања, такође 404: шаљите га тачно онако како вам га је АПИ вратио.
- 401 unauthorized
- 404 not_found
- 429 rate_limited
/api/v1/invoices/{uuid}/journal
Журнал рачуна као чист текст (text/plain; charset=utf-8), 40 колона, ћирилицом, спреман за термални штампач од 58 или 80 mm. Идентичан журналу који апликација сама штампа. Ово је једини одговор који није JSON.
Захтев
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/journal
Одговор, скраћено
======== ФИСКАЛНИ РАЧУН =======
106884584
Пример д.о.о.
Продавница 1
Булевар ослобођења 1
Београд
Касир: Веб продавница
ЕСИР број: у поступку одобравања/1.0
------ ПРОМЕТ - ПРОДАЈА -------
Артикли
========================================
Назив Цена Кол. Укупно
Мајица /ком (Ђ)
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ПП
======== КРАЈ ФИСКАЛНОГ РАЧУНА ========
Нефискалне врсте (Копија, Предрачун, Обука) уместо заглавља и краја фискалног рачуна носе линију „ОВО НИЈЕ ФИСКАЛНИ РАЧУН“, коју апликација при штампи увећава на двоструку величину фонта. Копија-Рефундација додатно штампа простор „Потпис купца“ за потпис при враћању новца.
- 401 unauthorized
- 404 not_found
- 429 rate_limited
/api/v1/invoices/{uuid}/refund
Рефундација целог постојећег рачуна, једним позивом. Ставке се преузимају са оригинала, референтни број и време показују на оригинал, а нов рачун се издаје кроз исти пут као и сваки други. Ово је исто што ради дугме Рефундација у апликацији.
Normal или Advance, врсте трансакције Sale и у статусу издат. Копија, предрачун, обука и већ издата рефундација се одбијају са 422. Ово су исти услови под којима се дугме приказује у апликацији.| Поље тела | Тип | Подразумевано | Опис |
|---|---|---|---|
buyerId | string | купац са оригинала | Идентификација купца. Обавезна је за рефундацију: ако је нема ни у телу ни на оригиналу, одговор је 422 са поруком која објашњава да за поништавање сопственог рачуна треба унети свој ПИБ |
buyerCostCenterId | string | са оригинала | Место трошка купца |
payments | array | плаћања са оригинала | Начини враћања новца. Збир мора да одговара укупном износу рачуна |
cashier | string | API | Име касира на рачуну рефундације |
adText | string | празно | Рекламни текст, до 500 знакова |
Ставке се увек узимају са оригиналног рачуна, у целости, и не могу се задати телом захтева. За делимичну рефундацију (враћа се само део ставки или мања количина) користите POST /invoices са transactionType: "Refund", где сами наводите шта се враћа.
Захтев
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": "Веб продавница" }'
Одговор 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
- 502 pfr_error
/api/v1/invoices/{uuid}/copy
Издавање копије постојећег рачуна, кроз исти пут који користи дугме Копија у апликацији. Копија носи све ставке и плаћања оригинала, референтни број и време оригинала, и његову идентификацију купца.
422, исто као у апликацији.Захтев
curl -X POST \ https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/copy \ -H "Authorization: Bearer kasir_vas_kljuc"
Одговор 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"
}
Копија није фискални рачун: у журналу нема заглавља фискалног рачуна, него линија „ОВО НИЈЕ ФИСКАЛНИ РАЧУН“. Копија рефундације додатно носи простор за потпис купца.
- 401 unauthorized
- 404 not_found
- 422 validation
- 429 rate_limited
- 502 pfr_error
/api/v1/invoices/{uuid}/email
Слање рачуна купцу е-поштом. Порука садржи журнал и линк за проверу рачуна, и потпуно је иста као она коју шаље дугме у апликацији, јер обе иду кроз исти код. Слање се уписује у ревизиони траг као invoice.email.
| Поље тела | Тип | Обавезно | Опис |
|---|---|---|---|
to | string | да | Адреса примаоца, исправна е-адреса, највише 190 знакова |
Захтев
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]" }'
Одговор
{
"ok": true,
"uuid": "9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30",
"to": "[email protected]"
}
Ограничење слања је 30 порука на сат по кључу, независно од општег ограничења од 240 захтева у минуту. Прекорачење враћа 429 са поруком о ограничењу поште. Ако сервер поште одбије поруку, одговор је 502 са кодом mail_error: то није ни грешка ПФР-а ни грешка у захтеву, него неуспело слање, и захтев се сме поновити касније.
- 400 bad_request
- 401 unauthorized
- 404 not_found
- 422 validation
- 429 rate_limited
- 502 mail_error
Токови: продаја, рефундација, копија, предрачун
Сваки законом прописани ток, са пуним телом захтева. Одговор је увек исти облик рачуна као код POST /invoices.
Промет-продаја, са подељеним плаћањем
Најчешћи рачун. На једном рачуну сме више начина плаћања; збир мора одговарати збиру ставки.
{
"invoiceType": "Normal",
"transactionType": "Sale",
"cashier": "Веб продавница",
"payments": [
{ "paymentType": "Cash", "amount": 1000.00 },
{ "paymentType": "Card", "amount": 1400.00 }
],
"items": [
{ "name": "Мајица", "quantity": 2, "unitPrice": 1200.00, "label": "Ђ" }
]
}
Рефундација
Ако враћате цео рачун, најједноставније је позвати POST /invoices/{uuid}/refund. Ручна рефундација кроз POST /invoices мора да носи referentDocumentNumber (ПФР број оригиналног рачуна) и buyerId (идентификацију купца). Без било ког од та два поља захтев се одбија са 422. Ставке и износи су они који се враћају, дакле могу бити и део оригиналног рачуна.
{
"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": "Ђ" }
]
}
Рефундације умањују промет у извештајима. Купцу који прима готовину издаје се и Копија-Рефундација са простором за потпис.
Поништавање сопственог рачуна
Погрешно издат рачун се поништава рефундацијом целог износа, при чему продавац као идентификацију купца уноси свој ПИБ, у облику 10:СВОЈПИБ.
{
"invoiceType": "Normal",
"transactionType": "Refund",
"buyerId": "10:106884584",
"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": "Ђ" }]
}
Копија
Копија постојећег рачуна се најлакше издаје кроз POST /invoices/{uuid}/copy. Ручно, кроз POST /invoices, копија мора да носи referentDocumentNumber, иначе 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": "Ђ" }]
}
Предрачун, па фискални рачун
Предрачун (ProForma) није фискални рачун: служи као понуда. Када купац плати, издаје се прави Промет-Продаја са референцом на предрачун.
Корак 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": "Ђ" }]
}
У самој апликацији дугме „Издај фискални рачун“ на предрачуну ради тачно ово: препуни касу и сама постави референцу.
Авансни ток, од прве уплате до коначног рачуна
Авансна ставка носи прописан назив по пореској ознаци: 10: Аванс (Ђ), 11: Аванс (Е), 12: Аванс (Г) или 13: Аванс (А). Количина је увек 1, а цена је износ примљеног аванса. Ток има три врсте докумената: авансни рачун (АП) за сваку уплату, аванс-рефундација (АР) која затвара збир свих аванса, и коначни Промет-Продаја.
-
Први авансни рачун (АП). Уплата од 5.000,00 стигла преносом на рачун 18.08, а књижи се 19.08: зато
dateAndTimeOfIssueноси стварно време уплате (штампа се као „ЕСИР време“).{ "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": "Ђ" }] }ПФР врати нпр.
invoiceNumber: "DMX7K2QP-DMX7K2QP-311". -
Додатни авансни рачуни за сваку следећу уплату, сваки са референцом на претходни АП:
{ "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": "Ђ" }] }ПФР врати нпр.
invoiceNumber: "DMX7K2QP-DMX7K2QP-312". -
Затварање: аванс-рефундација (АР) на цео примљени износ, са референцом на последњи АП и обавезним
buyerId, јер је реч о рефундацији:{ "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": "Ђ" }] }ПФР врати нпр.
invoiceNumber: "DMX7K2QP-DMX7K2QP-313". -
Коначни Промет-Продаја са стварним ставкама испоруке, референцом на АР и рекламним текстом који носи број и датум последњег авансног рачуна:
{ "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": "Ђ" }] }
У апликацији дугме „Затвори аванс“ на авансном рачуну само одради кораке 3 и 4: издаје АР на цео износ и препуни касу за коначни рачун са постављеном референцом и рекламним текстом.
Режим по чл. 6 ст. 2 Правилника
Инсталација може да ради у режиму у ком су начини плаћања Card, Check и MobileMoney искључени, а такве наплате се евидентирају као готовина. Једна инсталација ради у тачно једном режиму, који бира администратор у Подешавањима. Када је режим укључен, захтев са неким од искључених начина враћа:
422 { "error": { "code": "validation",
"message": "Начин плаћања „Card“ је искључен подешавањем (режим по чл. 6 ст. 2 Правилника). Унесите као готовину." } }
Интеграција тада шаље {"paymentType": "Cash"} за те наплате.
Артикли
Шифарник артикала који каса нуди на додир. Артикли нису услов за издавање рачуна: POST /invoices прима ставке слободно, са називом и ценом из вашег система.
/api/v1/items
Листа артикала, сортирано по називу, до 1.000 комада. Поље active разликује активне од деактивираних; деактивирани су такође у одговору.
Захтев
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/items
Одговор
{
"items": [
{
"id": 12,
"plu": "1001",
"name": "Мајица",
"unit": "ком",
"price": 1200.0,
"label": "Ђ",
"gtin": "8600000000000",
"active": true
}
]
}
- 401 unauthorized
- 429 rate_limited
/api/v1/items
Додавање артикла у шифарник.
| Поље | Тип | Обавезно | Опис |
|---|---|---|---|
name | string | да | Назив, до 200 знакова |
label | string | да | Пореска ознака из шифарника у важности (GET /tax-labels). Непозната ознака враћа 422 са списком дозвољених |
price | number | не (подразумевано 0) | Цена са ПДВ, 0 или више, заокружује се на 2 децимале |
plu | string | не | Шифра артикла за брзо куцање |
unit | string | не | Јединица мере, подразумевано ком |
gtin | string | не | ГТИН или бар-код, 8 до 14 цифара |
Нов артикал се увек уписује као активан. Поље category се овим позивом не поставља.
Захтев
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": "ком" }'
Одговор 201
{ "id": 12, "ok": true }
- 400 bad_request
- 401 unauthorized
- 422 validation
- 429 rate_limited
/api/v1/items/{id}
Делимична измена артикла: шаљете само поља која мењате, остала остају нетакнута. Оба метода, PUT и PATCH, раде исто.
label уписује онаква каква је послата, без провере да ли таква пореска ознака постоји, за разлику од додавања артикла где се проверава. Артикал са ознаком које нема у шифарнику ће при издавању рачуна произвести 422. Шаљите само ознаке које враћа GET /tax-labels.Захтев
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 }'
Одговор
{ "ok": true }
- 400 bad_request
- 401 unauthorized
- 404 not_found
- 429 rate_limited
/api/v1/items/{id}
Мека деактивација: артикал се не брише из базе, јер га постојећи рачуни и даље референцирају. Добија active: false и нестаје са касе. Поновно активирање је PUT са {"active": true}.
Захтев
curl -X DELETE https://otkucaj.com/api/v1/items/12 \ -H "Authorization: Bearer kasir_vas_kljuc"
Одговор
{ "ok": true }
{"ok": true}, јер се ништа не мења и ништа не пуца. Ако вам је важно да ли артикал постоји, проверите га претходно кроз GET /items.- 401 unauthorized
- 429 rate_limited
Извештаји
Оба извештаја рачунају промет по истом правилу и истим упитима као страница Извештаји у апликацији, па се бројке не могу разићи.
Normal и Advance у статусу издат. Рефундације се одузимају. Предрачун, копија и обука не улазе ни у један збир. Исто правило враћа и сам одговор, у пољу rule./api/v1/reports/summary
Промет за период, са разрадом по пореској ознаци, начину плаћања и касиру.
| Параметар | Тип | Подразумевано | Опис |
|---|---|---|---|
from | string | данас | Датум од, строго YYYY-MM-DD, укључује цео дан |
to | string | данас | Датум до, строго YYYY-MM-DD, укључује цео дан. Не сме бити ранији од from |
Неисправан облик датума или период у ком је from касније од to враћају 422, за разлику од листе рачуна где се датуми не проверавају.
Захтев
curl -H "Authorization: Bearer kasir_vas_kljuc" \ "https://otkucaj.com/api/v1/reports/summary?from=2026-08-01&to=2026-08-19"
Одговор
{
"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."
}
Значење поља: totals.invoices је број рачуна у периоду, укључујући и рефундације; totals.turnover је промет умањен за рефундације; totals.refunds посебно приказује број и укупан износ рефундација, као позитиван број. У byTaxLabel су name и rate вредности null ако ознака више не постоји у шифарнику, а рачуни са њом су остали у бази. Начини плаћања се сабирају из самог рачуна, па за подељена плаћања сваки део улази у свој ред.
- 401 unauthorized
- 422 validation
- 429 rate_limited
/api/v1/reports/daily
Промет и број рачуна по данима, за исти период и по истом правилу као сабирни извештај.
Захтев
curl -H "Authorization: Bearer kasir_vas_kljuc" \ "https://otkucaj.com/api/v1/reports/daily?from=2026-08-01&to=2026-08-05"
Одговор
{
"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."
}
Дани без промета се изостављају, тачно као у табели у апликацији. Ако вам треба непрекидан низ датума, попуните празнине на својој страни.
- 401 unauthorized
- 422 validation
- 429 rate_limited
Шифарници
Две листе које интеграцији требају да би слала исправне вредности, обе из истог извора који апликација и сама користи.
/api/v1/tax-labels
Пореске ознаке и стопе које су тренутно на снази, из истог извора који користи и валидатор рачуна (само активни редови). Ознака коју врати овај позив је ознака коју ће POST /invoices прихватити.
Захтев
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/tax-labels
Одговор
{
"taxLabels": [
{ "label": "Ђ", "name": "О-ПДВ", "rate": 20.0 },
{ "label": "Е", "name": "П-ПДВ", "rate": 10.0 },
{ "label": "Г", "name": "Без ПДВ", "rate": 0.0 }
]
}
POST /invoices подразумевано не прихвата. Ознаке и стопе се мењају у Подешавањима, а у режиму Л-ПФР или В-ПФР меродаван је списак који даје сам ПФР. Зато читајте овај позив, а не преписујте ознаке у свој код.- 401 unauthorized
- 429 rate_limited
/api/v1/categories
Категорије активних артикала, без празних вредности, сортиране азбучно. Исти упит који каса користи за дугмад категорија.
Захтев
curl -H "Authorization: Bearer kasir_vas_kljuc" \ https://otkucaj.com/api/v1/categories
Одговор
{ "categories": ["Пиће", "Слаткиши", "Храна"] }
Категорије се уносе уз артикал у апликацији. Артикли без категорије не праве празан унос у овој листи.
- 401 unauthorized
- 429 rate_limited
Грешке
Свака грешка има исти JSON облик, без обзира на путању:
{ "error": { "code": "validation", "message": "Рефундација мора имати референтни број оригиналног рачуна (referentDocumentNumber)." } }
| HTTP | code | Узрок | Шта урадити |
|---|---|---|---|
| 400 | bad_request | Тело захтева није исправан JSON | Проверите серијализацију и Content-Type: application/json |
| 401 | unauthorized | Нема Authorization заглавља, кључ је погрешан или опозван | Проверите кључ; по потреби генеришите нов у Подешавањима |
| 404 | not_found | Непозната путања, непостојећи UUID рачуна или ИД артикла при измени | Проверите путању и идентификатор |
| 422 | validation | Захтев не пролази валидацију; порука прецизно каже које поље и зашто | Исправите захтев. Не понављајте исти захтев непромењен: резултат ће увек бити исти |
| 429 | rate_limited | Више од 240 захтева у минуту по кључу, или више од 30 послатих порука на сат | Сачекајте до истека прозора, па наставите смањеним темпом |
| 502 | pfr_error | ПФР је одбио захтев или није доступан | Рачун НИЈЕ издат, ништа није уписано. Проверите GET /status, па поновите исти захтев |
| 502 | mail_error | Порука са рачуном није могла да буде послата | Рачун постоји и није дирнут. Слање се сме поновити касније |
WooCommerce и интеграције
Готов додатак повезује WooCommerce продавницу без иједне линије кода: otkucaj-fiskalizacija.zip.
- У WordPress администрацији отворите Додаци · Додај нови · Отпреми додатак, изаберите преузети zip и активирајте додатак.
- У Откуцају (Подешавања · АПИ кључеви) генеришите кључ и одмах га ископирајте: приказује се само једном.
- Отворите Подешавања · Откуцај фискализација у WordPress-у и унесите: адресу (
https://otkucaj.com), АПИ кључ, статус поруџбине који окида издавање рачуна (нпр. „Завршено“ или „У обради“) и мапирање WooCommerce начина плаћања на фискалне (нпр. картице наCard, поузеће наCash, вирман наWireTransfer). - Готово. Када поруџбина пређе у изабрани статус, додатак издаје рачун, тачно једном по поруџбини, и у саму поруџбину уписује ПФР број рачуна и линк за проверу, видљиве и вама и купцу.
Пореска ознака по производу: додатку се за појединачни производ може задати мета поље otkucaj_label (вредност из GET /tax-labels); производи без њега користе подразумевану ознаку из подешавања додатка.
Пример: чист 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/502: поновити касније
}
$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']
Пример: 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 === 502; // 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);
Пример: 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'])
Добра пракса
- Издајте рачун тек по наплати. Фискални рачун прати новац; поруџбина која није плаћена нема шта да фискализује.
- Чувајте
uuidиpfr.invoiceNumberуз поруџбину. Без њих не можете касније да издате копију, рефундацију, да пошаљете рачун е-поштом нити да преузмете журнал. - Читајте шифарнике, не преписујте их. Пореске ознаке узмите са GET /tax-labels при подешавању интеграције, јер се списак разликује од инсталације до инсталације.
- Поставите временско ограничење од 30 секунди, да позив ка АПИ-ју не блокира вашу апликацију унедоглед.
- Никада не позивајте АПИ из прегледача купца ни из мобилне апликације: кључ би био јаван. Сав саобраћај иде са вашег сервера.
- Не шаљите дупле захтеве. Уз сваку поруџбину чувајте свој идемпотентни кључ (нпр. број поруџбине) и пре слања проверите да ли је за њу рачун већ издат. Мрежни прекид после успешног издавања је најчешћи узрок дуплог рачуна: пре понављања проверите GET /invoices за тај дан.
- Развијте и тестирајте у демо режиму. Прелазак на Л-ПФР или В-ПФР је измена подешавања у Откуцају, без иједне измене у вашем коду.
Промене
| Верзија | Новине |
|---|---|
| 1.2 | Проширен АПИ: GET /me, GET /tax-labels, GET /categories, POST /invoices/{uuid}/refund, POST /invoices/{uuid}/copy, POST /invoices/{uuid}/email, GET /reports/summary и GET /reports/daily. У самој апликацији: регистрација налога са одобравањем, Cloudflare Turnstile заштита пријаве, Телеграм обавештења о издатим рачунима, нова контролна табла, колапсибилни сајдбар, категорије артикала, попусти по ставци и на цео рачун, конверзија предрачуна у фискални рачун, аутоматско затварање аванса, слање рачуна е-поштом. |
| 1.1 | Офлајн ред на каси (рачун чека у локалном реду и издаје се по повратку везе), подељена плаћања на једном рачуну, WooCommerce додатак. |
| 1.0 | Почетна верзија: издавање рачуна, листа и детаљ рачуна, журнал, артикли, АПИ кључеви. |
Уговор АПИ-ја v1 је стабилан кроз све наведене верзије апликације: додаване су нове могућности, постојећа поља се нису мењала.
Питања и помоћ при интеграцији: [email protected]. Упутство за рад у самој апликацији: Упутство (PDF).