Job monitors

Alerts you when a scheduled job misses its check-in, runs too long, or fails. $0.50 per monitor per month. The base URL is https://www.motor.dev/job_monitors, and the OpenAPI 3.1 specification describes each endpoint.

Try it

Sign in to try the API here with one of your team's keys. Requests use that key's permissions and spending limit. Standard charges apply.

Quick start

Know when your scheduled jobs need attention. Give each job a monitor, then have it ping its secret URL whenever it runs. If a ping doesn't arrive on time, Motor alerts you.

  1. First, choose where alerts should go. This example adds an email channel. Open the confirmation link sent to the address if it belongs to someone outside your team. New monitors automatically use channels with "default": true.

    curl https://api.motor.dev/job_monitors/alert_channels \
      -H "Authorization: Bearer $MOTOR_KEY" \
      -H "Content-Type: application/json" \
      -d '{"kind": "email", "address": "ops@example.com", "default": true}'
  2. Create a monitor. This example expects a daily backup at 03:00 New York time, with a 10-minute grace period:

    curl https://api.motor.dev/job_monitors/monitors \
      -H "Authorization: Bearer $MOTOR_KEY" \
      -H "Content-Type: application/json" \
      -d '{"name": "Nightly backup", "schedule": "0 3 * * *",
           "time_zone": "America/New_York", "grace_seconds": 600}'
  3. Copy the ping_url from the response and use it in your crontab to send a ping when the backup succeeds. Set the cron scheduler to the same time zone as the monitor:

    0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 5 https://api.motor.dev/job_monitors/ping/0b1f8c2e-...

Monitoring starts as soon as you create the monitor, so you'll hear about a missed run even if the job has never sent a ping.

Schedules and grace periods

Choose the schedule that matches your job:

  • Cron. schedule is a five-field cron expression, such as 0 3 * * *, or a macro, such as @daily. time_zone is an IANA time zone name, such as America/New_York. It defaults to UTC.
  • Interval. interval_seconds, from 60 seconds to 31 days. The next ping is due one interval after the last ping that started a run.

grace_seconds gives your job extra time after its scheduled check-in before Motor marks the run as missed. The default is 5 minutes. Pings can also arrive up to 30 seconds early: a ping at 02:59:58 counts for a 03:00 run.

When daylight saving time skips a scheduled time, Motor shifts it forward by the clock change: for a one-hour change, 02:30 becomes 03:30. When a time occurs twice, Motor expects only one run, at the first occurrence.

Runs

Send a start ping when the job starts, then a success or failure ping when it finishes. Motor tracks the duration and alerts you if the run exceeds max_runtime_seconds.

URL=https://api.motor.dev/job_monitors/ping/0b1f8c2e-...
RID=$(uuidgen)
curl -fsS "$URL/start?rid=$RID"
/usr/local/bin/backup.sh
curl -fsS "$URL/$?"?rid=$RID   # the exit code: 0 is a success
  • /start opens a run. /fail, /<exit code>, and the plain URL close it. /log adds a log message without changing the monitor's state.
  • ?rid= is a UUID that you choose for each execution. With run IDs, runs can overlap, and repeated pings don't count twice. Without a run ID, a second start abandons the open run.
  • A POST body of any content type becomes the ping's log. Motor keeps the first 100,000 bytes. To send output from a shell, use --data-binary: curl -d @file removes the newlines.

Statuses and incidents

Each monitor has one status:

StatusWhen
pausedSomeone paused the monitor.
downAn incident is open: a missed run, a failure, or a run that took too long.
lateA check-in is due, or consecutive failures are still within the configured tolerance.
newWaiting for the first ping; no check-in is due yet.
upThe monitor has received a ping and has no outstanding failures or overdue check-ins.

failure_tolerance sets how many consecutive unsuccessful results to allow before opening an incident. The default is 0, so the first unsuccessful result opens one. recovery_threshold sets how many consecutive successful results are needed to resolve it. realert_seconds sets the interval between reminders while an incident is open.

Ping from your jobs

The ping URLs work with clients for Healthchecks.io. Use https://api.motor.dev/job_monitors/ping as the base URL.

runitor

runitor -api-url https://api.motor.dev/job_monitors/ping \
  -uuid 0b1f8c2e-... -- /usr/local/bin/backup.sh

borgmatic

healthchecks:
    ping_url: https://api.motor.dev/job_monitors/ping/0b1f8c2e-...

Kubernetes CronJob

containers:
  - name: report
    image: example/report:1.4
    command: ["/bin/sh", "-c"]
    args:
      - >
        curl -fsS "$PING_URL/start" &&
        /app/report;
        curl -fsS "$PING_URL/$?"

Logs with curl

/usr/local/bin/backup.sh 2>&1 | curl -fsS --data-binary @- \
  -H "Content-Type: text/plain" "$PING_URL/$?"

Ping keys. A ping key lets a script send pings by slug: /ping/<ping key>/<slug>. With ?create=1 and a key that has can_create enabled, the first ping creates the monitor.

Alerts and webhooks

Get alerts by email, webhook, Slack, Discord, or PagerDuty when an incident opens or resolves, plus optional reminders while it's open. Email recipients outside your team must confirm their address first. Enable include_logs on a channel to include the last 2,000 bytes of the failed ping's log.

Motor sends each webhook event as a JSON POST request. The Motor-Signature header contains a timestamp and an HMAC-SHA256 signature of <time>.<body>. Verify the signature and reject events with timestamps more than 5 minutes from your clock. During secret rotation, the header includes one v1 signature for each secret.

Motor-Event-Id: evt_jma_3XzPd6LcVhT
Motor-Signature: t=1790000000,v1=5d3c...

{"id": "evt_jma_3XzPd6LcVhT", "type": "job_monitors.incident.opened",
 "created_at": "2026-10-05T07:10:00Z",
 "data": {"incident": {...}, "monitor": {...}, "message": "..."}}

Check a signature in Python

import base64, hashlib, hmac, time

def verified(secret, header, body):
    key = base64.urlsafe_b64decode(secret.removeprefix("whsec_") + "==")
    parts = [part.split("=", 1) for part in header.split(",")]
    t = next(value for name, value in parts if name == "t")
    if abs(time.time() - int(t)) > 300:
        return False
    expected = hmac.new(key, f"{t}.{body}".encode(), hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, value) for name, value in parts if name == "v1")

Check a signature in Node.js

import crypto from "node:crypto";

export function verified(secret, header, body) {
  const key = Buffer.from(secret.replace("whsec_", ""), "base64url");
  const parts = header.split(",").map((part) => part.split("="));
  const t = parts.find(([name]) => name === "t")?.[1];
  if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = crypto.createHmac("sha256", key).update(`${t}.${body}`).digest("hex");
  return parts.some(([name, value]) => name === "v1" && value.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected)));
}

Monitors as code

Keep monitor definitions alongside your job code. POST /monitors/sync creates and updates monitors by slug. With prune_tag, it also deletes monitors with that tag whose slugs are missing from the request. Motor validates the entire request before making changes. Set dry_run to true to preview the changes.

curl https://api.motor.dev/job_monitors/monitors/sync \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"monitors": [{"slug": "nightly-backup", "name": "Nightly backup",
                    "schedule": "0 3 * * *", "tags": ["deploy:api"]}],
       "prune_tag": "deploy:api", "dry_run": true}'

Pricing and limits

$0.50 per monitor per month, prorated daily. Monitors paused for the entire day are free for that day. Alerts are included at no extra cost.

LimitValue
Pings per monitor4 per minute, bursts of up to 20
Monitors per team10,000
Monitors per hour with create=160
Ping history per monitorUp to 10,000 pings, up to 90 days
Stored ping bodies per monitorLatest 100 bodies, up to 100 kB each
Resolved incidents kept1 year

Once an API key reaches its spending limit, it can't create more monitors. Existing monitors keep running and accruing charges.

Ping endpoints

POST/job_monitors/ping/{token}

Report a successful run

Records a success ping. Use GET, HEAD, or POST. A POST body (the first 100,000 bytes) becomes the ping's log. Use the same ?rid=<uuid> on start and completion pings to link them to one run.

This endpoint authenticates with the secret in its URL. Keep the URL private.

ParameterLocationDescription
tokenpathThe monitor's ping token, a UUID.
ridqueryA run ID: a UUID that you choose for each execution of the job.
Responses

200Motor recorded the ping.

OK

400The path or a parameter is incorrect.

invalid url format

404The token doesn't exist.

not found

429The monitor exceeded its ping limit: 4 pings per minute, with bursts of up to 20.

rate limited

POST/job_monitors/ping/{token}/start

Report the start of a run

Records a start ping. Use GET, HEAD, or POST. A POST body (the first 100,000 bytes) becomes the ping's log. Use the same ?rid=<uuid> on start and completion pings to link them to one run. A start opens a run. A run that doesn't complete before its maximum runtime times out.

This endpoint authenticates with the secret in its URL. Keep the URL private.

ParameterLocationDescription
tokenpathThe monitor's ping token, a UUID.
ridqueryA run ID: a UUID that you choose for each execution of the job.
Responses

200Motor recorded the ping.

OK

400The path or a parameter is incorrect.

invalid url format

404The token doesn't exist.

not found

429The monitor exceeded its ping limit: 4 pings per minute, with bursts of up to 20.

rate limited

POST/job_monitors/ping/{token}/fail

Report a failed run

Records a fail ping. Use GET, HEAD, or POST. A POST body (the first 100,000 bytes) becomes the ping's log. Use the same ?rid=<uuid> on start and completion pings to link them to one run.

This endpoint authenticates with the secret in its URL. Keep the URL private.

ParameterLocationDescription
tokenpathThe monitor's ping token, a UUID.
ridqueryA run ID: a UUID that you choose for each execution of the job.
Responses

200Motor recorded the ping.

OK

400The path or a parameter is incorrect.

invalid url format

404The token doesn't exist.

not found

429The monitor exceeded its ping limit: 4 pings per minute, with bursts of up to 20.

rate limited

POST/job_monitors/ping/{token}/log

Send a log message

Records a log ping. It doesn't change the monitor's state.

This endpoint authenticates with the secret in its URL. Keep the URL private.

ParameterLocationDescription
tokenpathThe monitor's ping token, a UUID.
ridqueryA run ID: a UUID that you choose for each execution of the job.
Responses

200Motor recorded the ping.

OK

400The path or a parameter is incorrect.

invalid url format

404The token doesn't exist.

not found

429The monitor exceeded its ping limit: 4 pings per minute, with bursts of up to 20.

rate limited

POST/job_monitors/ping/{token}/{exit_code}

Report an exit code

Records the exit code of a run, from 0 to 255. 0 is a success. Other codes are failures.

This endpoint authenticates with the secret in its URL. Keep the URL private.

ParameterLocationDescription
tokenpathThe monitor's ping token, a UUID.
exit_codepathFrom 0 to 255.
ridqueryA run ID: a UUID that you choose for each execution of the job.
Responses

200Motor recorded the ping.

OK

400The path or a parameter is incorrect.

invalid url format

404The token doesn't exist.

not found

429The monitor exceeded its ping limit: 4 pings per minute, with bursts of up to 20.

rate limited

POST/job_monitors/ping/{ping_key}/{slug}

Send a ping by monitor slug

Send pings using a ping key and a monitor slug. Add /start, /fail, /log, or an exit code to report the result. With create=1, Motor creates the monitor if the slug doesn't exist and the ping key has can_create enabled. Use schedule, interval, tz, and grace to configure the new monitor.

This endpoint authenticates with the secret in its URL. Keep the URL private.

ParameterLocationDescription
ping_keypathA ping key belonging to your team.
slugpathThe slug of the monitor.
ridqueryA run ID: a UUID that you choose for each execution of the job.
createquerySet to 1 to create the monitor if it doesn't exist.
schedulequeryA cron expression for a new monitor.
intervalqueryAn interval in seconds for a new monitor.
tzqueryThe time zone of a new monitor.
gracequeryThe grace period for a new monitor, in seconds.
Responses

200Motor recorded the ping.

OK

201Motor created the monitor and recorded the ping.

Created

403The monitor couldn't be created. The response body explains why.

forbidden: this ping key can't create monitors

400The path or a parameter is incorrect.

invalid url format

404The token doesn't exist.

not found

429The monitor exceeded its ping limit: 4 pings per minute, with bursts of up to 20.

rate limited

Monitors

POST/job_monitors/monitors

Create a monitor

Creates a monitor and starts checking its schedule immediately. Motor can alert you to a missed run even if the job has never sent a ping.

Request body
{
  "name": "Nightly backup",
  "schedule": "0 3 * * *",
  "time_zone": "America/New_York",
  "grace_seconds": 600,
  "max_runtime_seconds": 3600,
  "tags": [
    "db",
    "prod"
  ]
}
FieldTypeDescription
namerequiredstring1 to 100 characters. Required when you create a monitor.
slugstring1 to 64 characters from a-z, 0-9, -, and _. Must be unique within your team. Defaults to a slug derived from name.
tagsarrayAt most 10 tags of 1 to 32 characters from a-z, 0-9, -, _, and :.
schedulestring or nullA five-field cron expression or a macro such as @daily. Set either schedule or interval_seconds.
interval_secondsinteger or nullFrom 60 to 2,678,400 (31 days).
time_zonestringThe IANA time zone of a cron schedule. UTC by default.
grace_secondsintegerExtra time after a scheduled check-in before Motor marks the run as missed, in seconds. From 60 to 2,678,400; defaults to 300 (5 minutes).
max_runtime_secondsinteger or nullMaximum run duration after a start ping, in seconds. From 60 to 604,800. Set to null to use grace_seconds.
failure_toleranceintegerConsecutive unsuccessful results to allow before opening an incident. From 0 to 100; defaults to 0, so the first unsuccessful result opens an incident.
recovery_thresholdintegerConsecutive successful results needed to resolve an incident. From 1 to 100; defaults to 1.
realert_secondsinteger or nullSeconds between reminders while an incident is open. From 300 to 604,800, or null to disable reminders.
alert_channel_idsarrayAt most 20 channels. Without this field, a new monitor uses the team's default channels.
Responses

201The monitor.

{
  "id": "jmm_4ZpQ7bLxN2c",
  "name": "Nightly backup",
  "slug": "nightly-backup",
  "tags": [
    "db",
    "prod"
  ],
  "schedule": "0 3 * * *",
  "interval_seconds": null,
  "time_zone": "America/New_York",
  "grace_seconds": 600,
  "max_runtime_seconds": 3600,
  "failure_tolerance": 0,
  "recovery_threshold": 1,
  "realert_seconds": 86400,
  "alert_channel_ids": [
    "jmc_9kTe2WvRmQa"
  ],
  "status": "up",
  "down_reason": null,
  "running": false,
  "ping_url": "https://api.motor.dev/job_monitors/ping/0b1f8c2e-6d1a-4f6b-9c3e-2a7d5e8f1b40",
  "last_ping_at": "2026-10-05T07:05:14Z",
  "last_duration_seconds": 312,
  "next_due_at": "2026-10-06T07:00:00Z",
  "paused_until": null,
  "created_at": "2026-10-01T18:22:05Z"
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

402The key has reached its monthly spending limit.

{
  "error": {
    "code": "limit_reached",
    "message": "This key reached its monthly spending limit, so it can't create monitors. Existing monitors keep running. Raise the limit on the API keys page."
  }
}

409A monitor in your team already uses this slug.

{
  "error": {
    "code": "slug_taken",
    "message": "Another monitor of the team has the slug `nightly-backup`."
  }
}

422A field is invalid.

{
  "error": {
    "code": "invalid_field",
    "message": "`grace_seconds` must be from 60 to 2678400."
  }
}

GET/job_monitors/monitors

List monitors

Lists the team's monitors, oldest first.

ParameterLocationDescription
limitqueryThe number of items, from 1 to 200. 50 by default.
cursorqueryThe next_cursor returned by the previous page.
statusqueryOnly monitors with this status.
tagqueryOnly monitors with this tag.
slugqueryOnly the monitor with this slug.
Responses

200A page of monitors.

{
  "data": [
    {
      "id": "jmm_4ZpQ7bLxN2c",
      "name": "Nightly backup",
      "slug": "nightly-backup",
      "tags": [
        "db",
        "prod"
      ],
      "schedule": "0 3 * * *",
      "interval_seconds": null,
      "time_zone": "America/New_York",
      "grace_seconds": 600,
      "max_runtime_seconds": 3600,
      "failure_tolerance": 0,
      "recovery_threshold": 1,
      "realert_seconds": 86400,
      "alert_channel_ids": [
        "jmc_9kTe2WvRmQa"
      ],
      "status": "up",
      "down_reason": null,
      "running": false,
      "ping_url": "https://api.motor.dev/job_monitors/ping/0b1f8c2e-6d1a-4f6b-9c3e-2a7d5e8f1b40",
      "last_ping_at": "2026-10-05T07:05:14Z",
      "last_duration_seconds": 312,
      "next_due_at": "2026-10-06T07:00:00Z",
      "paused_until": null,
      "created_at": "2026-10-01T18:22:05Z"
    }
  ],
  "next_cursor": null
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

GET/job_monitors/monitors/{id}

Read a monitor

Returns a monitor with its status: paused, down, late, new, or up.

ParameterLocationDescription
idpathThe ID of the monitor, such as jmm_4ZpQ7bLxN2c.
Responses

200The monitor.

{
  "id": "jmm_4ZpQ7bLxN2c",
  "name": "Nightly backup",
  "slug": "nightly-backup",
  "tags": [
    "db",
    "prod"
  ],
  "schedule": "0 3 * * *",
  "interval_seconds": null,
  "time_zone": "America/New_York",
  "grace_seconds": 600,
  "max_runtime_seconds": 3600,
  "failure_tolerance": 0,
  "recovery_threshold": 1,
  "realert_seconds": 86400,
  "alert_channel_ids": [
    "jmc_9kTe2WvRmQa"
  ],
  "status": "up",
  "down_reason": null,
  "running": false,
  "ping_url": "https://api.motor.dev/job_monitors/ping/0b1f8c2e-6d1a-4f6b-9c3e-2a7d5e8f1b40",
  "last_ping_at": "2026-10-05T07:05:14Z",
  "last_duration_seconds": 312,
  "next_due_at": "2026-10-06T07:00:00Z",
  "paused_until": null,
  "created_at": "2026-10-01T18:22:05Z"
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

PATCH/job_monitors/monitors/{id}

Update a monitor

Updates only the fields included in the request. Changing the schedule recalculates the next check-in from the current time. To switch between a cron schedule and an interval, set the previous schedule field to null.

ParameterLocationDescription
idpathThe ID of the monitor, such as jmm_4ZpQ7bLxN2c.
Request body
{
  "grace_seconds": 900,
  "schedule": "0 4 * * *"
}
FieldTypeDescription
namestring1 to 100 characters. Required when you create a monitor.
slugstring1 to 64 characters from a-z, 0-9, -, and _. Must be unique within your team. Defaults to a slug derived from name.
tagsarrayAt most 10 tags of 1 to 32 characters from a-z, 0-9, -, _, and :.
schedulestring or nullA five-field cron expression or a macro such as @daily. Set either schedule or interval_seconds.
interval_secondsinteger or nullFrom 60 to 2,678,400 (31 days).
time_zonestringThe IANA time zone of a cron schedule. UTC by default.
grace_secondsintegerExtra time after a scheduled check-in before Motor marks the run as missed, in seconds. From 60 to 2,678,400; defaults to 300 (5 minutes).
max_runtime_secondsinteger or nullMaximum run duration after a start ping, in seconds. From 60 to 604,800. Set to null to use grace_seconds.
failure_toleranceintegerConsecutive unsuccessful results to allow before opening an incident. From 0 to 100; defaults to 0, so the first unsuccessful result opens an incident.
recovery_thresholdintegerConsecutive successful results needed to resolve an incident. From 1 to 100; defaults to 1.
realert_secondsinteger or nullSeconds between reminders while an incident is open. From 300 to 604,800, or null to disable reminders.
alert_channel_idsarrayAt most 20 channels. Without this field, a new monitor uses the team's default channels.
Responses

200The monitor.

{
  "id": "jmm_4ZpQ7bLxN2c",
  "name": "Nightly backup",
  "slug": "nightly-backup",
  "tags": [
    "db",
    "prod"
  ],
  "schedule": "0 3 * * *",
  "interval_seconds": null,
  "time_zone": "America/New_York",
  "grace_seconds": 600,
  "max_runtime_seconds": 3600,
  "failure_tolerance": 0,
  "recovery_threshold": 1,
  "realert_seconds": 86400,
  "alert_channel_ids": [
    "jmc_9kTe2WvRmQa"
  ],
  "status": "up",
  "down_reason": null,
  "running": false,
  "ping_url": "https://api.motor.dev/job_monitors/ping/0b1f8c2e-6d1a-4f6b-9c3e-2a7d5e8f1b40",
  "last_ping_at": "2026-10-05T07:05:14Z",
  "last_duration_seconds": 312,
  "next_due_at": "2026-10-06T07:00:00Z",
  "paused_until": null,
  "created_at": "2026-10-01T18:22:05Z"
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

422A field is invalid.

{
  "error": {
    "code": "invalid_field",
    "message": "`grace_seconds` must be from 60 to 2678400."
  }
}

DELETE/job_monitors/monitors/{id}

Delete a monitor

Deletes the monitor and its pings, runs, and incidents. Billing records are retained.

ParameterLocationDescription
idpathThe ID of the monitor, such as jmm_4ZpQ7bLxN2c.
Responses

200The monitor is deleted.

{
  "id": "jmm_4ZpQ7bLxN2c",
  "deleted": true
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

POST/job_monitors/monitors/{id}/pause

Pause a monitor

Pauses monitoring until the until timestamp, the next ping, or an explicit resume request. Motor abandons active runs and resolves any open incident. Monitors paused for an entire day are free for that day.

ParameterLocationDescription
idpathThe ID of the monitor, such as jmm_4ZpQ7bLxN2c.
Request body
{
  "until": "2026-10-06T12:00:00Z"
}
FieldTypeDescription
untilstringOptional timestamp at which monitoring resumes.
Responses

200The monitor.

{
  "id": "jmm_4ZpQ7bLxN2c",
  "name": "Nightly backup",
  "slug": "nightly-backup",
  "tags": [
    "db",
    "prod"
  ],
  "schedule": "0 3 * * *",
  "interval_seconds": null,
  "time_zone": "America/New_York",
  "grace_seconds": 600,
  "max_runtime_seconds": 3600,
  "failure_tolerance": 0,
  "recovery_threshold": 1,
  "realert_seconds": 86400,
  "alert_channel_ids": [
    "jmc_9kTe2WvRmQa"
  ],
  "status": "paused",
  "down_reason": null,
  "running": false,
  "ping_url": "https://api.motor.dev/job_monitors/ping/0b1f8c2e-6d1a-4f6b-9c3e-2a7d5e8f1b40",
  "last_ping_at": "2026-10-05T07:05:14Z",
  "last_duration_seconds": 312,
  "next_due_at": null,
  "paused_until": "2026-10-06T12:00:00Z",
  "created_at": "2026-10-01T18:22:05Z"
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

422A field is invalid.

{
  "error": {
    "code": "invalid_field",
    "message": "`grace_seconds` must be from 60 to 2678400."
  }
}

POST/job_monitors/monitors/{id}/resume

Resume a monitor

Resumes a paused monitor and calculates its next check-in from the current time.

ParameterLocationDescription
idpathThe ID of the monitor, such as jmm_4ZpQ7bLxN2c.
Responses

200The monitor.

{
  "id": "jmm_4ZpQ7bLxN2c",
  "name": "Nightly backup",
  "slug": "nightly-backup",
  "tags": [
    "db",
    "prod"
  ],
  "schedule": "0 3 * * *",
  "interval_seconds": null,
  "time_zone": "America/New_York",
  "grace_seconds": 600,
  "max_runtime_seconds": 3600,
  "failure_tolerance": 0,
  "recovery_threshold": 1,
  "realert_seconds": 86400,
  "alert_channel_ids": [
    "jmc_9kTe2WvRmQa"
  ],
  "status": "up",
  "down_reason": null,
  "running": false,
  "ping_url": "https://api.motor.dev/job_monitors/ping/0b1f8c2e-6d1a-4f6b-9c3e-2a7d5e8f1b40",
  "last_ping_at": "2026-10-05T07:05:14Z",
  "last_duration_seconds": 312,
  "next_due_at": "2026-10-06T07:00:00Z",
  "paused_until": null,
  "created_at": "2026-10-01T18:22:05Z"
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

POST/job_monitors/monitors/{id}/rotate_token

Rotate the ping token

Creates a new ping URL for the monitor. The previous URL remains valid for overlap_seconds. Set this to 0 to immediately invalidate all previous ping URLs.

ParameterLocationDescription
idpathThe ID of the monitor, such as jmm_4ZpQ7bLxN2c.
Request body
{
  "overlap_seconds": 86400
}
FieldTypeDescription
overlap_secondsintegerFrom 0 to 86,400. 86,400 by default.
Responses

200The monitor with its new ping_url.

{
  "id": "jmm_4ZpQ7bLxN2c",
  "name": "Nightly backup",
  "slug": "nightly-backup",
  "tags": [
    "db",
    "prod"
  ],
  "schedule": "0 3 * * *",
  "interval_seconds": null,
  "time_zone": "America/New_York",
  "grace_seconds": 600,
  "max_runtime_seconds": 3600,
  "failure_tolerance": 0,
  "recovery_threshold": 1,
  "realert_seconds": 86400,
  "alert_channel_ids": [
    "jmc_9kTe2WvRmQa"
  ],
  "status": "up",
  "down_reason": null,
  "running": false,
  "ping_url": "https://api.motor.dev/job_monitors/ping/0b1f8c2e-6d1a-4f6b-9c3e-2a7d5e8f1b40",
  "last_ping_at": "2026-10-05T07:05:14Z",
  "last_duration_seconds": 312,
  "next_due_at": "2026-10-06T07:00:00Z",
  "paused_until": null,
  "created_at": "2026-10-01T18:22:05Z"
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

422A field is invalid.

{
  "error": {
    "code": "invalid_field",
    "message": "`grace_seconds` must be from 60 to 2678400."
  }
}

POST/job_monitors/monitors/sync

Sync monitors

Creates and updates monitors by slug for deployment scripts and agents. Set prune_tag to delete monitors with that tag whose slugs are missing from the request. Motor validates the entire request before making changes. Set dry_run to true to preview the changes.

Request body
{
  "monitors": [
    {
      "slug": "nightly-backup",
      "name": "Nightly backup",
      "schedule": "0 3 * * *",
      "tags": [
        "deploy:api"
      ]
    }
  ],
  "prune_tag": "deploy:api",
  "dry_run": false
}
FieldTypeDescription
monitorsrequiredarrayAt most 1,000 monitors. Each needs a slug.
prune_tagstring
dry_runboolean
Responses

200The monitors created, updated, and deleted, or the proposed changes for a dry run.

{
  "created": [
    "nightly-backup"
  ],
  "updated": [],
  "unchanged": [],
  "deleted": [
    "old-report"
  ],
  "dry_run": false
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

422A field is invalid.

{
  "error": {
    "code": "invalid_field",
    "message": "`grace_seconds` must be from 60 to 2678400."
  }
}

Runs and pings

GET/job_monitors/monitors/{monitor_id}/runs

List runs

Lists the runs of a monitor, newest first. result is running, succeeded, failed, timed_out, or abandoned.

ParameterLocationDescription
monitor_idpathThe ID of the monitor.
limitqueryThe number of items, from 1 to 200. 50 by default.
cursorqueryThe next_cursor returned by the previous page.
resultqueryOnly runs with this result.
Responses

200A page of runs.

{
  "data": [
    {
      "id": "jmr_7GhLs3NqPzE",
      "rid": "6f1c2b8e-1d2a-4c4e-9a63-1f0f9b3c2d10",
      "occurrence_at": "2026-10-05T07:00:00Z",
      "started_at": "2026-10-05T07:00:02Z",
      "finished_at": "2026-10-05T07:05:14Z",
      "result": "succeeded",
      "exit_code": 0,
      "duration_seconds": 312
    }
  ],
  "next_cursor": null
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

GET/job_monitors/monitors/{monitor_id}/pings

List pings

Lists the pings of a monitor, newest first.

ParameterLocationDescription
monitor_idpathThe ID of the monitor.
limitqueryThe number of items, from 1 to 200. 50 by default.
cursorqueryThe next_cursor returned by the previous page.
kindqueryOnly pings of this kind.
ridqueryOnly pings with this run ID.
Responses

200A page of pings.

{
  "data": [
    {
      "id": "jmp_2YcVbN8xKfM",
      "kind": "success",
      "received_at": "2026-10-05T07:05:14Z",
      "rid": "6f1c2b8e-1d2a-4c4e-9a63-1f0f9b3c2d10",
      "run_id": "jmr_7GhLs3NqPzE",
      "exit_code": 0,
      "method": "POST",
      "user_agent": "runitor/v1.3.0",
      "body_bytes": 18213,
      "body_truncated": false,
      "body_stored": true
    }
  ],
  "next_cursor": null
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

GET/job_monitors/monitors/{monitor_id}/pings/{id}/body

Download a ping body

Returns the stored body of a ping as plain text. Motor keeps the bodies of the newest 100 pings of each monitor.

ParameterLocationDescription
monitor_idpathThe ID of the monitor.
idpathThe ID of the ping.
Responses

200The body.

Backup finished: 1,204 files, 3.2 GB.

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The ping or its body doesn't exist.

{
  "error": {
    "code": "not_found",
    "message": "The ping body doesn't exist."
  }
}

Incidents

GET/job_monitors/monitors/{monitor_id}/incidents

List a monitor's incidents

Lists the incidents of a monitor, newest first.

ParameterLocationDescription
monitor_idpathThe ID of the monitor.
limitqueryThe number of items, from 1 to 200. 50 by default.
cursorqueryThe next_cursor returned by the previous page.
Responses

200A page of incidents.

{
  "data": [
    {
      "id": "jmi_5RmWq1JtBnD",
      "monitor_id": "jmm_4ZpQ7bLxN2c",
      "reason": "missed",
      "opened_at": "2026-10-05T07:10:00Z",
      "resolved_at": null
    }
  ],
  "next_cursor": null
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

GET/job_monitors/incidents

List incidents

Lists the team's incidents, newest first.

ParameterLocationDescription
limitqueryThe number of items, from 1 to 200. 50 by default.
cursorqueryThe next_cursor returned by the previous page.
openquerytrue for open incidents, false for resolved incidents.
monitor_idqueryOnly the incidents of this monitor.
Responses

200A page of incidents.

{
  "data": [
    {
      "id": "jmi_5RmWq1JtBnD",
      "monitor_id": "jmm_4ZpQ7bLxN2c",
      "reason": "missed",
      "opened_at": "2026-10-05T07:10:00Z",
      "resolved_at": null
    }
  ],
  "next_cursor": null
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

GET/job_monitors/incidents/{id}

Read an incident

Returns an incident and the delivery status of each alert. Alert status is pending, sending, delivered, failed, or superseded.

ParameterLocationDescription
idpathThe ID of the incident.
Responses

200The incident.

{
  "id": "jmi_5RmWq1JtBnD",
  "monitor_id": "jmm_4ZpQ7bLxN2c",
  "reason": "missed",
  "opened_at": "2026-10-05T07:10:00Z",
  "resolved_at": null,
  "alerts": [
    {
      "id": "jma_3XzPd6LcVhT",
      "channel_id": "jmc_9kTe2WvRmQa",
      "kind": "opened",
      "status": "delivered",
      "attempts": 1,
      "sent_at": "2026-10-05T07:10:04Z",
      "error": null
    }
  ]
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

Alert channels

POST/job_monitors/alert_channels

Create an alert channel

Creates a destination for alerts. For webhooks, save the signing_secret from the response; it is shown only once.

Request body
{
  "kind": "webhook",
  "url": "https://hooks.example.com/motor",
  "default": true
}
FieldTypeDescription
kindrequiredstring
namestring
addressstringFor email channels. Recipients outside your team must confirm their address before receiving alerts.
urlstringFor webhook (https), slack (https://hooks.slack.com/services/...) and discord (https://discord.com/api/webhooks/...).
routing_keystringFor pagerduty: an Events API v2 routing key.
defaultbooleanNew monitors without alert_channel_ids use default channels.
include_logsbooleanFailure alerts include the last 2,000 bytes of the failed ping's body.
Responses

201The channel.

{
  "id": "jmc_9kTe2WvRmQa",
  "kind": "webhook",
  "name": "Ops webhook",
  "url": "https://hooks.example.com/motor",
  "default": true,
  "include_logs": false,
  "confirmed": true,
  "created_at": "2026-10-01T18:20:00Z",
  "signing_secret": "whsec_Q2hhbmdlIHRoaXMgc2VjcmV0IGJlZm9yZSB1c2luZyBpdA"
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

422A field is invalid.

{
  "error": {
    "code": "invalid_field",
    "message": "`grace_seconds` must be from 60 to 2678400."
  }
}

429The team sent 10 confirmation emails in the last 24 hours.

{
  "error": {
    "code": "rate_limited",
    "message": "The team sent 10 confirmation emails in the last 24 hours. Try again later, or use the address of a team member."
  }
}

GET/job_monitors/alert_channels

List alert channels

Lists the team's channels. Credentials are hidden.

ParameterLocationDescription
limitqueryThe number of items, from 1 to 200. 50 by default.
cursorqueryThe next_cursor returned by the previous page.
Responses

200A page of channels.

{
  "data": [
    {
      "id": "jmc_9kTe2WvRmQa",
      "kind": "webhook",
      "name": "Ops webhook",
      "url": "https://hooks.example.com/motor",
      "default": true,
      "include_logs": false,
      "confirmed": true,
      "created_at": "2026-10-01T18:20:00Z"
    }
  ],
  "next_cursor": null
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

GET/job_monitors/alert_channels/{id}

Read an alert channel

Returns a channel.

ParameterLocationDescription
idpathThe ID of the channel.
Responses

200The channel.

{
  "id": "jmc_9kTe2WvRmQa",
  "kind": "webhook",
  "name": "Ops webhook",
  "url": "https://hooks.example.com/motor",
  "default": true,
  "include_logs": false,
  "confirmed": true,
  "created_at": "2026-10-01T18:20:00Z"
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

PATCH/job_monitors/alert_channels/{id}

Update an alert channel

Changes name, default, and include_logs. To change the destination, delete the channel and add a new one.

ParameterLocationDescription
idpathThe ID of the channel.
Request body
{
  "include_logs": true
}
FieldTypeDescription
namestring
defaultbooleanNew monitors without alert_channel_ids use default channels.
include_logsbooleanFailure alerts include the last 2,000 bytes of the failed ping's body.
Responses

200The channel.

{
  "id": "jmc_9kTe2WvRmQa",
  "kind": "webhook",
  "name": "Ops webhook",
  "url": "https://hooks.example.com/motor",
  "default": true,
  "include_logs": true,
  "confirmed": true,
  "created_at": "2026-10-01T18:20:00Z"
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

422A field is invalid.

{
  "error": {
    "code": "invalid_field",
    "message": "`grace_seconds` must be from 60 to 2678400."
  }
}

DELETE/job_monitors/alert_channels/{id}

Delete an alert channel

Removes the channel from all monitors and cancels pending alerts for that channel.

ParameterLocationDescription
idpathThe ID of the channel.
Responses

200The channel is deleted.

{
  "id": "jmc_9kTe2WvRmQa",
  "deleted": true
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

POST/job_monitors/alert_channels/{id}/test

Send a test alert

Sends a test alert to the channel. At most 10 per channel per hour.

ParameterLocationDescription
idpathThe ID of the channel.
Responses

202The test alert, queued for delivery.

{
  "id": "jma_3XzPd6LcVhT",
  "channel_id": "jmc_9kTe2WvRmQa",
  "kind": "test",
  "status": "pending",
  "attempts": 0,
  "sent_at": null,
  "error": null
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

429The channel sent 10 test alerts in the last hour.

{
  "error": {
    "code": "rate_limited",
    "message": "The channel sent 10 test alerts in the last hour. Try again later."
  }
}

POST/job_monitors/alert_channels/{id}/rotate_secret

Rotate a signing secret

Creates a new webhook signing secret. For the next 24 hours, deliveries include signatures from both the old and new secrets so you can update your integration.

ParameterLocationDescription
idpathThe ID of the channel.
Responses

200The channel with its new signing_secret.

{
  "id": "jmc_9kTe2WvRmQa",
  "kind": "webhook",
  "name": "Ops webhook",
  "url": "https://hooks.example.com/motor",
  "default": true,
  "include_logs": false,
  "confirmed": true,
  "created_at": "2026-10-01T18:20:00Z",
  "signing_secret": "whsec_TmV3IHNlY3JldCBhZnRlciBhIHJvdGF0aW9uIGFiYw"
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}

Ping keys

POST/job_monitors/ping_keys

Create a ping key

Creates a ping key for sending pings by monitor slug. Save the key from the response; it is shown only once. Enable can_create to allow pings with create=1 to create monitors.

Request body
{
  "can_create": true
}
FieldTypeDescription
can_createbooleanfalse by default.
Responses

201The ping key.

{
  "id": "jmk_6TnBv2QwLsJ",
  "prefix": "pk3X",
  "can_create": true,
  "created_at": "2026-10-01T18:25:00Z",
  "key": "pk3Xz8QvL2mN5rT7wY9aBc"
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

GET/job_monitors/ping_keys

List ping keys

Lists your team's ping keys. Secret values are omitted.

ParameterLocationDescription
limitqueryThe number of items, from 1 to 200. 50 by default.
cursorqueryThe next_cursor returned by the previous page.
Responses

200A page of ping keys.

{
  "data": [
    {
      "id": "jmk_6TnBv2QwLsJ",
      "prefix": "pk3X",
      "can_create": true,
      "created_at": "2026-10-01T18:25:00Z"
    }
  ],
  "next_cursor": null
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

DELETE/job_monitors/ping_keys/{id}

Delete a ping key

Deletes a ping key. Any slug-based ping URLs using this key stop working immediately.

ParameterLocationDescription
idpathThe ID of the ping key.
Responses

200The ping key is deleted.

{
  "id": "jmk_6TnBv2QwLsJ",
  "deleted": true
}

401The key is missing or incorrect.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The key can't use job monitors.

{
  "error": {
    "code": "forbidden",
    "message": "This key cannot access this API. Update its permissions on the API keys page."
  }
}

404The resource wasn't found in your team.

{
  "error": {
    "code": "not_found",
    "message": "The monitor doesn't exist."
  }
}