Alert
The Alert API lets you provision and manage the same single-series threshold alerts available in the Telemetry UI.
If you are asking a coding agent to provision alerts, start with Create alerts with a coding agent. It gives the agent a safe discovery and validation sequence, an agent-ready prompt, and complete requests for common use cases.
Supported operations:
- List:
GET https://api.telemetry.sh/alert - Create:
POST https://api.telemetry.sh/alert - Edit:
PATCH https://api.telemetry.sh/alert - Delete:
DELETE https://api.telemetry.sh/alert
Headers
| Name | Type | Description |
|---|---|---|
Content-Type |
String | Must be application/json for create, edit, and delete requests. |
Authorization |
String | Your API key, either as the raw key or as Bearer <key>. |
Scope rules:
GET /alertacceptsread,write, orread-and-writePOST /alert,PATCH /alert, andDELETE /alertrequirewriteorread-and-write
List alerts
GET /alert returns alerts for the API key's team, ordered by updated_at DESC.
Supported query parameters:
| Field | Type | Required | Exact contract | Default |
|---|---|---|---|---|
page |
Integer | No | Positive, 1-based page number. | 1 |
pageSize |
Integer | No | Positive page size. Values above 100 are clamped to 100. The legacy alias page_size is also accepted. |
50 |
curl "https://api.telemetry.sh/alert?page=1&pageSize=25" \
-H "Authorization: $API_KEY"
The response includes standard pagination metadata and complete alert records:
{
"status": "success",
"pagination": {
"page": 1,
"pageSize": 25,
"total": 1,
"totalPages": 1,
"hasNextPage": false,
"hasPreviousPage": false
},
"alerts": [
{
"id": "d7463946-8c6a-4a54-8a47-74cc98247c54",
"team_id": "a7d4...",
"name": "API error rate",
"slug": "api-error-rate",
"description": "Notify the API on-call rotation",
"alert_type": "query",
"payload": {
"querySql": "SELECT time_bucket, error_rate FROM api_health",
"timestampColumn": "time_bucket"
},
"aggregation": "avg",
"metric": "error_rate",
"last_n_data_points": 3,
"ignore_last_data_point": true,
"check_interval_minutes": 60,
"comparison": "greater_than",
"threshold": 0.05,
"recipients": [
{ "type": "email", "recipient": "[email protected]" }
],
"status": "inactive",
"enabled": true,
"last_evaluated_at": null,
"last_value": null,
"evaluation_version": 0,
"created_by": null,
"created_by_api_key_id": "key_123",
"created_at": "2026-09-02T17:05:00.000Z",
"updated_at": "2026-09-02T17:05:00.000Z",
"url": "/team/acme/alert/api-error-rate"
}
]
}
pagination.totalPages is 0 when the team has no alerts. Requesting a page beyond the last page returns an empty alerts array.
Alert fields
| Field | Type | Writable | Exact contract |
|---|---|---|---|
id |
String | No | Alert id. |
team_id |
String | No | Owning team id. |
name |
String | Yes | Trimmed, non-empty display name of at most 200 characters. |
slug |
String | Yes | Unique team-scoped slug. See slug normalization. |
description |
String or null |
Yes | Optional description. Empty strings are stored as null. |
alert_type |
String | Yes | query or explorer. The payload must match the selected type. |
payload |
Object | Yes | Saved query definition. See query payloads and Explorer payloads. |
aggregation |
String | Yes | count, sum, avg, min, max, p50, p90, p95, or p99. |
metric |
String or null |
Yes | Numeric result column to aggregate. If null, the evaluator uses the first numeric non-timestamp column. An explicit value is recommended. |
last_n_data_points |
Integer | Yes | One of 1, 3, 5, 10, 20, 50, or 100. |
ignore_last_data_point |
Boolean | Yes | Whether to skip the newest result row, which can represent an incomplete time bucket. |
check_interval_minutes |
Integer | Yes | One of 1, 60, or 1440. |
comparison |
String | Yes | greater_than, less_than, greater_than_or_equal, or less_than_or_equal. |
threshold |
Number | Yes | Finite comparison threshold. JSON strings such as "10" are rejected. |
recipients |
Array | Yes | One to 25 unique email recipient objects. Addresses are trimmed and lowercased. |
status |
String | No | Current evaluation state: active when the condition is met, otherwise inactive. |
enabled |
Boolean | Yes | Whether the evaluator should run the alert. |
last_evaluated_at |
String or null |
No | Timestamp of the latest completed evaluation. |
last_value |
Number or null |
No | Latest aggregated value. |
evaluation_version |
Integer | No | Internal optimistic-concurrency version. |
created_by |
String or null |
No | User id for UI-created alerts; null for API-created alerts. |
created_by_api_key_id |
String or null |
No | API key id for API-created alerts; null for UI-created alerts. |
created_at |
String | No | Creation timestamp. |
updated_at |
String | No | Latest update timestamp. |
url |
String | No | Relative alert URL in the Telemetry UI. |
Create an alert
POST /alert creates one alert and returns 201 Created.
| Field | Required | Default |
|---|---|---|
name |
Yes | — |
slug |
No | Normalized from name |
description |
No | null |
alert_type |
Yes | — |
payload |
Yes | — |
aggregation |
No | avg |
metric |
No | null |
last_n_data_points |
No | 3 |
ignore_last_data_point |
No | true |
check_interval_minutes |
No | 60 |
comparison |
No | greater_than |
threshold |
Yes | — |
recipients |
Yes | — |
enabled |
No | true |
Create a query alert
curl -X POST https://api.telemetry.sh/alert \
-H "Authorization: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "API error rate",
"description": "Notify the API on-call rotation",
"alert_type": "query",
"payload": {
"querySql": "SELECT timestamp_utc AS time_bucket, error_rate FROM api_health ORDER BY timestamp_utc DESC",
"timestampColumn": "time_bucket",
"sourceUrl": "/team/acme/default/error-rate/1"
},
"aggregation": "avg",
"metric": "error_rate",
"last_n_data_points": 3,
"ignore_last_data_point": true,
"check_interval_minutes": 60,
"comparison": "greater_than",
"threshold": 0.05,
"recipients": [
{ "type": "email", "recipient": "[email protected]" }
]
}'
Successful response:
{
"status": "success",
"alert": {
"id": "d7463946-8c6a-4a54-8a47-74cc98247c54",
"name": "API error rate",
"slug": "api-error-rate",
"created_by": null,
"created_by_api_key_id": "key_123",
"url": "/team/acme/alert/api-error-rate"
}
}
The returned alert object contains every field shown in Alert fields; the shortened example highlights the creation identity and URL.
Query payloads
| Field | Type | Required | Exact contract |
|---|---|---|---|
querySql |
String | Yes | Non-empty, read-only SQL. Statements containing writes or DDL are rejected. |
timestampColumn |
String or null |
No | Result column used to order rows newest first. When omitted or null, Telemetry looks for common timestamp columns. |
queryId |
String or null |
No | Optional saved-query id for attribution. |
sourceUrl |
String or null |
No | Optional relative Telemetry URL for returning to the source query. |
The query should return one ordered time series with one numeric value per row. The alert evaluator orders rows by the timestamp column, optionally skips the newest row, takes the requested number of points, aggregates metric, and applies comparison to threshold.
Explorer payloads
Explorer alerts persist a table name and Explorer state:
{
"name": "Checkout failures",
"alert_type": "explorer",
"payload": {
"tableName": "checkout_events",
"explorerState": {
"graphType": "line",
"aggregation": "count",
"metric": null,
"timeZone": "UTC",
"timePreset": "24h",
"granularity": "hour",
"splitBy": [],
"filters": [
{
"logic": "AND",
"conditions": [
{ "field": "outcome", "operator": "=", "value": "failed" }
]
}
],
"selectedColumns": [],
"orderBy": null,
"orderDirection": "DESC",
"limit": 200
},
"fields": [
{ "name": "outcome", "type": "Utf8" }
]
},
"metric": "count",
"threshold": 10,
"recipients": [
{ "type": "email", "recipient": "[email protected]" }
]
}
Explorer alert rules:
payload.tableNamemust be a non-empty string.payload.explorerStatemust be an object and itsgraphTypemust belinewhen specified.splitBymust be empty because an alert evaluates one series.aggregationaccepts the same aggregation names as the top-level alert condition. A non-countExplorer aggregation requiresexplorerState.metric.timePresetaccepts1h,6h,24h,7d,30d,90d, orcustom.granularityacceptsauto,minute,hour,day,week, ormonth.fieldsis optional. Include{ "name", "type" }records when filters or numeric field selection depend on schema types.- Filter operators are
=,!=,>,>=,<,<=,LIKE,NOT LIKE,IS NULL, andIS NOT NULL.
Create alerts with a coding agent
An agent can create a syntactically valid alert that still watches the wrong table, uses the wrong unit, or emails the wrong people. Give it an operational goal and require it to inspect and validate the data before it calls POST /alert.
Query alerts are usually the easiest type for an agent to build because it can run the exact SQL through POST /query before saving it. Explorer alerts are useful for standard counts and percentiles because Telemetry generates the time buckets and fills missing buckets with zero.
Information the agent needs
Provide these inputs or tell the agent to stop and ask for them:
- the condition that should trigger and the unit of its threshold
- the expected table or event name, if known
- the environment, service, route, account, or other population to monitor
- the lookback, bucket size, and how many completed buckets must breach
- a stable alert name and slug
- the recipient who owns the response
- whether the agent may enable delivery or should only create a disabled draft
Do not ask the agent to infer a production paging address or invent a threshold from a few sample rows.
Recommended request sequence
- Call
GET /tablesto discover the canonical table name. - Call
GET /tables/<table>/schemaand use only fields that actually exist with compatible types. - Call
GET /alert?page=1&pageSize=100and look for the intended stable slug. If it exists, usePATCH /alert; do not create a duplicate. - For a query alert, run the exact proposed SQL with
POST /query. Confirm that it returns one timestamp column, one numeric metric column, the expected unit, and newest-first ordering. - Create a new alert with
enabled: false. Review the returned alert, query, threshold, point window, and recipients. - Enable the reviewed alert with
PATCH /alert. Enabling an alert can cause real email delivery after a state transition, so treat this as the side-effecting step.
There is no alert upsert endpoint. Repeating POST /alert with an existing slug returns 409 Conflict; a well-behaved agent lists first and patches the existing alert deliberately.
The discovery calls are:
curl "https://api.telemetry.sh/tables?page=1&pageSize=100" \
-H "Authorization: $API_KEY"
curl https://api.telemetry.sh/tables/http_request_completed/schema \
-H "Authorization: $API_KEY"
curl "https://api.telemetry.sh/alert?page=1&pageSize=100" \
-H "Authorization: $API_KEY"
Prompt for a coding agent
Copy this prompt and replace the bracketed values:
Create a Telemetry alert for [operational condition] using https://api.telemetry.sh.
Use the API key already available as API_KEY. The expected event or table is
[table, or "unknown"]. Monitor [population] over [window and bucket size]. The
threshold is [value and unit], and the owner is [recipient]. Use the stable slug
[slug].
Before changing anything:
1. List tables and inspect the selected table's schema.
2. List existing alerts and look for the stable slug.
3. Build a single-series query and run the exact SQL through POST /query.
4. Show me the returned columns and representative rows, the proposed alert
request, and how its bucket aggregation differs from its top-level aggregation.
Do not guess field names, units, thresholds, or recipients. Do not create a
duplicate or delete an alert. If the slug does not exist, create the alert with
enabled set to false. If it exists, propose a PATCH instead. Do not enable the
alert until I confirm the query, threshold, and recipient.
Choose both aggregations deliberately
Explorer alerts have two aggregation layers. explorerState.aggregation calculates the value inside each time bucket; the top-level aggregation reduces the recent bucket values selected by last_n_data_points. Query alerts calculate each row in SQL and use only the top-level aggregation across recent rows.
| Operational goal | Value per bucket | Top-level condition |
|---|---|---|
| Sustained server error rate | SQL calculates server_error_rate_pct |
Average the last three completed buckets and compare with 5 percent |
| Any latency spike | Explorer calculates p95 duration_ms |
Maximum of the last five completed buckets exceeds 850 milliseconds |
| Missing heartbeat | Explorer counts events and fills missing buckets with zero | Sum of the last five completed buckets is less than 1 event |
ignore_last_data_point: true is appropriate for these time-bucketed examples because the newest bucket can be incomplete. Set it to false for a query that returns only one fully calculated row; otherwise the evaluator drops that only row.
Example: sustained server error rate
This query calculates one error-rate percentage for each five-minute bucket and suppresses rate alerts below 100 requests per bucket. The alert then averages the three newest completed buckets and compares that average with 5 percent.
First run the exact SQL and inspect the result:
ALERT_QUERY=$(cat <<'SQL'
SELECT
date_bin(
INTERVAL '5 minutes',
timestamp_utc,
TIMESTAMP '1970-01-01'
) AS time_bucket,
CASE
WHEN COUNT(*) >= 100 THEN
100.0 * SUM(CASE WHEN status_code >= 500 THEN 1 ELSE 0 END)
/ NULLIF(COUNT(*), 0)
ELSE 0.0
END AS server_error_rate_pct
FROM http_request_completed
WHERE
timestamp_utc >= now() - INTERVAL '35 minutes'
AND environment = 'production'
GROUP BY time_bucket
ORDER BY time_bucket DESC;
SQL
)
curl -X POST https://api.telemetry.sh/query \
-H "Authorization: $API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg query "$ALERT_QUERY" \
'{query: $query, realtime: true, json: true}')"
After verifying that time_bucket is a timestamp and server_error_rate_pct is numeric, create a disabled draft:
curl -X POST https://api.telemetry.sh/alert \
-H "Authorization: $API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg query "$ALERT_QUERY" '{
name: "Production API error rate",
slug: "production-api-error-rate",
description: "Investigate recent deploys and affected routes before escalating.",
alert_type: "query",
payload: {
querySql: $query,
timestampColumn: "time_bucket"
},
aggregation: "avg",
metric: "server_error_rate_pct",
last_n_data_points: 3,
ignore_last_data_point: true,
check_interval_minutes: 1,
comparison: "greater_than",
threshold: 5,
recipients: [
{type: "email", recipient: "[email protected]"}
],
enabled: false
}')"
Replace the example recipient with the reviewed owner before enabling the alert.
Example: p95 latency spike
This Explorer alert calculates p95 request duration inside each minute. The top-level max means one of the five most recent completed minutes must exceed 850 milliseconds.
curl -X POST https://api.telemetry.sh/alert \
-H "Authorization: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Production API p95 latency",
"slug": "production-api-p95-latency",
"description": "Inspect slow routes, dependencies, and the latest deploy.",
"alert_type": "explorer",
"payload": {
"tableName": "http_request_completed",
"explorerState": {
"graphType": "line",
"aggregation": "p95",
"metric": "duration_ms",
"timeZone": "UTC",
"timePreset": "1h",
"granularity": "minute",
"splitBy": [],
"filters": [
{
"logic": "AND",
"conditions": [
{
"field": "environment",
"operator": "=",
"value": "production"
}
]
}
],
"selectedColumns": ["duration_ms"],
"orderBy": null,
"orderDirection": "DESC",
"limit": 200
},
"fields": [
{ "name": "duration_ms", "type": "Float64" },
{ "name": "environment", "type": "Utf8" }
]
},
"aggregation": "max",
"metric": "duration_ms",
"last_n_data_points": 5,
"ignore_last_data_point": true,
"check_interval_minutes": 1,
"comparison": "greater_than",
"threshold": 850,
"recipients": [
{ "type": "email", "recipient": "[email protected]" }
],
"enabled": false
}'
The fields types must come from the schema response. They let the Explorer SQL generator serialize filter values correctly and identify numeric measures.
Example: missing heartbeat
Use an Explorer count when absence is the signal. Explorer time-series queries include zero-valued missing buckets, so this alert triggers when the sum across the last five completed one-minute buckets is less than one event.
curl -X POST https://api.telemetry.sh/alert \
-H "Authorization: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Production worker heartbeat missing",
"slug": "production-worker-heartbeat-missing",
"description": "Check the worker process, queue, and ingestion path.",
"alert_type": "explorer",
"payload": {
"tableName": "worker_heartbeat",
"explorerState": {
"graphType": "line",
"aggregation": "count",
"metric": null,
"timeZone": "UTC",
"timePreset": "1h",
"granularity": "minute",
"splitBy": [],
"filters": [
{
"logic": "AND",
"conditions": [
{
"field": "environment",
"operator": "=",
"value": "production"
}
]
}
],
"selectedColumns": [],
"orderBy": null,
"orderDirection": "DESC",
"limit": 200
},
"fields": [
{ "name": "environment", "type": "Utf8" }
]
},
"aggregation": "sum",
"metric": "count",
"last_n_data_points": 5,
"ignore_last_data_point": true,
"check_interval_minutes": 1,
"comparison": "less_than",
"threshold": 1,
"recipients": [
{ "type": "email", "recipient": "[email protected]" }
],
"enabled": false
}'
Do not use a query that only groups existing heartbeat events: if no events arrive, that query may return no row for the missing interval. The Explorer time-series form is useful here because it produces the zero-valued buckets the condition needs.
Example: review and enable a draft
List alerts again and inspect the saved record for the stable slug. Then enable only that alert:
curl "https://api.telemetry.sh/alert?page=1&pageSize=100" \
-H "Authorization: $API_KEY"
curl -X PATCH https://api.telemetry.sh/alert \
-H "Authorization: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"alertSlug": "production-api-error-rate",
"enabled": true
}'
If the stable slug already existed, use the same PATCH /alert shape to change only the reviewed fields. Keep enabled: false during query or recipient changes if notification delivery should remain paused.
Slug normalization
For create and edit requests, Telemetry trims and lowercases slug, removes characters other than ASCII letters, numbers, spaces, and -, converts whitespace to -, collapses repeated -, and trims leading or trailing -.
If create omits slug, the API normalizes name. For example, "API Errors!!!" becomes "api-errors". The normalized slug must contain at least one letter or number and must be unique within the team. A duplicate returns 409 Conflict.
Edit an alert
PATCH /alert is a partial update. Identify the alert with alertId or alertSlug; every other omitted field remains unchanged.
curl -X PATCH https://api.telemetry.sh/alert \
-H "Authorization: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"alertSlug": "api-error-rate",
"threshold": 0.08,
"last_n_data_points": 5
}'
When both identifiers are provided, they must resolve to the same alert. Send payload together with alert_type when changing between query and explorer.
Updating the query, condition, schedule, or recipients clears last_value and last_evaluated_at, returns status to inactive, and increments evaluation_version. Renaming, changing the description or slug, and toggling enabled do not clear evaluation state.
The response is 200 OK with the same { "status": "success", "alert": { ... } } shape as create.
Delete an alert
DELETE /alert accepts alertId, alertSlug, or both. Deleting an alert also deletes its evaluation history.
curl -X DELETE https://api.telemetry.sh/alert \
-H "Authorization: $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "alertSlug": "api-error-rate" }'
Successful response:
{
"status": "success",
"deleted_alert": {
"id": "d7463946-8c6a-4a54-8a47-74cc98247c54",
"name": "API error rate",
"slug": "api-error-rate",
"description": "Notify the API on-call rotation",
"url": "/team/acme/alert/api-error-rate"
}
}
Common errors
400 Bad Requestfor invalid JSON, invalid alert fields, incompatible payloads, unsupported pagination, or a read-scoped key used for a mutation401 Unauthorizedwhen the API key is missing or invalid404 Not Foundwhen an alert identifier does not belong to the API key's team409 Conflictwhen the normalized slug already exists for the team429 Too Many Requestswhen the API key exceeds the gateway rate limit500 Internal Server Errorwhen a valid persistence operation fails
Alert definitions can generate email. Use temporary recipients while testing, verify the condition with synthetic data, and disable or delete test alerts after validation. See Alerts for evaluation semantics and Alert delivery and troubleshooting for delivery guidance.