eFiskalizacija.cloud

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:

  1. Ulogujte se na admin panel
  2. Idite na Podešavanja → Webhooks
  3. Unesite vaš HTTPS URL endpoint
  4. Izaberite eventi koje želite primati
  5. Kliknite Sačuvaj
Napomena: URL mora počinjati sa https://. HTTP nije dozvoljen iz bezbednosnih razloga.

Dostupni Eventi

EventOpisKada 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:

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

Važno: Ako vaš endpoint vrati 4xx (osim 429), isporuka će biti označena kao trajno neuspela bez ponovnog pokušaja.

Retry logika

PokušajČekanjeUkupno
1-Odmah
25 sekundi5s
325 sekundi30s

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 Native
<?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']);
Python Flask
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()
Node.js Express
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);
C# ASP.NET Core
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:

  1. Poslati test webhook - Šalje simulirani payload na vaš URL
  2. Pregledati istoriju - Vidite sve isporuke sa statusom i trajanjem
  3. 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.