> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spidra.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a Monitor

> Watch a template, a preset or a list of pages on a schedule, and get told what changed.

A monitor runs its sources on a schedule, compares each check with the previous one, and alerts you when something changed. The [Monitors guide](https://docs.spidra.io/monitors.md) explains each option; this page shows the request.

## Example request

Watch Google Maps for new dentists in Lekki, every morning at 8:

```bash cURL theme={null}
curl -X POST https://api.spidra.io/api/monitors \
  -H "Authorization: Bearer $SPIDRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Dentists in Lekki",
    "sources": [
      { "kind": "template", "template": "google-maps-leads", "inputs": { "query": "dentist", "areas": ["Lekki, Lagos"] } }
    ],
    "schedule": { "every": "1d", "at": "08:00", "timezone": "Africa/Lagos" },
    "alertOn": "new",
    "notify": { "email": { "enabled": true } }
  }'
```

```javascript JavaScript theme={null}
const res = await fetch("https://api.spidra.io/api/monitors", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SPIDRA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Dentists in Lekki",
    sources: [{ kind: "template", template: "google-maps-leads", inputs: { query: "dentist", areas: ["Lekki, Lagos"] } }],
    schedule: { every: "1d", at: "08:00", timezone: "Africa/Lagos" },
    alertOn: "new",
    notify: { email: { enabled: true } },
  }),
});
const { monitor } = await res.json();
```

```python Python theme={null}
import os, requests

res = requests.post(
    "https://api.spidra.io/api/monitors",
    headers={"Authorization": f"Bearer {os.environ['SPIDRA_API_KEY']}"},
    json={
        "name": "Dentists in Lekki",
        "sources": [{"kind": "template", "template": "google-maps-leads", "inputs": {"query": "dentist", "areas": ["Lekki, Lagos"]}}],
        "schedule": {"every": "1d", "at": "08:00", "timezone": "Africa/Lagos"},
        "alertOn": "new",
        "notify": {"email": {"enabled": True}},
    },
)
monitor = res.json()["monitor"]
```

## Response

`201` with the monitor. This one watches a single page for text changes, with email alerts off:

```json theme={null}
{
  "status": "success",
  "monitor": {
    "id": "d6761e12-b011-4633-a593-fa6be2ef466c",
    "name": "Docs test page",
    "status": "active",
    "sources": [{ "id": "ae22524a61ad", "kind": "urls", "urls": ["https://httpbin.org/uuid"], "label": "httpbin.org" }],
    "schedule": { "every": "1d", "at": "09:00", "timezone": "Africa/Lagos" },
    "cron": "0 9 * * *",
    "alertOn": "page",
    "watchFields": [],
    "rule": null,
    "goal": null,
    "notify": { "email": { "enabled": false } },
    "nextRunAt": "2026-10-25T08:00:00.000Z",
    "lastCheckAt": null,
    "lastChangeAt": null,
    "lastStatus": null,
    "lastSummary": null,
    "createdAt": "2026-10-24T18:50:07.000Z",
    "updatedAt": "2026-10-24T18:50:07.000Z"
  },
  "recipients": []
}
```

The first check runs straight away and records the starting point ("Started watching (1 item)"). It doesn't alert. Send `"checkNow": false` to wait for the schedule instead. `nextRunAt` is in UTC.

`recipients` lists any extra email addresses with their status. Each one gets a confirmation email and receives alerts only after confirming.

## Sources

| `kind` | Fields | Watches |
| - | - | - |
| `template` | `template`, `inputs` | A template's rows. Each row has a stable key, so new, changed and removed items are exact. |
| `preset` | `presetId` | A saved scrape, batch, map, search or template preset. |
| `urls` | `urls` (up to 50) | Public pages. |

Up to 10 sources per monitor. Each can have a `label`, shown in alerts.

## Errors

| Status | When |
| - | - |
| `422` | A field needs fixing. `errors` lists each one, for example `{ "field": "schedule", "message": "Your plan checks at most every 15 minutes." }` or `{ "field": "sources[0].urls[0]", "message": "\"http://localhost/x\" can't be a private or internal address." }`. |
| `403` | Your plan's monitor limit is reached (`MONITOR_LIMIT`). Paused monitors count. |
| `409` | Another monitor is being created on your account at the same moment (`BUSY`). Try again. |


## OpenAPI

````yaml POST /monitors
openapi: 3.1.0
info:
  title: Spidra API
  version: 1.0.0
  description: >-
    Public API endpoints for web scraping via Spidra. Authenticate with
    `Authorization: Bearer YOUR_API_KEY`.
servers:
  - url: https://api.spidra.io/api
security:
  - BearerAuth: []
  - ApiKeyAuth: []
paths:
  /monitors:
    post:
      tags:
        - Monitors
      summary: Create a Monitor
      description: >-
        Creates a monitor and, unless `checkNow` is false, runs its first check,
        which records the starting point without alerting.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MonitorInput'
            example:
              name: Dentists in Lekki
              sources:
                - kind: template
                  template: google-maps-leads
                  inputs:
                    query: dentist
                    areas:
                      - Lekki, Lagos
              schedule:
                every: 1d
                at: '08:00'
                timezone: Africa/Lagos
              alertOn: new
              notify:
                email:
                  enabled: true
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - success
                  monitor:
                    $ref: '#/components/schemas/Monitor'
                  recipients:
                    type: array
                    items:
                      type: object
                      properties:
                        email:
                          type: string
                        status:
                          type: string
                          enum:
                            - pending
                            - confirmed
                            - unsubscribed
                    description: Extra email recipients and whether they confirmed.
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Your plan's monitor limit is reached (`MONITOR_LIMIT`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: >-
            Another monitor is being created on your account right now (`BUSY`).
            Try again.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Some inputs need fixing. `errors` lists each field.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvalidInputError'
components:
  schemas:
    MonitorInput:
      type: object
      properties:
        name:
          type: string
          description: Defaults to the first source's label.
        sources:
          type: array
          items:
            $ref: '#/components/schemas/MonitorSource'
          minItems: 1
          maxItems: 10
        schedule:
          $ref: '#/components/schemas/MonitorSchedule'
        alertOn:
          type: string
          enum:
            - new
            - changed
            - removed
            - page
            - every
          description: >-
            What counts as a change. Default `page` when every source is a URL
            list, otherwise `new`.
        watchFields:
          type: array
          items:
            type: string
          maxItems: 20
          description: 'alertOn changed: only these fields count. Empty means every field.'
        rule:
          $ref: '#/components/schemas/MonitorRule'
        goal:
          type:
            - string
            - 'null'
          maxLength: 1000
          description: >-
            Plain-language goal. When something changed, AI decides whether it
            matches before alerting, and says why.
        notify:
          $ref: '#/components/schemas/MonitorNotify'
        checkNow:
          type: boolean
          default: true
          description: >-
            Create only: run the first check straight away. It records the
            starting point and doesn't alert.
    Monitor:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        status:
          type: string
          enum:
            - active
            - paused
        sources:
          type: array
          items:
            $ref: '#/components/schemas/MonitorSource'
        schedule:
          $ref: '#/components/schemas/MonitorSchedule'
        cron:
          type: string
          description: The schedule as a cron expression.
        alertOn:
          type: string
        watchFields:
          type: array
          items:
            type: string
        rule:
          $ref: '#/components/schemas/MonitorRule'
        goal:
          type:
            - string
            - 'null'
        notify:
          $ref: '#/components/schemas/MonitorNotify'
        nextRunAt:
          type:
            - string
            - 'null'
          format: date-time
        lastCheckAt:
          type:
            - string
            - 'null'
          format: date-time
        lastChangeAt:
          type:
            - string
            - 'null'
          format: date-time
        lastStatus:
          type:
            - string
            - 'null'
        lastSummary:
          type:
            - string
            - 'null'
          description: The last check in a few words, such as "3 new" or "No change".
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ErrorResponse:
      type: object
      properties:
        status:
          type: string
          enum:
            - error
        message:
          type: string
      required:
        - status
        - message
    InvalidInputError:
      type: object
      properties:
        status:
          type: string
          enum:
            - error
        code:
          type: string
          enum:
            - INVALID_INPUT
        message:
          type: string
        errors:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string
          description: One entry per field that needs fixing.
    MonitorSource:
      type: object
      required:
        - kind
      properties:
        id:
          type: string
          description: >-
            Optional. Kept per source so changes are compared against the same
            source next time.
        kind:
          type: string
          enum:
            - template
            - preset
            - urls
        label:
          type: string
          description: Shown in alerts. Defaults to the template, preset or site name.
        template:
          type: string
          description: 'kind template: the template''s slug.'
        inputs:
          type: object
          description: 'kind template: the template''s inputs.'
        presetId:
          type: string
          description: >-
            kind preset: the preset's ID. Scrape, batch, map, search and
            template presets can be watched.
        urls:
          type: array
          items:
            type: string
          maxItems: 50
          description: 'kind urls: up to 50 public pages.'
    MonitorSchedule:
      type: object
      properties:
        every:
          type: string
          enum:
            - 5m
            - 15m
            - 30m
            - 1h
            - 6h
            - 12h
            - 1d
            - 1w
        at:
          type: string
          description: >-
            "HH:MM". The time for 1d and 1w, and the minute past the hour for
            1h, 6h and 12h. Default 09:00.
        day:
          type: integer
          minimum: 0
          maximum: 6
          description: '1w only: 0 is Sunday. Default 1 (Monday).'
        cron:
          type: string
          description: 'Instead of `every`: a 5-field cron expression.'
        timezone:
          type: string
          description: An IANA time zone such as Africa/Lagos. Default UTC.
      description: >-
        Your plan sets the shortest interval: daily on Free, hourly on Starter,
        every 15 minutes on Builder, every 5 minutes on Pro.
    MonitorRule:
      type:
        - object
        - 'null'
      properties:
        field:
          type: string
        op:
          type: string
          enum:
            - lt
            - lte
            - gt
            - gte
            - eq
            - neq
            - contains
            - drops_by_pct
            - rises_by_pct
        value:
          type:
            - number
            - string
      description: >-
        Only alert about items where this is true, for example `{ "field":
        "price", "op": "drops_by_pct", "value": 10 }`.
    MonitorNotify:
      type: object
      properties:
        email:
          type: object
          properties:
            enabled:
              type: boolean
              default: true
            recipients:
              type: array
              items:
                type: string
              maxItems: 10
              description: >-
                Extra addresses. Each gets alerts only after confirming by
                email.
          description: Your account's email is on unless `enabled` is false.
        webhook:
          type: object
          properties:
            url:
              type: string
              description: A public https URL.
            secret:
              type: string
              description: >-
                16 to 128 letters, digits, - or _. Generated when you leave it
                out.
        slack:
          type: object
          properties:
            webhookUrl:
              type: string
              description: A Slack incoming webhook URL (https://hooks.slack.com/...).
        discord:
          type: object
          properties:
            webhookUrl:
              type: string
              description: A Discord webhook URL (https://discord.com/api/webhooks/...).
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.