The CLV1 protocol is now publicRead the spec
Browse the library

Documentation libraryIntegration

Webhooks

Webhooks push an app's events (licenses, devices, sessions, users, Protect 24/7, releases) to your own server as an HMAC-signed HTTP `POST`, a few seconds after they happen. Use them to sync a CRM, email customers, open tickets or drive automation without polling the API.

10 min read

How webhooks work

  • Each webhook is an endpoint that belongs to one app and receives only the events you pick. An app can have several endpoints; an event that matches more than one is delivered to each of them separately.
  • Every delivery is signed with the endpoint's own signing secret, retried for about 3 days when it fails, and kept in a delivery log you can inspect and resend from.
  • Every workspace member can view the webhooks page. Only Admins and Owners can create, toggle, test, rotate secrets, delete and resend.

Creating an endpoint

  1. Open the app and go to Integration › Webhooks.
  2. Click Add webhook. The New webhook panel opens.
  3. Enter the Endpoint URL: the full address, up to 500 characters, for example https://example.com/clavis.
  4. Optionally add a Note (up to 200 characters), such as "CRM sync". It is shown under the URL in the list.
  5. Under Events, tick what you want to receive. Events are grouped by prefix (license, device, session…), and license.created and security.incident are preselected. At least one event is required.
  6. Click Create webhook. The Signing secret dialog opens right away (see below).

What the endpoint URL must meet:

  • It uses https://. At send time the host must resolve to a public IP address: localhost and private ranges such as 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16 are refused.
  • It answers with a 2xx status within 10 seconds. Redirects (3xx) are not followed and count as a failure.

The dashboard has no way to edit an existing endpoint's URL, note or events yet. To change them, delete the endpoint (trash icon Delete, confirm "Delete this webhook?") and create it again. The new endpoint gets a new signing secret, and the old endpoint's delivery log is deleted with it.

Event catalog

EventWhenMain fields in data
license.createdLicenses are created from the dashboard, the Seller API or the reseller portal; one event per batchtargetType, targetId, count
license.activatedA license is activated for the first time from your softwarelicenseId, deviceId
license.expiringA license enters the expiry notice window of its license plan, once per termlicenseId, expiresAt
license.expiredA license passes its end date plus the grace dayslicenseId, expiresAt
license.suspendedA license is suspended, one at a time or in bulktargetType, targetId
license.revokedA license is revoked, one at a time or in bulktargetType, targetId
license.extendedA license is extended, one at a time or in bulktargetType, targetId
validation.failedThe server refuses a request from your softwarereason (error code), event, licenseId, ip
device.addedA new machine is bound to a licenselicenseId, deviceId, platform
device.removedA machine is removed from a license in the dashboard or the Seller APItargetType, targetId
device.blockedA machine is blockedtargetType, targetId
device.transferredA customer moves their license to another machine, in your software or the customer portallicenseId, deviceId, by
session.startedYour software signs in successfully and a session openslicenseId, deviceId, sessionId, endUserId
session.endedYour software calls logout()sessionId, licenseId, deviceId
session.revokedA session is revoked in the dashboard or the Seller APItargetType, targetId
user.createdAn end-user account is created in the dashboard or the Seller APItargetType, targetId
user.bannedAn end-user account is bannedtargetType, targetId
security.incidentProtect 24/7 opens a new incidentincidentId, signal, severity, subjectType, subjectId
security.actionProtect 24/7 acts on its own under Graduated EnforcementactionId, action, subjectType, subjectId, reason
release.publishedA release is publishedtargetType, targetId
reseller.credit_lowA reseller's credit drops below the price of 5 keys of the dearest plan they sell, once per crossingresellerId, balance
webhook.testYou click Send testmessage

Good to know:

  • Actions taken in the dashboard or the Seller API always carry targetType, targetId and the details of that action; the Seller API adds apiKeyId and apiKeyName. Read data defensively and tolerate missing or new fields.
  • Events raised by your software (license.activated, validation.failed, device.added, device.transferred from the software, session.started, session.ended) are only recorded while the app has at least one enabled webhook subscribed to them. The server caches that list for about 30 seconds, so events in the first moments after you create or re-enable an endpoint can be missed.
  • In the current version, accounts that your software registers through register() do not raise user.created.
  • License dates are checked every hour, so license.expiring and license.expired can arrive up to about an hour late.

The signing secret

Right after creation, the Signing secret dialog shows a whsec_… secret with the line "Shown once, store it somewhere safe now", along with a ready-made Node snippet for checking signatures. Copy the secret into your receiving server's secret store, then click Saved it. ClavisAUH stores the secret encrypted and can never show it again.

To rotate it, click New secret on the endpoint's card. There is no confirmation step: the new secret replaces the old one at once and appears in the same Signing secret dialog. Every delivery from then on, including retries of older events, is signed with the new secret.

Payload and headers

Each delivery is a POST with a JSON body:

{
  "id": "evt_…",
  "type": "license.activated",
  "createdAt": "2026-11-02T08:15:30.000Z",
  "appId": "app_…",
  "data": { "licenseId": "lic_…", "deviceId": "dev_…" }
}
FieldMeaning
idEvent id. It stays the same across retries and resends, and across endpoints. Use it to drop duplicates. Test events look like evt_test_…
typeThe event name from the catalog
createdAtWhen the event happened, ISO 8601 in UTC
appIdThe app the event belongs to
dataEvent details, which vary by event and source

Headers sent with it:

HeaderValue
Content-Typeapplication/json
User-AgentClavisAUH-Webhooks/1
Clavis-Signaturet=<unix>,v1=<hex>
Clavis-EventThe event name, same as type
Clavis-DeliveryThe delivery's id in the log. It stays the same across automatic retries and changes when you click Resend

Verifying Clavis-Signature

v1 is the hex HMAC-SHA256 of <t>.<body>, where t is the signing time in Unix seconds, body is the raw request body exactly as received, and the key is the full whsec_… string. t is computed fresh on every attempt, so a 5-minute tolerance check also works for retries.

import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

// Clavis-Signature: t=<unix>,v1=<hex>
export function verifyClavis(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false; // 5 minutes of tolerance
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest();
  const received = Buffer.from(parts.v1 ?? "", "hex");
  return received.length === expected.length && timingSafeEqual(received, expected);
}

const app = express();

// Keep the raw body so the signature is checked before JSON parsing
app.post("/clavis", express.raw({ type: "application/json" }), async (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyClavis(raw, req.get("clavis-signature") ?? "", process.env.CLAVIS_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(raw);
  await saveOnce(event.id, event); // store by event.id, skip if already there; do heavy work in a background job
  res.sendStatus(204);
});

Rules for your receiver:

  • Check the signature on the raw body, before parsing. Parsing and re-serializing the JSON breaks the signature.
  • Answer 2xx quickly and do slow work in the background. Anything over 10 seconds counts as a failure.
  • Deduplicate on id: the same event can arrive more than once through retries or resends.
  • Do not rely on arrival order. Deliveries run in parallel and a retry can land after a newer event; sort by createdAt when order matters.
  • Answer 2xx even for events you deliberately ignore. Every other status, 4xx included, is retried.

Retries and backoff

A delivery succeeds when your endpoint answers with a 2xx status. Any other status, a timeout past 10 seconds, a network error or a refused address is a failure, and ClavisAUH retries on its own:

  • Up to 15 attempts in total: the first try plus 14 retries.
  • The wait doubles after each failure: 1 minute, 2, 4, 8… up to a ceiling of 12 hours between attempts. All told, about 3 days.
  • While attempts remain, the log shows the delivery as Pending with the latest error and the attempt count. Failed only appears after the last attempt.

Pausing after repeated failures

When a delivery fails its final attempt, the endpoint is paused: the switch on its card turns off, the line "Paused after repeated failures, switch it back on once fixed" appears, and every Owner and Admin of the workspace gets an email with a button that opens the webhooks page.

While an endpoint is paused:

  • New events are not recorded for it, and they cannot be sent later.
  • Deliveries still waiting for a retry turn Failed with the error webhook_disabled.

To resume, fix your endpoint and flip the switch back on. This also resets the failure counter. Then open the log and Resend the deliveries that failed. Turning the switch off yourself has the same effect as a pause, minus the email.

Sending a test event

Click Send test on the endpoint's card. The "Test event sent" toast means the event was queued, not that it arrived; the result shows up in the log a few seconds later. The webhook.test event goes through the same signed, logged path as real events, whether or not you ticked webhook.test, and its data is { "message": "Signed and delivered by ClavisAUH" }.

The delivery log and resending

Each endpoint card shows the last 24 hours at a glance: "… delivered", "… failed", and "… pending" when there are any. Click Delivery log to open that endpoint's Delivery log panel. It refreshes every 5 seconds and shows the 50 most recent deliveries, each with the event name, status (Pending, Delivered, Failed), HTTP status, response time, attempt count (×3) and age.

Click a row to see the last error, the payload, and the first 1,000 characters of your server's response. Resend creates a new delivery with the exact same payload (same id); the toast "Queued again" confirms it, and the original row stays in the log. If the endpoint is paused, switch it back on first, or the resend fails straight away with webhook_disabled.

Error in the logCause
HTTP 500 (or another status)Your endpoint answered with a non-2xx status
webhook_disabledThe endpoint was off or paused at send time
endpoint resolves to a private addressThe host resolves to an internal address
only https endpoints are allowedThe URL does not use https
unreadable_secretThe server cannot decrypt the stored secret; retrying will not help, so click New secret and update your receiver

Delivery log entries are cleared according to your plan's log retention, except deliveries that are still pending.

Plan limits

The webhook count covers the whole workspace, all apps together. By default Basic allows 1 webhook and Premium 50; the number that applies to you is in Account & organization › Plan & usage. Once an app's list reaches the limit, Add webhook is disabled and the page says "Your plan allows N webhooks", with N your limit. If the remaining allowance is already used by other apps, creating fails with plan_limit (HTTP 402). See Basic, Premium and limits and pricing.

Managing webhooks with the Seller API

A Seller API key with the webhooks:read or webhooks:write scope can manage webhooks from your own systems:

  • GET /apps/{appId}/webhooks: list the app's endpoints.
  • POST /apps/{appId}/webhooks: create an endpoint with url, events and an optional description. The secret is returned once, in data.secret. Pass "events": ["*"] to receive every event, including ones added later; the dashboard does not offer this.
  • DELETE /webhooks/{id}: delete an endpoint.
  • POST /webhooks/{id}/test: send a webhook.test event.

Rotating the secret, toggling, reading the log and resending are dashboard-only for now. Plan limits apply the same way. Details are in the Seller API reference.

How can we help?

Frequently asked

What is ClavisAUH?

A licensing and authentication service for desktop software: license keys, device binding, end-user sign-in and signed responses. All run from one dashboard

Why can't a cracker fake your server?

The SDK only trusts answers signed with your app's private key. That key never leaves our servers, so a fake server can't sign and gets rejected

What ends up in my binary?

Your app id and public key. Nothing secret, because each device makes its own key pair on first run

Does it work offline?

Yes. Every plan gets a signed offline token for 7 days, and your app checks it locally. Premium lets you choose how long. A revoked license can keep working offline for that time, unless Online-Only Mode is on

Which languages are supported?

JavaScript and TypeScript today. C++, C# and Python are next, on the same open protocol

Is Basic really free?

Yes. No time limit, no card. Move to Premium when you need more

Get in touch