Webhook Integration Guide
This guide is for developers building custom integrations that consume Shoplogix event webhooks — for example, to trigger CMMS work orders, update asset management systems, or feed dashboards with equipment condition data.
For a reference of all payload shapes and supported event types, see Webhooks.
Step 1: Register a Webhook Subscription
- In Shoplogix IMS, go to the Webhooks section (requires the Developer role)
- Provide your endpoint URL and select the event types you want to receive
- Save — you will receive a signing secret that you must store securely
Or manage subscriptions via the REST API at POST /v2020-07/webhooks.
Step 2: Implement Your Endpoint
Your endpoint must:
- Accept
POSTrequests - Return a
2xxstatus code within the request timeout to acknowledge receipt - Be reachable from the public internet (or configured to accept traffic from Shoplogix source IP ranges — contact your account team if you are behind a firewall)
Do not perform heavy processing synchronously in your handler. Return 200 immediately and process the payload asynchronously using an internal queue.
Step 3: Verify the HMAC-SHA256 Signature
Every event webhook request is signed. You must verify the signature on each request before acting on the payload.
Algorithm: HMAC-SHA256 per draft-cavage-http-signatures-12
Signed headers (included in every request):
| Header | Notes |
|---|---|
(request-target) | HTTP method and path |
date | RFC 7231 date — also used for replay protection |
digest | SHA-256 digest of the request body |
host | Your endpoint host |
content-length | Byte length of the request body |
The Signature header carries the key ID, algorithm, signed header list, and computed signature value.
Python Example
import hashlib
import hmac
import base64
from http.server import BaseHTTPRequestHandler
SIGNING_SECRET = "your-signing-secret"
def verify_signature(headers: dict, body: bytes, signing_secret: str) -> bool:
sig_header = headers.get("signature", "")
if not sig_header:
return False
# Parse the Signature header into key=value pairs
sig_parts = {}
for part in sig_header.split(","):
k, _, v = part.strip().partition("=")
sig_parts[k] = v.strip('"')
signed_headers = sig_parts.get("headers", "").split()
signature_b64 = sig_parts.get("signature", "")
# Reconstruct the signing string
signing_lines = []
for header_name in signed_headers:
if header_name == "(request-target)":
# Caller must pass this in; e.g. "post /webhooks/receive"
signing_lines.append(f"(request-target): {headers['(request-target)']}")
else:
signing_lines.append(f"{header_name}: {headers[header_name.lower()]}")
signing_string = "\n".join(signing_lines)
# Compute HMAC-SHA256
mac = hmac.new(signing_secret.encode(), signing_string.encode(), hashlib.sha256)
computed = base64.b64encode(mac.digest()).decode()
return hmac.compare_digest(computed, signature_b64)
Node.js Example
const crypto = require('crypto');
const SIGNING_SECRET = 'your-signing-secret';
function verifySignature(headers, rawBody, signingSecret) {
const sigHeader = headers['signature'];
if (!sigHeader) return false;
// Parse the Signature header
const sigParts = Object.fromEntries(
sigHeader.split(',').map((part) => {
const [key, ...rest] = part.trim().split('=');
return [key, rest.join('=').replace(/^"|"$/g, '')];
})
);
const signedHeaders = (sigParts['headers'] || '').split(' ');
const signatureB64 = sigParts['signature'] || '';
// Reconstruct the signing string
const signingLines = signedHeaders.map((headerName) => {
if (headerName === '(request-target)') {
return `(request-target): ${headers['(request-target)']}`;
}
return `${headerName}: ${headers[headerName.toLowerCase()]}`;
});
const signingString = signingLines.join('\n');
// Compute HMAC-SHA256
const computed = crypto
.createHmac('sha256', signingSecret)
.update(signingString)
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(signatureB64)
);
}
Reject any request that fails signature verification with a 403 response. Do not process the payload.
Step 4: Parse the Payload
Each request includes an everactive-event-type header identifying the event type.
| Header value | Payload type |
|---|---|
trap-state-change-event | Steam trap condition change |
machine-alarm-state-change-event | Machine alarm state change |
Full payload shapes and field descriptions are documented in Webhooks.
The integrationInfo Field
If a steam trap is linked to an external asset (for example, via the Armstrong SAGE integration), the payload includes an integrationInfo object:
{
"trapID": "c3d4e5f6-...",
"trapEndState": "Blowthrough",
"integrationInfo": {
"assetId": "5d30ca383794b500144a2576",
"name": "Trap 142 - Building C"
}
}
Use integrationInfo.assetId to correlate the Shoplogix event with the corresponding record in your external system.
Step 5: Respond Quickly
Return a 2xx response before performing any time-consuming processing. If your handler needs to do database writes, call downstream APIs, or send notifications, push the payload to an internal queue and acknowledge receipt immediately.
Failure to return 2xx within the request timeout will trigger a retry.
Handling Retries
The platform will retry failed deliveries up to 3 times with increasing backoff delays (approximately 0 s, 1 s, 2 s for successive attempts, with jitter). Your handler should be idempotent.
Use the eventId field in the payload as a deduplication key. If you receive the same eventId more than once, ignore the duplicate rather than processing it again.
Testing
During development, register a webhook pointing to a public inspection service such as webhook.site to see exactly what Shoplogix sends before connecting your production endpoint.
Once your endpoint is live, confirm end-to-end delivery by triggering a test state change in your environment and checking that your system received and processed the event.
Related
- Webhooks — payload reference and event types
- Armstrong SAGE Integration — how Shoplogix uses this same event stream for a built-in CMMS integration