Developer resources v1
Coilinx API
Read-only inventory sync and signed operational webhooks.
Verify webhook signatures
Each delivery includes:
X-Coilinx-Event-Id: evt_01abc
X-Coilinx-Signature: t=1784043130,v1=4d9d...
The signed message is <timestamp>.<raw UTF-8 request body>. Compute
HMAC-SHA256 with the endpoint's whsec_... secret and compare signatures using
a constant-time function.
Reject timestamps more than five minutes from your server clock to limit replay attacks. Store processed event IDs to prevent duplicate business actions.
Node.js
import crypto from "node:crypto";
export function verifyCoilinxWebhook(rawBody, signatureHeader, secret) {
const entries = Object.fromEntries(
signatureHeader.split(",").map((part) => part.split("=", 2)),
);
const timestamp = Number(entries.t);
const signature = entries.v1;
if (!Number.isFinite(timestamp) || !signature) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex");
const actualBuffer = Buffer.from(signature, "hex");
const expectedBuffer = Buffer.from(expected, "hex");
return (
actualBuffer.length === expectedBuffer.length &&
crypto.timingSafeEqual(actualBuffer, expectedBuffer)
);
}
Pass the raw Buffer, not a parsed-and-stringified JSON object.
Python
import hashlib
import hmac
import time
def verify_coilinx_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
entries = dict(part.split("=", 1) for part in signature_header.split(","))
timestamp = int(entries.get("t", "0"))
signature = entries.get("v1", "")
if not timestamp or not signature:
return False
if abs(time.time() - timestamp) > 300:
return False
signed = str(timestamp).encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Secret rotation
Webhook signing secrets are shown once when an endpoint is created. To rotate, create a replacement endpoint, deploy its secret, test it from the dashboard, then delete the old endpoint.