Documentation

Start monitoring your cron jobs in under 2 minutes.

Quick Start

1

Create a monitor

Go to your dashboard and click "New Monitor". Give it a name, pick how often it should be pinged, and set a grace period.

2

Add a ping to your job

At the end of your cron job or script, add an HTTP request to your monitor's unique ping URL.

# Add this at the end of your cron job
curl -fsS --retry 3 https://pulsemon.dev/api/ping/your-slug
3

Get alerted

If your job misses a ping, PulseMon detects it within 60 seconds and sends you an alert via email, Slack, Discord, or webhook.

How Monitoring Works

PulseMon uses a dead-man's-switch model. Your job pings us on success; if we don't hear from it in time, we alert you.

ConceptDescription
IntervalHow often your job runs (e.g. every 300 s for a 5-minute cron). PulseMon expects a ping at least once per interval.
Grace periodExtra time added on top of the interval for network latency and clock skew. Defaults scale with your interval: 30 s for fast jobs, up to 30 min for weekly ones.
Deadlinelast ping + interval + grace period. If no ping arrives before the deadline, the monitor is marked down.
Check cyclePulseMon checks every monitor once every 60 seconds. Worst case, a missed deadline is detected ~60s after it passes.

Alert escalation: When a monitor goes down you receive an immediate alert, then follow-up reminders at 1 h, 6 h, and 24 h. After that, reminders repeat daily until the monitor recovers.

Ping API

GET/api/ping/{slug}

Send a heartbeat ping for the given monitor. Always returns 200 OK, even on internal errors, so your cron job won't fail because of monitoring.

Parameters

ParameterTypeDescription
slugstringMonitor slug (path param)
status"success" | "fail"Optional. Report a failure explicitly
durationnumberOptional. Job duration in ms

Response

{ "ok": true }
POST/api/ping/{slug}

Same as GET, but accepts a JSON body for additional metadata.

Body Parameters

ParameterTypeDescription
status"success" | "fail"Optional. Defaults to "success"
durationnumberOptional. Job duration in ms
bodystringOptional. Job output or error message (max 10 KB)
curl -X POST https://pulsemon.dev/api/ping/your-slug \
  -H "Content-Type: application/json" \
  -d '{"status": "success", "duration": 1234, "body": "Processed 500 records"}'

Reporting Failures

By default, every ping is treated as a success and resets the monitor's deadline. If your job detects an error, you can report it explicitly with ?status=fail.

A fail ping immediately marks the monitor as down, creates an incident, and fires an alert. It does not reset the deadline. The monitor recovers when the next success ping arrives.

If you include a body with the fail ping, the error output will be included in the alert so you can see what went wrong without checking the server.

#!/bin/bash
set -e

if /usr/local/bin/backup.sh; then
  curl -fsS --retry 3 "https://pulsemon.dev/api/ping/nightly-backup"
else
  curl -fsS --retry 3 "https://pulsemon.dev/api/ping/nightly-backup?status=fail"
fi

Sending Job Output

Attach your job's stdout, stderr, or a summary to the ping. When PulseMon fires an alert, the output is included so you can see why a job failed without SSHing into the server.

Send output as the body field in a POST request. Up to 10 KB. The body is shown in your ping history and included in all alert notifications (email, Slack, Discord, and webhooks).

Pipe script output

#!/bin/bash
set -e

# Capture output and exit code
OUTPUT=$(./backup.sh 2>&1) || STATUS="fail"

# Send the output with the ping
curl -fsS --retry 3 -X POST https://pulsemon.dev/api/ping/nightly-backup \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg body "$OUTPUT" --arg status "${STATUS:-success}" \
    '{status: $status, body: $body}')"

Send structured data

import requests, time, json

start = time.time()
result = process_records()
duration = int((time.time() - start) * 1000)

requests.post(
    "https://pulsemon.dev/api/ping/record-processor",
    json={
        "duration": duration,
        "body": json.dumps({
            "rows_processed": result.count,
            "errors": result.errors,
            "source": "warehouse-db",
        }),
    },
    timeout=10,
)

If the body is valid JSON, PulseMon pretty-prints it in the dashboard with syntax highlighting. Plain text is shown in a monospace code block.

Duration Thresholds

Get alerted when a job completes but takes longer than expected.

Set a max duration on any monitor (under Advanced Settings). If a ping reports a duration that exceeds the threshold, PulseMon fires a [SLOW] alert and opens an incident. When the next ping comes in under the threshold, the incident auto-resolves.

# Report duration so PulseMon can check against your threshold
START=$(date +%s%3N)
./etl-pipeline.sh
END=$(date +%s%3N)
DURATION=$((END - START))

curl -fsS --retry 3 "https://pulsemon.dev/api/ping/etl-pipeline?duration=$DURATION"

Duration is in milliseconds. For example, set the max to 300 seconds (5 minutes) in the dashboard, then pass duration=360000 to trigger a slow alert.

Start Signal & Overlap Detection

Detect when a job starts a new run before the previous one finishes.

Send a ?status=start ping when your job begins. PulseMon tracks whether the job is currently running. If a second start ping arrives before a success or fail ping, PulseMon fires an [OVERLAP] alert.

Example usage

#!/bin/bash
set -e

# Signal start
curl -fsS https://pulsemon.dev/api/ping/nightly-backup?status=start

# Run the job
/usr/local/bin/backup.sh

# Signal completion
curl -fsS https://pulsemon.dev/api/ping/nightly-backup

Start pings do not reset the deadline. Only success pings reset the deadline and mark the monitor as up.

Rate Limiting

The ping endpoint enforces a rate limit of 1 request per second per slug per status. The bucket is separate for start, success, and fail pings, so a normal start → success lifecycle never rate-limits itself. If you exceed the limit, the endpoint returns a 429 status code.

// 429 response
{ "ok": false, "error": "Rate limited" }

This prevents accidental flood loops. Any real cron job or heartbeat stays well under 1 req/sec. If you use curl --retry 3, retries will back off naturally and won't hit this limit.

Integration Examples

# Run backup every day at 2am, ping PulseMon on success
0 2 * * * /usr/local/bin/backup.sh && curl -fsS --retry 3 https://pulsemon.dev/api/ping/nightly-backup

Alert Channels

EmailSends alerts to your account email by default. Add more addresses on the Alerts page.
SlackAdd a Slack incoming webhook URL to receive alerts in any Slack channel.
DiscordAdd a Discord webhook URL to receive alerts in any Discord channel.
Custom WebhookSends JSON POST requests to any URL, signed with HMAC-SHA256.

Webhook Payload

When an alert fires, PulseMon sends a JSON POST request with this payload:

{
  "event": "monitor.down",
  "monitor": {
    "id": "uuid",
    "name": "Nightly Backup",
    "slug": "nightly-backup",
    "status": "down",
    "lastPingAt": "2025-01-15T02:00:00Z",
    "expectedInterval": 86400,
    "lastPingBody": "pg_dump: error: connection refused"
  },
  "incident": {
    "id": "uuid",
    "startedAt": "2025-01-16T02:05:00Z"
  },
  "timestamp": "2025-01-16T02:05:00Z"
}

The lastPingBody field contains the output from the most recent ping, if any was sent. It's included in all alert types.

Recovery events use "event": "monitor.up". Slow alerts use "event": "monitor.slow" and include lastPingDuration and maxDuration on the monitor object.

Webhook Signature Verification

If you configure a secret on your webhook alert channel, PulseMon signs every request with an HMAC-SHA256 signature in the X-PulseMon-Signature header. The format is sha256=<hex>. Verify this before processing the payload.

import crypto from "crypto";

function verifySignature(body, secret, signatureHeader) {
  const expected = "sha256=" +
    crypto.createHmac("sha256", secret).update(body).digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader),
  );
}

// In your webhook handler:
app.post("/webhooks/pulsemon", (req, res) => {
  const signature = req.headers["x-pulsemon-signature"];
  const body = JSON.stringify(req.body);

  if (!signature || !verifySignature(body, process.env.WEBHOOK_SECRET, signature)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const event = req.body;
  console.log(`Monitor ${event.monitor.name} is ${event.event}`);
  res.json({ ok: true });
});

API Keys

Create API keys in Settings → API Keys to manage monitors programmatically. Keys use Bearer token auth with the prefix pm_. You only see the raw key once at creation, so store it somewhere safe.

Authentication

curl https://pulsemon.dev/api/v1/monitors \
  -H "Authorization: Bearer pm_your-api-key"

Endpoints

MethodPathDescription
GET/api/v1/monitorsList all monitors
POST/api/v1/monitorsCreate a monitor
GET/api/v1/monitors/{id}Get monitor details
PATCH/api/v1/monitors/{id}Update a monitor
DELETE/api/v1/monitors/{id}Delete a monitor
POST/api/v1/monitors/{id}/pausePause a monitor
POST/api/v1/monitors/{id}/resumeResume a monitor
GET/api/v1/monitors/{id}/pingsPing history (paginated)
GET/api/v1/monitors/{id}/incidentsIncident history (paginated)

Example: Create a Monitor

curl -X POST https://pulsemon.dev/api/v1/monitors \
  -H "Authorization: Bearer pm_your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Nightly Backup",
    "slug": "nightly-backup",
    "expectedInterval": 86400,
    "gracePeriod": 900
  }'

All endpoints return { "data": T, "error": null } on success and { "data": null, "error": { "code": "...", "message": "..." } } on failure. Missing or invalid keys return 401.

OpenClaw Skill

Use PulseMon from WhatsApp, Telegram, or Slack via OpenClaw. Install the skill and manage your monitors with natural language.

clawhub install pulsemon

Set your API key as an environment variable:

PULSEMON_API_KEY=pm_your-key-here

Then ask your agent things like "check my monitors", "create a monitor called nightly-backup that runs daily", or "pause the data-sync monitor".