Add Rule to Traffic Policy

POST /v1/endpoints/{uuid}/policies/{policy_id}/rules

Append a rule to an existing traffic policy. A rule has a phase (when it runs), a rule_type (what it does), an optional CEL expression (a filter — when it should fire), and a config object whose schema depends on the rule type.

Per-rule-type plan gating runs on this endpoint: even if your plan allows traffic policies, individual rule types (e.g. cache-response, jwt-validation) may need higher-tier plans — each rule type is gated by its own traffic_rule:<rule_type> entitlement (a 403 is returned when the plan lacks it). Update calls don't re-check this per-type gate so admins can keep editing rules of types they no longer have access to.

Body is strict-decoded + the rule's config is validated against a per-type schema and run through an AI safety check (for rule types that serve user-controlled content).

Path parameters

uuid string (uuid) Req
Endpoint UUID.
policy_id int64 Req
Policy ID.

Request body

phase string Req
When the rule runs. One of: on_tcp_connect, on_udp_connect, on_http_request, on_http_response.
rule_type string Req
What the rule does. See /traffic-policy/rule-types for the catalog. One of (hyphenated): restrict-ips, rate-limit, add-headers, remove-headers, url-rewrite, redirect, custom-response, require-client-cert, jwt-validation, verify-webhook, force-https, webhook, custom-error, circuit-breaker, api-key-validation, cache-response, validate-body, cors, set-vars, replace-body, compress-response, waf, bot-management. Note on phases: restrict-ips and rate-limit are valid in the on_tcp_connect/on_udp_connect phases; cors, set-vars, waf and bot-management must be in on_http_request; custom-error, replace-body and compress-response only in on_http_response; the rest in on_http_request.
name string Req
Free-form label.
expression string
Optional CEL filter. The rule only fires when the expression evaluates truthy. Variables: req.method, req.path, req.host, req.headers["x-…"], req.cookies["…"], req.query_params["…"], conn.client_ip, conn.ja3 (client TLS JA3 fingerprint), and vars.<name> (values set by earlier set-vars rules). Examples: req.method == "POST", req.path.startsWith("/api/"), vars.tier == "gold". Absent keys read "". Leave empty to always fire.
config object
Rule-type-specific JSON. Schema depends on rule_type — see rule-types catalog. Examples:
rate-limit: {"limit": int, "window": int (sec), "scope": "ip"|"endpoint"|"global"|"header"|"cookie"|"jwt", "key_name": string (required for header/cookie/jwt)}. The header/cookie/jwt scopes key on client-controlled values (JWT claim read without verifying the signature) — fair-sharing, not abuse protection; pair with an ip rule as the hard ceiling.
url-rewrite: {"pattern": regex, "replacement": string, "target": "path"|"query"|"host"|"path_query"} (target defaults to path).
cors: {"allow_origins": ["https://app.example.com"|"*"], "allow_methods": [...], "allow_headers": [...], "expose_headers": [...], "allow_credentials": bool, "max_age": int}. "*" may not be combined with allow_credentials.
set-vars: {"vars": {"tier": "req.headers[\"x-tier\"]", "is_admin": "req.path.startsWith(\"/admin\")"}} — each value is a CEL string expression; results are readable by later rules as vars.<name> and ${vars.<name>}.
replace-body (response phase): {"find": string, "replace": string, "regex": bool, "max_body": int (default 1 MiB)}. Applies to complete, uncompressed bodies only.
compress-response (response phase): {"min_size": int (default 1024), "content_types": ["text/","application/json"]}. Negotiates br (preferred) or gzip from Accept-Encoding.
waf (request phase): {"mode": "detect"|"block", "ruleset": "core"|"custom"|"core+custom", "custom_rules": string, "inspect_body": bool, "max_body_kb": int (default 128, max 1024)}. OWASP-style inspection via Coraza; mode defaults to detect (log only). core = built-in baseline (SQLi/XSS/traversal/scanners); custom supplies your own SecLang.
bot-management (request phase): {"mode": "detect"|"block", "ja3_denylist": ["<32-hex JA3>"], "ja3_allowlist": [...], "block_missing": bool, "message": string}. Matches the client's TLS JA3 fingerprint; mode defaults to detect. Denylist blocks listed fingerprints; a non-empty allowlist blocks everything NOT listed.
redirect: {"url": string, "status": 301|302|307|308, "pattern": string (optional regex)}; restrict-ips: {"allow": [cidr...], "deny": [cidr...]}.
Values in add-headers, redirect and custom-response support ${cel-expression} interpolation (e.g. ${vars.tier}, ${conn.client_ip}), CRLF-sanitized.
priority int
Within-policy ordering (lower runs first).
enabled boolean
Whether the rule is active.

Response (201 Created)

The newly-created rule with assigned id and resolved config.

Errors

400 Bad Request
Strict decoding rejected unknown fields, invalid phase, invalid rule_type, missing name, schema-invalid config, or AI safety check flagged user-controlled content.
401 Unauthorized
Missing auth.
403 Forbidden
Plan doesn't include the entitlement for this rule_type.
404 Not Found
Endpoint or policy doesn't exist.
Request — rate limit
curl -X POST "https://api.ngris.com/v1/endpoints/<uuid>/policies/1/rules" \ -H "X-API-KEY: <your_api_key>" \ -H "Content-Type: application/json" \ -d '{ "phase": "on_http_request", "rule_type": "rate-limit", "name": "100rps-per-ip", "config": {"limit": 100, "window": 1, "scope": "ip"}, "priority": 0, "enabled": true }'
Request — redirect /old to /new
curl -X POST "https://api.ngris.com/v1/endpoints/<uuid>/policies/1/rules" \ -H "X-API-KEY: <your_api_key>" \ -H "Content-Type: application/json" \ -d '{ "phase": "on_http_request", "rule_type": "redirect", "name": "old-to-new", "expression": "request.path.startsWith(\"/old/\")", "config": {"url": "/new/", "status": 301}, "priority": 5, "enabled": true }'
Python
from ngris import Ngris client = Ngris(api_key="<your_api_key>") rule = client.traffic_policies.create_rule( "<endpoint_uuid>", "1", { "phase": "on_http_request", "rule_type": "rate-limit", "name": "100rps-per-ip", "config": {"limit": 100, "window": 1, "scope": "ip"}, "priority": 0, "enabled": True, }, )
Response — 201 Created
{ "id": 7, "policy_id": 1, "phase": "on_http_request", "rule_type": "rate-limit", "name": "100rps-per-ip", "config": {"limit": 100, "window": 1, "scope": "ip"}, "priority": 0, "enabled": true, "created_at": "2026-04-29T13:00:00Z", "updated_at": "2026-04-29T13:00:00Z" }
Iris