New: the AI firewall watches every module and explains each incident in plain words. See how it works

Webhooks

Receive signed HTTP calls when devices, alarms, commands, firmware and rules change – setup, signatures, CloudEvents, retries, testing and the webhook API.

A webhook is an address in your system that Hermetry calls when an event happens. Use it to feed a ticket system, an ERP, a message bus or your own service. Webhooks are managed by administrators on Webhooks (in the Developer part of the menu). Sending webhooks needs a plan that includes them.

Create a webhook

  1. Open Webhooks and select New Webhook.
  2. Enter a Name and, optionally, a Description (“What is this webhook for?”).
  3. Enter the URL. It must be HTTPS: “Webhook URL must be a valid HTTPS URL.”
  4. Under Events, pick at least one event. The list shows only the events of the modules your organisation uses.
  5. Optionally:
    • Custom headers (optional): up to 20 headers sent on every call, for example an authorisation token your receiver checks. Content-Type, User-Agent, Host, Content-Length and X-Webhook-* cannot be set. Values are one line, at most 1,000 characters.
    • Event filters (optional, advanced): deliver an event only when its payload matches (see below).
    • Tags (optional, comma-separated): labels to organise your webhooks, up to 20.
    • Rate limit (optional, deliveries/minute): empty means unlimited.
  6. Save. Copy the Signing secret now: it is shown only once.

All events of the IoT modules are listed with example payloads in the Webhook event reference.

Event filters

An event filter is a JSON map from an event to the payload fields it must match. A delivery is skipped when the payload does not match. Events without a filter are always delivered.

{
  "alarm.created": { "severity": "CRITICAL" },
  "device.disconnected": { "profileId": "6f1c0a2e-0000-4000-8000-000000000001" }
}

Each path names a field of the event’s data and may be dotted, for example originator.type. Every listed field must equal its value (numbers and text compare as text, so "2" matches 2). At most 50 events can have a filter.

What a delivery looks like

Hermetry sends a POST with these headers:

HeaderValue
Content-Typeapplication/json (default format) or application/cloudevents+json; charset=utf-8
User-AgentHermetry-Webhooks/1.0
X-Webhook-EventThe event name, for example device.disconnected
X-Webhook-DeliveryThe delivery id. The same on every retry and redelivery of one delivery.
X-Webhook-TimestampUnix seconds when this attempt was signed
X-Webhook-Signature-V2sha256= + HMAC-SHA256 of "<timestamp>.<body>" with your secret. Use this one.
X-Webhook-SignatureHMAC-SHA256 of the body alone. Older form, kept for a transition period.
X-Webhook-Signature-Prev, X-Webhook-Signature-V2-PrevThe same with the previous secret, during a secret rotation
X-Webhook-Manualtrue on an event an administrator sent by hand
X-Idempotency-KeyOn messages a flow sends with its webhook block

With the default format, the body is an envelope:

{
  "webhookId": "…",
  "tenantId": "…",
  "event": "device.disconnected",
  "createdAt": "2026-11-03T12:00:30.000Z",
  "data": { "deviceId": "…", "name": "router-17", "reason": "timeout", "…": "…" }
}

An event an administrator sent by hand also has "manual": true. Answer with any 2xx status within the timeout. The first 4 KB of your answer’s body are kept in the delivery log.

CloudEvents format

An endpoint can receive CloudEvents 1.0 in structured mode instead. The webhook form has no format choice yet: set "format": "cloudevents" through the API (POST or PATCH /api/webhooks/..., see below). "json" is the default.

{
  "specversion": "1.0",
  "id": "8d2c…",
  "source": "urn:hermetry:tenant:<tenantId>",
  "type": "com.hermetry.device.connected",
  "datacontenttype": "application/json",
  "time": "2026-11-03T10:00:30.000Z",
  "data": { "deviceId": "…", "name": "router-17", "transport": "mqtts", "…": "…" },
  "tenantid": "<tenantId>",
  "webhookid": "<webhookId>"
}

data holds exactly what the default format holds under data. id is the same on every retry and redelivery, so deduplicate on source and id. The signature headers are the same in both formats. Batch and binary modes are not offered.

Verify the signature

Verify against the raw body, before you parse it, and reject old timestamps.

import crypto from 'crypto';

function verifyWebhook(body: string, timestamp: string, signatureV2: string, secret: string): boolean {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
  // Reject deliveries older than 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureV2);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

During a secret rotation, accept a call when either X-Webhook-Signature-V2 or X-Webhook-Signature-V2-Prev matches.

Delivery, retries and dead letters

SettingDefaultRange
Request Timeout (ms)15,0001,000 to 120,000
Max Delivery Attempts (the first one included)31 to 10
Retry Backoff (ms): the wait before retry 1, 2, …60000,300000,9000001 to 9 values, each 1,000 to 86,400,000; past the list, the last value
Circuit Breaker Threshold: consecutive failures before the endpoint is switched off101 to 100,000
Default Rate Limit (per minute): for endpoints without their ownunlimitedblank or 0 = unlimited

Set them on Webhooks > settings (the gear button). A delivery is one of PENDING, SUCCESS, FAILED (will be retried) or DEAD_LETTERED (all attempts used).

  • Each delivery is logged with its status, attempts, response code, error, duration and next retry time.
  • An endpoint over its rate limit gets its deliveries about 60 seconds later; nothing is dropped.
  • After the threshold of failures in a row, the endpoint is switched off and shows as auto-disabled. Queued retries stop. Switch it on again to reset the counter, then redeliver or replay what failed.
  • Deliveries are at least once. Deduplicate on X-Webhook-Delivery (or the CloudEvents id).
  • Events are not guaranteed to arrive in order. Use the times in the payload.

Device connection and device change events have their own volume guard: at most 600 of each kind per organisation per minute by default. See the Webhook event reference.

Rotate the secret

Select Rotate signing secret (Owner role). The new secret is shown once. The previous secret stays valid for 48 hours, and during that time every call carries both signatures. Rotating again while that window is open replaces only the current secret; the previous one keeps its end date.

Test

From a webhook’s row actions or its page:

  • Send test event sends an event named test at once and shows the result (“Test delivered — HTTP 200 in 85 ms”). A test never counts towards the circuit breaker.
  • Trigger event… sends a real, signed event of your choice with a sample payload (JSON object, at most 64 KB), marked manual: true and X-Webhook-Manual: true. It respects the endpoint’s event filter: a sample the filter would drop is refused.
  • View deliveries lists the deliveries; a failed or dead-lettered one can be redelivered, and all dead letters can be replayed at once.

Network rules

Webhook addresses must be public HTTPS addresses. Private, loopback, link-local and cloud metadata addresses are refused when you save and again when each call is made, and redirects are not followed. For an intentionally internal target, an administrator can set an explicit ipAllowlist (up to 50 addresses or CIDR ranges) on the webhook through the API.

The webhook API

Paths are under /tenant/<tenantId>. All need an administrator’s permissions.

MethodPathDoes
GET/api/webhooks/eventsThe events you can subscribe to
GET, POST/api/webhooksList (page from 1, pageSize, search, sort, isActive, autoDisabled); create: 201 { webhook, secret }
GET, PATCH, DELETE/api/webhooks/{webhookId}Read, change, delete
POST/api/webhooks/{webhookId}/rotate-secret{ webhook, newSecret, overlapMs } (Owner)
POST/api/webhooks/{webhookId}/test{ delivery }
POST/api/webhooks/{webhookId}/trigger{ event, payload } → { message, deliveryId }
GET/api/webhooks/{webhookId}/deliveries{ data, total }; filters status, event, createdFrom, createdTo
POST/api/webhooks/{webhookId}/deliveries/{deliveryId}/redeliverRe-queue a FAILED or DEAD_LETTERED delivery
GET, POST/api/webhooks/{webhookId}/deliveries/replay-dead-letterCount, or re-queue, every dead letter
GET/api/webhooks/metrics, /api/webhooks/reportDelivery statistics (?webhookId=&days=)

Create body: name, description, url, events, headers, eventFilters, tags, rateLimitPerMinute (1 to 100,000), ipAllowlist, format (json or cloudevents).

RefusalMeaning
403 WEBHOOKS_NOT_IN_PLANYour plan does not include webhooks
409 ENDPOINT_DISABLEDThe endpoint is switched off; switch it on first
409 DELIVERY_NOT_REDELIVERABLEThe delivery is pending or already succeeded
422 EVENT_FILTERED_OUTThe endpoint’s filter would drop the sample you triggered
422An event no module of your organisation offers