Skip to content

Webhooks

Instead of constantly calling GET /api/v1/requests/{requestId} (polling), register an HTTPS endpoint of your own. Novus calls it every time something changes in one of your requests.

Registration

POST /api/v1/webhooks

{
  "url": "https://to-logismiko-sas.gr/novus-events",
  "events": [
    "request.status_changed",
    "request.provisioned"
  ]
}
Field
url Your HTTPS endpoint
events The events you want, at least one (see below)

The response (201) contains endpointId, url, events, createdAt and a secret. The secret is returned only this once, so store it: you use it to verify the signature of every call.

Call
GET /api/v1/webhooks Your endpoints (without the secrets)
DELETE /api/v1/webhooks/{endpointId} Stops notifications to this endpoint (204)
GET /api/v1/webhooks/deliveries?endpointId=…&page=1&pageSize=50 Delivery history, for debugging: what was sent and what you replied (eventId, event, attempt, responseStatus, deliveredAt, succeeded). Filters: endpointId, eventId.

Events

event When
request.created A request was created
request.status_changed The status of a request changed. The most useful event.
request.provisioned The client was activated (provisioning.status = COMPLETED) and can start issuing
request.provisioning_failed Activation failed. Novus is notified automatically and handles it. Do not send the request again.
client.statement_recalled The Provider declaration to AADE (the Greek tax authority) was recalled

The call you receive

POST https://to-logismiko-sas.gr/novus-events
Content-Type: application/json
X-Novus-Event-Id: evt_8f7b2c9a1d4e
X-Novus-Signature: sha256=4f1c…
{
  "event": "request.status_changed",
  "eventId": "evt_8f7b2c9a1d4e",
  "occurredAt": "2026-09-17T10:15:00Z",
  "data": {
    "requestId": "req_8f7b2c9a",
    "status": "APPROVED",
    "previousStatus": "UNDER_REVIEW",
    "message": "Η αίτηση εγκρίθηκε."
  }
}

A webhook tells you that something changed. It is not the source of truth

Always confirm with GET /api/v1/requests/{requestId} before you act.

  • Reply 2xx as fast as possible and do your processing afterwards. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, and then abandoned.
  • Duplicates: the same event can arrive again on a retry, with the same X-Novus-Event-Id. Ignore the ones you have already seen.
  • Signature: always verify X-Novus-Signature.

Verifying the signature

X-Novus-Signature = sha256= + the HMAC-SHA256 of the exact body, keyed with your secret, in lowercase hex. Compute it on the bytes as you received them, before any JSON parsing.

using System.Security.Cryptography;
using System.Text;

static bool IsValid(string rawBody, string header, string secret)
{
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var expected = "sha256=" + Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody))).ToLowerInvariant();
    return CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(header ?? ""));
}
uses System.Hash, System.SysUtils;

function IsValid(const RawBody, Header, Secret: string): Boolean;
var
  Expected: string;
begin
  Expected := 'sha256=' + LowerCase(THash.DigestAsString(
    THashSHA2.GetHMACAsBytes(TEncoding.UTF8.GetBytes(RawBody), TEncoding.UTF8.GetBytes(Secret))));
  Result := SameStr(Expected, Header);
end;
const crypto = require('crypto');

function isValid(rawBody, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
  return header && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
import hmac, hashlib

def is_valid(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")