Connect your own system to ColdLatch with a webhook
Short answer: give ColdLatch an HTTPS address and a secret. Each time a temperature excursion is confirmed, and each time QA decides what happens to an affected batch, ColdLatch sends that address a signed JSON message describing it. Your code checks the signature and does whatever your process needs: open a ticket, place stock on hold in your ERP, notify a team.
A webhook is how ColdLatch reaches a system it has no built-in connection for: a quality system, a ticketing tool, a data warehouse, or an ERP other than SAP S/4HANA. ColdLatch calls you; you do not have to poll.
What it sends today, and what it does not
- The excursion:
alert.confirmed, when an excursion has lasted longer than the profile's grace period, or when a logger has been silent for too long. - QA's decisions, batch by batch:
action.approvedandaction.rejected, with the material, batch, quantity, location, who signed and the reason they gave. - What happened in SAP:
action.succeededwith the material document number once the stock is blocked, oraction.failedwith the error. - The end of the incident:
alert.resolved, with who signed and their reason. - Batch-level messages still need SAP S/4HANA. ColdLatch finds the affected batches by asking SAP, and an approval is posted to SAP. Without that connection an alert names the storage unit or shipment, not the batches in it, and no
actionmessages are sent; your system has to work the batches out.
Setting it up
- In ColdLatch, an administrator opens Settings, then Integrations, and adds one of type Webhook.
- Enter the URL ColdLatch should call. Use HTTPS.
- Enter a secret: a long random string that only ColdLatch and your system know. ColdLatch stores it encrypted and never shows it again.
- Save it, then press Send test on its row. ColdLatch sends a test message (below) and reports whether your address answered with success.
The request
ColdLatch sends an HTTP POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-ColdLatch-Event | The event name, for example alert.confirmed or action.approved; integration.test for Send test |
X-ColdLatch-Signature | sha256= followed by the signature of the body, in hexadecimal |
The alert.confirmed message
{
"alertId": "8f3c2a1e-6b1d-4c0a-9a55-2f0d6f1b7c21",
"excursion": {
"id": "c1d2e3f4-0a1b-4c2d-8e3f-4a5b6c7d8e9f",
"type": "HIGH",
"startedAt": "2026-10-05T07:30:00.000Z",
"confirmedAt": "2026-10-05T07:45:00.000Z",
"peakC": 9.4
},
"subject": {
"kind": "storage_unit",
"id": "5a6b7c8d-1e2f-4a3b-9c4d-5e6f7a8b9c0d",
"name": "Vaccine store B"
},
"profile": { "name": "Pharma 2-8", "minC": 2, "maxC": 8 }
}
| Field | Meaning |
|---|---|
alertId | The alert in ColdLatch. Use it to recognise a message you have already handled. |
excursion.type | HIGH (too warm), LOW (too cold) or OFFLINE (the logger stopped reporting) |
excursion.startedAt, confirmedAt | Times in UTC, ISO 8601 |
excursion.peakC | The most extreme temperature so far, in °C. null for OFFLINE. |
subject.kind | storage_unit (then name is present) or shipment (then reference is present) |
profile | The allowed range that was broken |
Send test sends {"test": true, "message": "ColdLatch test event"} with the event name integration.test.
The decision messages
The four action events share one shape. This is action.approved:
{
"alertId": "8f3c2a1e-6b1d-4c0a-9a55-2f0d6f1b7c21",
"action": {
"id": "0b7e6d5c-4a3f-4e2d-9c1b-0a9f8e7d6c5b",
"ref": "TC-000042",
"type": "SAP_BLOCK_BATCH",
"status": "approved",
"material": "VAC-MMR-2026",
"batch": "MMR260411",
"plant": "1710",
"storageLocation": "0002",
"quantity": 1200,
"unit": "EA"
},
"decision": {
"meaning": "approve",
"by": "qa@example.com",
"reason": "Exposed above the labelled range; quarantine pending assessment.",
"signedAt": "2026-10-05T08:10:00.000Z"
}
}
| Field | Meaning |
|---|---|
alertId | The alert this batch belongs to: the same value as in alert.confirmed |
action.id, action.ref | The proposed block. ref is the short reference shown in ColdLatch and written into the SAP document header. |
action.status | The state after this event: approved, rejected, succeeded or failed |
action.material, batch, plant, storageLocation, quantity, unit | The stock concerned. batch is an empty string for stock that is not batch-managed. |
decision | On action.approved and action.rejected: who signed (by, an email address), the reason they typed, and when. by is null when ColdLatch approved by itself in automatic mode. |
sap.materialDocument | On action.succeeded: the SAP material document, as number and year, for example 4900000012/2026 |
error | On action.failed: why SAP did not take the block. QA can retry it in ColdLatch; a second failure sends a second message. |
An approval for a shipment that is still on the road is sent at once, but the block is posted to SAP only when the shipment is received, so action.succeeded can follow much later.
alert.resolved carries only the alert and the signed decision:
{
"alertId": "8f3c2a1e-6b1d-4c0a-9a55-2f0d6f1b7c21",
"decision": {
"meaning": "resolve",
"by": "qa@example.com",
"reason": "Cold-room door left open during a delivery. Staff retrained.",
"signedAt": "2026-10-05T11:30:00.000Z"
}
}
Checking the signature
The signature proves the message came from ColdLatch and was not altered. It is the HMAC-SHA256 of the raw request body, keyed with your secret. Compute it over the bytes you received, before parsing the JSON, and compare it with the header in constant time.
// Node.js
const crypto = require('node:crypto');
function isFromColdLatch(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Reject anything that fails the check with 401.
How to answer, and what happens if you do not
| Your answer | What ColdLatch does |
|---|---|
Any 2xx within 15 seconds | Marks the message delivered |
5xx, a timeout, or no connection | Tries again after 1 minute, 5 minutes, 15 minutes, 1 hour, 2 hours, 4 hours and 8 hours: eight attempts in all, then gives up |
401 or 403 | Stops at once, and tells your other notification channels (email, Slack) that the webhook is rejecting it |
Any other 4xx | Stops at once |
- Answer quickly, work later. Accept the message, answer
200, then do the slow part in the background. - Expect the same message twice. If your answer is lost on the way back, ColdLatch sends again. Remember what you have handled and ignore repeats: the event name with
alertIdfor the two alert events, and the event name withaction.idfor the others (adderroror the time you received it foraction.failed, which can happen again after a retry). - Do not rely on the order. Each message is delivered and retried on its own, so a late
action.approvedcan arrive after itsaction.succeeded.action.statussays where the block stood when the message was written. - Every attempt is visible. The Integration log in ColdLatch shows each delivery, its status and the error.
Also: How to block a batch in SAP S/4HANA after a temperature excursion