eFiskalizacija.cloud

Vodič za integraciju

Detaljan vodič za integraciju eFiskalizacija API-ja u vaš web šop ili e-commerce platformu.

Preduslovi

Registracija

Za pristup platformi kontaktirajte nas na info@efiskalizacija.cloud. Nakon registracije dobićete:

  1. API Key - javni identifikator vašeg naloga
  2. API Secret - tajni ključ za HMAC potpisivanje (prikazuje se samo jednom)
  3. Sandbox pristup - za testiranje pre produkcije
Napomena: API Secret se prikazuje samo jednom prilikom generisanja. Sačuvajte ga na sigurnom mestu.

Implementacija korak-po-korak

1. Konfigurisanje kredencijala

Sačuvajte kredencijale u environment varijable (nikada u izvorni kôd):

EFISK_API_KEY=vaš_api_ključ
EFISK_API_SECRET=vaš_tajni_ključ
EFISK_API_URL=https://efiskalizacija.cloud/api/multitenant.php

2. HMAC potpisivanje zahteva

Svaki API zahtev mora sadržati HMAC-SHA256 potpis u headerima:

HeaderOpis
X-Api-KeyVaš API ključ
X-TimestampUnix timestamp (tolerancija +-5 min)
X-SignatureHMAC-SHA256 potpis

Klijent klase

Izaberite programski jezik za kompletnu klijent klasu sa svim metodama:

PHP cURL
<?php
declare(strict_types=1);

class EfiskalizacijaClient
{
    private string $apiKey;
    private string $apiSecret;
    private string $baseUrl;

    public function __construct(string $apiKey, string $apiSecret, string $baseUrl)
    {
        $this->apiKey = $apiKey;
        $this->apiSecret = $apiSecret;
        $this->baseUrl = rtrim($baseUrl, '/');
    }

    public function fiskalizuj(array $podaci): array
    {
        return $this->request('POST', '/fiskalizacija', $podaci);
    }

    public function status(): array
    {
        return $this->request('GET', '/status');
    }

    public function preuzmiPdf(string $pfrBroj): array
    {
        return $this->request('GET', '/pdf?pfr=' . urlencode($pfrBroj));
    }

    private function request(string $method, string $endpoint, array $body = []): array
    {
        $url = $this->baseUrl . $endpoint;
        $path = parse_url($url, PHP_URL_PATH);
        $timestamp = (string) time();
        $bodyJson = $method === 'POST' ? json_encode($body, JSON_UNESCAPED_UNICODE) : '';

        // HMAC potpis (isti algoritam kao u autentifikacija.php)
        $bodyHash = hash('sha256', $bodyJson);
        $stringToSign = "{$timestamp}{$method}{$path}{$bodyHash}";
        $signature = base64_encode(hash_hmac('sha256', $stringToSign, $this->apiSecret, true));

        $headers = [
            'Content-Type: application/json',
            'X-API-Key: ' . $this->apiKey,
            'X-Timestamp: ' . $timestamp,
            'X-Signature: ' . $signature,
        ];

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_TIMEOUT => 30,
        ]);

        if ($method === 'POST' && $bodyJson) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $bodyJson);
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        $result = json_decode($response, true);

        if ($httpCode !== 200) {
            throw new RuntimeException(
                'API greška: ' . ($result['error'] ?? 'Nepoznata greška'),
                $httpCode
            );
        }

        return $result;
    }
}

// Korišćenje:
$client = new EfiskalizacijaClient(
    getenv('EFISK_API_KEY'),
    getenv('EFISK_API_SECRET'),
    'https://efiskalizacija.cloud/api/multitenant.php'
);

$result = $client->fiskalizuj([
    'stavke' => [
        ['naziv' => 'Laptop', 'kolicina' => 1, 'jedinicna_cena' => 89990, 'pdv_stopa' => 20, 'rabat_procenat' => 5]
    ],
    'nacin_placanja' => 'kartica'
]);

echo "PFR: " . $result['data']['rezultat']['pfr_broj'];
Python requests
import hmac
import hashlib
import base64
import time
import requests
import json
import os
from urllib.parse import urlparse

class EfiskalizacijaClient:
    def __init__(self, api_key: str, api_secret: str, base_url: str):
        self.api_key = api_key
        self.api_secret = api_secret
        self.base_url = base_url.rstrip('/')

    def fiskalizuj(self, podaci: dict) -> dict:
        return self._request('POST', '/fiskalizacija', podaci)

    def status(self) -> dict:
        return self._request('GET', '/status')

    def preuzmi_pdf(self, pfr_broj: str) -> dict:
        return self._request('GET', f'/pdf?pfr={pfr_broj}')

    def _request(self, method: str, endpoint: str, body: dict = None) -> dict:
        url = f"{self.base_url}{endpoint}"
        path = urlparse(url).path
        timestamp = str(int(time.time()))
        body_json = json.dumps(body, ensure_ascii=False) if body else ''

        # HMAC potpis
        body_hash = hashlib.sha256(body_json.encode()).hexdigest()
        string_to_sign = f"{timestamp}{method}{path}{body_hash}"
        signature = base64.b64encode(
            hmac.new(
                self.api_secret.encode(),
                string_to_sign.encode(),
                hashlib.sha256
            ).digest()
        ).decode()

        headers = {
            'Content-Type': 'application/json',
            'X-API-Key': self.api_key,
            'X-Timestamp': timestamp,
            'X-Signature': signature,
        }

        response = requests.request(
            method, url, headers=headers,
            data=body_json if body else None, timeout=30
        )
        response.raise_for_status()
        return response.json()


# Korišćenje:
client = EfiskalizacijaClient(
    os.getenv('EFISK_API_KEY'),
    os.getenv('EFISK_API_SECRET'),
    'https://efiskalizacija.cloud/api/multitenant.php'
)

result = client.fiskalizuj({
    'stavke': [
        {'naziv': 'Laptop', 'kolicina': 1, 'jedinicna_cena': 89990, 'pdv_stopa': 20}
    ],
    'nacin_placanja': 'kartica'
})

print(f"PFR: {result['data']['rezultat']['pfr_broj']}")
JavaScript / Node.js crypto
const crypto = require('crypto');
const https = require('https');

class EfiskalizacijaClient {
    constructor(apiKey, apiSecret, baseUrl) {
        this.apiKey = apiKey;
        this.apiSecret = apiSecret;
        this.baseUrl = baseUrl.replace(/\/$/, '');
    }

    async fiskalizuj(podaci) {
        return this.request('POST', '/fiskalizacija', podaci);
    }

    async status() {
        return this.request('GET', '/status');
    }

    async preuzmiPdf(pfrBroj) {
        return this.request('GET', `/pdf?pfr=${encodeURIComponent(pfrBroj)}`);
    }

    async request(method, endpoint, body = null) {
        const url = new URL(this.baseUrl + endpoint);
        const path = url.pathname;
        const timestamp = Math.floor(Date.now() / 1000).toString();
        const bodyJson = body ? JSON.stringify(body) : '';

        // HMAC potpis
        const bodyHash = crypto.createHash('sha256').update(bodyJson).digest('hex');
        const stringToSign = `${timestamp}${method}${path}${bodyHash}`;
        const signature = crypto
            .createHmac('sha256', this.apiSecret)
            .update(stringToSign)
            .digest('base64');

        const options = {
            method,
            hostname: url.hostname,
            path: url.pathname + url.search,
            headers: {
                'Content-Type': 'application/json',
                'X-API-Key': this.apiKey,
                'X-Timestamp': timestamp,
                'X-Signature': signature,
            },
            timeout: 30000,
        };

        return new Promise((resolve, reject) => {
            const req = https.request(options, (res) => {
                let data = '';
                res.on('data', chunk => data += chunk);
                res.on('end', () => {
                    if (res.statusCode !== 200) {
                        reject(new Error(`HTTP ${res.statusCode}: ${data}`));
                    } else {
                        resolve(JSON.parse(data));
                    }
                });
            });
            req.on('error', reject);
            if (bodyJson) req.write(bodyJson);
            req.end();
        });
    }
}

// Korišćenje:
const client = new EfiskalizacijaClient(
    process.env.EFISK_API_KEY,
    process.env.EFISK_API_SECRET,
    'https://efiskalizacija.cloud/api/multitenant.php'
);

(async () => {
    const result = await client.fiskalizuj({
        stavke: [
            { naziv: 'Laptop', kolicina: 1, jedinicna_cena: 89990, pdv_stopa: 20 }
        ],
        nacin_placanja: 'kartica'
    });
    console.log(`PFR: ${result.data.rezultat.pfr_broj}`);
})();

module.exports = EfiskalizacijaClient;
C# / .NET HttpClient
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

public class EfiskalizacijaClient : IDisposable
{
    private readonly string _apiKey;
    private readonly string _apiSecret;
    private readonly HttpClient _httpClient;

    public EfiskalizacijaClient(string apiKey, string apiSecret, string baseUrl)
    {
        _apiKey = apiKey;
        _apiSecret = apiSecret;
        _httpClient = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/')) };
    }

    public Task<JsonDocument> FiskalizujAsync(object podaci)
        => RequestAsync(HttpMethod.Post, "/fiskalizacija", podaci);

    public Task<JsonDocument> StatusAsync()
        => RequestAsync(HttpMethod.Get, "/status");

    public Task<JsonDocument> PreuzmiPdfAsync(string pfrBroj)
        => RequestAsync(HttpMethod.Get, $"/pdf?pfr={Uri.EscapeDataString(pfrBroj)}");

    private async Task<JsonDocument> RequestAsync(
        HttpMethod method, string endpoint, object body = null)
    {
        var path = new Uri(_httpClient.BaseAddress + endpoint).AbsolutePath;
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var bodyJson = body != null ? JsonSerializer.Serialize(body) : "";

        // HMAC potpis
        var bodyHash = ComputeSha256Hash(bodyJson);
        var stringToSign = $"{timestamp}{method.Method}{path}{bodyHash}";
        var signature = ComputeHmacSignature(stringToSign);

        var request = new HttpRequestMessage(method, endpoint);
        if (body != null)
        {
            request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json");
        }

        request.Headers.Add("X-API-Key", _apiKey);
        request.Headers.Add("X-Timestamp", timestamp);
        request.Headers.Add("X-Signature", signature);

        var response = await _httpClient.SendAsync(request);
        var content = await response.Content.ReadAsStringAsync();

        if (!response.IsSuccessStatusCode)
        {
            throw new HttpRequestException($"API greška {(int)response.StatusCode}: {content}");
        }

        return JsonDocument.Parse(content);
    }

    private string ComputeSha256Hash(string input)
    {
        using var sha256 = SHA256.Create();
        var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
        return BitConverter.ToString(bytes).Replace("-", "").ToLower();
    }

    private string ComputeHmacSignature(string input)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(input));
        return Convert.ToBase64String(hash);
    }

    public void Dispose() => _httpClient?.Dispose();
}

// Korišćenje:
// using var client = new EfiskalizacijaClient(
//     Environment.GetEnvironmentVariable("EFISK_API_KEY"),
//     Environment.GetEnvironmentVariable("EFISK_API_SECRET"),
//     "https://efiskalizacija.cloud/api/multitenant.php"
// );
//
// var result = await client.FiskalizujAsync(new {
//     stavke = new[] {
//         new { naziv = "Laptop", kolicina = 1, jedinicna_cena = 89990, pdv_stopa = 20 }
//     },
//     nacin_placanja = "kartica"
// });
//
// Console.WriteLine($"PFR: {result.RootElement.GetProperty("data")
//     .GetProperty("rezultat").GetProperty("pfr_broj").GetString()}");
Java / Spring Boot RestTemplate
package rs.efiskalizacija.client;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Base64;

public class EfiskalizacijaClient {

    private final String apiKey;
    private final String apiSecret;
    private final String baseUrl;
    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;

    public EfiskalizacijaClient(String apiKey, String apiSecret, String baseUrl) {
        this.apiKey = apiKey;
        this.apiSecret = apiSecret;
        this.baseUrl = baseUrl.replaceAll("/$", "");
        this.restTemplate = new RestTemplate();
        this.objectMapper = new ObjectMapper();
    }

    public JsonNode fiskalizuj(Object podaci) throws Exception {
        return request(HttpMethod.POST, "/fiskalizacija", podaci);
    }

    public JsonNode status() throws Exception {
        return request(HttpMethod.GET, "/status", null);
    }

    public JsonNode preuzmiPdf(String pfrBroj) throws Exception {
        return request(HttpMethod.GET, "/pdf?pfr=" +
            java.net.URLEncoder.encode(pfrBroj, StandardCharsets.UTF_8), null);
    }

    private JsonNode request(HttpMethod method, String endpoint, Object body) throws Exception {
        String url = baseUrl + endpoint;
        String path = new URI(url).getPath();
        String timestamp = String.valueOf(Instant.now().getEpochSecond());
        String bodyJson = body != null ? objectMapper.writeValueAsString(body) : "";

        // HMAC potpis
        String bodyHash = sha256Hash(bodyJson);
        String stringToSign = timestamp + method.name() + path + bodyHash;
        String signature = hmacSignature(stringToSign);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("X-API-Key", apiKey);
        headers.set("X-Timestamp", timestamp);
        headers.set("X-Signature", signature);

        HttpEntity<String> entity = new HttpEntity<>(
            body != null ? bodyJson : null, headers
        );

        ResponseEntity<String> response = restTemplate.exchange(
            url, method, entity, String.class
        );

        if (!response.getStatusCode().is2xxSuccessful()) {
            throw new RuntimeException("API greška " +
                response.getStatusCodeValue() + ": " + response.getBody());
        }

        return objectMapper.readTree(response.getBody());
    }

    private String sha256Hash(String input) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
        StringBuilder sb = new StringBuilder();
        for (byte b : hash) sb.append(String.format("%02x", b));
        return sb.toString();
    }

    private String hmacSignature(String input) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(
            apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"
        ));
        return Base64.getEncoder().encodeToString(
            mac.doFinal(input.getBytes(StandardCharsets.UTF_8))
        );
    }
}

// Korišćenje:
// EfiskalizacijaClient client = new EfiskalizacijaClient(
//     System.getenv("EFISK_API_KEY"),
//     System.getenv("EFISK_API_SECRET"),
//     "https://efiskalizacija.cloud/api/multitenant.php"
// );
//
// Map<String, Object> stavka = Map.of(
//     "naziv", "Laptop", "kolicina", 1,
//     "jedinicna_cena", 89990, "pdv_stopa", 20
// );
// Map<String, Object> podaci = Map.of(
//     "stavke", List.of(stavka),
//     "nacin_placanja", "kartica"
// );
//
// JsonNode result = client.fiskalizuj(podaci);
// System.out.println("PFR: " + result.get("data")
//     .get("rezultat").get("pfr_broj").asText());

Obrada grešaka i retry logika

API može vratiti sledeće HTTP kodove grešaka:

KôdZnačenjeAkcija
400Neispravan zahtevProverite parametre
401NeautorizovanProverite HMAC potpis
429Previše zahtevaSačekajte i pokušajte ponovo
500Greška serveraRetry sa exponential backoff
503VSDC nedostupanRetry nakon 30 sekundi
Važno: Za kodove 500 i 503 koristite exponential backoff strategiju: 1s, 2s, 4s, 8s, maksimalno 3 pokušaja.

Webhook notifikacije

Sistem automatski šalje POST zahteve na vaš webhook URL nakon fiskalizacije:

Webhook URL konfigurišete u admin panelu: Podešavanja → Webhooks.

Napomena: Webhook mora vratiti HTTP 2xx u roku od 10 sekundi. Neuspešne isporuke se ponavljaju sa exponential backoff (5s, 25s, 125s).
Sledeći koraci: Pogledajte konfiguraciju za detaljnija podešavanja ili PDF račune za preuzimanje fiskalnih dokumenata.