Webhook Notifikacije
eFiskalizacija.cloud automatski šalje POST notifikacije na vaš URL nakon svake fiskalizacije - bilo uspešne ili neuspele.
Aktiviranje webhooks-a
Webhooks se aktiviraju u admin panelu:
- Ulogujte se na admin panel
- Idite na Podešavanja → Webhooks
- Unesite vaš HTTPS URL endpoint
- Izaberite eventi koje želite primati
- Kliknite Sačuvaj
https://. HTTP nije dozvoljen iz bezbednosnih razloga.
Dostupni Eventi
| Event | Opis | Kada se šalje |
|---|---|---|
invoice.fiscalized |
Uspešna fiskalizacija | Kada VSDC vrati uspešan odgovor |
invoice.failed |
Neuspela fiskalizacija | Kada VSDC vrati grešku ili timeout |
Payload struktura
Webhook payload je JSON objekt sa sledećom strukturom:
Uspešna fiskalizacija (invoice.fiscalized)
{
"event": "invoice.fiscalized",
"timestamp": "2026-01-25T14:30:00+01:00",
"data": {
"invoice_id": 12345,
"pfr_number": "AB12CD34-Ef5Gh6i7-101",
"invoice_number": "SHOP-2026-001",
"invoice_type": "promet",
"transaction_type": "prodaja",
"total_amount": "11900.00",
"payment_type": "kartica",
"buyer": {
"name": "Petar Petrović",
"identifier": "10:123456789",
"address": "Knez Mihailova 10, Beograd"
},
"items": [
{
"name": "Laptop HP ProBook",
"quantity": 1,
"unit_price": "100000.00",
"vat_rate": 20,
"vat_amount": "1666.67",
"total": "10000.00"
}
],
"qr_code_url": "https://suf.purs.gov.rs/v/?...",
"verification_url": "https://suf.purs.gov.rs/v/?...",
"fiscalized_at": "2026-01-25T14:30:00+01:00",
"reference_number": null
}
}
Neuspela fiskalizacija (invoice.failed)
{
"event": "invoice.failed",
"timestamp": "2026-01-25T14:30:00+01:00",
"data": {
"invoice_id": 12346,
"invoice_number": "SHOP-2026-002",
"invoice_type": "promet",
"transaction_type": "prodaja",
"total_amount": "5900.00",
"error_code": "2100",
"error_message": "Sertifikat je istekao",
"failed_at": "2026-01-25T14:30:00+01:00"
}
}
HTTP Headers
Svaki webhook zahtev sadrži sledeće headere:
| Header | Vrednost | Opis |
|---|---|---|
Content-Type |
application/json; charset=utf-8 |
JSON payload |
X-Webhook-Event |
invoice.fiscalized ili invoice.failed |
Tip eventa |
X-Webhook-Delivery-Attempt |
1, 2, ili 3 |
Redni broj pokušaja |
User-Agent |
eFiskalizacija-Webhook/1.2.0 |
Identifikacija servisa |
Očekivani odgovor
Vaš endpoint treba da vrati HTTP status kod 2xx (200-299) da bi isporuka bila označena kao uspešna.
4xx (osim 429), isporuka će biti označena kao trajno neuspela bez ponovnog pokušaja.
Retry logika
| Pokušaj | Čekanje | Ukupno |
|---|---|---|
| 1 | - | Odmah |
| 2 | 5 sekundi | 5s |
| 3 | 25 sekundi | 30s |
Maksimalno 3 pokušaja sa eksponencijalnim backoff-om (5s × 5n-1).
Zaštita endpointa
Preporučujemo da zaštitite vaš endpoint na jedan od sledećih načina:
1. Query parametar (preporučeno)
Dodajte tajni ključ u URL kao query parametar:
https://vasadomena.rs/webhook?secret=vas_tajni_kljuc_123
Zatim u vašem endpointu proverite:
<?php
$expectedSecret = getenv('WEBHOOK_SECRET');
$receivedSecret = $_GET['secret'] ?? '';
if (!hash_equals($expectedSecret, $receivedSecret)) {
http_response_code(403);
exit('Forbidden');
}
2. IP Whitelist
eFiskalizacija.cloud šalje webhooks sa fiksne IP adrese. Kontaktirajte nas za detalje.
Primeri implementacije
<?php
declare(strict_types=1);
// 1. Verifikacija tajnog ključa
$expectedSecret = getenv('WEBHOOK_SECRET');
$receivedSecret = $_GET['secret'] ?? '';
if (!hash_equals($expectedSecret, $receivedSecret)) {
http_response_code(403);
exit;
}
// 2. Parsiranje payload-a
$payload = json_decode(file_get_contents('php://input'), true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
exit;
}
// 3. Obrada eventa
$event = $payload['event'] ?? '';
$data = $payload['data'] ?? [];
switch ($event) {
case 'invoice.fiscalized':
// Ažuriraj narudžbinu kao fiskalizovanu
updateOrder($data['invoice_number'], [
'pfr_number' => $data['pfr_number'],
'fiscalized_at' => $data['fiscalized_at'],
'qr_code_url' => $data['qr_code_url'],
]);
// Pošalji email kupcu sa fiskalnim računom
sendFiscalReceiptEmail($data['buyer']['email'] ?? null, $data);
break;
case 'invoice.failed':
// Logiraj grešku za ručnu obradu
logFiscalizationError($data['invoice_number'], $data['error_message']);
// Obavesti administratora
notifyAdmin('Fiskalizacija neuspela', $data);
break;
}
// 4. Uspešan odgovor
http_response_code(200);
echo json_encode(['status' => 'ok']);
from flask import Flask, request, jsonify
import hmac
import os
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
# 1. Verifikacija tajnog ključa
expected_secret = os.getenv('WEBHOOK_SECRET')
received_secret = request.args.get('secret', '')
if not hmac.compare_digest(expected_secret, received_secret):
return jsonify({'error': 'Forbidden'}), 403
# 2. Parsiranje payload-a
payload = request.get_json()
if not payload:
return jsonify({'error': 'Bad Request'}), 400
# 3. Obrada eventa
event = payload.get('event')
data = payload.get('data', {})
if event == 'invoice.fiscalized':
# Ažuriraj narudžbinu
update_order(data['invoice_number'], {
'pfr_number': data['pfr_number'],
'fiscalized_at': data['fiscalized_at'],
})
elif event == 'invoice.failed':
# Logiraj grešku
log_error(data['invoice_number'], data['error_message'])
return jsonify({'status': 'ok'}), 200
if __name__ == '__main__':
app.run()
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
// 1. Verifikacija tajnog ključa
const expectedSecret = process.env.WEBHOOK_SECRET;
const receivedSecret = req.query.secret || '';
if (!crypto.timingSafeEqual(
Buffer.from(expectedSecret),
Buffer.from(receivedSecret)
)) {
return res.status(403).json({ error: 'Forbidden' });
}
// 2. Parsiranje payload-a
const { event, data } = req.body;
if (!event || !data) {
return res.status(400).json({ error: 'Bad Request' });
}
// 3. Obrada eventa
switch (event) {
case 'invoice.fiscalized':
// Ažuriraj narudžbinu
updateOrder(data.invoice_number, {
pfrNumber: data.pfr_number,
fiscalizedAt: data.fiscalized_at,
});
break;
case 'invoice.failed':
// Logiraj grešku
logError(data.invoice_number, data.error_message);
break;
}
res.json({ status: 'ok' });
});
app.listen(3000);
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.Text;
[ApiController]
[Route("webhook")]
public class WebhookController : ControllerBase
{
private readonly IConfiguration _config;
private readonly IOrderService _orderService;
public WebhookController(IConfiguration config, IOrderService orderService)
{
_config = config;
_orderService = orderService;
}
[HttpPost]
public IActionResult HandleWebhook([FromQuery] string secret, [FromBody] WebhookPayload payload)
{
// 1. Verifikacija tajnog ključa
var expectedSecret = _config["WEBHOOK_SECRET"];
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expectedSecret),
Encoding.UTF8.GetBytes(secret ?? "")))
{
return Forbid();
}
// 2. Obrada eventa
switch (payload.Event)
{
case "invoice.fiscalized":
_orderService.UpdateOrder(payload.Data.InvoiceNumber, new OrderUpdate
{
PfrNumber = payload.Data.PfrNumber,
FiscalizedAt = payload.Data.FiscalizedAt
});
break;
case "invoice.failed":
_orderService.LogError(payload.Data.InvoiceNumber, payload.Data.ErrorMessage);
break;
}
return Ok(new { status = "ok" });
}
}
public record WebhookPayload(string Event, WebhookData Data);
public record WebhookData(
int InvoiceId, string PfrNumber, string InvoiceNumber,
string FiscalizedAt, string ErrorMessage);
Testiranje
U admin panelu možete:
- Poslati test webhook - Šalje simulirani payload na vaš URL
- Pregledati istoriju - Vidite sve isporuke sa statusom i trajanjem
- Videti primer payload-a - Kopirajte strukturu za razvoj
Često postavljana pitanja
Koliko dugo se čuvaju webhook podaci?
Istorija isporuka se čuva 30 dana. Nakon toga se automatski briše.
Šta ako moj server ne odgovori?
Pokušaćemo još 2 puta sa rastućim čekanjem (5s, 25s). Nakon 3 neuspela pokušaja, isporuka se označava kao neuspela.
Da li mogu primati webhooks na HTTP (ne HTTPS)?
Ne. Iz bezbednosnih razloga, webhook URL mora počinjati sa https://.
Kako da testiram lokalno?
Koristite servise kao što su ngrok ili webhook.site za izlaganje lokalnog servera na internet.