ScriptHaul API
Log inGet API key

Channel monitors

A monitor checks a channel or playlist's recent RSS uploads. With auto_fetch: true, it creates an ordinary single-video transcript job for each new video, so new source-language captions also enter your transcript library.

Monitoring itself costs 0 credits. Automatic jobs reserve and settle credits exactly like jobs you create yourself: one credit per successful cold transcript, zero for cached captions or failures. Use auto_fetch: false when you want notifications without automatic fetching.

Monitors belong to your account. Rotating or deleting the creating API key, or disconnecting its OAuth app, does not cancel or pause them. Automatic jobs use the account's daily caps rather than the creating credential's allowances; their admission includes the account's existing API-key usage and monitor work. Ordinary API-key requests retain their per-key limits and may continue when monitor admission reaches its account cap. Cancel a monitor explicitly when you want its future work to stop.

Create and manage a monitor

Method and endpointPurpose
POST /v1/monitorsCreate a monitor with exactly one channel or playlist.
GET /v1/monitorsList your account's monitors.
GET /v1/monitors/{id}Inspect status, pause reason, checks, and new-video counts.
DELETE /v1/monitors/{id}Cancel future monitoring.

Creation accepts auto_fetch, language, fallback, and webhook_url alongside the source. Use a UC channel ID, a channel URL containing one, or a handle. The server resolves non-ID channel inputs through channel resolution, including its separate daily allowance and 24-hour negative cache; warm resolutions also count. Direct UC IDs and playlist IDs need no resolution call. RSS checking never spends proxy or Data API capacity. A playlist can be identified by ID or public playlist URL.

Defaults are auto_fetch: false, language: en, and fallback: true. A new monitor records the current feed as its baseline; those existing uploads do not trigger historical automatic jobs. Its response is 201 with { ok: true, idempotent: false, monitor }, including its 32-character ID. Status is active, paused, or cancelled.

Send an optional Idempotency-Key header containing 1–128 nonblank characters and reuse it for retries of the same creation. The key belongs to the account, so another key on that account can replay it. A replay returns 200 with idempotent: true and the original monitor, even if you send different options or have since cancelled it; it does not resolve the source or read the feed again. Reusing a creation key never changes a watch's settings or restarts cancelled work.

Creating a source already watched by an active or paused monitor also returns that existing monitor unchanged with 200 and idempotent: true. If you supply a new idempotency key, it becomes another replay key for that monitor. Replay keys remain until the monitor record is deleted during account cleanup. To replace a cancelled watch, make a new creation with a fresh key. Both SDKs generate a fresh key once per create call and preserve it across automatic retries; supply your own to recover the same creation across separate calls.

Listing returns up to 1,000 account records. Non-cancelled monitors (active or paused) come first, then cancelled monitors; within each group, newer creation dates come first, with IDs in ascending order for equal dates. Creation requires an API key. The dashboard can also list, inspect, and cancel monitors through its signed-in session.

Free accounts may have one monitor; paid accounts may have 25. The normal API request rate applies to these endpoints. A monitor checks no more often than every 15 minutes on the background schedule. Its feed is cached for ten minutes and contains at most the newest 15 entries, so it is a recent-upload watch, not an audit of a channel's full history. Use library coverage and a bulk job for the back catalog.

A paused monitor keeps watching its feed at the same cadence and records new videos as events, so an upload during a pause is never lost; only automatic job creation and webhook delivery wait for the pause reason to clear (review 2026-09-06).

Each scheduled run keeps taking due monitors and pending monitor webhook deliveries until the respective drain has no work or reaches its 45-second limit. These drains share an operation budget with library backfill, and their starting priority rotates between ticks. Work left at a budget boundary stays eligible for a later run, so 15 minutes is a minimum check interval, not a delivery-time guarantee. In practice one run covers a few dozen checks, which keeps roughly forty to fifty active monitors on the 15-minute cadence; beyond that, checks spread out and the interval between them grows. Larger fleets will move polling onto queue-driven checks, a planned change.

If the baseline feed for a well-formed source ID returns 404, creation returns 404 NotFound with retryable: false and costs zero credits. Check the ID before retrying. An existing monitor records that same feed failure in last_error and consecutive_failures; its pause and daily recovery checks follow the policy below.

Pauses and recovery

A paused monitor exposes one of three pause_reason values:

Pause reasonWhat happenedHow it resumes
insufficient_creditsAvailable account credits cannot cover the next automatic job.A purchase restores enough available credits.
webhook_missingThe account webhook this monitor relies on was deleted. This includes a per-monitor URL using that webhook's signing secret.Configure an account webhook again.
feed_unavailableSix consecutive RSS checks failed.The feed is retried once a day; a successful check clears the feed pause.

last_error contains the latest bounded feed-error summary or null; consecutive_failures counts consecutive failed checks and returns to zero after a successful check. When causes overlap, the displayed reason prioritizes a missing webhook, then an unavailable feed, then insufficient credits. Clearing one cause does not bypass another.

Existing jobs keep their ordinary status, reservations, and outputs. Cancelling the monitor stops future checks; use job controls for work already created. Job reads, listings, manifests, and completion events identify automatic work with monitor_id; the dashboard labels it Monitor.

Events and delivery

channel.new_videos reports newly observed uploads and includes the monitor ID. When an automatic transcript job finishes, its job.completed event also carries the monitor ID. Configure a signed account webhook before setting an optional per-monitor webhook_url; the override changes the destination and keeps the same signing secret. Verify signatures and deduplicate delivery IDs as for every ScriptHaul webhook. Webhook bodies are never logged.

A monitor created without an account webhook can keep checking and automatically fetching. It starts using the account webhook when you configure one; deleting that webhook then pauses the monitor until a replacement is configured.

The dashboard shows each monitor's status, last check, new-video count, and cancellation control. MCP assistants use watch_channel(channel, auto_fetch?, language?) and list_monitors(). Enabling automatic fetching authorizes future jobs and their delivery charges; state that cost when setting it up.

Monitor lifecycle in four languages

These examples create a notification-only monitor, inspect it, and cancel it. Set auto_fetch to true only when you want future captions fetched automatically. Runnable versions are in the examples directory.

curl https://api.scripthaul.com/v1/monitors \
  -X POST -H "Authorization: Bearer $SCRIPTHAUL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channel":"UC4QobU6STFB0P71PMvOGN5A","auto_fetch":false}'
curl https://api.scripthaul.com/v1/monitors -H "Authorization: Bearer $SCRIPTHAUL_API_KEY"
# Set MONITOR_ID to the id returned by creation.
curl "https://api.scripthaul.com/v1/monitors/$MONITOR_ID" -H "Authorization: Bearer $SCRIPTHAUL_API_KEY"
curl "https://api.scripthaul.com/v1/monitors/$MONITOR_ID" -X DELETE -H "Authorization: Bearer $SCRIPTHAUL_API_KEY"
import json, os, urllib.request
base = "https://api.scripthaul.com/v1/monitors"
def call(method, endpoint, payload=None):
    data = None if payload is None else json.dumps(payload).encode()
    request = urllib.request.Request(endpoint, data=data, method=method)
    request.add_header("Authorization", f"Bearer {os.environ['SCRIPTHAUL_API_KEY']}")
    if data is not None: request.add_header("Content-Type", "application/json")
    with urllib.request.urlopen(request) as response: return json.load(response)
created = call("POST", base, {"channel": "UC4QobU6STFB0P71PMvOGN5A", "auto_fetch": False})
endpoint = base + "/" + created["monitor"]["id"]
print(call("GET", base))
print(call("GET", endpoint))
print(call("DELETE", endpoint))
const base = "https://api.scripthaul.com/v1/monitors";
async function call(method, url, body) {
  const response = await fetch(url, {
    method,
    headers: { Authorization: `Bearer ${process.env.SCRIPTHAUL_API_KEY}`, "Content-Type": "application/json" },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
const created = await call("POST", base, { channel: "UC4QobU6STFB0P71PMvOGN5A", auto_fetch: false });
const endpoint = `${base}/${created.monitor.id}`;
console.log(await call("GET", base));
console.log(await call("GET", endpoint));
console.log(await call("DELETE", endpoint));
base := "https://api.scripthaul.com/v1/monitors"
call := func(method, endpoint string, body io.Reader) map[string]any {
    req, _ := http.NewRequest(method, endpoint, body)
    req.Header.Set("Authorization", "Bearer "+os.Getenv("SCRIPTHAUL_API_KEY"))
    req.Header.Set("Content-Type", "application/json")
    response, err := http.DefaultClient.Do(req)
    if err != nil { log.Fatal(err) }
    defer response.Body.Close()
    var result map[string]any
    if err := json.NewDecoder(response.Body).Decode(&result); err != nil { log.Fatal(err) }
    return result
}
created := call("POST", base, strings.NewReader(`{"channel":"UC4QobU6STFB0P71PMvOGN5A","auto_fetch":false}`))
endpoint := base + "/" + created["monitor"].(map[string]any)["id"].(string)
fmt.Println(call("GET", base, nil))
fmt.Println(call("GET", endpoint, nil))
fmt.Println(call("DELETE", endpoint, nil))

With the JavaScript SDK:

const created = await client.monitors.create(
  { channel: "@TED", auto_fetch: false },
  { idempotencyKey: "ted-watch-2026-09-06" },
);
console.log(await client.monitors.list());
console.log(await client.monitors.get(created.monitor.id));
await client.monitors.cancel(created.monitor.id);

With the Python SDK:

created = client.monitors.create(channel="@TED", auto_fetch=False,
                                 idempotency_key="ted-watch-2026-09-06")
print(client.monitors.list())
print(client.monitors.get(created["monitor"]["id"]))
client.monitors.cancel(created["monitor"]["id"])

See credits and limits for account caps and MCP for assistant setup.