Skip to main content

Webhooks

Webhooks are a powerful tool for real-time communication between your application and Kaunt's platform. By integrating webhooks into your system, you can receive instant updates about important events, such as the completion of proposal processing. This section outlines the process for setting up and handling webhooks.

Managing Webhooks​

Webhooks can be managed in two ways:

  • Kaunt Partner Portal: The web-based UI for setting up, modifying, and deleting webhooks.
  • Management API: The programmatic equivalent — use the webhook endpoints under /v1/partners/{partnerId}/webhooks to create, list, and delete webhooks via API calls.

Both approaches manage the same underlying webhooks. Use whichever fits your workflow.

Partner-level vs Tenant-level webhooks​

When creating a webhook, you can optionally scope it to a specific tenant by providing a tenantId:

  • Partner-level webhook (no tenantId): Receives events for all tenants under your partner account.
  • Tenant-level webhook (with tenantId): Receives events only for that specific tenant.

Choose tenant-level webhooks when you need to route events to different endpoints per customer, or partner-level webhooks when a single endpoint handles all events.

Handling Webhook Notifications​

To receive webhook notifications, you need to implement an endpoint in your application that can handle the incoming POST requests from Kaunt's system. An example of the payload provided in the POST request is shown below:

{
"Id": "2125d21e-49a3-44fe-bc2e-a74e4fbba446",
"Url": "https://api.kaunt.com/v1/tenants/a9f76627-30b0-4ea4-915e-882b6f5f189e/companies/kaunt/invoicecodingproposals/2125d21e-49a3-44fe-bc2e-a74e4fbba446",
"TenantId": "a9f76627-30b0-4ea4-915e-882b6f5f189e",
"CompanyName": "kaunt",
"CorrelationId": "e7f382eb-5339-45a7-ba57-64a6dd860338",
"EventType": "InvoiceCodingProposalProcessingCompleted"
}
  • Id: The unique identifier of the proposal.
  • Url: The unique identifier of the proposal's resource location in Kaunt's API. The base URL will match the region of the tenant.
  • TenantId: The identifier of the tenant associated with the proposal.
  • CompanyName: The name of the company associated with the proposal.
  • EventType: The type of event that triggered the webhook notification. In this case, the event type is InvoiceCodingProposalProcessingCompleted. This event type indicates that the invoice coding proposal processing has completed and that the proposal is ready to be retrieved at the URL endpoint.
  • CorrelationId: A unique identifier regarding the request made towards the Kaunt API for the proposal.
Fields may be absent

Kaunt omits null fields from the payload rather than sending them as null. A field you expect may therefore not be present at all.

This matters most for ModelReady, which is a tenant-level event and carries no Id, Url or CorrelationId. Parse defensively and do not assume any field other than TenantId and EventType is present.

When your client receives a POST request on the webhook endpoint with the provided payload, it indicates that proposal processing has completed. The webhook payload contains the necessary information, including the URL property, which represents the endpoint where the invoice coding proposal resource can be retrieved.

By making a GET-request to the URL endpoint in the payload received in the webhook response, your client can fetch the invoiceCodingProposal using the GET-request for invoiceCodingProposals.

Retry Policy​

If a webhook request fails, Kaunt's system will automatically retry the request using exponential backoff. The retries will continue until a maximum of 24 hours has passed since the initial request. This retry mechanism ensures that webhook notifications have a higher chance of being successfully delivered, even in the event of temporary issues or transient failures.

Webhook Event Types​

Kaunt webhooks can have the following event types:

  • InvoiceCodingProposalProcessingCompleted: Indicates that the invoice coding proposal is ready for retrieval.
  • InvoiceCodingProposalProcessingFailed: Indicates that an error occurred during invoice coding. The specific error type can be identified from the invoice proposal available at the URL endpoint provided in the webhook payload.
  • PostedInvoiceProcessingCompleted: Indicates that a posted invoice has been processed and is ready for retrieval.
  • PostedInvoiceProcessingFailed: Indicates that an error occurred while processing a posted invoice.
  • ModelReady: Indicates that the model is ready for providing coding proposals. Is only sent for the first model.
  • DocumentAIDocumentResultReady: Document AI document has been successfully processed.
  • DocumentAIDocumentResultFailed: An error has occurred while processing a Document AI document.

Delivery Headers​

Every webhook request carries the following headers:

HeaderExampleDescription
X-Kaunt-Timestamp1700000000Unix time in seconds at which Kaunt sent this HTTP request. Each retry carries a fresh timestamp.
X-Kaunt-Delivery-Id0f9a4c1e-...Identifies the delivery. Stable across retries of the same delivery — use it as your idempotency key.
X-Signature-SHA256-V27brWFr65Jl...Signature over the timestamp and body. Use this one.
X-Signature-SHA256ChfEbKAHCK...Legacy signature over the body alone. Retained for backwards compatibility.

The two signature headers are present only when the webhook has a signing secret. X-Kaunt-Timestamp and X-Kaunt-Delivery-Id are always sent.

Signature Verification​

Both signatures are HMAC-SHA256 digests encoded as base64 — not hexadecimal. The secret key is shown when you create the webhook and can be retrieved from the Kaunt Partner Portal.

SchemeHeaderSigned material
v2 (recommended)X-Signature-SHA256-V2{timestamp}.{raw body}
LegacyX-Signature-SHA256{raw body}

The legacy scheme signs the body alone. Because a retry resends a byte-identical body, it produces a byte-identical signature — so a captured request stays verifiable indefinitely. The v2 scheme binds the timestamp into the signed material, which lets you reject anything outside a tolerance window.

Both headers are sent on every delivery. Verify v2 if you can; the legacy header is not being removed, so existing integrations keep working unchanged.

Verification steps​

  1. Read the raw request body as bytes, before any JSON parsing. Re-serialising a parsed object will not reproduce the signature — Kaunt serialises without whitespace and omits null fields entirely.
  2. Read X-Kaunt-Timestamp. Reject the request if the header is missing, is not an integer, or differs from your current time by more than your tolerance window. Five minutes is a reasonable default.
  3. Compute HMAC-SHA256(secret, timestamp + "." + rawBody) and base64-encode it.
  4. Compare against X-Signature-SHA256-V2 using a constant-time comparison. A plain string equality check leaks timing information.
  5. If the values differ, discard the request. Do not return an error that distinguishes a signature failure from any other rejection.
  6. Deduplicate on X-Kaunt-Delivery-Id before acting (see Handling duplicates).

Example — Node.js​

const crypto = require('crypto');

// express.raw({ type: 'application/json' }) gives req.body as a Buffer
function verify(req, secret, toleranceSeconds = 300) {
const timestamp = req.get('X-Kaunt-Timestamp');
const signature = req.get('X-Signature-SHA256-V2');
if (!timestamp || !signature) return false;

const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!Number.isFinite(age) || age > toleranceSeconds) return false;

const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(req.body) // raw bytes, not the parsed object
.digest('base64');

const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Example — C#​

public static bool Verify(byte[] rawBody, string timestampHeader, string signatureHeader,
string secret, int toleranceSeconds = 300)
{
if (!long.TryParse(timestampHeader, out var timestamp))
return false;

var age = Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp);
if (age > toleranceSeconds)
return false;

var signed = Encoding.UTF8.GetBytes($"{timestamp}.")
.Concat(rawBody) // raw bytes, not a re-serialised object
.ToArray();

using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Convert.ToBase64String(hmac.ComputeHash(signed));

return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signatureHeader));
}

Handling duplicates​

Webhook delivery is at-least-once. Your endpoint must be idempotent.

Kaunt guarantees that one internal event produces one delivery per configured webhook, but it does not suppress duplicates at the delivery layer, because:

  • failed deliveries are retried for up to 24 hours, and every retry carries the same payload and the same X-Kaunt-Delivery-Id; and
  • several webhooks may deliberately point at the same URL — this is how a signing secret is rotated without a gap in delivery, and it produces overlapping deliveries for the duration of the rotation.

Record X-Kaunt-Delivery-Id and ignore a delivery id you have already processed. Retain those ids for at least 24 hours to cover the full retry window. If you are still on the legacy signature scheme and do not read the delivery id, deduplicate on the EventType, Id and CorrelationId fields together — but remember that ModelReady carries neither Id nor CorrelationId.

Best Practices for Webhooks​

To ensure your webhook integration is robust and future-proof, consider the following best practices:

  • Secure Your Webhook Endpoints: Use HTTPS to encrypt data in transit and ensure secure communication between Kaunt and your application.
  • Handle Additional Fields Gracefully: Be prepared for Kaunt to add new fields to the webhook payload, and for optional fields to be absent entirely. Your application should handle both without breaking.
  • Implement Retry Logic: Although Kaunt retries failed webhook deliveries, implement your own retry logic to handle temporary failures on your end.
  • Verify Payloads: Always verify the signature of incoming webhook payloads over the raw request body, using a constant-time comparison, and reject requests outside your timestamp tolerance window.
  • Log Webhook Events: Maintain logs of received webhook events for debugging and auditing purposes.
  • Respond Quickly: Ensure your webhook endpoint responds quickly to acknowledge receipt. Long processing times can lead to timeouts and unnecessary retries.
  • Handle Duplicate Events: Deduplicate on X-Kaunt-Delivery-Id. Delivery is at-least-once, so the same event will reach you more than once.
  • Monitor and Alert: Set up monitoring and alerting for your webhook endpoints to detect and respond to failures promptly.