Webhook events
The events Euler sends to your webhook endpoints, what they contain and how to check them.
Every webhook is a JSON POST to your endpoint. This page lists the events, shows what they
contain and explains how to check that a request comes from Euler. To set up an endpoint, see
Webhooks.
Events
| Event | Sent when |
|---|---|
critical_alert.opened | A critical alert is raised on a print. |
critical_alert.responded | Someone acknowledges, dismisses or snoozes the alert, or marks the print stopped. |
critical_alert.reminder | A snoozed alert is still active when the print reaches the snooze layer. |
critical_alert.resolved | A critical alert clears on its own. |
print.created | A print appears in Euler, from a machine or an upload. |
print.completed | A print from a machine has finished. Sent about an hour after its last layer. |
print.analysis_completed | Euler finishes analyzing a print. |
device.offline | A device stops sending data to Euler. |
device.online | A device that went offline is sending data again. |
ping | You choose Send test event. |
Euler sends critical_alert.responded, .reminder and .resolved to an endpoint only if it sent
that endpoint critical_alert.opened for the same alert. In the same way, device.online is sent
only after device.offline for the same device.
What Euler sends
Each event is a POST with a JSON body:
{
"id": "evt_01JBXR4T6G1Q3V9E2M8K0ZP5HD",
"type": "critical_alert.opened",
"api_version": "2026-11-01",
"occurred_at": "2026-10-27T09:14:03.120Z",
"org": { "slug": "northwind" },
"data": {
"alert": {
"id": "01JBXR4SZ8C2N6W4H1T7Y3D9QF",
"state": "open",
"first_layer": 412,
"last_layer": 416,
"opened_at": "2026-10-27T09:14:02.981Z",
"responded_at": null,
"resolved_at": null,
"snoozed_until_layer": null,
"url": "https://www.euler3d.com/app/northwind/print/01JBXQ…?layerNumber=412"
},
"print": {
"id": "01JBXQ0M7R5K2D8V6C4N1B3H9T",
"name": "Bracket batch 12",
"path": "jobs/bracket-batch-12",
"url": "https://www.euler3d.com/app/northwind/print/01JBXQ…",
"created_at": "2026-10-27T06:02:41.004Z"
},
"device": {
"id": "01HZ8D6P2W4E7R1T9Y3U5I0O8K",
"name": "M2 North",
"url": "https://www.euler3d.com/app/northwind/device/01HZ8D…"
}
}
}data depends on the event:
- Critical alert events carry
alert,printanddevice.alert.stateisopen,snoozed,respondedorresolvedat the time of the event.critical_alert.respondedalso carriesresponse, with anoutcome(acknowledged,dismissed,print_stoppedorsnoozed), thecommentleft with it andresponded_by. - Print events carry
printanddevice.print.completedaddslayer_countandcritical_alert_counttoprint.print.analysis_completedaddsanalysis, with anoutcomeofsucceededorfailed. - Device events carry
device, andoffline_sinceoronline_at.
print.path is the print's path on the device, the same as path in the API's list of prints.
Together with device.id it identifies a print. It's null for uploaded prints.
New fields can appear in any event. Ignore fields you don't recognize. A change that removes or
renames a field comes with a new api_version.
Check the signature
Every request carries three headers:
| Header | Contains |
|---|---|
webhook-id | The event's ID, the same as id in the body. |
webhook-timestamp | When this attempt was sent, in seconds since the Unix epoch. |
webhook-signature | One or more signatures, separated by spaces. |
To check a request, compute an HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{body},
using your signing secret without its whsec_ prefix, base64-decoded, as the key. Compare the
base64-encoded result, prefixed with v1,, to each signature in webhook-signature. Use the raw
request body, exactly as received, before parsing it, and reject requests whose timestamp is more
than five minutes off.
The examples below need no extra libraries. Euler follows the Standard Webhooks specification, so you can also use one of its libraries.
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 5 * 60;
/**
* Returns true if the request was signed by Euler.
* Pass the raw request body, exactly as received, before parsing it.
*/
export function verifyEulerWebhook(
secret: string,
headers: { id: string; timestamp: string; signature: string },
rawBody: string,
): boolean {
const timestamp = Number(headers.timestamp);
const now = Math.floor(Date.now() / 1000);
if (
!Number.isInteger(timestamp) ||
Math.abs(now - timestamp) > TOLERANCE_SECONDS
)
return false;
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = Buffer.from(
'v1,' +
createHmac('sha256', key)
.update(`${headers.id}.${headers.timestamp}.${rawBody}`)
.digest('base64'),
);
// The header can hold several signatures, separated by spaces.
return headers.signature.split(' ').some((candidate) => {
const given = Buffer.from(candidate);
return given.length === expected.length && timingSafeEqual(given, expected);
});
}
// ========= EXAMPLE USAGE =========
// const ok = verifyEulerWebhook(
// process.env.EULER_WEBHOOK_SECRET!,
// {
// id: req.headers['webhook-id'],
// timestamp: req.headers['webhook-timestamp'],
// signature: req.headers['webhook-signature'],
// },
// rawBody,
// );
// if (!ok) return res.status(401).end();Responses and retries
Respond with a 2xx status code within 10 seconds. Euler counts any other status code, a redirect or
no response within 10 seconds as a failed delivery. If processing an event takes longer, respond
first and process the event afterwards.
Euler retries a failed delivery several times over about an hour. If your endpoint responds with
410 Gone, Euler turns the endpoint off and does not retry.
The same event can be delivered more than once, and events can arrive in a different order from the
one they happened in. Use webhook-id to recognize events you have already processed, and
occurred_at to order them.
Next steps
- Webhooks: add, test and manage endpoints.
- Getting started: fetch more data about an event through the API.