This guide explains how to receive webhooks from SUPER. It is written for developers who build the receiving endpoint on the client side.
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:
https and publicly reachable),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.
invoice.received eventRight now one event is available:
invoice.received - a new inbound invoice is available for your company.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.
Every webhook POST has these headers:
| Header | Meaning |
|---|---|
X-Super-Webhook-Id | Unique id of this delivery. Same value as id in the body. Use it to detect duplicates. |
X-Super-Webhook-Event | The event type, for example invoice.received. |
X-Super-Webhook-Timestamp | Time we created the request, as a Unix timestamp in seconds (UTC). |
X-Super-Webhook-Signature | The 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.
{
"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.
| Field | Meaning |
|---|---|
id | Unique id of this delivery. Same as the X-Super-Webhook-Id header. |
type | The event type, for example invoice.received. |
createdUtc | When we built the payload (UTC). |
companyGuid | The GUID of your company that this event belongs to. |
data | Event data. For invoice.received it holds invoiceGuid. |
data.invoiceGuid | The GUID of the new invoice. Call GetInvoice with this value to read the full invoice. |
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.
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:
X-Super-Webhook-Timestamp header (Unix seconds)."{timestamp}.{body}". The body is the exact raw JSON we sent.whsec_ secret as the key.sha256=.To verify, do the same steps with the raw body you received, then compare your result with the X-Super-Webhook-Signature header.
CryptographicOperations.FixedTimeEquals.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 ) );
}
Return a 2xx status code as fast as you can (200, 201, 202, 204 all work). This tells us the delivery succeeded.
If you return any other status code, or your endpoint is slow or unreachable, we treat the delivery as failed and retry it.
If a delivery fails, we retry it. We wait longer after each try:
| After failed attempt | We wait |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 6 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.
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.