e-Faktura API
v1 Guides MK Get started

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

EventFired when
einvoice.validated_failedIssue or validate failed UJP’s rules.
einvoice.signature_requiredA signing request is waiting on your agent.
einvoice.submittedUJP assigned a EUID — status: "delivered".
einvoice.accepted / einvoice.auto_acceptedThe buyer accepted, explicitly or by silence.
einvoice.rejectedThe buyer rejected it, with a reason.
einvoice.cancelled_by_storno / einvoice.correctedA linked storno/correction was filed.
einvoice.failedTerminal failure.
inbox.receivedA new incoming e-invoice arrived.
inbox.deadline_approaching5, 2 or 1 days left to accept/reject (see Document lifecycle).
inbox.auto_acceptedThe deadline passed without a decision.
signing_agent.offline / signing_agent.onlineYour Bridge/SDK worker’s connection state changed.
archive.export_readyA 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.