е-Фактура API
Webhooks
Создавање endpoint
curl -X POST "https://api.efaktura.merot.com/v1/webhook-endpoints" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/merot-efaktura",
"events": [
"einvoice.submitted",
"einvoice.rejected",
"inbox.received",
"inbox.deadline_approaching"
]
}' const response = await fetch("https://api.efaktura.merot.com/v1/webhook-endpoints", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://example.com/hooks/merot-efaktura",
"events": [
"einvoice.submitted",
"einvoice.rejected",
"inbox.received",
"inbox.deadline_approaching"
]
}),
});
const data = await response.json();
console.log(data); import requests
response = requests.post(
"https://api.efaktura.merot.com/v1/webhook-endpoints",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"url": "https://example.com/hooks/merot-efaktura",
"events": [
"einvoice.submitted",
"einvoice.rejected",
"inbox.received",
"inbox.deadline_approaching"
]
},
)
print(response.json()) <?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('POST', 'https://api.efaktura.merot.com/v1/webhook-endpoints', [
'headers' => [
'Authorization' => 'Bearer YOUR_API_KEY',
'Content-Type' => 'application/json',
],
'json' => [
'url' => 'https://example.com/hooks/merot-efaktura',
'events' => [
'einvoice.submitted',
'einvoice.rejected',
'inbox.received',
'inbox.deadline_approaching',
],
],
]);
echo $response->getBody(); using System.Net.Http;
using System.Text;
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.efaktura.merot.com/v1/webhook-endpoints");
request.Headers.Add("Authorization", "Bearer YOUR_API_KEY");
var json = /* lang=json */ @"{
""url"": ""https://example.com/hooks/merot-efaktura"",
""events"": [
""einvoice.submitted"",
""einvoice.rejected"",
""inbox.received"",
""inbox.deadline_approaching""
]
}";
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body); import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.efaktura.merot.com/v1/webhook-endpoints"))
.headers("Authorization", "Bearer YOUR_API_KEY", "Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(/* JSON */ "{ \"url\": \"https://example.com/hooks/merot-efaktura\", \"events\": [ \"einvoice.submitted\", \"einvoice.rejected\", \"inbox.received\", \"inbox.deadline_approaching\" ] }"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body()); Тајниот клуч secret (whsec_...) во одговорот се прикажува само еднаш — зачувајте го.
Ротирањето на тајниот клуч го задржува стариот важечки за потпишување уште 24 часа, така што и
двата се прифатени додека го внесувате новиот.
Настани
| Настан | Кога се испраќа |
|---|---|
einvoice.validated_failed | Издавањето или валидацијата не поминале според правилата на УЈП. |
einvoice.signature_required | Барање за потпишување чека на вашиот агент. |
einvoice.submitted | УЈП доделил EUID — status: "delivered". |
einvoice.accepted / einvoice.auto_accepted | Купувачот прифатил, експлицитно или по молчење. |
einvoice.rejected | Купувачот одбил, со причина. |
einvoice.cancelled_by_storno / einvoice.corrected | Поднесено е поврзано сторно/корекција. |
einvoice.failed | Крајна грешка. |
inbox.received | Пристигнала нова влезна е-фактура. |
inbox.deadline_approaching | Останати се 5, 2 или 1 ден до крајниот рок (видете Животен циклус на документ). |
inbox.auto_accepted | Рокот поминал без одлука. |
signing_agent.offline / signing_agent.online | Се променила состојбата на конекцијата на вашиот Bridge/SDK-агент. |
archive.export_ready | Групен извоз од архивата завршил. |
Испораката не е гарантирано подредена — користете ги createdAt на настанот и сопственото
updatedAt на објектот, не редоследот на пристигнување.
Проверка на потписи
Секоја испорака носи Merot-Signature: t=<unix временска ознака>,v1=<hex HMAC-SHA256 на "t.rawBody">, пресметан со тајниот клуч на вашиот endpoint — истата шема што ја популаризираше
Stripe. Одбивајте сè каде временската ознака е постара од 300 секунди, и споредувајте ги
дигестите во константно време.
# Проверката на потписот е локална пресметка во вашиот webhook handler, не HTTP
# повик — видете ги табовите Node.js / Python / PHP / C# / Java за целосна имплементација. import crypto from "node:crypto";
function verifyMerotSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=").map((s) => s.trim())));
const timestamp = Number(parts.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
throw new Error("Timestamp outside tolerance");
}
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const provided = Buffer.from(parts.v1 ?? "", "utf8");
const expectedBuf = Buffer.from(expected, "utf8");
if (provided.length !== expectedBuf.length || !crypto.timingSafeEqual(provided, expectedBuf)) {
throw new Error("Signature mismatch");
}
}
// Express: app.post("/hooks/efaktura", express.raw({ type: "application/json" }), (req, res) => {
// verifyMerotSignature(req.body.toString("utf8"), req.header("Merot-Signature"), process.env.WEBHOOK_SECRET);
// ...
// }); import hashlib
import hmac
import time
def verify_merot_signature(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> None:
parts = dict(kv.split("=", 1) for kv in header.split(","))
timestamp = int(parts["t"])
if abs(time.time() - timestamp) > tolerance_seconds:
raise ValueError("Timestamp outside tolerance")
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts["v1"]):
raise ValueError("Signature mismatch") <?php
function verify_merot_signature(string $rawBody, string $header, string $secret, int $toleranceSeconds = 300): void
{
$parts = [];
foreach (explode(',', $header) as $kv) {
[$key, $value] = explode('=', $kv, 2);
$parts[$key] = $value;
}
$timestamp = (int) ($parts['t'] ?? 0);
if (abs(time() - $timestamp) > $toleranceSeconds) {
throw new RuntimeException('Timestamp outside tolerance');
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
if (!hash_equals($expected, $parts['v1'] ?? '')) {
throw new RuntimeException('Signature mismatch');
}
} using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
public static class MerotWebhookSignature
{
public static void Verify(string rawBody, string header, string secret, int toleranceSeconds = 300)
{
var parts = header.Split(',')
.Select(kv => kv.Split('=', 2))
.ToDictionary(kv => kv[0], kv => kv[1]);
var timestamp = long.Parse(parts["t"]);
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Math.Abs(now - timestamp) > toleranceSeconds)
throw new InvalidOperationException("Timestamp outside tolerance");
var signedPayload = Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}");
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Convert.ToHexString(hmac.ComputeHash(signedPayload)).ToLowerInvariant();
var a = Encoding.UTF8.GetBytes(expected);
var b = Encoding.UTF8.GetBytes(parts["v1"]);
if (a.Length != b.Length || !CryptographicOperations.FixedTimeEquals(a, b))
throw new InvalidOperationException("Signature mismatch");
}
} import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
public class MerotWebhookSignature {
public static void verify(String rawBody, String header, String secret, int toleranceSeconds) throws Exception {
Map<String, String> parts = new HashMap<>();
for (String kv : header.split(",")) {
String[] pair = kv.split("=", 2);
parts.put(pair[0], pair[1]);
}
long timestamp = Long.parseLong(parts.get("t"));
long now = System.currentTimeMillis() / 1000;
if (Math.abs(now - timestamp) > toleranceSeconds) {
throw new IllegalStateException("Timestamp outside tolerance");
}
String signedPayload = timestamp + "." + rawBody;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(signedPayload.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hash) hex.append(String.format("%02x", b));
if (!MessageDigest.isEqual(hex.toString().getBytes(StandardCharsets.UTF_8), parts.get("v1").getBytes(StandardCharsets.UTF_8))) {
throw new IllegalStateException("Signature mismatch");
}
}
} Важно
Секогаш проверувајте според точните бајти на телото на барањето, пред каква било JSON парсинг — повеќето рамки стандардно го парсираат телото вместо вас, а тоа ги менува празните места и го расипува потписот. Конфигурирајте посебна рута со сурово (raw) тело за вашиот webhook endpoint.
Повторни обиди
Merot повторува неуспешна испорака со зголемувачки паузи (1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 12 ч —
вкупно околу 21 час) пред да ја означи како напуштена (abandoned). Endpoint што само неуспева 72
часа по ред автоматски се деактивира, а сопствениците добиваат е-маил. Повторете испорака на кој
било настан рачно од конзолата или преку POST /v1/events/{id}/replay.