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

# Create monitor

`POST /v2/monitors`

## Request

- Base URL: `https://api.cloudzero.com`
- URL: `https://api.cloudzero.com/v2/monitors`
- Auth: API key in header `Authorization`
- TypeScript: <https://skmtc.dev/cloudzero/apis/cloudzero-api-v2-resources/docs/v2/monitors/post.ts> (a function with typed inputs; its response is checked against the schema)

Creates a monitor. Returns 409 with the existing monitor in `error.existing_id` if another monitor already has the same `filter_set`, `group_by`, `threshold`, and `cost_type`, so a retried create can adopt it.

## Request body

- CreateMonitorRequest: A monitor definition. Fields left out take their defaults.
  - `cost_type` 'REAL_COST' | 'AMORTIZED_COST' | 'BILLED_COST' | 'CUSTOM_COST' | 'DISCOUNTED_AMORTIZED_COST' | 'DISCOUNTED_COST' | 'INVOICED_AMORTIZED_COST' | 'ON_DEMAND_COST'
  - `description` string, nullable: Optional human-readable description for the monitor
  - `detection_type` 'anomaly': The type of detection to perform. Currently only "anomaly" is supported.
  - `filter_set` object: Cost filters for the monitor. An empty object is valid.
  - `group_by` string[]: Dimensions to group by. Empty means no grouping; create/update requests are capped to a single dimension.
  - `is_enabled` boolean: Whether the monitor is running. When false the monitor will not be sent for evaluation.
  - `monitor_name` string, required: Human-readable name for the monitor
  - `threshold` MonitorThreshold: Optional significance threshold: an anomaly is surfaced as an incident only if its cost impact meets it. Leave both `min_cost_impact` and `percentage` unset to surface every anomaly.
    - `min_cost_impact` number, nullable: Minimum daily cost impact: the day's spend above the expected range.
    - `operator` 'AND' | 'OR'
    - `percentage` number, nullable: Minimum daily cost impact, as a percentage of the upper end of the expected range.

## Response `200`

Successful Response

- Monitor
  - `cost_type` 'REAL_COST' | 'AMORTIZED_COST' | 'BILLED_COST' | 'CUSTOM_COST' | 'DISCOUNTED_AMORTIZED_COST' | 'DISCOUNTED_COST' | 'INVOICED_AMORTIZED_COST' | 'ON_DEMAND_COST'
  - `created_at` string, date-time, required: When the monitor was created, as an ISO 8601 UTC timestamp
  - `description` string, nullable: Optional human-readable description for the monitor
  - `detection_type` 'anomaly': The type of detection to perform. Currently only "anomaly" is supported.
  - `filter_set` object: Cost filters for the monitor. An empty object is valid.
  - `group_by` string[]: Dimensions to group by. Empty means no grouping; create/update requests are capped to a single dimension.
  - `is_enabled` boolean: Whether the monitor is running. When false the monitor will not be sent for evaluation.
  - `last_triggered_at` string, date-time, nullable: When the monitor last raised an incident, as an ISO 8601 UTC timestamp; null until it first raises one.
  - `monitor_id` string, required: Unique identifier for the monitor
  - `monitor_name` string, required: Human-readable name for the monitor
  - `notification_channels` NotificationChannel[]: Where incident notifications for this monitor are delivered. Set them with `PUT /v2/monitors/{monitorId}/notifications`; monitor create and update leave them unchanged.
    - `channel_type` 'email' | 'slack', required: The delivery channel: "email" or "slack".
    - `recipients` string[], required: Recipients to notify. At least one is required. For "email", these are email addresses, normalized and de-duplicated per mailbox. For "slack", these are Slack channel IDs (never channel names), case-preserved and de-duplicated exactly. At most 250 email addresses and 1 Slack channel after de-duplication.
  - `open_incident_count` integer, nullable: Number of unresolved incidents at read time; null if they could not be counted.
  - `owner_id` string, required: Email of the user who created the monitor; `system` for monitors CloudZero creates; `Set via API` for monitors created with an API key.
  - `threshold` MonitorThreshold: Optional significance threshold: an anomaly is surfaced as an incident only if its cost impact meets it. Leave both `min_cost_impact` and `percentage` unset to surface every anomaly.
    - `min_cost_impact` number, nullable: Minimum daily cost impact: the day's spend above the expected range.
    - `operator` 'AND' | 'OR'
    - `percentage` number, nullable: Minimum daily cost impact, as a percentage of the upper end of the expected range.
  - `total_incident_count` integer: Total incidents this monitor has ever fired, including resolved ones.
  - `updated_at` string, date-time, required: When the monitor was last updated, as an ISO 8601 UTC timestamp

## Other responses

- `401`: Unauthorized: the API key is missing or invalid (gateway `{"message": ...}` body), or the key carries no organization
- `403`: Forbidden: the API key lacks the scope for this operation (gateway `{"message": ...}` body)
- `409`: Conflict: another monitor already has this configuration; `error.existing_id` names it
- `422`: A parameter failed validation; `details` lists each field failure when present

## Changes

- **2026-11-03** `56a692cf51b1`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/cloudzero/apis/cloudzero-api-v2-resources/changes/v2/monitors/post.md)

---

[API](https://skmtc.dev/cloudzero/apis/cloudzero-api-v2-resources.md) - [All operations](https://skmtc.dev/cloudzero/apis/cloudzero-api-v2-resources/llms.txt) - [OpenAPI document](https://skmtc.dev/cloudzero/apis/cloudzero-api-v2-resources/revisions/56a692cf51b1?raw)
