---
title: "Create a monitor"
method: POST
path: "/monitors"
tags: ["monitors"]
---

# Create a monitor

`POST /monitors`

## Request

- Base URL: `https://www.socialcrawl.dev/v1`
- URL: `https://www.socialcrawl.dev/v1/monitors`
- Auth: API key in header `x-api-key`

Creates a monitor: a saved recipe plus the parameters to replay, a cadence, and a webhook to deliver each result to. The recipe is any GET endpoint or Prism composite in the catalogue, given as its path without the `/v1/` prefix. The batch endpoints that read a JSON body (`prism/post-stats`, `prism/comment-lookup`, `prism/profiles`, `youtube/transcripts`, `youtube/videos`, `youtube/channels`) are POST and cannot be monitored; call them directly. The first run is at `next_run_at`, the next slot of the cadence. The signing secret is returned exactly once, in this response.

Add `track` to make a numbers-only monitor: every run reads the paths you list off the unified response (the same shape a `/v1` call returns), stores those numbers per row plus the change against the previous run, and keeps nothing else. `webhook_url` then becomes optional (read the numbers with `GET /v1/monitors/{id}/export` or `/timeseries`), and `webhook_format: "rows"` delivers flat rows ready to append to a spreadsheet. Without `track` a monitor behaves exactly as before.

## Request body

- object
  - `alert_rules` MonitorAlertRule[]: Rules evaluated after each run. A fired alert is carried in the webhook payload, and with `suppress_webhook_unless_alert` it decides whether the webhook is sent at all. With `track`, every rule metric must be one of `track.metrics`, except `rows_new` (alert on new rows: `gt`/`gte` only).
    - `metric` string, required: Dot path to a numeric key in the recipe's own result, for example `coverage` or `summary.total`. On a numbers-only monitor (created with `track`) the metric must be one of `track.metrics`, and an `items[].<path>` metric is evaluated once per row: a fired alert then carries `row_id`. The special metric `rows_new` (numbers-only monitors only; it needs an `items[].` metric in `track.metrics` and is not itself tracked) counts the list rows this run has that the previous run did not: `{"metric":"rows_new","op":"gt","value":0}` alerts on any new post or video. It accepts only `gt` and `gte`, and is skipped on the first run. "New" means absent from the previous successful run's stored rows (capped at `max_rows`, default 100), so a row that drops off and returns later counts as new again. An empty-page run is refunded and is not a baseline, so on a sparse feed the first run that holds rows has no baseline and stays silent: the first new post after a quiet period may not alert. Use a daily cadence or leave `max_rows` headroom.
    - `op` 'gt' | 'lt' | 'gte' | 'lte' | 'abs_change_gt' | 'pct_change_gt' | 'pct_change_lt', required
    - `value` number, required
    - `window` '1d' | '1w': Accepted and stored, but ignored: a change op always compares against the immediately preceding comparable run, whatever the value.
  - `cadence` string, required: How often to re-run the recipe: `"hourly"`, `"daily"`, `"weekly"`, or `{ "cron": "0 9 * * 1" }`. One hour is the shortest interval available: a cron that fires more often is accepted, flagged, and then throttled up to the one-hour floor, so a 30-minute schedule runs hourly.
  - `name` string: Label for the monitor. Defaults to "<recipe> monitor".
  - `output_schema` object: Optional projection applied to each stored run result.
  - `params` object: The recipe's own parameters, frozen at create time and replayed verbatim on every run. Required parameters are validated here, not at the first run.
  - `recipe` string, required: Endpoint path without the `/v1/` prefix, for example `search/everywhere` or `prism/comments`. Must be a GET endpoint.
  - `suppress_webhook_unless_alert` boolean: When true, only a run with at least one fired alert delivers a webhook. The run still executes and is still billed.
  - `track` object: Makes this a numbers-only monitor. Each run stores only these numbers, per row, plus the change against the previous run; the full response is delivered to the webhook but never stored. Validated at create, free.
    - `max_rows` integer: Rows kept per run, in page order.
    - `metrics` string[], required: 1 to 20 paths into the unified response. `items[].post.engagement.views` reads one number per row of a list; `author.followers` or `total` reads one number off the page.
    - `row_key` string: Path inside a row to its stable id. Defaults to `post.id`, `comment.id` or `author.id` by the endpoint's row type; required for other list types.
  - `webhook_format` 'full' | 'rows': `full`: the run payload. With `track` it carries `result` (the unified response, delivered but not stored), `numbers` and `deltas`. `rows` (needs `track`): `{ monitor_id, run_id, recipe, status, scheduled_for, alerts_fired, rows: [{ t, row_id, metric, value, prev, abs_change, pct_change }] }`, one entry per row and metric. Same signature either way.
  - `webhook_secret` string: Supply your own signing secret, or leave it out and one is generated. Either way it is returned exactly once and stored encrypted.
  - `webhook_url` string: HTTPS URL on a public host. Loopback, private and link-local addresses are rejected. Required unless `track` is set; a monitor with `track` and no `webhook_url` is download-only.

## Response `201`

Monitor created. `webhook_secret` appears in this response and never again.

- object
  - `monitor` object
    - `alert_rules` MonitorAlertRule[]: The alert rules evaluated after each run.
      - `metric` string, required: Dot path to a numeric key in the recipe's own result, for example `coverage` or `summary.total`. On a numbers-only monitor (created with `track`) the metric must be one of `track.metrics`, and an `items[].<path>` metric is evaluated once per row: a fired alert then carries `row_id`. The special metric `rows_new` (numbers-only monitors only; it needs an `items[].` metric in `track.metrics` and is not itself tracked) counts the list rows this run has that the previous run did not: `{"metric":"rows_new","op":"gt","value":0}` alerts on any new post or video. It accepts only `gt` and `gte`, and is skipped on the first run. "New" means absent from the previous successful run's stored rows (capped at `max_rows`, default 100), so a row that drops off and returns later counts as new again. An empty-page run is refunded and is not a baseline, so on a sparse feed the first run that holds rows has no baseline and stays silent: the first new post after a quiet period may not alert. Use a daily cadence or leave `max_rows` headroom.
      - `op` 'gt' | 'lt' | 'gte' | 'lte' | 'abs_change_gt' | 'pct_change_gt' | 'pct_change_lt', required
      - `value` number, required
      - `window` '1d' | '1w': Accepted and stored, but ignored: a change op always compares against the immediately preceding comparable run, whatever the value.
    - `cadence` unknown
    - `created_at` string, date-time
    - `credits_spent` integer: Credits this monitor has spent across every run so far.
    - `estimated_cost_per_run` integer: The recipe's own credit cost plus 1 credit for orchestration. A run that fails outright is refunded in full.
    - `estimated_monthly_cost` integer: `estimated_cost_per_run` multiplied by the runs the cadence produces in a month.
    - `id` string
    - `name` string
    - `next_run_at` string, date-time
    - `output_schema` object, nullable
    - `params` object: The recipe parameters frozen at create time. They are replayed verbatim on every run.
    - `recipe` string: The endpoint path this monitor re-runs, without the `/v1/`.
    - `runs_total` integer
    - `status` 'active' | 'paused'
    - `suppress_webhook_unless_alert` boolean
    - `track` object, nullable: Set on a numbers-only monitor (created with `track`): the tracked paths, the row id path, and the row cap. Null on a monitor that stores full results.
      - `max_rows` integer
      - `metrics` string[]
      - `row_key` string, nullable
    - `updated_at` string, date-time
    - `webhook_format` 'full' | 'rows': The webhook body: `full` (the default) or `rows` (flat rows, numbers-only monitors).
    - `webhook` object, nullable: The webhook. Null in list responses and on a download-only monitor (created with `track` and no `webhook_url`).
      - `failure_count` integer: Consecutive delivery failures. The webhook auto-pauses at 10.
      - `last_delivery_at` string, date-time, nullable
      - `signing_secret_hint` string: A non-reversible hint at the signing secret. The secret itself is returned exactly once, by `POST /v1/monitors`.
      - `status` 'active' | 'paused'
      - `url` string
  - `webhook_secret` string, nullable: The HMAC-SHA256 signing secret for this monitor's webhook. Store it now. Null for a download-only monitor.

## Other responses

- `400`: Invalid request - missing or malformed parameters
- `401`: Unauthorized - missing or invalid API key
- `402`: Payment required - the account balance is too low, or the calling key has spent its own per-key credit limit
- `403`: MONITOR_LIMIT_REACHED: the account is at its active-monitor limit, which depends on the highest credit pack purchased: Free 3, Starter 10, Growth 25, Pro 100, Enterprise 500. Pause or delete a monitor to free a slot.
- `404`: Not found - the endpoint does not exist, or the requested resource was not found upstream
- `413`: Payload too large: the JSON request body exceeds the 1 MB size limit and is rejected before parsing, or a single cohort result cannot fit beneath the 1 MB response-page ceiling
- `429`: Rate or concurrency limit exceeded. `RATE_LIMITED`: more than 600 requests in a 1-minute sliding window on this API key (headers `X-RateLimit-Limit`/`Remaining`/`Reset`; `Retry-After` is seconds until the window resets). `CONCURRENCY_LIMIT`: more than 50 simultaneous in-flight requests (headers `X-Concurrency-Limit`/`Remaining`; short static `Retry-After`). Both are unbilled. Honor `Retry-After`, then back off with jitter. See /docs/rate-limits.
- `500`: Internal server error - an immediate refund is attempted; credits_used reports the committed net charge and exact compensation is queued if that refund cannot commit

---

[API](https://skmtc.dev/socialcrawl/apis/socialcrawl-api.md) · [All operations](https://skmtc.dev/socialcrawl/apis/socialcrawl-api/llms.txt) · [OpenAPI document](https://skmtc.dev/socialcrawl/apis/socialcrawl-api/revisions/a0ad2511004a?raw)
