Teams docs

Session webhook

Each Mac posts a JSON event to your URL when a session starts and when it ends. Requests are signed so you can tell they came from your Macs.

Events

session.started and session.ended, sent as a POST with Content-Type: application/json.

{
  "event": "session.ended",
  "id": "5e0b6b0e-1f0f-4c1a-9a63-2c0d7c4a1e55",
  "occurredAt": "2026-10-02T07:00:03Z",
  "device": { "id": "mac_0b4b5e2c-…", "label": "Build Mac 1", "appVersion": "1.4.0" },
  "session": {
    "startedAt": "2026-10-01T23:00:00Z",
    "endedAt": "2026-10-02T07:00:03Z",
    "source": "schedule",
    "sourceName": "Overnight agents",
    "allowsDisplaySleep": true,
    "staysAwakeWithLidClosed": false,
    "endCause": "scheduleEnded",
    "awakeSeconds": 28803,
    "awaySeconds": 27950
  }
}
  • id is unique per delivery. Retries reuse it, so you can ignore one you've already handled.
  • source is manual, schedule, or trigger; sourceName is the schedule or trigger's name.
  • endsAt appears when the session has an end time. endedAt, endCause, awakeSeconds, and awaySeconds appear on session.ended.
  • endCause is one of you, timeUp, appQuit, downloadFinished, lowBattery, unplugged, triggerEnded, scheduleEnded, schedulePaused, policyLimit, replaced, midnightOilQuit.

Verifying the signature

The profile builder requires a signing secret (except for Slack), and each request carries X-MidnightOil-Signature: t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of <t>.<raw body> using your secret, the same scheme as Stripe's. Compare in constant time, and reject timestamps more than five minutes old.

// Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
# Python
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(part.split("=", 1) for part in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Delivery

  • Your URL must use https; plain http is allowed only to the Mac itself, for a local relay. Redirects aren't followed.
  • Each retry first checks the license and URL are still in place, so removing either stops further attempts.
  • Answer with any 2xx. Network errors, 429, and 5xx are retried after 2 seconds, 10 seconds, and a minute; other responses aren't.
  • When Midnight Oil quits mid-session, it waits up to three seconds to send session.ended.

Slack

Use a Slack incoming-webhook URL (https://hooks.slack.com/…) and Macs post readable messages instead, like “🔥 Build Mac 1 is staying awake on the “Overnight agents” schedule.” Slack messages aren't signed.