Monitoring API examples
This guide builds a read-only scheduled health check from the documented Control API. It detects failed Workflow runs, Instant Trigger errors or fallback, active channel alerts, and result-window overflow without retrieving payload previews.
Required configuration
Run the example with Node.js 18 or later. It uses only built-in fetch and node:crypto.
| Variable | Required | Purpose |
| --- | --- | --- |
| FASTHOOK_API_KEY | Yes | Project API key for the monitored team. |
| FASTHOOK_TEAM_ID | Yes | Team sent as x-team-id. Must match the project key. |
| FASTHOOK_API_BASE | No | Defaults to https://api.fasthook.io. |
| FASTHOOK_WINDOW_FROM | No | Activity start, defaults to now-15m. |
| FASTHOOK_ALERT_WEBHOOK_URL | No | Optional Slack-compatible webhook that receives a summary without payloads or error messages. |
Store these values in the scheduler’s secret manager. Do not place the API key or alert webhook in the script, command line, repository, logs, or report output.
Dependency-free health check
Save this as fasthook-health-check.mjs:
import { createHash } from "node:crypto";
const API_BASE = (process.env.FASTHOOK_API_BASE || "https://api.fasthook.io").replace(/\/$/, "");
const API_KEY = process.env.FASTHOOK_API_KEY;
const TEAM_ID = process.env.FASTHOOK_TEAM_ID;
const WINDOW_FROM = process.env.FASTHOOK_WINDOW_FROM || "now-15m";
const ALERT_WEBHOOK_URL = process.env.FASTHOOK_ALERT_WEBHOOK_URL || "";
if (!API_KEY || !TEAM_ID) {
console.error("FASTHOOK_API_KEY and FASTHOOK_TEAM_ID are required");
process.exit(3);
}
async function fasthook(path) {
const response = await fetch(`${API_BASE}${path}`, {
headers: {
authorization: `Bearer ${API_KEY}`,
"x-team-id": TEAM_ID,
accept: "application/json"
},
signal: AbortSignal.timeout(20_000)
});
if (!response.ok) throw new Error(`FastHook ${path} returned HTTP ${response.status}`);
return response.json();
}
function finding(severity, kind, id, detail = {}) {
return { severity, kind, id, ...detail };
}
async function main() {
const activityQuery = new URLSearchParams({
status: "failed",
from: WINDOW_FROM,
to: "now",
limit: "100"
});
const [channelResponse, alertResponse, activityResponse] = await Promise.all([
fasthook("/v1/sources/channels"),
fasthook("/v1/sources/channel-alerts?status=active&limit=100"),
fasthook(`/v1/workflow-activity?${activityQuery}`)
]);
const channels = Array.isArray(channelResponse.models) ? channelResponse.models : [];
const alerts = Array.isArray(alertResponse.models) ? alertResponse.models : [];
const runs = Array.isArray(activityResponse.models) ? activityResponse.models : [];
const findings = [];
for (const source of channels) {
const status = source?.channel?.status;
if (status === "error") {
findings.push(finding("critical", "trigger_channel_error", source.source_id, {
provider_key: source.provider_key || null
}));
} else if (status === "fallback") {
findings.push(finding("warning", "trigger_polling_fallback", source.source_id, {
provider_key: source.provider_key || null
}));
} else if (status === "expiring" || status === "connecting") {
findings.push(finding("warning", `trigger_channel_${status}`, source.source_id, {
provider_key: source.provider_key || null
}));
}
}
for (const alert of alerts) {
findings.push(finding("critical", "active_trigger_alert", alert.id, {
source_id: alert.source_id || null,
code: alert.code || null,
occurrences: Number(alert.occurrences || 0)
}));
}
for (const run of runs) {
findings.push(finding("critical", "failed_workflow_run", run.id, {
workflow_id: run.workflow_id || null,
source_id: run.source?.id || null,
request_id: run.request_id || null,
error_code: run.error_code || null
}));
}
const activityTotal = Number(activityResponse.total || 0);
if (activityTotal > runs.length) {
findings.push(finding("critical", "activity_window_overflow", "workflow-activity", {
returned: runs.length,
total: activityTotal
}));
}
if (channels.length >= 250) {
findings.push(finding("warning", "channel_inventory_may_be_truncated", "sources-channels", {
returned: channels.length
}));
}
findings.sort((a, b) => `${a.kind}:${a.id}`.localeCompare(`${b.kind}:${b.id}`));
const fingerprint = createHash("sha256")
.update(JSON.stringify(findings.map(({ severity, kind, id }) => ({ severity, kind, id }))))
.digest("hex").slice(0, 16);
const critical = findings.filter((item) => item.severity === "critical").length;
const warning = findings.filter((item) => item.severity === "warning").length;
const report = {
schema_version: 1,
generated_at: new Date().toISOString(),
team_id: TEAM_ID,
window: { from: WINDOW_FROM, to: "now" },
status: critical ? "critical" : warning ? "warning" : "healthy",
fingerprint,
counts: {
critical,
warning,
failed_runs: runs.length,
active_trigger_alerts: alerts.length,
instant_sources_returned: channels.length
},
findings
};
console.log(JSON.stringify(report));
if (ALERT_WEBHOOK_URL && findings.length) {
const response = await fetch(ALERT_WEBHOOK_URL, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
text: `FastHook health ${report.status}: ${critical} critical, ${warning} warning; fingerprint ${fingerprint}`
}),
signal: AbortSignal.timeout(10_000)
});
if (!response.ok) throw new Error(`Alert webhook returned HTTP ${response.status}`);
}
process.exitCode = critical ? 2 : warning ? 1 : 0;
}
main().catch((error) => {
console.error(JSON.stringify({
schema_version: 1,
generated_at: new Date().toISOString(),
status: "monitor_error",
error: error instanceof Error ? error.message : "unknown_monitor_error"
}));
process.exitCode = 3;
});The script excludes provider error messages, payloads, Step outputs, account metadata, and credentials from the optional escalation message.
| Exit code | Meaning |
| --- | --- |
| 0 | No finding in the checked window. |
| 1 | Warning: connecting, expiring, fallback, or possible channel-inventory truncation. |
| 2 | Critical: failed run, error channel, active Trigger alert, or Activity overflow. |
| 3 | The monitor itself failed or was misconfigured. |
Treat monitor_error as actionable. A monitor that cannot authenticate or query FastHook must not report the system as healthy.
Schedule it safely
Run the script every 5–15 minutes from a scheduler that injects secrets safely, prevents overlap, enforces a timeout, captures one JSON report per execution, and alerts on exit codes 2 and 3.
Use an Activity window slightly wider than the schedule interval so a short scheduler delay does not create a gap. That overlap can repeat the same finding. Deduplicate notifications by fingerprint in the alert receiver or apply its cooldown policy.
This example is read-only. Do not automatically retry a channel, change delivery mode, rotate credentials, pause Workflows, or replay events from a basic health-check finding.
Focused failure-window queries
Failed runs for one Source during the last hour:
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer $FASTHOOK_API_KEY" \
--header "x-team-id: $FASTHOOK_TEAM_ID" \
"$FASTHOOK_API_BASE/v1/workflow-activity?status=failed&source_id=src_xxx&from=now-1h&to=now&limit=100"Active channel alerts:
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer $FASTHOOK_API_KEY" \
--header "x-team-id: $FASTHOOK_TEAM_ID" \
"$FASTHOOK_API_BASE/v1/sources/channel-alerts?status=active&limit=100"Instant Sources currently in fallback:
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer $FASTHOOK_API_KEY" \
--header "x-team-id: $FASTHOOK_TEAM_ID" \
"$FASTHOOK_API_BASE/v1/sources/channels?status=fallback"Always compare workflow-activity.total with count. If total is larger, split the time range; do not silently report only the first 100 rows. The channel inventory endpoint returns at most 250 rows and has no total, so a response of exactly 250 is potentially incomplete.
Escalation payload contract
Forward identifiers and counts rather than raw provider errors or customer data:
{
"schema_version": 1,
"event_type": "fasthook.health.findings",
"generated_at": "2026-08-27T12:00:00.000Z",
"team_id": "tm_example",
"window": { "from": "now-15m", "to": "now" },
"status": "critical",
"fingerprint": "da73f11d00ce4b5b",
"counts": { "critical": 2, "warning": 1 },
"references": {
"workflow_ids": ["wfl_example"],
"source_ids": ["src_example"],
"run_ids": ["wfr_example"],
"request_ids": ["req_example"]
}
}The receiver can enrich identifiers through authenticated API calls during triage. Continue with Reconciliation reports and the incident response runbook.