M00N ReportNotifications
Documentation/Features/Notifications

Notifications

Tell your team when a run finishes. Channels belong to a project, are configured by an admin, and every enabled one fires on its own condition.

Channels

Six destinations. Enable as many as you like on a project, each with its own trigger condition and its own message template.

Slack

Post notifications to Slack channels via webhook.

Microsoft Teams

Deliver notifications to Teams channels.

Discord

Send formatted notifications with status badges.

Telegram

Notify any Telegram group or channel.

Email

Send email notifications via Gmail OAuth or custom SMTP.

Custom webhook

Post signed JSON to your own endpoint, script, or AI agent.

Setup: Navigate to Settings → Notifications to configure your channels.

Slack

  • Paste your Slack webhook URL into the notification settings.
  • Receive notifications for run completions, failures, and other events.
  • Customize the message format with test data variables.

Microsoft Teams

  • Use an Incoming Webhook connector to receive notifications.
  • Notifications appear directly in your Teams channel.
  • Customize the message template to suit your team's preferences.

Discord

  • Paste your Discord webhook URL into the notification settings.
  • Receive formatted notifications with status badges.

Telegram

  • Configure with your bot token and chat ID.
  • Receive notifications in any Telegram group or channel.

Email Notifications

Two setup options are available for sending email notifications:

  • Gmail OAuth- connect your organization's Gmail account for quick setup.
  • Custom SMTP- configure host, port, and credentials for any mail server.
  • Test email sending before saving to verify your configuration works.

When Notifications Fire

  • Every channel fires when an automated run finishes.
  • Each channel has its own trigger: all runs, failures only, or successes only. A branch filter can narrow it further.
  • Use the test button in Settings to verify a channel before relying on it.

Custom Webhook

The custom channel posts JSON to any URL you choose. Unlike the chat channels it is meant for machines: it carries the identifiers a script or an AI agent needs to fetch the run and act on it.

{
  "event": "run.completed",
  "runId": "7c3e9a2b-1f44-4c8e-9d0a-3b7e5f2c1d88",
  "projectId": "5fa6b1b7-e66e-4d5e-a0c2-26a5b4dd0124",
  "organizationId": "1e44aa00-0000-4000-8000-000000000000",
  "project": "WEB-CRM",
  "launch": "nightly-e2e",
  "status": "failed",
  "duration": "12m 04s",
  "url": "https://app.example.com/share/run/7c3e9a2b?t=...",
  "stats": { "total": 412, "passed": 405, "failed": 3, "skipped": 4, "flaky": 2, "retries": 7 },
  "attributes": { "branch": "main", "commit": "a1b2c3d" },
  "tags": ["nightly", "e2e"],
  "message": "the rendered message template",
  "timestamp": "2026-07-26T03:12:44.101Z"
}

Alongside these headers:

  • X-M00N-Event- the event name, currently always run.completed.
  • X-M00N-Delivery- a unique id per delivery, so a receiver can ignore a repeat.
  • X-M00N-Timestamp- unix seconds, and part of what is signed.
  • X-M00N-Signature- present only when a signing secret is configured.

Verifying the Signature

Generate a signing secret in Settings → Notifications → Webhook. It is shown once. Your receiver recomputes the signature over the timestamp and the exact request body and compares it to the header. The timestamp is part of the signed value, so you can also reject a delivery that is too old to be genuine.

import crypto from 'node:crypto';

function verify(rawBody, headers, secret) {
  const timestamp = headers['x-m00n-timestamp'];
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + rawBody)
    .digest('hex');

  const a = Buffer.from(headers['x-m00n-signature'] || '');
  const b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;

  // Reject anything older than five minutes.
  return Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
}

Verify against the raw request body, before any JSON parsing. Parsing and re-serializing changes the bytes and the signature will not match.

Shaping the Request

Some receivers expect a particular request. Two settings cover that, both on the custom channel only.

  • Request headers- up to ten name and value pairs, for receivers that authenticate. Values are stored encrypted and are never shown again after you save them.
  • Body template- leave it empty to send the payload above, or write your own JSON using the same variables as the message template. Values are escaped for you, so a launch name containing a quote cannot break the JSON.
{"text": "m00n run={{run.id}} project={{project.id}} status={{run.status}}"}

Content-Type and the X-M00N- headers are set by M00N and cannot be overridden.

Template Variables

These work in the message template on every channel, and in the body template on the custom channel.

  • {{project.id}} and {{project.name}}
  • {{run.id}}, {{run.launch}}, {{run.status}}, {{run.status_emoji}}
  • {{run.duration}}, {{run.started_at}}, {{run.ended_at}}
  • {{run.url}} and {{run.tags}}
  • {{stats.total}}, {{stats.passed}}, {{stats.failed}}, {{stats.skipped}}, {{stats.flaky}}, {{stats.retries}}
  • {{attr.NAME}}- any attribute your reporter sends with the run, such as {{attr.branch}} or {{attr.commit}}. The picker lists the ones your recent runs actually carry.

Use the test button in Settings to see the rendered result before relying on it.

Last updated