Skip to main content

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

FieldTypeNotes
namestring (1–255)Required. Shown in the dashboard and triggered emails.
descriptionstring (≤2000)Optional. Shown in the email body.
enabledbooleanDefaults to true. When false, status becomes paused and the alert is skipped by the evaluator.
directioninbound | webhook | events | deliverabilityWhich stream to watch. events counts tracked customer events from the events table. deliverability watches message-health rates (% of sent).
metriccalls | total_calls | rps | event_count | total_events | unique_users | bounceRate | hardBounceRate | softBounceRate | complaintRate | unsubscribeRate | deliveryRateWhat to measure. The first three apply to inbound/webhook; the next three to events; the six *Rate metrics to deliverability.
codesstring[]HTTP status codes or buckets (2xx, 4xx, 5xx). Only meaningful when metric: calls.
modeabsolute | changeThreshold model. deliverability is always absolute.
op> | <Absolute mode only. Direction of the threshold.
thresholdstring (numeric)Required. Stored as a numeric string. For deliverability, a percentage (e.g. "5" = 5%).
duration1m | 5m | 10m | 30m | 1hAbsolute mode only. Sustained-breach window. For deliverability, the rate lookback window - use 1h | 4h | 1d | 7d.
changeDirincreased | decreasedChange mode only. Direction of the change.
changeKindpercent | valueChange mode only. Interpret threshold as a percentage or absolute count.
vsWindow15m | 1h | 4h | 1d | 7dChange mode only. Size of the comparison window.
vsComparisonprevious | last_week | average | same_weekday_medianChange 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.
scopeApiKeyPrefixstring | nullNarrow to a specific API key. Use the key's visible prefix (e.g. jry_live_98f31a72). Inbound only.
scopeEndpointstring | nullNarrow to a specific route (e.g. /users/:id). Use the canonicalized form. Inbound only.
scopeWebhookUrlstring | nullNarrow to a specific webhook URL. Query string is stripped before comparison. Outbound only.
scopeEventNamestring | nullEvents direction. Which event_name to count. null = count all events. Ignored when metric: total_events.
notifyChannelemail | webhookHow the alert is delivered. Defaults to email.
recipientsstring[] (1–20)Email addresses notified on fire. Required when notifyChannel: email.
webhookUrlstring | nullDestination URL for the POST. Required when notifyChannel: webhook.
webhookSecretstring | nullOptional HMAC-SHA256 signing secret. When set, requests carry an X-Joryio-Signature header.
cooldown5m | 10m | 30m | 1hMinimum time between re-fires.
statushealthy | triggered | snoozed | pausedRuntime 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 in recipients.
  • Webhook channel (notifyChannel: webhook) requires a valid http(s) webhookUrl; recipients is optional. webhookSecret is optional.
  • Mode-specific fields are validated against mode (e.g. you cannot set op in change mode; vsComparison only applies in change mode).
  • For direction: events, set scopeEventName to count one event (or omit it to count all). metric: total_events always counts all events regardless of scopeEventName.
  • For direction: deliverability, use mode: absolute with one of the *Rate metrics, an op, a percentage threshold, and a duration of 1h/4h/1d/7d (the rate lookback). Scope fields and codes are 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 valueBehavior
1hSnooze for 1 hour.
4hSnooze for 4 hours.
24hSnooze for 24 hours.
until_morningSnooze 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
}
FieldMeaning
currentValueThe raw metric value from the metric source.
displayValueHuman-formatted version of the value. For change mode, includes the direction (e.g. ↓ 92%).
thresholdLabelHuman-readable threshold expression matching the rule.
wouldFiretrue 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:

ParamTypeDefaultNotes
alertIdstring-Restrict to a single alert.
statefiring | resolved | snoozed-Restrict to one transition type.
limitinteger200Cap 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"
}
FieldTypeNotes
alertstringThe alert name.
statustriggered | resolvedWhich transition this POST represents.
metricstringHuman-readable label of the metric being watched.
accountNamestringThe account (organization) the alert belongs to.
workspaceNamestringThe workspace the alert belongs to.
valuenumberThe raw metric value at the transition.
displayValuestringHuman-formatted value (for change mode, includes direction, e.g. ↓ 92%).
firedAtstring (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.

Per-org logging controls

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.

ColumnTypeNotes
tsDateTimeUTC timestamp of the request completion.
organization_idStringOwning organization.
workspace_idStringOwning workspace.
api_key_idNullable(String)UUID of the API key row.
api_key_prefixStringPublic key prefix (e.g. jry_live_98f31a72).
methodLowCardinality(String)HTTP verb.
endpointLowCardinality(String)Canonicalized path. UUIDs and long numeric segments are replaced with :id.
raw_pathStringOriginal path with query string, capped at 512 chars.
statusUInt16HTTP response status.
duration_msUInt32Latency in milliseconds.
request_ipNullable(String)Source IP (after X-Forwarded-For resolution).
  • Partition: monthly (toYYYYMM(ts)).
  • Retention: per-row TTL via a delete_at column, set to ts + retentionDays at 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).

ColumnTypeNotes
tsDateTimeUTC timestamp of the attempt completion.
organization_idStringOwning organization.
workspace_idStringOwning workspace.
canvas_idNullable(String)Source User Journey ID.
execution_idNullable(String)Source Journey execution ID.
node_idNullable(String)Source webhook node ID.
urlStringFull destination URL.
url_canonicalStringURL with query string stripped and trailing slash removed. Used by alert filters.
methodLowCardinality(String)HTTP verb.
statusUInt16Response status. 0 for transport-level errors (timeout, DNS failure, connection refused).
duration_msUInt32Latency in milliseconds.
attemptUInt8Attempt number (1 on first try).
errorNullable(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:

MetricQuery
event_count / total_eventscount() over (organization_id, workspace_id, [event_name], time range).
unique_usersuniqExact(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

StatusWhen
400Validation failed - required field missing, mode/op mismatch, etc. The response body lists the offending field(s).
401Missing or invalid JWT.
403JWT is valid but lacks settings:read (list/get/history/preview) or settings:write (create/update/delete/snooze/resume/duplicate).
404Alert 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"}'