Webhooks
Receive an HTTP request when issues, work orders, or alarms change, and verify that it came from DUET.
Webhooks send an HTTP POST request to your endpoint when something changes in DUET, so your systems stay up to date without polling.
Events
| Event | Sent when |
|---|---|
issue.created, issue.updated, issue.resolved, issue.reopened, issue.deleted, issue.merged | An issue changes. |
workorder.created, workorder.updated, workorder.synced, workorder.completed | A work order changes. |
alarm.raised, alarm.updated, alarm.resolved | An alarm changes. |
Each event's payload is in the API reference.
Add an endpoint
Webhooks must be turned on for your organization. If you don't see Integrations, contact your account manager.
- In the DUET app, open a site, and then open Integrations.
- Click Add endpoint.
- Enter your endpoint's URL. It must be an
httpsaddress that's reachable from the internet. - Choose whether to send events from every site, or from this site only.
- Select the events to send. To receive every event, including events added later, select none.
- Save the endpoint.
- Copy the signing secret, and store it somewhere safe. DUET shows the secret only once.
To change, pause, or remove an endpoint later, use its menu on the Integrations page. Activity shows each delivery attempt.
Request format
Every request has these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Duet-Event-Id | The event's ID. It's the same on every retry. |
X-Duet-Event-Type | The event type, such as issue.created. |
X-Duet-Signature | The signatures to verify. |
The body is a JSON object:
{
"schema_version": 1,
"id": "evt_8f14e45fceea167a5a36dedd4bea2543",
"type": "issue.created",
"occurred_at": "2026-05-25T14:03:11.000Z",
"organization_id": "…",
"project_id": "…",
"data": {}
}data holds the issue, work order, or alarm as it was after the change.
Verify the signature
Verify every request before you act on it, so you know it came from DUET and wasn't changed on the way.
The X-Duet-Signature header looks like this:
t=1716645791,v1=5257a869…,v1a=9c1f7e3b…tis when DUET signed the request, in Unix seconds.v1is an HMAC-SHA256 signature, keyed with your endpoint's signing secret.v1ais an Ed25519 signature, which you verify with DUET's public keys.
Both sign the same string: the value of t, a period, and the raw request body. Verify either one.
To verify v1:
- Read the raw request body before you parse it as JSON.
- Build the string
<t>.<raw body>. - Calculate its HMAC-SHA256 with your signing secret, as lowercase hex. Use the whole secret as the key, including the
whsec_prefix. - Compare the result with each
v1value in the header, using a constant-time comparison. If none match, reject the request. - If
tis more than 5 minutes from your current time, reject the request.
import hashlib
import hmac
import time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = [p.split("=", 1) for p in header.split(",")]
t = next(v for k, v in parts if k == "t")
if abs(time.time() - int(t)) > 300:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, v) for k, v in parts if k == "v1")To verify v1a instead, fetch DUET's public keys from https://api.enurgen.com/webhooks/jwks.json. Cache the keys for up to 5 minutes, and fetch them again when a signature doesn't verify.
Replace a signing secret
To replace a secret, open the endpoint's menu on the Integrations page, and click Replace signing secret. The old secret keeps working for 24 hours, so you have time to update your endpoint. During that time, the header carries a v1 signature for each secret.
Respond to a request
Return any 2xx status within the endpoint's timeout, which is 10 seconds unless you change it. To stay within it, store the event and process it after you respond.
Retries
If your endpoint doesn't respond with a 2xx, DUET retries the request.
| Your response | What DUET does |
|---|---|
2xx | Marks the event as delivered. |
408, 429, 5xx, or no response in time | Retries after 30 seconds, 2 minutes, 10 minutes, 1 hour, and 6 hours. After the last attempt, the delivery fails. |
410 Gone | Turns off the endpoint. |
Any other 4xx | Fails the delivery without retrying. |
To send a failed delivery again, find it under Activity, and click Send again. It keeps its original event ID.
Handle duplicates and order
- Duplicates: you might receive the same event more than once. Use
X-Duet-Event-Idto ignore events you've already processed. - Order: events can arrive out of order. Keep the version of each record with the latest
updated_at. - Your own changes: changes you send to DUET through inbound sync aren't sent back to you as webhooks.
Network
Webhooks come from the IP address 3.133.25.142. If your firewall allows only known addresses, add it to your allow list. We notify you before it changes.
DUET doesn't follow redirects. Give the final URL of your endpoint.
Requests and responses
How the Platform API identifies sites, handles time, pages through results, formats responses, and reports errors.
Get an alarm occurrence GET
Returns one alarm occurrence with its full lifecycle timeline, oldest event first. Returns `404` if the project has no occurrence with this ID. All IDs are DUET IDs.

