Vodič za integraciju
Detaljan vodič za integraciju eFiskalizacija API-ja u vaš web šop ili e-commerce platformu.
Preduslovi
- Registrovan nalog na eFiskalizacija.cloud
- PFX sertifikat izdat od Poreske uprave (više u vodiču za sertifikate)
- API kredencijali (ključ i tajni ključ)
- HTTPS podrška na vašem serveru
- Programski jezik koji podržava HMAC-SHA256 (PHP, Python, Node.js, itd.)
Registracija
Za pristup platformi kontaktirajte nas na info@efiskalizacija.cloud. Nakon registracije dobićete:
- API Key - javni identifikator vašeg naloga
- API Secret - tajni ključ za HMAC potpisivanje (prikazuje se samo jednom)
- 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:
| Header | Opis |
|---|---|
X-Api-Key | Vaš API ključ |
X-Timestamp | Unix timestamp (tolerancija +-5 min) |
X-Signature | HMAC-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ôd | Značenje | Akcija |
|---|---|---|
| 400 | Neispravan zahtev | Proverite parametre |
| 401 | Neautorizovan | Proverite HMAC potpis |
| 429 | Previše zahteva | Sačekajte i pokušajte ponovo |
| 500 | Greška servera | Retry sa exponential backoff |
| 503 | VSDC nedostupan | Retry 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:
invoice.fiscalized- uspešna fiskalizacija računainvoice.failed- neuspešna fiskalizacija (VSDC greška)
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.