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

EventSent when
critical_alert.openedA critical alert is raised on a print.
critical_alert.respondedSomeone acknowledges, dismisses or snoozes the alert, or marks the print stopped.
critical_alert.reminderA snoozed alert is still active when the print reaches the snooze layer.
critical_alert.resolvedA critical alert clears on its own.
print.createdA print appears in Euler, from a machine or an upload.
print.completedA print from a machine has finished. Sent about an hour after its last layer.
print.analysis_completedEuler finishes analyzing a print.
device.offlineA device stops sending data to Euler.
device.onlineA device that went offline is sending data again.
pingYou 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, print and device. alert.state is open, snoozed, responded or resolved at the time of the event. critical_alert.responded also carries response, with an outcome (acknowledged, dismissed, print_stopped or snoozed), the comment left with it and responded_by.
  • Print events carry print and device. print.completed adds layer_count and critical_alert_count to print. print.analysis_completed adds analysis, with an outcome of succeeded or failed.
  • Device events carry device, and offline_since or online_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:

HeaderContains
webhook-idThe event's ID, the same as id in the body.
webhook-timestampWhen this attempt was sent, in seconds since the Unix epoch.
webhook-signatureOne 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

On this page