Откуцај
SRENРУ Вход

Otkucaj API v1

REST API для выдачи фискальных чеков, возвратов и копий, отправки чека по электронной почте, отчётов об обороте и работы с товарами. Всё, что делает касса в браузере, может и ваша программа: интернет-магазин, ERP, система приёма платежей.

ДЕМО-РЕЖИМ JSON · UTF-8 240 запросов/мин

Всё, что этот документ говорит о нормах, является нашим пониманием, а не юридической или налоговой консультацией; прежде чем действовать, уточните у бухгалтера или в Налоговой администрации.

Для ИИ-агентов и языковых моделей

Машиночитаемые описания этого API существуют и поддерживаются вместе с этой страницей.

Введение и базовый адрес

Базовый адрес всех вызовов:

https://otkucaj.com/api/v1

Все запросы и ответы это JSON в кодировке UTF-8, кроме журнала, который возвращается чистым текстом (text/plain). Обмен идёт исключительно по HTTPS. Суммы в JSON используют точку как десятичный разделитель (2400.00), потому что таков стандарт JSON; на самом чеке и в журнале суммы записаны в сербском формате (2.400,00).

Версионирование. Версия API является частью пути: /api/v1. Существующие поля и поведение внутри v1 обратно несовместимо не меняются; новые возможности добавляются как новые необязательные поля. Если когда-нибудь понадобится разрыв контракта, он будет жить по пути /api/v2, а v1 продолжит работать.

Завершающая косая черта игнорируется: /api/v1/items/ и /api/v1/items: это один и тот же путь. Неизвестный путь возвращает 404 с кодом not_found.

Otkucaj это одобренный ЕСИР, классификация 3, в двух одобренных версиях: регистрационный номер 1651, версия 1.3.5 (одобрена 31.08.2026) и регистрационный номер 1667, версия 1.3.6 (одобрена 08.09.2026, чек как документ на A4 и A5). Новые аккаунты выпускают под 1667/1.3.6. Новый аккаунт всё же начинает работу в тестовой среде с демо-симулятором, где каждый чек несёт надпись ОВО НИЈЕ ФИСКАЛНИ РАЧУН, номера из демо-счётчика и ссылку для проверки на этом сайте, а не на suf.purs.gov.rs. Интеграцию можно разработать и протестировать уже сейчас: при переходе на Л-ПФР или В-ПФР в контракте API не меняется ничего, просто чеки становятся фискальными.

Быстрый старт

От ключа до первого чека за три шага.

  1. Проверьте связь и состояние ПФР.
    curl -H "Authorization: Bearer kasir_vas_kljuc" \
      https://otkucaj.com/api/v1/status
  2. Выдайте первый чек.
    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.

  3. Скачайте журнал (текст чека, готовый к печати в 40 колонок):
    curl -H "Authorization: Bearer kasir_vas_kljuc" \
      https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/journal
GET

/api/v1/status

Состояние приложения и связи с ПФР. Используйте для проверки конфигурации и для мониторинга.

Аутентификация
Authorization: Bearer <api-kljuc>
Параметры
нет
Ответ
200, JSON

Запрос

curl -H "Authorization: Bearer kasir_vas_kljuc" \
  https://otkucaj.com/api/v1/status

Ответ

{
  "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 равно demo, lpfr или vpfr, в зависимости от настроек установки. Когда режим Л-ПФР или В-ПФР, объект pfr передаёт и данные, которые вернул сам процессор фискальных чеков; ok: false с сообщением означает, что ПФР сейчас недоступен. esirNumber складывается из номера и версии ЕСИР из Настроек.

  • 401 unauthorized
  • 429 rate_limited

Аутентификация

Каждый запрос обязан нести заголовок:

Authorization: Bearer <api-kljuc>

Ключи создаёт администратор в приложении, в разделе Настройки · Ключи API. Ключ показывается только один раз, в момент создания: в базе хранится исключительно его отпечаток SHA-256, поэтому потерянный ключ прочитать заново невозможно, его отзывают и создают новый. Отзыв мгновенный: каждый следующий запрос с отозванным ключом получает 401.

Ключи не ограничиваются по правам по отдельности: любой активный ключ может вызвать любой путь. Если вы хотите разделить системы, создайте отдельный ключ на каждую систему и отзывайте его по отдельности, когда понадобится; это тот уровень изоляции, который приложение действительно даёт.

Держите ключ только на сервере. Ключ никогда не должен оказаться ни в JavaScript, исполняемом в браузере покупателя, ни в мобильном приложении, ни в публичном репозитории, ни в параметрах URL. Любой, кто увидит ключ, сможет выдавать чеки от вашего имени. Вызовы к Otkucaj всегда отправляйте со своего сервера.
GET

/api/v1/me

Данные о ключе, которым подписан запрос: название, сохранённый префикс, даты, реально применяемое ограничение числа запросов и то, что ключу разрешено. Полезно, чтобы убедиться, что интеграция использует именно тот ключ, который вы думаете.

Аутентификация
Authorization: Bearer <api-kljuc>
Параметры
нет
Ответ
200, JSON

Запрос

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 до 500422 validation
Чеков на страницу (GET /invoices)100следующая страница через page
Товаров за вызов (GET /items)1 000избыток не возвращается, постраничности нет
Длина названия позиции и товара200 знаков422 validation
Длина buyerId20 знаков, вид метка:значение422 validation
Длина adText500 знаков422 validation

Окно ограничения фиксированное, а не скользящее: первый запрос открывает окно в 60 секунд и считает до 240; когда окно истечёт, счётчик начинается заново. Превышение возвращает HTTP 429:

{ "error": { "code": "rate_limited", "message": "Rate limit exceeded (240 requests/minute)." } }

Получив 429, дождитесь истечения текущего окна и продолжайте. При обычной работе магазина (один чек на заказ) это ограничение на практике не достигается. Ограничение, которое применяется, и то, о котором сообщает GET /me, приходят из одного места в коде, поэтому разойтись они не могут.

Среды: тест и рабочая

У каждой установки Otkucaj две раздельные среды, и в каждый момент активна одна. API всегда работает в активной среде, а какая это, видно в ответе GET /status и в каждом списке и отчёте.

СредаЧто этоФискальны ли чеки
sandboxТестовая система Налоговой администрации (*.sandbox.suf.purs.gov.rs). Служит для технической проверки до одобрения и для обучения.Нет. Чеки не входят ни в оборот, ни в бухгалтерию.
productionНастоящая система Налоговой администрации.Да. Каждый чек вида Промет и Аванс является фискальным; Копия, Предварительный счёт и Обучение нет.

У двух сред разный элемент безопасности, разный ПАК и разный номер ЕСИР. Приложение отказывается поставить в обе один и тот же сертификат и отказывается направить рабочую среду на тестовый адрес Налоговой администрации (и наоборот).

Данные полностью разделены. Чек несёт обозначение среды, в которой он выдан, поэтому GET /invoices, GET /reports/summary и GET /reports/daily возвращают только то, что относится к активной среде. Тестовый чек не может попасть в рабочий оборот.

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"
}

Прежде чем начать отправлять настоящие чеки, проверьте environment и fiscal. Если fiscal: false, то, что вы выдаёте, не является фискальным чеком, что бы ни было написано в журнале.

Каждая выданная позиция несёт ту же информацию:

"pfr": {
  "mode": "vpfr",
  "environment": "production",
  "fiscal": true,
  "invoiceNumber": "…",
  "verificationUrl": "https://suf.purs.gov.rs/v/?vl=…",
  "verificationQRCode": "R0lGODlh…"   // base64 GIF, нарисованный самим ПФР
}

Когда verificationQRCode присутствует, это QR, принадлежащий чеку: его рисует ПФР по параметрам Налоговой администрации. Не рисуйте поверх него свой. В демо-режиме этого поля нет, поэтому QR строится из verificationUrl.

Идемпотентность: как не выдать один и тот же чек дважды

Если связь оборвётся после того, как запрос дошёл, но до того, как ответ вернулся, это выглядит точно так же, как если бы запрос вообще не дошёл. Повтор тогда создаёт два фискальных чека на одну продажу, а их нельзя удалить, только возместить возвратом.

Поэтому каждый POST /invoices вправе нести ключ запроса. Отправьте его одним из двух способов:

POST /api/v1/invoices
Authorization: Bearer <api-kljuc>
Idempotency-Key: order-2026-08-20-4711
Content-Type: application/json

{ "items": [ … ] }

или в самом теле:

{ "clientRequestId": "order-2026-08-20-4711", "items": [ … ] }

Правила простые:

  • Ключ может содержать от 8 до 64 знаков: буквы, цифры и . _ : -. Всё остальное возвращает 422.
  • Ключ резервируется до обращения к ПФР, поэтому два одновременных повтора оба пройти не могут.
  • Повтор с тем же ключом возвращает 201 и тот же чек, с тем же uuid и тем же номером ПФР. Новый не выдаётся.
  • Если ПФР отклонит запрос, ключ освобождается, и правильная попытка снова работает.

Лучше всего взять то, что у вас уже есть и что уникально, например номер заказа из вашей системы. Тогда и обрыв сети, и ваш собственный повтор приведут к одному и тому же чеку.

Вызовы из браузера (CORS)

API разрешает вызовы с любого источника (Access-Control-Allow-Origin: *) и отвечает на проверку OPTIONS. Аутентификация идёт исключительно заголовком Authorization, никогда файлом cookie, а Access-Control-Allow-Credentials намеренно не отправляется.

Это значит: чужой сайт без вашего ключа не может ничего. Но это значит и то, что ключ не должен стоять в JavaScript публичной страницы, потому что там он виден любому. Из браузера вызывайте свой сервер, а свой сервер пусть вызывает Otkucaj.

Чеки

Семь путей вокруг чеков: выдача, список, подробности, журнал, возврат, копия и отправка по электронной почте.

POST

/api/v1/invoices

Выдача чека. Запрос проверяется, передаётся в ПФР, а чек записывается в базу только тогда, когда ПФР его подписал. Ответ 201 возвращает весь чек, включая журнал и ссылку для проверки.

Аутентификация
Authorization: Bearer <api-kljuc>
Тело
JSON, Content-Type: application/json
Ответ
201, весь чек
ПолеТипОбязательноОписание
invoiceTypestringнет (по умолчанию Normal)Normal (продажа), Advance (аванс), ProForma (предварительный счёт), Training (обучение), Copy (копия)
transactionTypestringнет (по умолчанию Sale)Sale (продажа) или Refund (возврат)
items[]arrayдаот 1 до 500 позиций, сумма должна быть больше 0
items[].namestringдаНазвание позиции, до 200 знаков
items[].unitstringнетЕдиница измерения, по умолчанию ком
items[].quantitynumberдаБольше 0 и не более 999.999; округляется до 3 десятичных знаков (например, 0.375)
items[].unitPricenumberдаЦена за единицу с НДС, 0 или больше, округляется до 2 десятичных знаков
items[].totalAmountnumberнетСумма строки. Сервер вычисляет её сам как количество × цену, отправленное значение не используется
items[].labelstringдаНалоговая метка из действующего справочника, смотрите GET /tax-labels. Ставки определяет исключительно ПФР
items[].gtinstringнетGTIN или штрихкод, от 8 до 14 цифр
items[].itemIdnumberнетID товара из справочника товаров. Он только запоминается вместе со строкой чека как связь с товаром; цена и название всегда берутся из самого запроса
payments[]arrayнетСпособы оплаты. Если поле отсутствует или пусто, вся сумма идёт как Cash
payments[].paymentTypestringдаCash (наличные), Card (платёжная карта), Check (банковский чек), WireTransfer (перевод на счёт), Voucher (ваучер), MobileMoney (мгновенный платёж), Other (другой безналичный)
payments[].amountnumberдаСумма. Позиции с суммой 0 или меньше отбрасываются. Если после этого останется ровно одна оплата, её сумма сама выставляется в общую сумму чека; если их несколько, сумма должна совпадать с суммой позиций (допуск 0,01)
cashierstringнетИмя кассира на чеке, не более 50 символов, как принимает ПФР: более длинное значение отклоняется (422), ничего не сокращается; для вызовов API по умолчанию API
buyerIdstringусловноИД покупателя, до 64 знаков, вид код:значение. Справочник: 10 ПИБ, 11 ЈМБГ, 12 ПИБ:ЈБКЈС, 20 удостоверение личности, 23 паспорт, 30 иностранный паспорт, 40 иностранный TIN. Обязателен при каждом возврате
buyerCostCenterIdstringнетДополнительное поле покупателя (место затрат)
referentDocumentNumberstringусловноНомер ПФР оригинального чека. Обязателен для Refund и для Copy
referentDocumentDTstringнетВремя оригинального чека, YYYY-MM-DD HH:MM:SS
dateAndTimeOfIssuestringнетВремя ЕСИР, YYYY-MM-DD HH:MM:SS. Используется для аванса, когда оплата пришла раньше, чем выдаётся чек (например, перевод на счёт, проведённый на следующий день); печатается на чеке как «ЕСИР време». Чек Аванс Продаја тогда выдаётся не позднее следующего рабочего дня после поступления оплаты (ст. 11 п. 5 Правилника о видах фискальных чеков)
adTextstringнетРекламный текст, до 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
  • 409 pfr_error

При 409 чек не выдан, и в базу ничего не записано.

GET

/api/v1/invoices

Список чеков, новые первыми, до 100 на страницу. Каждый элемент это весь чек, в той же форме, что и ответ на POST /invoices, вместе с журналом.

Аутентификация
Authorization: Bearer <api-kljuc>
Параметры
в строке запроса (query string)
Ответ
200, JSON
ПараметрТипОписание
fromstringДата с, YYYY-MM-DD (включает весь день с 00:00:00)
tostringДата по, YYYY-MM-DD (включает весь день до 23:59:59)
typestringВид чека: Normal, Advance, ProForma, Training, Copy
pagenumberСтраница, с 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
GET

/api/v1/invoices/{uuid}

Один чек по UUID, в полной форме, описанной у POST /invoices.

Аутентификация
Authorization: Bearer <api-kljuc>
Путь
uuid: 36 знаков, строчные буквы и цифры с дефисами, ровно так, как его вернул API
Ответ
200, весь чек

Запрос

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: отправляйте его ровно так, как его вернул API.

  • 401 unauthorized
  • 404 not_found
  • 429 rate_limited
GET

/api/v1/invoices/{uuid}/journal

Журнал чека чистым текстом (text/plain; charset=utf-8), 40 колонок, кириллицей, готовый для термопринтера 58 или 80 мм. Он идентичен журналу, который печатает само приложение. Это единственный ответ, который не является JSON.

Аутентификация
Authorization: Bearer <api-kljuc>
Параметры
нет
Ответ
200, text/plain; charset=utf-8

Запрос

curl -H "Authorization: Bearer kasir_vas_kljuc" \
  https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/journal

Ответ, сокращённо

======== ФИСКАЛНИ РАЧУН =======
              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ПП
======== КРАЈ ФИСКАЛНОГ РАЧУНА ========

Один чек — один журнал, и печатается он сам по себе. Любое исправление чека — возврат, аннулирование, копия — это новый фискальный документ со своим номером, своим журналом и своим QR-кодом. Никогда не склеивайте два журнала в одну запись и не вставляйте между ними ни одной своей строки (ни «===== АННУЛИРОВАНИЕ =====», ни номера заказа, ни даты): так на одном листе оказываются два фискальных документа с вашим текстом внутри рамки, что, как мы понимаем, не согласуется с правилом выдачи чека в одном экземпляре (ст. 13 п. 5 Правилника) и с 16.П13, по которому ничто ниже завершающей строки не является частью чека, а на A4 чек ещё и разрывается на несколько страниц. Храните каждый журнал в своём поле и печатайте по одному документу на лист. Исправляющий чек ссылается на исходный строкой Реф. број, которую ПФР пишет сам.

A4 — это не тот же документ, что чек с ленты. Заявка ИБ 1651 (13.П3) одобряет обе формы: и журнал на A4, и чек как документ A4 с шапкой продавца, таблицей позиций, налоговой сводкой, QR-кодом и фискальным слипом дословно. Для офисного принтера берите вторую форму — поместите фискальный слип в документ без изменений, а адрес проверки как настоящую ссылку вне рамки чека. Лучше вообще его не рисуйте: вызовите GET /api/v1/invoices/{uuid}/document своим API-ключом и напечатайте то, что придёт, символ в символ. Это тот вид, который печатает Otkucaj и который был одобрен, и единственный способ получить его через API — страница /racun/dokument требует входа, и API-ключ её не откроет. С параметром ?base=https://ваш-домен стили и шрифты загрузятся и тогда, когда документ показан на вашем сайте, а с &print=1 сразу откроется диалог печати.

Если вы сами отрисовываете чек из поля journal: QR-код размещается внутри рамки чека — между последней строкой ==== и строкой КРАЈ ФИСКАЛНОГ РАЧУНА, но никогда под ней. Всё напечатанное ниже этой строки не считается частью фискального чека (Техническое руководство 16.П13), поэтому там допустимо только рекламное поле: ни QR, ни ссылка для проверки, ни ваш номер заказа рядом с ними. Напечатанный QR должен быть квадратом от 40 до 50 мм (16.П12) и не должен уменьшаться средствами CSS, так как масштабирование убирает колонки модулей и код перестаёт считываться. Это тот же макет, который печатает сам Откуцај и который одобрен по заявке ИБ 1651.

Нефискальные виды (Копия, Предварительный счёт, Обучение) вместо шапки и конца фискального чека несут строку «ОВО НИЈЕ ФИСКАЛНИ РАЧУН», которую приложение при печати увеличивает до двойного размера шрифта. Копия-Возврат дополнительно печатает место «Потпис купца» для подписи при выдаче денег.

  • 401 unauthorized
  • 404 not_found
  • 429 rate_limited
GET

/api/v1/invoices/{uuid}/document

Чек как готовый документ для печати, ровно так, как его печатает само приложение, чтобы интегратор никогда не рисовал чек сам. Параметр paper выбирает лист: a4 (по умолчанию) - документ A4, одобренный заявкой ИБ 1651 (13.П3): шапка продавца, таблица позиций, налоговая сводка, способ оплаты, QR-код и фискальный слип дословно; a5 - фискальный слип на листе A5 (13.П4), форма, которую курьер или фулфилмент-центр печатает и кладёт в посылку; a4slip - фискальный слип на листе A4. На каждом листе QR-код стоит внутри рамки чека, между блоком ПФР и завершающей строкой, размером 42-45 мм, а ниже „КРАЈ ФИСКАЛНОГ РАЧУНА“ ничего нашего не печатается. С format=pdf тот же лист приходит как PDF, отрисованный на сервере тем же браузерным движком и тем же правилом @page (измерено: страница A5 148 × 210 мм, QR 45 мм, читается). GET /status в поле documents.pdf сообщает, умеет ли установка отрисовать PDF; если нет, запросите HTML и напечатайте его. Не рисуйте свой A4 или A5 - берите этот. Ответ не JSON.

Аутентификация
Authorization: Bearer <api-kljuc>
Параметры
paper - a4 (по умолчанию), a5 или a4slip. format - html (по умолчанию) или pdf. base - ваш origin (например https://magazin.rs), чтобы стили и шрифты загрузились, когда HTML-документ показан на вашем сайте. print=1 - HTML открывает диалог печати сразу.
Ответ
200, text/html; charset=utf-8 или application/pdf

Запрос

curl -H "Authorization: Bearer kasir_vash_kljuc"   "https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/document?base=https://magazin.rs&print=1"

# слип на A5 как PDF, для курьера
curl -H "Authorization: Bearer kasir_vash_kljuc"   "https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/document?paper=a5&format=pdf" -o chek.pdf

Когда лист скачивает партнёр, который ждёт. Склад, который печатает чек вместе с посылкой, открывает ссылку, пока упаковщик стоит над коробкой, так что файл нужен за секунду две. Поэтому POST /invoices и POST /invoices/{uuid}/close-advance принимают поле "prefetchDocument": "a5": Откуцай отвечает сразу, а лист рисует сразу после ответа, и следующая загрузка находит готовый файл. Замерено 02.09.2026: 0,60 s без подготовки, 0,13 s с ней. Дальше лист отдаётся из кеша, так что каждая следующая загрузка стоит только передачи байтов.

Сам лист размечен комментариями <!--sheet--> и <!--/sheet-->, если он нужен без остальной страницы. Проверьте, что ответ содержит <!--sheet-->, прежде чем его показывать: так вы отличите документ от страницы с ошибкой.

  • 401 unauthorized
  • 404 not_found
  • 422 validation - paper, format или base некорректны; configuration - установка не умеет отрисовать PDF (запросите HTML)
POST

/api/v1/invoices/{uuid}/refund

Возврат всего существующего чека одним вызовом. Позиции берутся с оригинала, референтный номер и время указывают на оригинал, а новый чек выдаётся тем же путём, что и любой другой. Это то же самое, что делает кнопка Оформить возврат в приложении.

Аутентификация
Authorization: Bearer <api-kljuc>
Тело
необязательно, JSON
Ответ
201, новый чек возврата
Условия. Возврат оформляется только по чеку вида Normal или Advance, с типом транзакции Sale и в статусе «выдан». Копия, предварительный счёт, обучение и уже выданный возврат отклоняются с 422. Это те же условия, при которых кнопка показывается в приложении.
Поле телаТипПо умолчаниюОписание
buyerIdstringпокупатель с оригиналаИдентификация покупателя. Обязательна при возврате: если её нет ни в теле, ни на оригинале, ответом будет 422 с сообщением, объясняющим, что для аннулирования собственного чека нужно ввести свой ПИБ
buyerCostCenterIdstringс оригиналаМесто затрат покупателя
paymentsarrayоплаты с оригиналаСпособы возврата денег. Сумма должна соответствовать общей сумме чека
cashierstringAPIИмя кассира на чеке возврата, не более 50 символов
adTextstringпустоРекламный текст, до 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
  • 409 pfr_error
POST

/api/v1/invoices/{uuid}/close-advance

Закрытие аванса одним вызовом. Для выданного авансового чека продажи (Аванс Продаја) приложение по порядку выдаёт Аванс Рефундацију на всю сумму аванса (референс = авансовый чек) и сразу затем окончательный Промет Продаја с позициями поставки, ссылкой на этот возврат и рекламным полем, в котором Техничко упутство §9.1.1 обязательными называет номер и дату последнего авансового чека; приложение добавляет к ним сумму, оплаченную авансом, и номер возврата аванса. Это то же, что делает кнопка „Затвори аванс“ в приложении, от начала до конца, в форме одобренных образцов П9, П10 и П15 заявки ИБ 1651.

Аутентификация
Authorization: Bearer <api-kljuc>
Тело
items (обязательно) - позиции поставки, как в POST /invoices. payments - по умолчанию оплаты с авансового чека: авансированная сумма идёт тем же способом оплаты, которым была получена; укажите свои, если часть цены оплачивается при доставке. buyerId - по умолчанию покупатель с аванса; возврат аванса, который покупателю не выдаётся, при отсутствии берёт ПИБ продавца. adText - добавляется ниже обязательных строк об авансе. cashier, buyerCostCenterId, dateAndTimeOfIssue, clientRequestId - как в POST /invoices; идентификатор запроса относится к окончательному чеку, а с суффиксом :ar - к возврату.
Ответ
201, когда окончательный чек выдан сейчас, 200, когда оба документа уже существовали; в обоих случаях advance, advanceRefund и invoice

Запрос

curl -X POST \
  https://otkucaj.com/api/v1/invoices/9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30/close-advance \
  -H "Authorization: Bearer kasir_vash_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"
  }'

Вызов можно повторять. Если возврат выдан, а окончательный чек нет (ПФР отклонил второй запрос или связь оборвалась между ними), повторный вызов находит возврат по его референсному номеру и выдаёт только окончательный чек. Однажды выданный возврат никогда не выдаётся дважды.

  • 401 unauthorized
  • 404 not_found
  • 422 validation - чек не является выданным авансовым чеком продажи, или позиции и оплаты окончательного чека не проходят проверку; сообщение называет документ
  • 409 pfr_error - ПФР отклонил или недоступен; сообщение называет документ, а уже выданный возврат остаётся
POST

/api/v1/invoices/{uuid}/copy

Выдача копии существующего чека, тем же путём, который использует кнопка Копия в приложении. Копия несёт все позиции и оплаты оригинала, референтный номер и время оригинала и его идентификацию покупателя.

Аутентификация
Authorization: Bearer <api-kljuc>
Тело
нет, отправляется пустой POST
Ответ
201, новая копия
Условия. Копируется только чек в статусе «выдан», который сам копией не является. Копия копии отклоняется с 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
  • 409 pfr_error
POST

/api/v1/invoices/{uuid}/email

Отправка чека покупателю по электронной почте. Письмо содержит журнал и ссылку для проверки чека и полностью совпадает с тем, которое отправляет кнопка в приложении, потому что оба идут через один и тот же код. Отправка записывается в аудиторский след как invoice.email.

Аутентификация
Authorization: Bearer <api-kljuc>
Тело
JSON: {"to": "[email protected]"}
Ответ
200, подтверждение
Поле телаТипОбязательноОписание
tostringдаАдрес получателя, корректный адрес электронной почты, не более 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 с сообщением об ограничении почты. Если почтовый сервер отклонит письмо, ответом будет 409 с кодом mail_error: это не ошибка ПФР и не ошибка в запросе, а неудавшаяся отправка, и запрос можно повторить позже.

  • 400 bad_request
  • 401 unauthorized
  • 404 not_found
  • 422 validation
  • 429 rate_limited
  • 409 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: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": "Ђ" }]
}

Копия

Копию существующего чека проще всего выдать через 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, а цена равна сумме полученного аванса. В сценарии три вида документов: авансовый чек (АП) на каждую оплату, аванс-возврат (АР), который закрывает сумму всех авансов, и окончательный чек Промет-Продаја.

  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".

  2. Дополнительные авансовые чеки на каждую следующую оплату, каждый со ссылкой на предыдущий АП:
    {
      "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".

  3. Закрытие: аванс-возврат (АР) на всю полученную сумму, со ссылкой на последний АП и buyerId, который Otkucaj требует при каждом возврате:
    {
      "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".

  4. Окончательный чек Промет-Продаја с фактическими позициями поставки, ссылкой на АР и рекламным текстом, который несёт номер и дату последнего авансового чека:
    {
      "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: выдаёт АР на всю сумму и заполняет кассу для окончательного чека с проставленной ссылкой и рекламным текстом. Через API шаги 3 и 4 делает один вызов, POST /invoices/{uuid}/close-advance: отправьте позиции поставки, а возврат, референсы и обязательные строки рекламного поля приложение составит само. Авансовая позиция отправляется как 10: Аванс без единицы измерения: метку в скобках добавляет ПФР.

Режим по ст. 6 п. 2 Правилника

Установка может работать в режиме, в котором способы оплаты Card, Check и MobileMoney отключены, а такие поступления учитываются как наличные. Одна установка работает ровно в одном режиме, который выбирает администратор в Настройках. Когда режим включён, запрос с одним из отключённых способов возвращает:

422 { "error": { "code": "validation",
  "message": "Начин плаћања „Card“ је искључен подешавањем (режим по чл. 6 ст. 2 Правилника). Унесите као готовину." } }

Интеграция тогда отправляет {"paymentType": "Cash"} для таких поступлений.

Товары

Справочник товаров, который касса предлагает нажатием. Товары не являются условием для выдачи чека: POST /invoices принимает позиции свободно, с названием и ценой из вашей системы.

GET

/api/v1/items

Список товаров, отсортированный по названию, до 1 000 штук. Поле active отличает активные от отключённых; отключённые тоже присутствуют в ответе.

Аутентификация
Authorization: Bearer <api-kljuc>
Параметры
нет, ни постраничности, ни фильтров
Ответ
200, JSON

Запрос

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
POST

/api/v1/items

Добавление товара в справочник.

Аутентификация
Authorization: Bearer <api-kljuc>
Тело
JSON
Ответ
201, ID нового товара
ПолеТипОбязательноОписание
namestringдаНазвание, до 200 знаков
labelstringдаНалоговая метка из действующего справочника (GET /tax-labels). Неизвестная метка возвращает 422 со списком допустимых
pricenumberнет (по умолчанию 0)Цена с НДС, 0 или больше, округляется до 2 десятичных знаков
plustringнетКод товара для быстрого набора
unitstringнетЕдиница измерения, по умолчанию ком
gtinstringнетGTIN или штрихкод, от 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
PUTPATCH

/api/v1/items/{id}

Частичное изменение товара: вы отправляете только те поля, которые меняете, остальные остаются нетронутыми. Оба метода, PUT и PATCH, делают одно и то же.

Аутентификация
Authorization: Bearer <api-kljuc>
Путь
id: целочисленный ID товара
Тело
JSON, любые из полей plu, name, unit, price, label, gtin, active
Ответ
200, подтверждение
Осторожно с меткой при изменении. При изменении значение поля 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
DELETE

/api/v1/items/{id}

Мягкое отключение: товар не удаляется из базы, потому что существующие чеки на него по-прежнему ссылаются. Он получает active: false и исчезает с кассы. Обратная активация это PUT с {"active": true}.

Аутентификация
Authorization: Bearer <api-kljuc>
Путь
id: целочисленный ID товара
Ответ
200, подтверждение

Запрос

curl -X DELETE https://otkucaj.com/api/v1/items/12 \
  -H "Authorization: Bearer kasir_vas_kljuc"

Ответ

{ "ok": true }
Этот вызов не сообщает, что товара нет. Отключение несуществующего ID тоже возвращает {"ok": true}, потому что ничего не меняется и ничего не ломается. Если вам важно, существует ли товар, проверьте его заранее через GET /items.
  • 401 unauthorized
  • 429 rate_limited

Отчёты

Оба отчёта считают оборот по тому же правилу и теми же запросами, что и страница Отчёты в приложении, поэтому цифры разойтись не могут.

Правило расчёта. В оборот входят только выданные фискальные чеки, то есть Normal и Advance в статусе «выдан». Возвраты вычитаются. Предварительный счёт, копия и обучение не входят ни в одну сумму. То же правило возвращает и сам ответ, в поле rule.
GET

/api/v1/reports/summary

Оборот за период, с разбивкой по налоговой метке, способу оплаты и кассиру.

Аутентификация
Authorization: Bearer <api-kljuc>
Параметры
from, to
Ответ
200, JSON
ПараметрТипПо умолчаниюОписание
fromstringсегодняДата с, строго YYYY-MM-DD, включает весь день
tostringсегодняДата по, строго 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
GET

/api/v1/reports/daily

Оборот и число чеков по дням, за тот же период и по тому же правилу, что и сводный отчёт.

Аутентификация
Authorization: Bearer <api-kljuc>
Параметры
from, to, оба по умолчанию сегодня, строго YYYY-MM-DD
Ответ
200, JSON

Запрос

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

Справочники

Два списка, которые нужны интеграции, чтобы отправлять правильные значения, оба из того же источника, которым пользуется само приложение.

GET

/api/v1/tax-labels

Налоговые метки и ставки, действующие сейчас, из того же источника, который использует и валидатор чека (только активные строки). Метка, которую возвращает этот вызов, это та метка, которую примет POST /invoices.

Аутентификация
Authorization: Bearer <api-kljuc>
Параметры
нет
Ответ
200, JSON

Запрос

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
GET

/api/v1/categories

Категории активных товаров, без пустых значений, отсортированные по алфавиту. Тот же запрос, который касса использует для кнопок категорий.

Аутентификация
Authorization: Bearer <api-kljuc>
Параметры
нет
Ответ
200, JSON

Запрос

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)." } }
HTTPcodeПричинаЧто делать
400bad_requestТело запроса не является корректным JSONПроверьте сериализацию и Content-Type: application/json
401unauthorizedНет заголовка Authorization, ключ неверен или отозванПроверьте ключ; при необходимости создайте новый в Настройках
404not_foundНеизвестный путь, несуществующий UUID чека или ID товара при измененииПроверьте путь и идентификатор
422validationЗапрос не проходит проверку; сообщение точно называет поле и причинуИсправьте запрос. Не повторяйте тот же запрос без изменений: результат всегда будет тем же
429rate_limitedБолее 240 запросов в минуту на ключ либо более 30 отправленных писем в часДождитесь истечения окна и продолжайте в сниженном темпе
409pfr_errorПФР отклонил запрос или недоступенЧек НЕ выдан, ничего не записано. Проверьте GET /status и повторите тот же запрос
409mail_errorПисьмо с чеком отправить не удалосьЧек существует и не тронут. Отправку можно повторить позже
Самое важное правило. При 409 чек не возник, и запрос безопасно повторить; при 422 запрос нужно исправить, а повторять его без изменений бессмысленно. Если ваша система повторяет запросы автоматически, пусть она повторяет только 429, 409 и сетевые обрывы, никогда 422.

WooCommerce и интеграции

Готовый плагин подключает магазин на WooCommerce без единой строки кода: otkucaj-fiskalizacija.zip.

  1. В админке WordPress откройте Плагины · Добавить новый · Загрузить плагин, выберите скачанный zip и активируйте плагин.
  2. В Otkucaj (Настройки · Ключи API) создайте ключ и сразу его скопируйте: он показывается только один раз.
  3. Откройте Настройки · Otkucaj фискализация в WordPress и введите: адрес (https://otkucaj.com), ключ API, статус заказа, запускающий выдачу чека (тот, в котором, как мы понимаем ст. 6 п. 1 Закона о фискализации, происходит продажа или получение аванса; определите его с бухгалтером), и сопоставление способов оплаты WooCommerce с фискальными (например, карты на Card, оплату при получении на Cash, перевод на WireTransfer).
  4. Готово. Когда заказ перейдёт в выбранный статус, плагин выдаст чек, ровно один раз на заказ, и запишет в сам заказ номер чека ПФР и ссылку для проверки, видимые и вам, и покупателю. Чек покупателю всё равно нужно доставить, например по почте (POST /invoices/{uuid}/email).

Налоговая метка по товару: плагину для отдельного товара можно задать мета-поле otkucaj_label (значение из GET /tax-labels); товары без него используют метку по умолчанию из настроек плагина.

Уведомления в Telegram. В приложении (Настройки · Уведомления в Telegram) владелец может включить сообщение в Telegram по каждому чеку, выданному через API: вид чека, сумма, способы оплаты, номер ПФР, счётчик и ссылка для проверки приходят в выбранный чат или группу, как только заказ фискализирован.

Пример: чистый 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);

    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("Otkucaj 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']

Пример: 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(`Otkucaj 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);

Пример: 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"Otkucaj 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'])

Хорошая практика

  • Выдавайте чек в момент продажи или получения аванса. Как мы понимаем ст. 6 п. 1 Закона о фискализации, чек выдаётся, когда товар поставлен или услуга оказана, либо когда получена предоплата (авансовый чек), независимо от того, когда и как производится оплата. Оплата картой при оформлении заказа, до отправки, является авансом.
  • Храните uuid и pfr.invoiceNumber вместе с заказом. Без них вы позже не сможете ни выдать копию, ни оформить возврат, ни отправить чек по почте, ни скачать журнал.
  • Читайте справочники, а не переписывайте их. Налоговые метки берите из GET /tax-labels при настройке интеграции, потому что список отличается от установки к установке.
  • Ставьте таймаут в 30 секунд, чтобы вызов к API не блокировал ваше приложение бесконечно.
  • Никогда не вызывайте API из браузера покупателя и не из мобильного приложения: ключ стал бы публичным. Весь трафик идёт с вашего сервера.
  • Не отправляйте дублирующие запросы. С каждым заказом храните свой идемпотентный ключ (например, номер заказа) и перед отправкой проверяйте, не выдан ли по нему чек. Обрыв сети после успешной выдачи это самая частая причина двойного чека: перед повтором проверьте GET /invoices за этот день.
  • Разрабатывайте и тестируйте в демо-режиме. Переход на Л-ПФР или В-ПФР это изменение настроек в Otkucaj, без единого изменения в вашем коде.

Изменения

ВерсияНовое
1.3.6Чек как документ также на листе A5 (фискальный слип, форма для курьера в посылку) и как PDF, отрисованный на сервере: GET /invoices/{uuid}/document?paper=a5&format=pdf. Новый POST /invoices/{uuid}/close-advance закрывает аванс одним вызовом (Аванс Рефундација + окончательный Промет Продаја с референсом и обязательным рекламным полем), безопасно повторяемый. GET /status сообщает documents. Плагин WooCommerce 1.1.0: авансовый чек при оплате картой, окончательный при отправке, публичная PDF-ссылка для курьера, возвраты из панели заказа. Новое поле prefetchDocument при выписке: лист рисуется сразу после ответа, для партнёра, который скачивает его во время упаковки. Плагин 1.2.0: скачанный PDF хранится локально, а письма уходят только после того, как документ отправлен.
1.3.5Весь лист самооценки (Техническое руководство §7.4.2, разделы 9-16) проверен пункт за пунктом. Самое важное: демо-симулятор больше не печатает документ с заголовком «ФИСКАЛНИ РАЧУН», а несёт сообщение о том, что чек не фискальный, а код авансовой позиции выводится из ставки, а не из буквы метки. Добавлены: рекламный текст по умолчанию, показ рукопожатия с ПФР на Кассе, просмотр налоговых ставок для кассира, GTIN на чеке и перенос длинных названий товаров. Руководство получило главы 6.1, 18.2.1, 18.6.1, 25 и 26.
1.3.4Чек теперь можно отдать покупателю и как документ формата A4 (кнопка «Документ» на странице чека), а письмо переработано в тот же вид, с QR-кодом в самом письме. Фискальный слип остаётся в обоих видах, знак в знак, так, как его подписал ПФР. Исправлены и три ошибки: длинное название фирмы выходило за рамку чека (ПФР строки не переносит, а термопринтер переносит), печать на рулоне не работала, потому что правило @page было некорректным, и HTML-часть письма приходила с испорченными кириллическими знаками.
1.3.3Первая версия, которая действительно говорила с настоящим ПФР. С тестовым элементом безопасности Налоговой администрации и тестовой системой В-ПФР вышли наружу три ошибки, которых в демо-режиме увидеть было нельзя: заголовок RequestId обязан быть числом (с GUID каждый запрос возвращал 400), журнал приходит с переносом строки CRLF, из-за чего обязательное сообщение «ОВО НИЈЕ ФИСКАЛНИ РАЧУН» на Копии, Предварительном счёте и Обучении печаталось обычным размером, и настоящий ПФР не печатает строку «Повраћај», поэтому теперь её добавляет ЕСИР под фискальным блоком. Налоговые ставки берутся из currentTaxRates, а сообщение об ошибке ПФР теперь читается и тогда, когда приходит обычным текстом или как modelState.
1.3.2Оплата теперь принимает фактически переданную сумму. Сумма оплат может быть больше суммы чека: разница является сдачей, показывается на Кассе, печатается в строке «Повраћај» и возвращается в ответе как change. Сумма меньше общей по-прежнему отклоняется, а при возврате сдачи не бывает. Отчёт по способам оплаты вычитает сдачу из наличных, чтобы переданный излишек не увеличивал оборот. Чек, отправленный по почте, теперь несёт и HTML-версию, в которой адрес для проверки является настоящей гиперссылкой, а сообщение «ОВО НИЈЕ ФИСКАЛНИ РАЧУН» сохраняет двойной размер.
1.3.1Новое поле buyerCostCenterId (Дополнительное поле покупателя) на Кассе и в ответе API; копия чека берёт его с оригинала. Реестр чеков получил поиск конкретного чека по номеру ПФР, счётчику, референтному номеру, покупателю, сумме и товару, по всему журналу. Сообщение «ОВО НИЈЕ ФИСКАЛНИ РАЧУН» показывается двойным размером и тогда, когда ПФР выводит его внутри заглавной строки. Журнал чека всегда на кириллице, независимо от языка интерфейса. Настройки показывают серийный номер установки.
1.3Раздельные среды: тест и рабочая, у каждой свой элемент безопасности, ПАК и номер ЕСИР. Чек несёт обозначение среды, а списки и отчёты показывают только активную. Новые поля: environment и fiscal в GET /status и в каждом чеке, verificationQRCode (QR, нарисованный самим ПФР), clientRequestId. Идемпотентность: заголовок Idempotency-Key или поле clientRequestId предотвращает двойную выдачу после обрыва связи. CORS включён, поэтому API можно вызывать и из браузера. buyerId проверяется по виду метка:значение, не более 20 знаков.
1.2Расширенный API: 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, уведомления в Telegram о выданных чеках, новая панель управления, сворачиваемая боковая панель, категории товаров, скидки по позиции и на весь чек, превращение предварительного счёта в фискальный чек, автоматическое закрытие аванса, отправка чека по почте.
1.1Офлайн-очередь на кассе (чек ждёт в локальной очереди и выдаётся по возвращении связи), разделённые оплаты на одном чеке, плагин WooCommerce.
1.0Первая версия: выдача чеков, список и подробности чека, журнал, товары, ключи API.

Контракт API v1 стабилен во всех перечисленных версиях приложения: добавлялись новые возможности, существующие поля не менялись.

Вопросы и помощь при интеграции: форма обратной связи. Руководство по работе в самом приложении: Руководство (PDF).