Webhook Integration Guide

Hrvatska verzija

This guide explains how to receive webhooks from SUPER. It is written for developers who build the receiving endpoint on the client side.

1. What is a webhook

A webhook is an HTTP POST that SUPER sends to your URL when something happens. You do not have to poll our API. When an event happens, we call your endpoint.

You create an endpoint in the SUPER web app. You give us:

When you create the endpoint, we show you a secret one time. The secret starts with whsec_. Copy it and store it safely. You cannot see it again. If you lose it, rotate the secret in the app to get a new one.

2. The invoice.received event

Right now one event is available:

The webhook does not contain the full invoice. It tells you an invoice arrived and gives you its invoiceGuid. You then call GetInvoice in our API with that invoiceGuid to read the full invoice.

3. Request headers

Every webhook POST has these headers:

HeaderMeaning
X-Super-Webhook-IdUnique id of this delivery. Same value as id in the body. Use it to detect duplicates.
X-Super-Webhook-EventThe event type, for example invoice.received.
X-Super-Webhook-TimestampTime we created the request, as a Unix timestamp in seconds (UTC).
X-Super-Webhook-SignatureThe signature of the request. Use it to check the request is really from us.

The body is always JSON. The Content-Type is application/json.

4. The JSON body

{
  "id": "0f8c2a1e-4b7d-4a9e-9c3f-2b1d5e6a7c88",
  "type": "invoice.received",
  "createdUtc": "2026-07-03T09:15:42.1234567Z",
  "companyGuid": "8159a7b9-0770-4d27-afe3-e6eb0466d857",
  "data": {
    "invoiceGuid": "63c1964d-0d83-4f8d-b01d-57c94ba453fc"
  }
}

Field names are camelCase.

FieldMeaning
idUnique id of this delivery. Same as the X-Super-Webhook-Id header.
typeThe event type, for example invoice.received.
createdUtcWhen we built the payload (UTC).
companyGuidThe GUID of your company that this event belongs to.
dataEvent data. For invoice.received it holds invoiceGuid.
data.invoiceGuidThe GUID of the new invoice. Call GetInvoice with this value to read the full invoice.

Test sends

When you press "Send test event" in the app, we send a request in the same shape, but the data object is different:

{
  "id": "...",
  "type": "invoice.received",
  "createdUtc": "...",
  "companyGuid": "...",
  "data": {
    "test": true
  }
}

So a real event has data.invoiceGuid. A test send has data.test = true.

5. Verify the signature

Always verify the signature before you trust the request. This proves the request is from us and was not changed.

How we build the signature:

  1. We take the timestamp from the X-Super-Webhook-Timestamp header (Unix seconds).
  2. We build the string "{timestamp}.{body}". The body is the exact raw JSON we sent.
  3. We compute HMAC-SHA256 over that string, using your whsec_ secret as the key.
  4. We write the result as lowercase hex, with the prefix sha256=.

To verify, do the same steps with the raw body you received, then compare your result with the X-Super-Webhook-Signature header.

Two important rules:

C# sample:

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

public static bool IsValidWebhook( string secret, string timestampHeader, string signatureHeader, string rawBody )
{
    // 1) Reject old requests (replay protection).
    if ( !long.TryParse( timestampHeader, NumberStyles.Integer, CultureInfo.InvariantCulture, out var unixSeconds ) )
    {
        return false;
    }

    var sentAt = DateTimeOffset.FromUnixTimeSeconds( unixSeconds );
    if ( DateTimeOffset.UtcNow - sentAt > TimeSpan.FromMinutes( 5 ) )
    {
        return false;
    }

    // 2) Recompute the signature over "{timestamp}.{body}".
    var payload = $"{timestampHeader}.{rawBody}";
    using var hmac = new HMACSHA256( Encoding.UTF8.GetBytes( secret ) );
    var hash = hmac.ComputeHash( Encoding.UTF8.GetBytes( payload ) );
    var expected = "sha256=" + Convert.ToHexString( hash ).ToLowerInvariant( );

    // 3) Constant-time compare.
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes( expected ),
        Encoding.UTF8.GetBytes( signatureHeader ) );
}
Read the raw body as text, exactly as it arrived. Do not parse and re-serialize the JSON before you check the signature. Re-serializing can change the bytes and break the check.

6. How to answer

Return a 2xx status code as fast as you can (200, 201, 202, 204 all work). This tells us the delivery succeeded.

Do the heavy work later. Store the event, answer 2xx, then process it in the background. Our request times out after 10 seconds.

If you return any other status code, or your endpoint is slow or unreachable, we treat the delivery as failed and retry it.

7. Retry policy

If a delivery fails, we retry it. We wait longer after each try:

After failed attemptWe wait
11 minute
25 minutes
330 minutes
42 hours
56 hours

After 6 failed attempts the delivery becomes FailedFinal. We stop retrying that delivery automatically.

If one endpoint reaches FailedFinal 5 times in a row, we turn the endpoint off. We set a reason on the endpoint and send an email to your company. While the endpoint is off, we do not send new webhooks to it. Turn it back on in the app to resume; that also resets the failure counter.

A successful delivery resets the counter to zero.

8. Handle duplicates (idempotency)

The same webhook id can arrive more than once. This can happen after a retry, or when someone presses "Resend" in the app. A manual resend reuses the same id; it does not create a new one.

So process each id only once. Before you act, check if you already handled that id. If yes, answer 2xx and do nothing else. This keeps your side correct even when a webhook arrives twice.