| 🇬🇧 English | 🇷🇸 Srpski |
|---|
🧾 ipslip is an HTTP API for Serbian payment slip PDFs (uplatnice / naloge za prenos) with embedded NBS IPS QR codes.
| Endpoint | Method | Description |
|---|---|---|
/healthcheck |
GET |
✅ Liveness probe — {"status":"ok"} |
/generate-pdf |
POST |
📄 Payment slip PDF with NBS IPS QR code |
⚠️ Node.js 22 or 23 required (hyper-express/ uWebSockets.js). Node 24+ is not supported yet.
git clone https://github.com/CerealKiller97/e-payment-slip.git
cd e-payment-slip
npm install
cp .env.example .env # edit payee details
npm run start:dev # http://127.0.0.1:3000Production:
npm run build && npm startVerify everything works (server must be running):
npm run smokecp .env.example .env
docker compose up --build -d
curl http://127.0.0.1:3000/healthcheckdocker run (prebuilt image, default tag 1.0.0):
mkdir -p invoices-local
docker run -d --name ipslip -p 3000:3000 \
--env-file .env \
--restart unless-stopped \
--mount "type=bind,source=$(pwd)/invoices-local,target=/app/invoices" \
ghcr.io/cerealkiller97/ipslip:1.0.0Use
--mountinstead of-vif your project path contains:or spaces.
| Compose / run option | Purpose |
|---|---|
env_file: .env / --env-file .env |
Load all variables from file |
environment: / -e KEY=val |
Override file values (inline wins) |
./invoices-local:/app/invoices |
💾 Persist PDF cache on host |
| Form | type |
Status | Notes |
|---|---|---|---|
| Obrazac 1 / Form 1 | "1" |
⬜ TODO | o1 template in src/invoices.ts |
| Obrazac 2 / Form 2 | "2" |
⬜ TODO | Dedicated o2 template — currently renders form 3 |
| Obrazac 3 / Form 3 | "3" |
✅ Done | o3 template with NBS IPS QR code |
- Form 1 — nalog za uplatu / uplata gotovinom
- Form 2 — separate layout (not aliased to form 3)
- Form 3 — nalog za prenos with IPS QR
- Visual QA against official NBS paper forms
- Golden-file PDF tests per template
Loaded from .env (process.loadEnvFile()) or the process environment. A missing .env does not block startup.
| Variable | Required | Description |
|---|---|---|
PRIMALAC |
Conditional* | Payee name/address — comma-separated lines → payeeName |
RACUN_PRIMAOCCA |
Conditional* | Payee account — 18 digits → payeeAccount |
POZIV_NA_BROJ |
No | Reference number for NBS IPS QR (RO tag) |
RACUN_PLATIOCA |
No | Reserved — not used in PDF template yet |
PDF_CACHE_DIR |
No | Cache dir (./invoices-local locally, /app/invoices in Docker) |
PDF_CACHE_DEFAULT_EXPIRES_IN |
No | Default cache TTL seconds (default 21600) |
*Env or request body must supply both payee fields. Env wins when set.
PRIMALAC=JP EPS BEOGRAD,BALKANSKA 13
RACUN_PRIMAOCCA=845-000000-040484987
POZIV_NA_BROJ=97163220000111111111000
PDF_CACHE_DEFAULT_EXPIRES_IN=21600{ "status": "ok" }| Field | Type | Required | Description |
|---|---|---|---|
payerName |
string |
Yes | Payer (platilac) — commas → line breaks |
purposeOfPayment |
string |
Yes | Payment purpose (svrha plaćanja) |
type |
"1" | "2" | "3" |
Yes | 1 → form 1; 2 / 3 → form 3 |
amount |
number |
Yes | Amount in RSD |
cyrilic |
boolean |
No | Cyrillic PDF + lang=sr_RS QR; else Latin |
paymentCode |
number |
No | Šifra plaćanja (default 289) |
payeeName |
string |
Conditional | Omit if PRIMALAC is set |
payeeAccount |
string |
Conditional | Omit if RACUN_PRIMAOCCA is set |
expiresIn |
number |
No | Cache TTL seconds (positive integer) |
200 → application/pdf (invoice.pdf) · 422 validation · 502 NBS QR failure
payerName= who pays ·payeeName/PRIMALAC= who receives
Minimal (payee from .env):
curl -X POST http://127.0.0.1:3000/generate-pdf \
-H "Content-Type: application/json" \
-d '{
"payerName": "Bogdan Bogdanović, Krunska 23, 11000 Beograd",
"purposeOfPayment": "Radionica",
"type": "3",
"amount": 5000,
"cyrilic": true
}' \
--output invoice.pdfFull (payee in body):
curl -X POST http://127.0.0.1:3000/generate-pdf \
-H "Content-Type: application/json" \
-d '{
"payerName": "MRĐO MAČKATOVIĆ,ŽUPSKA 13,BEOGRAD 6",
"payeeName": "JP EPS BEOGRAD,BALKANSKA 13",
"payeeAccount": "845-000000-040484987",
"purposeOfPayment": "UPLATA PO RAČUNU ZA EL. ENERGIJU",
"type": "3",
"amount": 3596.13,
"paymentCode": 289,
"cyrilic": false
}' \
--output invoice.pdfJavaScript:
const response = await fetch('http://127.0.0.1:3000/generate-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
payerName: 'Bogdan Bogdanović, Krunska 23, 11000 Beograd',
purposeOfPayment: 'Radionica',
type: '3',
amount: 5000,
cyrilic: true,
}),
});
if (!response.ok) throw new Error(JSON.stringify(await response.json()));
const pdf = Buffer.from(await response.arrayBuffer());Identical requests hit disk cache ({hash}.pdf + {hash}.json) — no repeat NBS call.
Hash includes: payerName, purposeOfPayment, type, amount, cyrilic, paymentCode, payeeName, payeeAccount, POZIV_NA_BROJ.
TTL: expiresIn → PDF_CACHE_DEFAULT_EXPIRES_IN → 21600 s default.
POST https://nbs.rs/QRcode/api/qr/v1/generate/150 · 150×150 px · see src/services/qr-code.ts
| Script | Description |
|---|---|
npm run start:dev |
Dev server (nodemon + TypeScript) |
npm run build |
Compile to build/src/ |
npm start |
Run production build |
npm test |
Unit tests (72) |
npm run smoke |
Smoke-test README examples |
npm run lint / lint:fix |
oxlint |
npm run format / format:check |
oxfmt |
Built with hyper-express, VineJS, pdfmake. Docker image: node:22-bookworm-slim.
Apache License 2.0 © Stefan Bogdanović — see LICENSE
🧾 ipslip je HTTP API za generisanje PDF uplatnica / naloga za prenos sa ugrađenim NBS IPS QR kodovima.
| Endpoint | Metoda | Opis |
|---|---|---|
/healthcheck |
GET |
✅ Provera rada — {"status":"ok"} |
/generate-pdf |
POST |
📄 PDF uplatnica sa NBS IPS QR kodom |
⚠️ Potreban je Node.js 22 ili 23 (hyper-express/ uWebSockets.js). Node 24+ još nije podržan.
git clone https://github.com/CerealKiller97/e-payment-slip.git
cd e-payment-slip
npm install
cp .env.example .env # podesite podatke primaoca
npm run start:dev # http://127.0.0.1:3000Produkcija:
npm run build && npm startProvera (server mora da radi):
npm run smokecp .env.example .env
docker compose up --build -d
curl http://127.0.0.1:3000/healthcheckdocker run (gotov image, podrazumevani tag 1.0.0):
mkdir -p invoices-local
docker run -d --name ipslip -p 3000:3000 \
--env-file .env \
--restart unless-stopped \
--mount "type=bind,source=$(pwd)/invoices-local,target=/app/invoices" \
ghcr.io/cerealkiller97/ipslip:1.0.0Koristite
--mountumesto-vako putanja projekta sadrži:ili razmake.
| Compose / run opcija | Svrha |
|---|---|
env_file: .env / --env-file .env |
Učitava promenljive iz fajla |
environment: / -e KEY=val |
Prepisuje fajl (inline ima prednost) |
./invoices-local:/app/invoices |
💾 Perzistentan keš PDF-ova |
| Obrazac | type |
Status | Napomena |
|---|---|---|---|
| Obrazac 1 | "1" |
✅ Gotovo | o1 šablon u src/invoices.ts |
| Obrazac 2 | "2" |
⬜ TODO | Poseban o2 šablon — trenutno renderuje obrazac 3 |
| Obrazac 3 | "3" |
✅ Gotovo | o3 šablon sa NBS IPS QR kodom |
- Obrazac 1 — nalog za uplatu / uplata gotovinom
- Obrazac 2 — poseban layout (nije alias za obrazac 3)
- Obrazac 3 — nalog za prenos sa IPS QR kodom
- Vizuelna provera u odnosu na zvanične NBS obrasce
- Golden-file PDF testovi po šablonu
Učitavaju se iz .env (process.loadEnvFile()) ili okruženja procesa. Nedostajući .env ne sprečava pokretanje.
| Promenljiva | Obavezna | Opis |
|---|---|---|
PRIMALAC |
Uslovno* | Ime/adresa primaoca — zarezima odvojeni redovi → payeeName |
RACUN_PRIMAOCCA |
Uslovno* | Račun primaoca — 18 cifara → payeeAccount |
POZIV_NA_BROJ |
Ne | Poziv na broj za NBS IPS QR (RO tag) |
RACUN_PLATIOCA |
Ne | Rezervisano — još se ne koristi u šablonu |
PDF_CACHE_DIR |
Ne | Folder keša (./invoices-local lokalno, /app/invoices u Dockeru) |
PDF_CACHE_DEFAULT_EXPIRES_IN |
Ne | Podrazumevani TTL keša u sekundama (21600) |
*Env ili telo zahteva mora da sadrži oba polja primaoca. Env ima prednost.
PRIMALAC=JP EPS BEOGRAD,BALKANSKA 13
RACUN_PRIMAOCCA=845-000000-040484987
POZIV_NA_BROJ=97163220000111111111000
PDF_CACHE_DEFAULT_EXPIRES_IN=21600{ "status": "ok" }| Polje | Tip | Obavezno | Opis |
|---|---|---|---|
payerName |
string |
Da | Platilac — zarezi postaju prelomi reda |
purposeOfPayment |
string |
Da | Svrha plaćanja |
type |
"1" | "2" | "3" |
Da | 1 → obrazac 1; 2 / 3 → obrazac 3 |
amount |
number |
Da | Iznos u RSD |
cyrilic |
boolean |
Ne | Ćirilica na PDF-u + lang=sr_RS QR; inače latinica |
paymentCode |
number |
Ne | Šifra plaćanja (podrazumevano 289) |
payeeName |
string |
Uslovno | Izostaviti ako je PRIMALAC u env-u |
payeeAccount |
string |
Uslovno | Izostaviti ako je RACUN_PRIMAOCCA u env-u |
expiresIn |
number |
Ne | TTL keša u sekundama (pozitivan ceo broj) |
200 → application/pdf (invoice.pdf) · 422 validacija · 502 NBS QR greška
payerName= platilac ·payeeName/PRIMALAC= primalac
Minimalan (primalac iz .env):
curl -X POST http://127.0.0.1:3000/generate-pdf \
-H "Content-Type: application/json" \
-d '{
"payerName": "Bogdan Bogdanović, Krunska 23, 11000 Beograd",
"purposeOfPayment": "Radionica",
"type": "3",
"amount": 5000,
"cyrilic": true
}' \
--output uplatnica.pdfPun (primalac u telu):
curl -X POST http://127.0.0.1:3000/generate-pdf \
-H "Content-Type: application/json" \
-d '{
"payerName": "MRĐO MAČKATOVIĆ,ŽUPSKA 13,BEOGRAD 6",
"payeeName": "JP EPS BEOGRAD,BALKANSKA 13",
"payeeAccount": "845-000000-040484987",
"purposeOfPayment": "UPLATA PO RAČUNU ZA EL. ENERGIJU",
"type": "3",
"amount": 3596.13,
"paymentCode": 289,
"cyrilic": false
}' \
--output uplatnica.pdfJavaScript:
const response = await fetch('http://127.0.0.1:3000/generate-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
payerName: 'Bogdan Bogdanović, Krunska 23, 11000 Beograd',
purposeOfPayment: 'Radionica',
type: '3',
amount: 5000,
cyrilic: true,
}),
});
if (!response.ok) throw new Error(JSON.stringify(await response.json()));
const pdf = Buffer.from(await response.arrayBuffer());Identični zahtevi koriste keš na disku ({hash}.pdf + {hash}.json) — bez ponovnog NBS poziva.
Hash uključuje: payerName, purposeOfPayment, type, amount, cyrilic, paymentCode, payeeName, payeeAccount, POZIV_NA_BROJ.
TTL: expiresIn → PDF_CACHE_DEFAULT_EXPIRES_IN → podrazumevano 21600 s.
POST https://nbs.rs/QRcode/api/qr/v1/generate/150 · 150×150 px · vidi src/services/qr-code.ts
| Skripta | Opis |
|---|---|
npm run start:dev |
Dev server (nodemon + TypeScript) |
npm run build |
Kompajliranje u build/src/ |
npm start |
Pokretanje produkcionog build-a |
npm test |
Unit testovi (72) |
npm run smoke |
Smoke-test README primera |
npm run lint / lint:fix |
oxlint |
npm run format / format:check |
oxfmt |
Tehnologije: hyper-express, VineJS, pdfmake. Docker image: node:22-bookworm-slim.
Apache License 2.0 © Stefan Bogdanović — pogledajte LICENSE