Integracija preko API-ja · 13 min čitanja
Fiskalizacija za custom sajt: Next.js, Laravel, WordPress, Webflow
Custom sajt ne treba plugin da bi izdavao fiskalne račune: dovoljan je jedan serverski poziv ka Tezga API-ju posle uspešne naplate. Tekst pokazuje gde se poziv stavlja u Next.js, Laravelu, WordPressu bez WooCommerce, Webflow-u i Frameru, sa isečcima koda, pravilima za tajne i zaštitom od duplog računa.
Custom sajt izdaje fiskalni račun tako što tvoj server, posle potvrde naplate, pošalje jedan POST zahtev na Tezga API sa stavkama, kupcem i načinom plaćanja. Tezga račun fiskalizuje preko V-PFR-a Poreske uprave i pošalje ga kupcu mejlom. Nije potreban plugin, štampač ni hardver, samo ključ koji nikad ne napušta server.
- Poziv ka Tezga API-ju ide isključivo sa servera (route handler, job, hook, n8n), nikad iz JavaScript-a u pregledaču.
- Jedna porudžbina nosi jedan
externalId; koliko god puta sajt ponovi zahtev, nastaje tačno jedan račun. - Račun se šalje tek kad je naplata potvrđena od procesora ili kad je uplata viđena na izvodu, ne kad kupac klikne „Naruči”.
- Odgovor 202 znači da je V-PFR trenutno nedostupan i da Tezga sama ponavlja; sajt ne pravi novi zahtev.
- Webflow i Framer nemaju server, pa formu šalju na n8n webhook, a Tezga izda predračun sa IPS QR kodom i račun kad uplata legne.
Gde se zove API i zašto nikad iz pregledača?
API ključ Tezga eKase ide u zaglavlje X-Tezga-Api-Key, počinje sa tzg_ i nosi 48 heksadecimalnih znakova. Ko ima ključ, može da izdaje fiskalne račune u ime tvoje firme. Ako ključ staviš u JavaScript koji se izvršava u pregledaču kupca, svaki posetilac ga vidi u alatu za programere i može da ga zloupotrebi. Zato važi jedno pravilo bez izuzetka: poziv ka https://pos.narbiz.com/api/integrations/order ide sa servera koji ti kontrolišeš.
Ključ živi samo na serveru sajta ili u n8n-u; pregledač kupca šalje porudžbinu serveru, a server zove Tezgu.
Ključ čuvaš kao promenljivu okruženja (TEZGA_API_KEY u .env, u tajnama hosting platforme ili u n8n credentialu), a ne u kodu i ne u git repozitorijumu. Napravi zaseban ključ za svaki sajt, jer možeš da ih imaš do 20 po firmi i jer se rotiraju na 90 dana; kad rotiraš ključ jednog sajta, drugi nastavljaju da rade. Opciono možeš da vežeš ključ za IP adresu servera i da uključiš HMAC potpis tela, pa zaglavlje X-Tezga-Signature: sha256=... dokazuje da telo nije menjano u prenosu. Sve opcije ključa su opisane na strani API za fiskalizaciju i u OpenAPI opisu.
Kako izgleda ceo tok od kupca do Poreske uprave?
Redosled je važniji od tehnologije. Račun se po Zakonu o fiskalizaciji izdaje u trenutku prometa, a promet nastaje isporukom ili naplatom, šta pre nastupi. Za online prodaju sa plaćanjem karticom to je trenutak kad procesor potvrdi naplatu; za uplatu na račun to je trenutak kad uplata legne; za pouzeće je to isporuka, kako je objašnjeno u tekstu fiskalna kasa i pouzeće. Sajt zato ne zove Tezgu na klik „Naruči”, nego na potvrdu naplate.
Sajt zove Tezgu tek posle koraka 3, kad je naplata potvrđena; kupac dobija račun mejlom u koraku 7, a sajt broj računa u koraku 8.
Korak 4 je jedini koji ti programiraš. Sve posle njega radi Tezga: fiskalizaciju preko V-PFR-a, slanje mejla sa PDF-om i QR kodom ako je posaljiMejl uključen, i odlazni webhook racun.fiskalizovan ako ga podesiš. Odgovor na korak 4 je 200 sa podacima dokumenta, 202 ako je PFR privremeno nedostupan (Tezga sama ponavlja), 400 za loše telo, 401 za loš ključ, 402 kad je kredit u paketu „Po računu” potrošen i 429 kad je premašen broj zahteva.
Tabela: gde se poziv stavlja u svakoj tehnologiji
| Tehnologija | Gde se zove Tezga | Gde živi ključ | Ključ idempotentnosti | Ponavljanje posle greške |
|---|---|---|---|---|
| Next.js | Route handler ili server action, posle callbacka procesora | Promenljiva okruženja na hostingu | Broj porudžbine iz baze | Red čekanja ili cron koji ponavlja porudžbine bez računa |
| Laravel | Job u redu čekanja pokrenut iz callbacka | .env i config/services.php | orders.id | Ugrađeno ponavljanje joba sa razmakom |
| WordPress bez WooCommerce | Hook posle potvrde plaćanja, wp_remote_post | wp-config.php konstanta | ID unosa forme ili post ID porudžbine | WP-Cron zadatak koji ponavlja |
| Webflow | n8n webhook koji prima formu | n8n credential | ID pošiljke forme | n8n ponavljanje čvora |
| Framer | n8n webhook koji prima formu | n8n credential | ID pošiljke forme | n8n ponavljanje čvora |
Ako tvoj sajt koristi WooCommerce ili Shopify, ne treba ti ništa od ovoga: Tezga ima dvosmernu vezu koja preuzima porudžbine webhookom i povlačenjem na 30 minuta, fiskalizuje na izabrani status i upisuje broj računa nazad u porudžbinu. Za to pogledaj WooCommerce fiskalizaciju i Shopify fiskalizaciju. Ovaj tekst je za sve ostalo.
Next.js: kako route handler izdaje račun?
U Next.js aplikaciji poziv ide u route handler (app/api/.../route.js) ili u server action, jer se oba izvršavaju na serveru i vide process.env. Klijentska komponenta nikad ne vidi ključ. Uobičajen raspored: procesor pošalje callback na tvoj route handler kad je naplata uspela, handler proveri potpis procesora, upiše porudžbinu kao plaćenu i onda pozove Tezgu.
// app/api/naplata-potvrdjena/route.js (izvršava se na serveru)
export async function POST(req) {
const p = await req.json(); // podaci porudžbine iz tvoje baze
const odgovor = await fetch(
"https://pos.narbiz.com/api/integrations/order?provider=custom",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Tezga-Api-Key": process.env.TEZGA_API_KEY
},
body: JSON.stringify({
externalId: "shop-" + p.brojPorudzbine,
tipDokumenta: "racun",
tok: "fiskalni",
nacinPlacanja: "Platna kartica",
posaljiMejl: true,
kupac: { naziv: p.ime, email: p.email },
stavke: p.stavke
})
}
);
if (odgovor.status === 202) {
return Response.json({ stanje: "ceka_pfr" }); // Tezga ponavlja sama
}
if (!odgovor.ok) {
throw new Error("Tezga " + odgovor.status); // upiši u dnevnik, ponovi kasnije
}
return Response.json(await odgovor.json());
}
Stavke iz baze moraju da imaju oblik koji API očekuje: naziv, sifra, kolicina, cenaSaPdv i poreskaStopa. Poresku oznaku uzmi jednom iz GET /api/integrations/tax-labels i sačuvaj uz artikal. Dostavu dodaj kao zasebnu stavku, jer račun mora da odgovara naplaćenom iznosu. Ako sajt radi na hostingu koji uspava funkcije, poziv ka Tezgi ne stavljaj u isti zahtev koji vraća odgovor procesoru; upiši porudžbinu kao „za fiskalizaciju” i pokreni je iz zasebne pozadinske funkcije ili crona koji svakih nekoliko minuta prođe kroz porudžbine bez broja računa. Isti externalId garantuje da ponovljeni prolaz ne izda drugi račun.
Laravel: zašto poziv ide u job u redu čekanja?
Laravel ima gotov red čekanja, pa ga iskoristi. Callback procesora samo upiše status i pošalje job; job zove Tezgu i ako dobije 5xx ili istek vremena, Laravel ga sam ponovi sa razmakom. Ključ upiši u .env kao TEZGA_API_KEY i izloži ga kroz config/services.php, da ga kod nikad ne čita direktno iz okruženja.
// app/Jobs/FiskalizujPorudzbinu.php
public int $tries = 5;
public array $backoff = [30, 120, 600, 1800];
public function handle(): void
{
$p = $this->porudzbina;
$odgovor = Http::withHeaders([
'X-Tezga-Api-Key' => config('services.tezga.key'),
])->timeout(20)->post(
'https://pos.narbiz.com/api/integrations/order?provider=custom',
[
'externalId' => 'shop-' . $p->id,
'tipDokumenta' => 'racun',
'tok' => $p->pib ? 'fiskalni_i_sef' : 'fiskalni',
'nacinPlacanja' => $p->nacin_placanja,
'posaljiMejl' => true,
'kupac' => ['naziv' => $p->ime, 'pib' => $p->pib, 'email' => $p->email],
'stavke' => $p->stavkeZaTezgu(),
]
);
if ($odgovor->status() === 202) { return; } // PFR nedostupan, Tezga ponavlja
if ($odgovor->status() === 400) { $this->fail(); return; } // loše telo, ne ponavljaj
$odgovor->throw(); // 5xx ide u ponovni pokušaj
$p->update(['tezga_odgovor' => $odgovor->json()]);
}
Dva detalja iz koda su bitna. Prvo, 400 ne ponavljaj: telo je loše i biće loše i sledeći put; pošalji sebi obaveštenje i ispravi podatke. Drugo, ako je kupac uneo PIB, račun postaje B2B: Tezga ga označi sa Id kupca 10:PIB, a ako ste oboje u sistemu e-faktura, tok sef ili fiskalni_i_sef šalje e-fakturu u SEF. Koji tok je pravi za tvoju firmu, pitaj knjigovođu; tehnički rade sva tri. Za refundaciju napravi drugi job koji zove POST /api/integrations/refund sa istim externalId originala i, po potrebi, spiskom stavki za delimičan povraćaj.
WordPress bez WooCommerce: kako hook izdaje račun?
Sajt na WordPressu sa formom za porudžbine (bilo koji plugin za forme) i plaćanjem preko procesora ili uplatom na račun. Tezga u paketu ima WordPress integraciju za WooCommerce; kad WooCommerce nemaš, poziv pišeš sam u temi ili malom pluginu. Pravilo je isto: hook se izvršava na serveru, ključ je konstanta u wp-config.php, a poziv ide kroz wp_remote_post.
// wp-config.php
define('TEZGA_API_KEY', 'tzg_...');
// functions.php ili mali plugin; okidač je tvoj događaj potvrde plaćanja
add_action('moj_sajt_placanje_potvrdjeno', function (int $porudzbina_id) {
if (get_post_meta($porudzbina_id, '_tezga_dokument', true)) {
return; // već fiskalizovano, ne šalji ponovo
}
$telo = [
'externalId' => 'wp-' . $porudzbina_id,
'tipDokumenta' => 'racun',
'tok' => 'fiskalni',
'nacinPlacanja' => get_post_meta($porudzbina_id, '_nacin_placanja', true),
'posaljiMejl' => true,
'kupac' => [
'naziv' => get_post_meta($porudzbina_id, '_ime', true),
'email' => get_post_meta($porudzbina_id, '_email', true),
],
'stavke' => get_post_meta($porudzbina_id, '_stavke', true),
];
$odgovor = wp_remote_post(
'https://pos.narbiz.com/api/integrations/order?provider=custom',
[
'timeout' => 20,
'headers' => [
'Content-Type' => 'application/json',
'X-Tezga-Api-Key' => TEZGA_API_KEY,
],
'body' => wp_json_encode($telo),
]
);
$kod = wp_remote_retrieve_response_code($odgovor);
if ($kod === 200) {
update_post_meta($porudzbina_id, '_tezga_dokument', wp_remote_retrieve_body($odgovor));
wp_mail(get_option('admin_email'), 'Račun izdat za porudžbinu ' . $porudzbina_id, 'Kupac je dobio račun mejlom.');
} elseif ($kod !== 202) {
wp_schedule_single_event(time() + 600, 'moj_sajt_placanje_potvrdjeno', [$porudzbina_id]);
}
});
Mejl kupcu ne šalješ ti, šalje ga Tezga sa PDF-om i QR kodom; wp_mail u primeru ide vlasniku, kao obaveštenje da spakuje paket. Ako se plaćanje potvrđuje preko IPN ili callback adrese procesora, tvoj događaj moj_sajt_placanje_potvrdjeno okidaš iz te adrese, posle provere potpisa procesora. Ako kupac plaća uplatom na račun, umesto računa izdaj predracun sa obaveznim pozivNaBroj, pa kasa sama izda fiskalni račun kad uplata stigne na izvod; postupak je opisan na strani novac, banka i naplata.
Webflow i Framer: kako no-code sajt izdaje račun?
Webflow i Framer nemaju tvoj server, pa nemaju gde da sakriju ključ. Rešenje je posrednik: forma sa sajta šalje sadržaj na webhook adresu n8n-a (ili druge automatizacije), a n8n zove Tezgu. Ključ živi u n8n credentialu, sajt ga nikad ne vidi. Kako se webhook za formu uključuje, proveri u dokumentaciji svoje platforme; obe umeju da pošalju sadržaj forme na spoljnu adresu.
No-code sajt bez procesora: forma ide u n8n, Tezga izda predračun sa IPS QR kodom, a fiskalni račun nastaje sam kad uplata stigne na izvod.
Za sajt bez platnog procesora ovo je najjednostavniji put: n8n pošalje Tezgi predračun, kupac dobije mejl sa NBS IPS QR kodom, plati mobilnim bankarstvom, a kasa upari uplatu po pozivu na broj sa izvoda i izda fiskalni račun bez ikakvog ručnog rada. Telo koje n8n šalje:
{
"externalId": "webflow-forma-5f31a9",
"tipDokumenta": "predracun",
"pozivNaBroj": "2026-000318",
"posaljiMejl": true,
"kupac": { "naziv": "Jelena Ilić", "email": "jelena@primer.rs" },
"stavke": [
{ "naziv": "Onlajn kurs akvarela, 6 lekcija", "sifra": "KURS-AKV",
"kolicina": 1, "cenaSaPdv": 7900, "poreskaStopa": "Ђ" }
]
}
Ako sajt ipak ima kartično plaćanje kroz procesor koji radi u Srbiji, tok je isti kao za Next.js, samo procesor šalje callback u n8n umesto u tvoj kod. Detaljna podešavanja n8n čvorova i sedam gotovih tokova ima u tekstu Tezga i n8n.
Kako se čuva idempotentnost kad procesor pošalje callback dva puta?
Platni procesori šalju callback više puta kad ne dobiju odgovor na vreme, mreža ponavlja zahteve, a korisnici osvežavaju strane. Bez zaštite bi svaki od tih ponovljenih događaja izdao novi fiskalni račun, a storno svakog viška je posao za tebe i knjigovođu. Zaštita ima dva sloja.
- Tvoj sloj. Pre poziva proveri da li porudžbina već ima upisan dokument (kao u WordPress primeru). Callback obradi u transakciji ili sa zaključavanjem reda porudžbine, da dva istovremena callbacka ne prođu proveru u istom trenutku.
- Sloj Tezge. Polje
externalId(do 200 znakova) je ključ idempotentnosti: ista porudžbina se fiskalizuje tačno jednom, čak i kad dva zahteva stignu istovremeno. ZatoexternalIdizvedi iz broja porudžbine, ne iz vremena ili nasumičnog niza.
Isto pravilo važi za refundaciju: POST /api/integrations/refund traži original po documentId ili externalId, a kad PFR padne, vraća 202 i ponovljeni zahtev ne pravi dupli dokument. Ako nekad ipak nastane greška u računu, ne pokušavaj da je „prepišeš”: izdaje se storno ili refundacija, kako je objašnjeno na strani storno i refundacija. Deset najčešćih grešaka iz agencijske prakse, uključujući i ovu, opisano je u tekstu 10 grešaka web agencija pri fiskalizaciji.
Šta kupac dobija i kako se test prebacuje u produkciju?
Kupac dobija mejl sa PDF-om fiskalnog računa i QR kodom kojim račun može da proveri na portalu Poreske uprave. Prema tumačenju Poreske uprave za daljinsku prodaju, fiskalni račun kupcu na daljinu može da se dostavi elektronski, uz saglasnost kupca, pa štampani primerak u paketu nije obavezan ako je kupac dobio elektronski; uputstva su na purs.gov.rs. Saglasnost rešavaš kvadratićem u formi porudžbine ili odredbom u uslovima kupovine. Ako želiš, iz GET /api/integrations/documents/{id} uzmi javniPdfUrl i prikaži ga kupcu na strani „Hvala na porudžbini”.
Razvoj radiš na testnom nalogu koji Go Simple otvara na zahtev: njegovi računi idu u sandbox Poreske uprave i nisu fiskalni. Okruženje je osobina naloga, ne ključa, pa prelazak u produkciju znači zamenu ključa u okruženju i ništa više u kodu. Alternativa je tip računa „Обука” na produkciji, koji nosi napomenu „ОВО НИЈЕ ФИСКАЛНИ РАЧУН”, pogodan za probu kad je nalog već pravi. Ceo postupak od testnog naloga do predaje klijentu, sa kontrolnom listom, opisan je u tekstu sandbox za fiskalizaciju, a pogled iz ugla programera koji je kasu pravio u tekstu fiskalizacija iz sopstvene aplikacije.
Pre puštanja proveri četiri stvari: (1) ključ nije u repozitorijumu ni u klijentskom bundlu, (2) poziv ide posle potvrde naplate, (3) ponovljeni callback ne pravi drugi račun, (4) 202 se ne tretira kao greška. Ako sajt prodaje i u fizičkoj radnji, za prodaju na licu mesta Zakon o fiskalizaciji traži L-PFR u objektu; V-PFR je dozvoljen za online prodaju i prodaju na daljinu, a sopstveni L-PFR Tezge je u pripremi.
Izvori: Zakon o fiskalizaciji (Sl. glasnik RS 153/2020, 96/2021, 138/2022); Pravilnik o vrstama fiskalnih računa, tipovima transakcija, načinima plaćanja; Tehničko uputstvo za ESIR i tumačenja Poreske uprave za daljinsku prodaju (purs.gov.rs); Zakon o zaštiti potrošača (Sl. glasnik RS 35/2026); dokumentacija SEF-a (efaktura.mfin.gov.rs); NBS IPS QR (ips.nbs.rs); uputstvo za integratore i OpenAPI opis Tezga eKase, ИБ 1597. Tekst nije pravni savet; za svoju situaciju proveri sa knjigovođom.
