---
title: "Create a routing rule (v1.126)"
method: POST
path: "/agent/v1/notifications/rules"
tags: ["Agent"]
---

# Create a routing rule (v1.126)

`POST /agent/v1/notifications/rules`

## Request

- Base URL: `https://api.run402.com`
- URL: `https://api.run402.com/agent/v1/notifications/rules`
- Auth: API key in header `SIGN-IN-WITH-X`

operator_passkey assurance. One rule = one match -> one destination; every match dimension is optional and ANDed when present (absent = wildcard). telegram_binding_id must reference the caller's own ACTIVE binding; project_id, if given, must be visible to the caller's verified email (the project's owning org has that email among its notification recipients). Both references collapse missing/foreign/unusable into the identical RULE_REF_NOT_FOUND 404 — authorize-before-reveal (style.md).

## Headers

- `SIGN-IN-WITH-X` string, required

## Request body

- object
  - `telegram_binding_id` string, uuid, required
  - `project_id` string, nullable: Wildcard when omitted/null.
  - `source` 'app' | 'platform', nullable: Wildcard when omitted/null.
  - `event_types` string[], nullable: Flat snake_case, ^[a-z][a-z0-9_]{2,63}$ (the same grammar as project-events event_type). Wildcard when omitted/null — but an explicit [] matches NO event_type (a real, if unsatisfiable, filter), distinct from omitting the field.
  - `channel` 'telegram' | 'escalation': event-attention-rules: escalation needs project_id, escalation_level, owner authority on the project's organization (403 OWNER_REQUIRED otherwise), and no telegram_binding_id. 400 INVALID_CHANNEL for anything else.
  - `escalation_level` integer, nullable: Escalation rules only (400 INVALID_ESCALATION_LEVEL otherwise).
  - `min_severity` 'info' | 'notice' | 'warn' | 'critical', nullable: Only events at or above this severity match. Omitted/null resolves once: `notice` without event_types, `info` with them. Any other value is 400 `INVALID_MIN_SEVERITY`.
  - `group_by` string[], nullable: event-attention-rules: 1 to 3 top-level payload keys (^[A-Za-z_][A-Za-z0-9_]{0,63}$) whose values group a window's events; needs window_seconds. A rule holds at most 200 distinct keys per window; further keys are counted under (other), so varying a payload value multiplies neither messages nor stored state. 400 INVALID_GROUP_BY when malformed; 400 INVALID_WINDOW_SECONDS when given without window_seconds.
  - `window_seconds` integer, nullable: event-attention-rules: reduce a run of events to ONE message per closed wall-clock window (aligned to multiples of window_seconds), with one line per group key ordered by count, ten lines then a summary, plus the total and up to three bounded samples. 60 to 86400. Null delivers per event. 400 INVALID_WINDOW_SECONDS outside the range.
  - `classes` string[], nullable: Wildcard when omitted/null; same [] semantics as event_types.

## Response `201`

Rule created

- object: One rule = one match -> one destination (notification-channel-routing-telegram, v1.126). Every match dimension is optional; absent = wildcard, ANDed when present. No rules = no Telegram traffic; email/webhook are governed by preferences, untouched by this capability.
  - `id` string, uuid
  - `recipient_email` string
  - `project_id` string, nullable: Wildcard when null.
  - `source` 'app' | 'platform', nullable: Wildcard when null.
  - `event_types` string[], nullable: Wildcard when null. An explicit [] matches NO event_type — a real, if unsatisfiable, filter — distinct from omitting the field.
  - `classes` string[], nullable: Wildcard when null; same [] semantics as event_types.
  - `min_severity` 'info' | 'notice' | 'warn' | 'critical': event-attention-rules: only events at or above this severity match. Never null: a rule created without one stores `notice` when it names no event_types (routine traffic stays in the feed) and `info` when it names some (naming a type opts in to it). Rules that existed before severity shipped read `info` and deliver exactly what they did.
  - `group_by` string[], nullable: event-attention-rules: 1 to 3 top-level payload keys (^[A-Za-z_][A-Za-z0-9_]{0,63}$) whose values group a window's events; needs window_seconds. A rule holds at most 200 distinct keys per window; further keys are counted under (other), so varying a payload value multiplies neither messages nor stored state.
  - `window_seconds` integer, nullable: event-attention-rules: reduce a run of events to ONE message per closed wall-clock window (aligned to multiples of window_seconds), with one line per group key ordered by count, ten lines then a summary, plus the total and up to three bounded samples. 60 to 86400. Null delivers per event.
  - `next_window_closes_at` string, date-time: Windowed rules only: when the current window closes — the earliest a message can arrive. Computed at read.
  - `escalation_level` integer, nullable: event-attention-rules: set exactly for channel escalation — the ladder level a match raises at (the lowest staffed level at or above it).
  - `escalation_refused_count` integer: Escalation rules: raises the bounds refused (20 per rule per UTC day; 20 open per organization). 0 otherwise.
  - `last_escalation_refused_at` string, date-time, nullable
  - `channel` 'telegram' | 'escalation': event-attention-rules: `telegram` sends to telegram_binding_id; `escalation` raises on the organization's escalation ladder at escalation_level (mandatory delivery to the contacts there, one-tap ack, deadman climb), and one open escalation per rule: a repeat attaches. Was generic on purpose (widening to email/webhook rules later is a CHECK-constraint migration, not a reshape); v1 only ever writes 'telegram'.
  - `telegram_binding_id` string, uuid
  - `enabled` boolean
  - `created_at` string, date-time
  - `updated_at` string, date-time
  - `next_actions` object[]: test_notification is domain-specific (mirrors services/project-domains.ts's test_receive) — exercises the full binding+rule+render+send chain in one call.
    - `type` 'test_notification'
    - `method` string
    - `path` string
    - `why` string

## Other responses

- `400`: Malformed input: INVALID_SOURCE, INVALID_EVENT_TYPE, INVALID_CLASS, INVALID_TELEGRAM_BINDING_ID, or INVALID_PROJECT_ID
- `403`: operator_passkey assurance not met (FORBIDDEN)
- `404`: No agent contact registered for this wallet (RESOURCE_NOT_FOUND), or telegram_binding_id/project_id is missing, foreign, or unusable (RULE_REF_NOT_FOUND) — authorize-before-reveal, identical envelope either way

---

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