# API reference

Use this API to send job check-ins and server reports.
You can also read history, change monitor settings, and upload private images.

Base URL: `https://cronpulse.com`.
The API paths do not have an `/api` prefix or a version number.
Replace the example IDs and keys with your monitor details.

## Terms

- **Monitor:** A record for one job or one server report.
- **Check-in:** A request that tells CronPulse that a job completed or a server reporter sent data.
- **State:** The JSON data that you send with a check-in.
- **Reporter:** A program that collects server data and sends check-ins.
- **Heartbeat alert:** An email about a late check-in.
- **Condition alert:** An email about a reported value that meets a saved rule.
- **Dashboard definition:** The JSON instructions that control how CronPulse displays server data.

## Quick start

1. Open the [dashboard](/dashboard).
2. Create a job monitor.
3. Open the job details.
4. Copy the private check-in URL.
5. Send a check-in after the job completes successfully.

```sh
curl --fail --silent --show-error --max-time 30 \
  'https://cronpulse.com/monitors/MONITOR_ID/ACCESS_KEY/checkin'
```

To include run data, send a POST request with a `state` object:

```sh
curl --fail --silent --show-error --max-time 30 \
  -H 'Content-Type: application/json' \
  --data-binary '{"state":{"result":"ok","rows_processed":4200,"duration_seconds":12.5}}' \
  'https://cronpulse.com/monitors/MONITOR_ID/ACCESS_KEY/checkin'
```

A successful check-in returns **200 OK** and the monitor data.
CronPulse saves the check-in time and state in the history.
It also sets a new deadline for the next check-in.
These changes succeed or fail together.

## Authentication and access

Each monitor has a secret `access_key`.
The key is part of the URL path.
Do not put the key in an Authorization header.
CronPulse does not provide an account-wide API token.

Copy the private URL from the dashboard or the monitor confirmation email.
A person with this URL can read, update, check in, or delete that monitor.
The URL also gives access to its state history.

Store the URL in a protected configuration file or environment variable.
Keep the URL out of shared logs, screenshots, and source control.
API responses can contain the key.
Keep these responses private too.

Use an emailed sign-in link to open the account dashboard or view private images.
A monitor key does not give access to the account dashboard.
Sign-in links work once and expire after 15 minutes.
Browser sessions expire after 30 days.

For legacy job monitors, a valid key can also confirm monitor alerts.
A read request can cause this confirmation.
An accepted check-in also confirms monitor alerts.
This confirmation does not verify the account or sign you in.
An image upload does not confirm monitor alerts or record a check-in.

## Endpoint overview

Add these paths to the base URL.
Replace `:id` with the monitor ID.
Replace `:access_key` with its private key.

- **GET or POST** `/monitors/:id/:access_key/checkin` — send a check-in and optional state.
- **GET or POST** `/monitors/:id/:access_key/ping` — do the same as `/checkin`.
- **GET or POST** `/monitors/:id/:access_key/extend` — do the same as `/checkin`.
- **GET** `/monitors/:id/:access_key` — read monitor details and the latest state.
- **GET** `/monitors/:id/:access_key/checkins?page=1` — read check-in history.
- **POST** `/monitors` — create a job monitor and send its details by email.
- **PATCH or POST** `/monitors/:id/:access_key` — change monitor settings.
- **DELETE** `/monitors/:id/:access_key` — delete the monitor, its history, and its images.
- **GET** `/monitors/:id/:access_key/confirm_email` — confirm the legacy monitor's email setting.
- **POST** `/monitors/:id/:access_key/images` — upload a PNG for a server monitor.
- **GET** `/dashboard/images/:image_id` — read a private PNG with the owner's signed-in browser session.

Use the signed-in dashboard to create servers, group jobs, and set condition alerts.
These forms require a browser session.
Rails uses a CSRF token to reject unwanted form requests.
A monitor key cannot authorize these forms.
They do not provide a JSON management API.

## Check-ins and state

For structured data, send a POST request with `Content-Type: application/json`.
Form data such as `state[result]=ok` also works.
Use JSON to keep numbers, booleans, and null values in their original types.

`state` must be a JSON object.
The object can contain other objects, arrays, numbers, text, booleans, and null values.
Do not set `state` itself to null, an array, or text.

### State limits

- A job monitor accepts up to **16 KiB** of JSON state: 16,384 bytes.
- A server monitor accepts up to **64 KiB**: 65,536 bytes.
- The server limit includes the dashboard definition.

Invalid or oversized state returns **422 Unprocessable Entity**.
CronPulse does not change the heartbeat deadline, latest state, or history for that request.

Each check-in replaces the latest state.
CronPulse does not merge new state with old state.
If you omit `state`, CronPulse saves `{}` as the latest state.
Older entries stay in the history.

State cannot change internal confirmation or notification settings.
CronPulse sets the check-in time when it receives the request.
Include an observation time in state if you also need the time from the source server.

### Success and retries

A check-in records contact with CronPulse.
It does not check whether the reported job succeeded.
For example, `{"state":{"result":"error"}}` still records a successful heartbeat.
For a job monitor, send the check-in after the job completes successfully.

Each accepted request adds one history entry.
CronPulse does not combine repeated requests into one entry.
If the first request succeeds before a timeout, a retry can add another entry.
The API does not provide separate start, finish, and failure events.

### Successful response

Check-in, read, update, and delete requests return a monitor object.
This example shows some of its fields:

```json
{
  "id": 123,
  "name": "Nightly backup",
  "description": "Database archive",
  "kind": "cron_job",
  "server_id": null,
  "max_heartbeat_interval_in_seconds": 86400,
  "last_checkin_at": "2026-10-25T12:00:00.000Z",
  "latest_state": {
    "result": "ok",
    "rows_processed": 4200,
    "duration_seconds": 12.5
  },
  "seconds_since_last_checkin": 0
}
```

The response can include other database fields.
Only use the fields listed in this guide to update settings.

Before the first check-in, `last_checkin_at` and `seconds_since_last_checkin` are null.
The response can contain the private key.
Store the response securely.

## Read latest state and history

To read the monitor without recording a check-in, send this request:

```sh
curl --fail --silent --show-error \
  'https://cronpulse.com/monitors/MONITOR_ID/ACCESS_KEY'
```

To read its history, send this request:

```sh
curl --fail --silent --show-error \
  'https://cronpulse.com/monitors/MONITOR_ID/ACCESS_KEY/checkins?page=1'
```

CronPulse returns the newest entries first.
Each page contains up to **50 entries**:

```json
{
  "checkins": [
    {
      "id": 456,
      "checked_in_at": "2026-10-25T12:00:00.000Z",
      "state": {"result":"ok","rows_processed":4200}
    }
  ],
  "next_page": null
}
```

If more entries exist, `next_page` contains the next page number.
Request that page to continue.
Stop when `next_page` is null.

Page numbers start at 1.
A page after the last available entry returns an empty list.
CronPulse uses page 1 for zero or negative page numbers.
It limits page numbers to 1,000,000.

CronPulse keeps history until you delete the monitor.
Images have a separate expiry time.
Old history can remain after its images expire.

## Create a cron-job monitor

Send **POST** `/monitors` with a `monitor` object.
This request does not require an existing key or browser sign-in.
CronPulse sends a confirmation email with the new monitor's private URLs.

```sh
curl --fail --silent --show-error \
  -H 'Content-Type: application/json' \
  --data-binary '{"monitor":{"name":"Nightly backup","description":"Database archive","send_to":"you@example.com","max_heartbeat_interval_in_seconds":86400}}' \
  'https://cronpulse.com/monitors'
```

The request accepts these fields:

- `send_to` — required email address. CronPulse links the monitor to the account for that address.
- `name` — the job name.
- `description` — optional text about the job.
- `max_heartbeat_interval_in_seconds` — a positive integer. The default is 86,400 seconds, or 24 hours.

CronPulse removes spaces at the start and end of the email address.
It also converts the address to lowercase.

A successful request returns **200 OK**.
It does not return 201, the monitor ID, or the access key.
The response tells you to check your email:

```json
{
  "success": true,
  "message": "Please check your email to confirm your email address and start receiving pulse monitors."
}
```

Use the confirmation link or check-in URL from the email to confirm monitor alerts.
Use a separate emailed sign-in link to open the account dashboard.

Create and group server monitors in the dashboard.
This API request does not accept `kind`, `server_id`, server definitions, or condition-alert rules.

## Update or delete a monitor

### Update settings

Send **PATCH** or **POST** `/monitors/:id/:access_key` with a `monitor` object.
Include one or more of these fields:

- `name`
- `description`
- `max_heartbeat_interval_in_seconds`

```sh
curl --fail --silent --show-error --request PATCH \
  -H 'Content-Type: application/json' \
  --data-binary '{"monitor":{"name":"Hourly backup","max_heartbeat_interval_in_seconds":7200}}' \
  'https://cronpulse.com/monitors/MONITOR_ID/ACCESS_KEY'
```

A successful request returns **200 OK** and the monitor object.
It does not record a check-in.

A new interval sets a new deadline based on the last check-in.
If the monitor has no check-in, CronPulse uses its creation time.

This request cannot change the recipient, account, server, monitor kind, state, or condition-alert rules.

### Delete a monitor

Send **DELETE** `/monitors/:id/:access_key` to remove a monitor.
This action permanently deletes the monitor, its history, and its uploaded images.

A successful request returns **200 OK** and the deleted monitor's data.
Only send this request when you intend to remove the monitor.

### Confirm a legacy monitor

Send **GET** `/monitors/:id/:access_key/confirm_email` to confirm the monitor's email setting.
CronPulse returns **200 OK** with a plain-text message.
This request does not record a check-in.

## Publish a server dashboard

1. Open [Servers](/dashboard/servers).
2. Create or select a server.
3. Select **Set up status reporting**.
4. Open **Reporter connection details**.
5. Copy the private reporting URL.

This setup creates a server monitor.
Use its key to send reports and upload images for that monitor.

Put the readings and dashboard definition inside `state`:

```json
{
  "state": {
    "replica": {"seconds_behind_source":75},
    "dashboard": {
      "version": 1,
      "title": "Database health",
      "sections": [{
        "type": "metrics",
        "title": "Replication",
        "items": [{
          "label": "Seconds behind source",
          "path": "/replica/seconds_behind_source",
          "format": "duration_seconds"
        }]
      }]
    }
  }
}
```

Save this object as `snapshot.json`.
Send the file to the reporting URL:

```sh
curl --fail --silent --show-error --max-time 30 \
  -H 'Content-Type: application/json' \
  --data-binary @snapshot.json \
  'https://cronpulse.com/monitors/STATUS_MONITOR_ID/ACCESS_KEY/checkin'
```

The definition controls metrics, fields, tables, notices, and private images.
Include the definition with every report.
You do not need to register server names or change the Rails application.

Read the [dashboard publishing guide](/status-dashboards.html) for the full format.
You can also [download an example](/status-dashboard-example.json).

A new server monitor starts heartbeat monitoring after its first accepted report.
Its default deadline is 15 minutes.
The installed reporters send data every five minutes.
For a new reporter, set its schedule on the source server.

## Upload and view private screenshots

### Upload a PNG

Send **POST** `/monitors/:id/:access_key/images` with `multipart/form-data`.
Use the `image` field for the file.
Use a server monitor's ID and key.
Job-monitor keys cannot upload images.

Send the PNG file directly.
Do not encode the image as base64 in JSON.

```sh
curl --fail --silent --show-error --max-time 30 \
  --form 'image=@desktop.png;type=image/png' \
  'https://cronpulse.com/monitors/STATUS_MONITOR_ID/ACCESS_KEY/images'
```

A successful upload returns **201 Created**:

```json
{
  "id": "11111111-2222-4333-8444-555555555555",
  "uploaded_at": "2026-10-25T12:00:00Z",
  "expires_at": "2026-10-26T12:00:00Z",
  "width": 1920,
  "height": 1080
}
```

### Image limits

- The file must be a valid, complete PNG.
- The file must be at most **4 MiB**.
- The image must be at most **4096 × 4096 pixels**.
- The upload request must include `Content-Length`.
- The full request body can contain 4 MiB plus 64 KiB for multipart data.
- One monitor can store up to **2,000 active images**, with a combined size of **1 GiB**.
- An image expires **24 hours** after upload.
- Each upload creates a new image ID.

Active images are images that have not expired.
CronPulse returns 429 if an upload exceeds the image count or storage limit.

An upload does not confirm monitor alerts, record a check-in, or change the heartbeat deadline.
After the upload, send the image ID in a normal server check-in.

### Show the image in a report

Put the returned ID in `state.desktops`.
Add an images section to `state.dashboard`:

```json
{
  "state": {
    "desktops": [{
      "name": "Worker desktop",
      "image_id": "11111111-2222-4333-8444-555555555555",
      "captured_at": "2026-10-25T11:59:55Z"
    }],
    "dashboard": {
      "version": 1,
      "sections": [{
        "type": "images",
        "title": "Desktops",
        "path": "/desktops",
        "image_path": "/image_id",
        "caption_path": "/name",
        "captured_at_path": "/captured_at"
      }]
    }
  }
}
```

Use an image ID from the same server monitor.
External URLs, file paths, and base64 text cannot act as image sources.

Set `captured_at` to the time when the source server took the image.
CronPulse sets `uploaded_at` to the time when it receives the file.

### View a private image

Send **GET** `/dashboard/images/:image_id` with the verified owner's browser session.
A monitor key cannot replace this session.

- If you are signed out, CronPulse redirects to sign-in.
- If another account owns the image, CronPulse returns 404.
- If the image expired and its record remains, CronPulse returns 410.
- If the image is unknown or its record was removed, CronPulse returns 404.

Successful image reads use `Cache-Control: no-store`.
Old history keeps its original image IDs.
After an image expires, the dashboard shows that the image is unavailable.

## Heartbeat and condition alerts

### Late check-ins

Heartbeat alerts use the monitor's interval and confirmation setting.
Warnings repeat no more often than once every six hours.
They stop after 50 warnings without a successful check-in.
A successful check-in resets the warning count.
If CronPulse sent heartbeat warnings, it sends a recovery email after the successful check-in.

### Reported values

To add a server condition alert:

1. Open **Servers**.
2. Select a server.
3. Select **Add condition alert**.
4. Enter an **Alert name**.
5. Select a **Metric**.
6. Select a **Condition**.
7. Select the **Value type**.
8. If the condition needs a value, enter the **Comparison value**.
9. Set **Consecutive bad reports** to a number from 1 to 12.
10. Select **Enable alerts for this condition**.
11. Select **Create condition alert**.

You can also use the form's **Other reported field** controls to enter a custom JSON Pointer.

For example, set replica lag to **is greater than 60**.
Use seconds if the field reports seconds.
For a connection flag, set the condition to **equals false**.

Rules can compare numbers, text, and booleans.
A rule can also check for an unavailable value.
Only the verified owner can save these rules.
Posted JSON and display colors cannot create, enable, or change them.

Use **Consecutive bad reports** to require 1–12 matching reports in a row.
CronPulse sends one alert email when the condition first meets this requirement.
It does not repeat the alert while the condition continues.
You can also enable a recovery email.
CronPulse sends this email when a known value no longer meets the rule.
For normal comparisons, an unknown value or wrong data type cannot close an open alert.

CronPulse checks conditions when reports arrive.
A 60-second limit does not make CronPulse query the server every second.
Email delivery runs separately and retries failed sends.
If a process stops after the mail server accepts an email, a retry can send a duplicate.

### Schedules for your own installation

For heartbeat emails, schedule the `/monitors/cronjob` request.
For condition emails, run `cronpulse:deliver_server_alerts` every minute.
For image cleanup, run `cronpulse:purge_images` regularly.

The `/monitors/cronjob` request sends real warning emails.
Do not use it as a job check-in or a service health test.

## Errors and client behavior

Use the HTTP status to identify a successful request.
Check-in, create, and update validation errors use an `errors` array:

```json
{"errors":["State must be a JSON object"]}
```

For monitor JSON requests, an unknown monitor or wrong key returns **404** with this body:

```json
{"error":"Monitor not found"}
```

Image validation and quota errors use an `error` string.
Wrong image-upload keys return 404 with an empty body.
Other errors can also have an empty body or a different format.

### Status codes

- **200:** The monitor request succeeded. A confirmation request returns plain text.
- **201:** CronPulse stored the PNG. Send its ID in a separate check-in.
- **302:** A private-image request requires sign-in.
- **400:** The HTTP or JSON request is invalid, or a create request has no `monitor` object.
- **404:** The monitor, key, image, or image-upload monitor kind is invalid or inaccessible.
- **410:** The image expired, but its record remains.
- **411:** The image upload has no `Content-Length`.
- **413:** The image request body is too large.
- **422:** The settings, state, multipart request, or PNG are invalid.
- **429:** The monitor reached its image count or storage limit.
- **5xx:** The application or a network service failed. The check-in might already exist.

Always include the required `monitor` object for create and update requests.
Some invalid legacy update requests can return 5xx.

### Handle request failures

Set connection and read timeouts in each reporter.
For temporary network errors or 5xx responses, retry with a delay.
Increase the delay between attempts and limit the number of attempts.
A retry can add another history entry.

For a 4xx response, correct the request or access details before you retry.
Repeated uploads cannot fix a storage quota error.
Record delivery failures on the source server so you can investigate them.
