Moyo
Sign inGet started
REST API

Webhooks

Get notified when a dimension crosses the 50 response minimum or the certificate updates.

dimension.reportedevent
A dimension reached 50 responses.
certificate.updatedevent
Daily certificate refresh finished.
usage.thresholdevent
Shown check-ins passed a number you set.

None of them carries an answer. You learn that something is now reportable, never what anyone said.

Verifying a delivery

Every request carries a `moyo-signature` header holding a timestamp and an HMAC-SHA256 of that timestamp joined to the raw body. Verify against the raw bytes, before parsing: verifying a re-serialised object is verifying something other than what arrived, which is the usual way this check gets quietly defeated.

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

function verify(secret, header, rawBody) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=").map((s) => s.trim())),
  );
  // Reject anything older than five minutes, so a captured delivery
  // cannot be replayed next week as though it were new.
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Retries

Five attempts, backing off one minute, five, twenty-five, two hours, ten hours. A delivery is never retried with a different body: the payload is stored when the event happens, so a retry three hours later says what was true then rather than what is true now — which is what you need to reconcile your own records.

An endpoint that fails five deliveries in a row is switched off, and the dashboard says why. A webhook that silently stops is worse than none: you build on it, it stops, and nobody finds out.