e-Faktura API
Webhooks
Creating an endpoint
The response’s secret (whsec_...) is shown once — store it. Rotating a secret keeps the old
one valid for signing for 24 hours, so both are accepted while you roll out the new one.
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()); Events
| Event | Fired when |
|---|---|
einvoice.validated_failed | Issue or validate failed UJP’s rules. |
einvoice.signature_required | A signing request is waiting on your agent. |
einvoice.submitted | UJP assigned a EUID — status: "delivered". |
einvoice.accepted / einvoice.auto_accepted | The buyer accepted, explicitly or by silence. |
einvoice.rejected | The buyer rejected it, with a reason. |
einvoice.cancelled_by_storno / einvoice.corrected | A linked storno/correction was filed. |
einvoice.failed | Terminal failure. |
inbox.received | A new incoming e-invoice arrived. |
inbox.deadline_approaching | 5, 2 or 1 days left to accept/reject (see Document lifecycle). |
inbox.auto_accepted | The deadline passed without a decision. |
signing_agent.offline / signing_agent.online | Your Bridge/SDK worker’s connection state changed. |
archive.export_ready | A bulk archive export finished. |
Delivery isn’t ordered — use the event’s createdAt and the object’s own updatedAt, not arrival
order.
Verifying signatures
Every delivery carries Merot-Signature: t=<unix timestamp>,v1=<hex HMAC-SHA256 of "t.rawBody">,
computed with your endpoint’s secret — the same scheme Stripe popularized. Reject anything where
the timestamp is more than 300 seconds old, and compare digests in constant time.
Important
Always verify against the raw bytes of the request body, before any JSON parsing — most frameworks parse the body for you by default, which changes whitespace and breaks the signature. Configure a raw-body route for your webhook endpoint.
# Signature verification is a local computation in your webhook handler, not an
# HTTP call — see the Node.js / Python / PHP / C# / Java tabs for a full implementation. 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");
}
}
} Retries
Merot retries a failing delivery with backoff (1m, 5m, 30m, 2h, 6h, 12h — about 21 hours total)
before marking it abandoned. An endpoint that only fails for 72 hours straight is disabled
automatically and its owners are e-mailed. Replay any event manually from the console or
POST /v1/events/{id}/replay.