Monitoring API
The Monitoring API mirrors the Settings → Logs & Monitoring dashboard surface. Use it to create alerts from CI, audit fires from a script, or pipe history into a SIEM.
All endpoints require a JWT (dashboard session) and the settings:read or settings:write permission depending on the action. They are not callable with a regular workspace API key - alert management is a dashboard-tier operation.
All endpoints on this page are relative to the base URL: https://api-eu1.joryio.com - see API Overview.
Alert object
The canonical alert shape returned by every CRUD endpoint:
{
"id": "ak_01HXYZ...",
"organizationId": "org_...",
"workspaceId": "ws_...",
"name": "Server rejecting payloads",
"description": "Joryio returned 5xx on ingestion.",
"enabled": true,
"direction": "inbound",
"metric": "calls",
"codes": ["5xx"],
"mode": "absolute",
"op": ">",
"threshold": "100",
"duration": "5m",
"changeDir": null,
"changeKind": null,
"vsWindow": null,
"vsComparison": "previous",
"scopeApiKeyPrefix": null,
"scopeEndpoint": null,
"scopeWebhookUrl": null,
"scopeEventName": null,
"notifyChannel": "email",
"recipients": ["ops@your-company.com"],
"webhookUrl": null,
"webhookSecret": null,
"cooldown": "10m",
"status": "healthy",
"lastTriggeredAt": null,
"snoozedUntil": null,
"createdBy": "usr_...",
"createdAt": "2026-05-29T05:00:00.000Z",
"updatedAt": "2026-05-29T05:00:00.000Z"
}
Field reference
| Field | Type | Notes |
|---|---|---|
name | string (1–255) | Required. Shown in the dashboard and triggered emails. |
description | string (≤2000) | Optional. Shown in the email body. |
enabled | boolean | Defaults to true. When false, status becomes paused and the alert is skipped by the evaluator. |
direction | inbound | webhook | events | deliverability | Which stream to watch. events counts tracked customer events from the events table. deliverability watches message-health rates (% of sent). |
metric | calls | total_calls | rps | event_count | total_events | unique_users | bounceRate | hardBounceRate | softBounceRate | complaintRate | unsubscribeRate | deliveryRate | What to measure. The first three apply to inbound/webhook; the next three to events; the six *Rate metrics to deliverability. |
codes | string[] | HTTP status codes or buckets (2xx, 4xx, 5xx). Only meaningful when metric: calls. |
mode | absolute | change | Threshold model. deliverability is always absolute. |
op | > | < | Absolute mode only. Direction of the threshold. |
threshold | string (numeric) | Required. Stored as a numeric string. For deliverability, a percentage (e.g. "5" = 5%). |
duration | 1m | 5m | 10m | 30m | 1h | Absolute mode only. Sustained-breach window. For deliverability, the rate lookback window - use 1h | 4h | 1d | 7d. |
changeDir | increased | decreased | Change mode only. Direction of the change. |
changeKind | percent | value | Change mode only. Interpret threshold as a percentage or absolute count. |
vsWindow | 15m | 1h | 4h | 1d | 7d | Change mode only. Size of the comparison window. |
vsComparison | previous | last_week | average | same_weekday_median | Change mode only. Baseline to compare against. previous = the immediately-preceding window (default, and the least forgiving: a quiet Sunday reads as a drop against Saturday). last_week = the same window 7 days ago - seasonality-aware, but a single day, so an unusual one poisons the comparison. average = the MEAN of the same window over the last avgDays days (2-30, default 7); smooths noise, but one exceptional day lifts the baseline for that many days. same_weekday_median = the MEDIAN of the same window 7/14/21/28 days back - recommended for percent-drop alerts: seasonality-aware AND unmoved by a single campaign, article or Black Friday. Needs at least two of the four weeks to have data, otherwise the alert reports "not enough history yet" and does not fire. |
scopeApiKeyPrefix | string | null | Narrow to a specific API key. Use the key's visible prefix (e.g. jry_live_98f31a72). Inbound only. |
scopeEndpoint | string | null | Narrow to a specific route (e.g. /users/:id). Use the canonicalized form. Inbound only. |
scopeWebhookUrl | string | null | Narrow to a specific webhook URL. Query string is stripped before comparison. Outbound only. |
scopeEventName | string | null | Events direction. Which event_name to count. null = count all events. Ignored when metric: total_events. |
notifyChannel | email | webhook | How the alert is delivered. Defaults to email. |
recipients | string[] (1–20) | Email addresses notified on fire. Required when notifyChannel: email. |
webhookUrl | string | null | Destination URL for the POST. Required when notifyChannel: webhook. |
webhookSecret | string | null | Optional HMAC-SHA256 signing secret. When set, requests carry an X-Joryio-Signature header. |
cooldown | 5m | 10m | 30m | 1h | Minimum time between re-fires. |
status | healthy | triggered | snoozed | paused | Runtime state. Read-only from the API - use snooze/resume endpoints to transition. |
Endpoints
List alerts
GET /monitoring/alerts
Returns all alerts in the current workspace, newest first.
Response: 200 OK - MonitoringAlert[]
Get one alert
GET /monitoring/alerts/:id
Response: 200 OK - MonitoringAlert, or 404 if the ID isn't in this workspace.
Create alert
POST /monitoring/alerts
Content-Type: application/json
{
"name": "5xx error rate",
"direction": "inbound",
"metric": "calls",
"codes": ["5xx"],
"mode": "absolute",
"op": ">",
"threshold": "100",
"duration": "5m",
"recipients": ["ops@example.com"],
"cooldown": "10m"
}
The name, direction, metric, mode, and threshold fields are always required. Channel-specific and mode-specific fields are validated semantically:
- Email channel (
notifyChannel: email, the default) requires at least one entry inrecipients. - Webhook channel (
notifyChannel: webhook) requires a validhttp(s)webhookUrl;recipientsis optional.webhookSecretis optional. - Mode-specific fields are validated against
mode(e.g. you cannot setopin change mode;vsComparisononly applies in change mode). - For
direction: events, setscopeEventNameto count one event (or omit it to count all).metric: total_eventsalways counts all events regardless ofscopeEventName. - For
direction: deliverability, usemode: absolutewith one of the*Ratemetrics, anop, a percentagethreshold, and adurationof1h/4h/1d/7d(the rate lookback). Scope fields andcodesare ignored. If no messages were sent in the window, the alert does not fire.
Response: 201 Created - MonitoringAlert with id populated.
The new alert starts in status: healthy (or paused if enabled: false) and is picked up by the next evaluator tick (within 60 seconds).
Update alert
PATCH /monitoring/alerts/:id
Content-Type: application/json
{ "threshold": "200" }
All fields are optional. Send only the ones you want to change. Toggling enabled: false moves the alert to paused; toggling it back to true returns it to healthy (the next eval tick will retrigger it if the metric is still breaching).
Response: 200 OK - updated MonitoringAlert.
Delete alert
DELETE /monitoring/alerts/:id
Hard-deletes the alert. Its history rows are also cascade-deleted.
Response: 200 OK - { "ok": true }.
Snooze alert
POST /monitoring/alerts/:id/snooze
Content-Type: application/json
{ "window": "1h" }
window is optional. With a window, the alert moves to snoozed with snoozedUntil set, and auto-resumes when the time passes. Without a window, the alert moves to paused (indefinite).
window value | Behavior |
|---|---|
1h | Snooze for 1 hour. |
4h | Snooze for 4 hours. |
24h | Snooze for 24 hours. |
until_morning | Snooze until 09:00 server time the next day. |
| (omitted) | Pause indefinitely. |
Response: 200 OK - updated MonitoringAlert.
Resume alert
POST /monitoring/alerts/:id/resume
Clears snoozedUntil, sets enabled: true, and moves to status: healthy. The next evaluator tick re-checks the metric and may transition the alert to triggered immediately if it's still breaching.
Response: 200 OK - updated MonitoringAlert.
Duplicate alert
POST /monitoring/alerts/:id/duplicate
Creates a new alert with the same configuration. The copy's name is suffixed with (copy). The copy starts at status: healthy with lastTriggeredAt: null, regardless of the source's runtime state.
Response: 201 Created - the new MonitoringAlert.
Live preview
POST /monitoring/preview
Content-Type: application/json
{
"direction": "inbound",
"metric": "calls",
"codes": ["5xx"],
"mode": "absolute",
"op": ">",
"threshold": "100",
"duration": "5m"
}
Evaluates the supplied alert spec against current data without persisting anything. No alert row is created. No notification is sent. Use this to validate thresholds before creation.
The payload accepts the same evaluation fields as create - name, recipients, enabled, and cooldown are not needed and are ignored.
Response: 200 OK
{
"currentValue": 142,
"displayValue": "142",
"thresholdLabel": "> 100 in 5m",
"wouldFire": true
}
| Field | Meaning |
|---|---|
currentValue | The raw metric value from the metric source. |
displayValue | Human-formatted version of the value. For change mode, includes the direction (e.g. ↓ 92%). |
thresholdLabel | Human-readable threshold expression matching the rule. |
wouldFire | true if the rule would currently be in a triggered state. |
List history
GET /monitoring/history?alertId={id}&state={state}&limit={n}
Returns the audit log of fire/resolve transitions, newest first.
Query parameters:
| Param | Type | Default | Notes |
|---|---|---|---|
alertId | string | - | Restrict to a single alert. |
state | firing | resolved | snoozed | - | Restrict to one transition type. |
limit | integer | 200 | Cap on rows returned. Hard-capped at 1000. |
Response: 200 OK - MonitoringAlertHistoryEvent[]
[
{
"id": "ev_...",
"alertId": "ak_...",
"alertName": "Server rejecting payloads",
"metric": "Calls returning 5xx",
"valueAtFire": "184",
"valueLabel": "184",
"thresholdLabel": "> 100 in 5m",
"state": "firing",
"resolvedAt": null,
"recipients": ["ops@example.com"],
"notificationsSent": 1,
"firedAt": "2026-05-29T14:38:00.000Z"
}
]
History rows are snapshots - they capture the alert's name, metric, threshold, and recipients at the moment of the transition. Renaming or deleting the alert later doesn't change historical rows.
Helper endpoints
These power the dashboard's pickers and notification bell. All require settings:read.
List event names
GET /monitoring/event-names
Returns the workspace's distinct event names seen in the last 30 days, ordered by frequency (top 200). Drives the Events-direction scopeEventName picker so customers see their own event vocabulary.
Response: 200 OK
[
{ "name": "purchase_complete", "count": 18422 },
{ "name": "add_to_cart", "count": 51904 },
{ "name": "signup", "count": 1203 }
]
List webhook sources
GET /monitoring/webhook-sources
Returns the distinct webhook destination URLs configured on the live webhook nodes across the workspace's active and draft Journeys (archived canvases are excluded). Drives the outbound-direction scopeWebhookUrl picker.
Response: 200 OK
[
{
"url": "https://hooks.your-company.com/joryio",
"canvasId": "cv_...",
"canvasName": "Win-back flow",
"nodeId": "node_...",
"nodeLabel": "Notify CRM"
}
]
Recent notifications
GET /monitoring/notifications/recent
Returns the most recent alert fires across the workspace, for the dashboard header bell.
Unread fire count
GET /monitoring/notifications/unread-count
Response: 200 OK - { "count": 3 }. The badge count for the header bell.
Webhook notification payload
When an alert with notifyChannel: webhook transitions, Joryio sends an HTTP POST to webhookUrl. Unlike email (which only fires on triggered), the webhook channel posts on both triggered and resolved so the receiver can match incidents end-to-end.
Request:
POST {webhookUrl}
Content-Type: application/json
User-Agent: Joryio-Monitoring/1.0
X-Joryio-Signature: {hex hmac-sha256, only when a signing secret is set}
{
"alert": "Server rejecting payloads",
"status": "triggered",
"metric": "Calls returning 5xx",
"value": 184,
"displayValue": "184",
"accountName": "Acme Inc",
"workspaceName": "Production",
"firedAt": "2026-05-29T14:38:00.000Z"
}
| Field | Type | Notes |
|---|---|---|
alert | string | The alert name. |
status | triggered | resolved | Which transition this POST represents. |
metric | string | Human-readable label of the metric being watched. |
accountName | string | The account (organization) the alert belongs to. |
workspaceName | string | The workspace the alert belongs to. |
value | number | The raw metric value at the transition. |
displayValue | string | Human-formatted value (for change mode, includes direction, e.g. ↓ 92%). |
firedAt | string (ISO 8601) | When the transition occurred. |
Signature verification. When webhookSecret is set, Joryio computes HMAC-SHA256(rawBody) keyed by the secret and sends it as a lowercase hex string (no prefix) in X-Joryio-Signature. Recompute it over the exact raw request body and compare with a constant-time check before trusting the payload.
Delivery semantics. Joryio expects a 2xx response. On failure it retries up to 3 times with exponential backoff (≈0.5s, 1s, 2s); each attempt has a 10s timeout. After 3 failures the delivery is dropped (the state transition itself still persists to History).
SSRF protection. The URL is validated when the alert is created/updated, and re-validated at send time (DNS can rebind between write and fire) - a delivery to a private, link-local, or cloud-metadata address is blocked. Redirects are not followed (maxRedirects: 0), since a 3xx to an internal address would bypass that check.
Metric source
The metric sources live in the analytics event store and are populated automatically. They're the same sources the dashboard charts read from, so the alert engine and any custom analytics share a single source of truth.
The inbound API request log and the outbound webhook delivery log are each per-organization toggleable and have a configurable retention window (7–365 days, default 90), managed by Joryio staff in the admin console. When a stream is disabled for an org, no rows are written and alerts in that direction stop evaluating. Retention is enforced per-row via a delete_at column (existing rows were backfilled with ts + 90d).
api_request_logs
One row per API-key-authenticated request to the Joryio REST API.
| Column | Type | Notes |
|---|---|---|
ts | DateTime | UTC timestamp of the request completion. |
organization_id | String | Owning organization. |
workspace_id | String | Owning workspace. |
api_key_id | Nullable(String) | UUID of the API key row. |
api_key_prefix | String | Public key prefix (e.g. jry_live_98f31a72). |
method | LowCardinality(String) | HTTP verb. |
endpoint | LowCardinality(String) | Canonicalized path. UUIDs and long numeric segments are replaced with :id. |
raw_path | String | Original path with query string, capped at 512 chars. |
status | UInt16 | HTTP response status. |
duration_ms | UInt32 | Latency in milliseconds. |
request_ip | Nullable(String) | Source IP (after X-Forwarded-For resolution). |
- Partition: monthly (
toYYYYMM(ts)). - Retention: per-row TTL via a
delete_atcolumn, set tots + retentionDaysat write time. Default 90 days, configurable per org (7–365). Logging can be disabled per org. - What's excluded: JWT-authenticated dashboard traffic; health-check paths (
/health,/metrics).
webhook_delivery_logs
One row per outbound webhook delivery attempt (success or failure).
| Column | Type | Notes |
|---|---|---|
ts | DateTime | UTC timestamp of the attempt completion. |
organization_id | String | Owning organization. |
workspace_id | String | Owning workspace. |
canvas_id | Nullable(String) | Source User Journey ID. |
execution_id | Nullable(String) | Source Journey execution ID. |
node_id | Nullable(String) | Source webhook node ID. |
url | String | Full destination URL. |
url_canonical | String | URL with query string stripped and trailing slash removed. Used by alert filters. |
method | LowCardinality(String) | HTTP verb. |
status | UInt16 | Response status. 0 for transport-level errors (timeout, DNS failure, connection refused). |
duration_ms | UInt32 | Latency in milliseconds. |
attempt | UInt8 | Attempt number (1 on first try). |
error | Nullable(String) | Error message on non-2xx responses. |
- Partition: monthly.
- Retention: per-row TTL via
delete_at. Default 90 days, configurable per org (7–365). Logging can be disabled per org. - What's excluded: Webhooks fired before this feature shipped (older queued jobs lack the tenant metadata required to attribute them).
events
The Events direction reads the existing events table - the same table every tracked customer event lands in - rather than a dedicated monitoring stream. Two aggregations are used:
| Metric | Query |
|---|---|
event_count / total_events | count() over (organization_id, workspace_id, [event_name], time range). |
unique_users | uniqExact(user_id) over the same filter. |
scopeEventName adds an event_name = … predicate; omitting it counts across all event names. This watches the stored event count - a request Joryio accepts but whose payload it rejects shows up under Inbound API, not here.
Error responses
| Status | When |
|---|---|
400 | Validation failed - required field missing, mode/op mismatch, etc. The response body lists the offending field(s). |
401 | Missing or invalid JWT. |
403 | JWT is valid but lacks settings:read (list/get/history/preview) or settings:write (create/update/delete/snooze/resume/duplicate). |
404 | Alert ID not found in the current workspace. |
Sample integration
Creating, previewing, and snoozing an alert from a shell:
TOKEN=eyJ...
BASE=https://api-eu1.joryio.com
# 1) Preview before saving - would it fire right now?
curl -sX POST "$BASE/monitoring/preview" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"direction":"inbound","metric":"calls","codes":["5xx"],"mode":"absolute","op":">","threshold":"100","duration":"5m"}'
# → {"currentValue":42,"displayValue":"42","thresholdLabel":"> 100 in 5m","wouldFire":false}
# 2) Looks good - create the alert.
ALERT_ID=$(curl -sX POST "$BASE/monitoring/alerts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"5xx error rate","direction":"inbound","metric":"calls","codes":["5xx"],"mode":"absolute","op":">","threshold":"100","duration":"5m","recipients":["ops@example.com"]}' \
| jq -r .id)
# 3) Snooze it for 4 hours during a known maintenance window.
curl -sX POST "$BASE/monitoring/alerts/$ALERT_ID/snooze" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"window":"4h"}'