Stats API
Cleanuparr exposes a stats endpoint with aggregated application statistics, designed for integration with dashboard tools like Homepage, Homarr, or any tool that can consume JSON APIs.
/api/stats (v1) is deprecated. Use /api/v2/stats instead. The v1 endpoint still works but returns Deprecation, Sunset, and Link response headers. It stops responding on 1 September 2026 00:00 UTC: requests on or after that time return 404 Not Found. See the Deprecated: v1 section at the bottom of this page for the migration.
Authentication
API Key
This endpoint requires authentication. You can authenticate using your API key in one of two ways:
Header (recommended):
curl -H "X-Api-Key: YOUR_API_KEY" http://localhost:11011/api/v2/stats
Query parameter:
curl http://localhost:11011/api/v2/stats?apikey=YOUR_API_KEY
You can find your API key in the Cleanuparr UI under Account Settings.
JWT Bearer tokens are also supported for browser-based access.
Endpoint
GET /api/v2/stats
Returns aggregated statistics for a timeframe. Every section except health is scoped to the timeframe; health is a cached, point-in-time gauge. By default the timeframe-scoped sections count only live activity: dry-run activity is excluded unless you opt in.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
hours | integer | 168 | Timeframe in hours (range: 1-8760) |
includeDryRun | boolean | false | When true, dry-run activity is included in the timeframe-scoped sections |
Examples:
# Last 7 days (default)
curl -H "X-Api-Key: YOUR_API_KEY" http://localhost:11011/api/v2/stats
# Last 24 hours
curl -H "X-Api-Key: YOUR_API_KEY" http://localhost:11011/api/v2/stats?hours=24
# Last 30 days
curl -H "X-Api-Key: YOUR_API_KEY" http://localhost:11011/api/v2/stats?hours=720
# Include dry-run activity
curl -H "X-Api-Key: YOUR_API_KEY" "http://localhost:11011/api/v2/stats?includeDryRun=true"
If BASE_PATH is set, include it in the URL: http://localhost:11011/cleanuparr/api/v2/stats
Response Reference
Full example
{
"events": {
"total": 60,
"byType": {
"FailedImportStrike": 5,
"StalledStrike": 12,
"DownloadingMetadataStrike": 2,
"SlowSpeedStrike": 3,
"SlowTimeStrike": 1,
"DeadTorrentStrike": 1,
"QueueItemDeleted": 13,
"DownloadCleaned": 6,
"CategoryChanged": 4,
"DownloadMarkedForDeletion": 2,
"SearchTriggered": 8,
"StrikeReset": 3
},
"bySeverity": {
"Information": 30,
"Warning": 20,
"Important": 7,
"Error": 3
}
},
"strikes": {
"total": 24,
"byType": {
"Stalled": 12,
"DownloadingMetadata": 2,
"FailedImport": 5,
"SlowSpeed": 3,
"SlowTime": 1,
"DeadTorrent": 1
},
"recovered": 3
},
"removals": {
"total": 13,
"byReason": {
"Stalled": 2,
"FailedImport": 2,
"DownloadingMetadata": 1,
"SlowSpeed": 1,
"SlowTime": 1,
"AllFilesSkipped": 1,
"AllFilesSkippedByQBit": 1,
"AllFilesBlocked": 3,
"AtLeastOneFileBlocked": 1
}
},
"cleaned": {
"total": 6,
"byReason": {
"MaxRatioReached": 4,
"MaxSeedTimeReached": 2
}
},
"searches": {
"total": 8,
"completed": 6,
"failed": 1,
"grabbed": 5,
"byReason": {
"Missing": 4,
"QualityCutoffNotMet": 2,
"CustomFormatScoreBelowCutoff": 1,
"Replacement": 1
}
},
"jobs": {
"total": 168,
"completed": 165,
"failed": 3,
"byType": {
"QueueCleaner": {
"total": 48,
"completed": 46,
"failed": 2,
"lastRunAt": "2025-01-15T12:00:00Z",
"nextRunAt": "2025-01-15T12:30:00Z"
},
"MalwareBlocker": {
"total": 48,
"completed": 48,
"failed": 0,
"lastRunAt": "2025-01-15T12:00:00Z",
"nextRunAt": "2025-01-15T12:30:00Z"
},
"DownloadCleaner": {
"total": 24,
"completed": 24,
"failed": 0,
"lastRunAt": "2025-01-15T12:00:00Z",
"nextRunAt": "2025-01-15T13:00:00Z"
},
"BlacklistSynchronizer": {
"total": 12,
"completed": 12,
"failed": 0,
"lastRunAt": "2025-01-15T12:00:00Z",
"nextRunAt": "2025-01-15T14:00:00Z"
},
"Seeker": {
"total": 30,
"completed": 29,
"failed": 1,
"lastRunAt": "2025-01-15T12:00:00Z",
"nextRunAt": "2025-01-15T12:15:00Z"
},
"CustomFormatScoreSyncer": {
"total": 6,
"completed": 6,
"failed": 0,
"lastRunAt": "2025-01-15T12:00:00Z",
"nextRunAt": "2025-01-15T18:00:00Z"
}
}
},
"health": {
"downloadClients": [
{
"id": "...",
"name": "qBittorrent",
"type": "qBittorrent",
"isHealthy": true,
"lastChecked": "2025-01-15T12:00:00Z",
"responseTimeMs": 45.2,
"errorMessage": null
}
],
"arrInstances": [
{
"id": "...",
"name": "Sonarr",
"type": "Sonarr",
"isHealthy": true,
"lastChecked": "2025-01-15T12:00:00Z",
"errorMessage": null
}
]
},
"timeframeHours": 168,
"generatedAt": "2025-01-15T12:00:00Z"
}
The example lists every possible key for each breakdown for reference. In a real response, breakdown keys (byType, bySeverity, byReason) are PascalCase enum names and only entries with activity are present. A reason or type with zero events is omitted rather than sent as 0.
events
The raw event audit for the timeframe. The higher-level sections (strikes, removals, cleaned, searches) are ergonomic roll-ups derived from these same events.
- total: Total number of events in the timeframe, equal to the sum of
byType. - byType: Events grouped by event type. Possible types:
FailedImportStrike: A strike given because the download kept failing to import into the arr app.StalledStrike: A strike given because the download stopped making progress.DownloadingMetadataStrike: A strike given because the torrent was stuck fetching its metadata (qBittorrent only).SlowSpeedStrike: A strike given because the download stayed below the minimum speed.SlowTimeStrike: A strike given because the download's estimated time was over the allowed maximum.DeadTorrentStrike: A strike given because the torrent had no seeders (dead).QueueItemDeleted: A download was removed from the *arr queue.DownloadCleaned: A finished download was removed after meeting its seeding goals.CategoryChanged: A download's category or tag was changed.DownloadMarkedForDeletion: A download was flagged for removal, just before it is deleted.SearchTriggered: The Seeker started a search for a missing or upgradeable item.StrikeReset: A download recovered, so its accumulated strikes were cleared.
- bySeverity: Events grouped by severity. Possible severities:
InformationWarningImportantError
strikes
Strike activity in the timeframe.
- total: Total strikes issued in the timeframe, equal to the sum of
byType. - byType: Strikes grouped by type. Possible types:
Stalled: Strikes for downloads that stopped making progress.DownloadingMetadata: Strikes for downloads stuck fetching torrent metadata.FailedImport: Strikes for downloads that repeatedly failed to import.SlowSpeed: Strikes for downloads staying below the minimum speed.SlowTime: Strikes for downloads whose estimated time exceeded the maximum.DeadTorrent: Strikes for torrents with no seeders.
- recovered: Downloads that recovered and had their strikes reset.
removals
Downloads removed from the queue in the timeframe, broken down by reason. This is the single source of truth for removals.
- total: Total removals in the timeframe, across all reasons.
- byReason: Removals grouped by reason. Possible reasons:
Stalled: Removed for being stalled (not downloading).FailedImport: Removed for being stuck atfailed to importin the *arrs.DownloadingMetadata: Removed for being stuck fetching metadata (qBittorrent only).SlowSpeed: Removed for staying below the minimum speed.SlowTime: Removed because its estimated time exceeded the maximum allowed.AllFilesSkipped: Removed because every file in it was marked asunwanted/skipped/do not download.AllFilesSkippedByQBit: Removed because qBittorrent marked every file asdo not download, so nothing was ever downloaded.AllFilesBlocked: Removed because every file matched the malware blocklist.AtLeastOneFileBlocked: Removed because at least one file matched the malware blocklist.
The number of Malware blocked can be derived from the sum of the AllFilesBlocked and AtLeastOneFileBlocked.
cleaned
Downloads cleaned by the Download Cleaner in the timeframe.
- total: Total cleaned downloads in the timeframe.
- byReason: Cleaned downloads grouped by reason. Possible reasons:
MaxRatioReached: Cleaned after reaching its maximum seed ratio and minimum seed time.MaxSeedTimeReached: Cleaned after reaching its maximum seeding time.
searches
Seeker search activity in the timeframe. A single record is tracked per search and updated as it completes.
- total: Total searches started in the timeframe, regardless of their current status.
- completed: Searches that finished successfully.
- failed: Searches that did not succeed, counting both failed and timed-out searches.
- grabbed: Total items grabbed as a result of the triggered searches.
- byReason: Searches grouped by why they were started. Possible reasons:
Missing: The item had no file at all, so a search was started.QualityCutoffNotMet: The item's quality was below the configured cutoff.CustomFormatScoreBelowCutoff: The item's custom format score was below the configured cutoff.Replacement: A search started to replace a download that Cleanuparr removed.
jobs
Scheduled job run outcomes in the timeframe. Job runs are recorded regardless of dry-run mode.
- total: Total job runs in the timeframe across all job types. completed and failed break that total down by outcome.
- byType: Per-job-type stats. Each entry contains its own total (runs of that job type), completed, failed, lastRunAt (most recent run), and nextRunAt (next scheduled run). Possible job types:
QueueCleaner: Watches the *arr queues and removes downloads caught by the strike rules.MalwareBlocker: Scans queued downloads and removes ones containing blocked or malicious files.DownloadCleaner: Removes finished downloads once they meet their seeding goals.BlacklistSynchronizer: Refreshes the qBittorrent malware blocklist patterns from their source.Seeker: Searches for missing items and quality upgrades.CustomFormatScoreSyncer: Syncs custom format scores from the *arr apps.
health
Current health of all configured download clients and arr instances.
- downloadClients: Health of each enabled download client.
- arrInstances: Health of each enabled arr instance: Sonarr, Radarr, Lidarr, Readarr, Whisparr.
health is a gauge, not a timeframe-scoped metric. It reflects the last background check (roughly every 5 minutes) and ignores the hours parameter, keeping the endpoint fast for frequent polling.
timeframeHours & generatedAt
- timeframeHours: The timeframe the response covers, echoing the requested
hoursafter clamping. - generatedAt: When the response was generated (UTC).
Timeline
GET /api/v2/stats/timeline
Returns a bucketed time series for a single metric, useful for charts.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
metric | string | events | One of strikesIssued, recovered, removed, malwareBlocked, events |
hours | integer | 720 | Timeframe in hours (range: 1-8760) |
bucket | string | (auto) | Bucket size: hour, day, week, month. When omitted, defaults to hour for timeframes up to 24h and day otherwise |
includeDryRun | boolean | false | When true, dry-run activity is included |
Example:
curl -H "X-Api-Key: YOUR_API_KEY" \
"http://localhost:11011/api/v2/stats/timeline?metric=removed&hours=8760&bucket=month"
Response:
[
{ "date": "2025-01-01T00:00:00Z", "count": 12 },
{ "date": "2025-02-01T00:00:00Z", "count": 8 },
{ "date": "2025-03-01T00:00:00Z", "count": 15 }
]
- date: Start of the bucket (UTC). Granularity matches the requested
bucket. - count: Count of the metric within that bucket.
An unsupported bucket value returns 400 Bad Request.
Dashboard Integration
Homepage Example
Homepage can use the customapi widget to display Cleanuparr stats:
- Cleanuparr:
icon: cleanuparr.png
href: http://localhost:11011
description: Arr Cleanup Service
widget:
type: customapi
url: http://localhost:11011/api/v2/stats
headers:
X-Api-Key: YOUR_API_KEY
mappings:
- field: events.total
label: Events (7d)
format: number
- field: strikes.total
label: Strikes issued
format: number
- field: removals.total
label: Removed
format: number
- field: cleaned.total
label: Cleaned
format: number
You can reference any field in the response using dot notation (e.g., jobs.byType.QueueCleaner.failed, removals.byReason.AllFilesBlocked).
To change the timeframe, add a query parameter to the URL: http://localhost:11011/api/v2/stats?hours=720 for a 30-day view.
Other Dashboard Tools
Any tool that can fetch and parse JSON can use this endpoint. The response is a flat, predictable JSON structure designed for easy field mapping.
Key fields for dashboards:
events.total: Total events in the timeframe.strikes.total: Strikes issued in the timeframe.removals.total: Downloads removed.removals.byReason.AllFilesBlocked: Malware removals (combine withAtLeastOneFileBlocked).cleaned.total: Downloads cleaned after meeting seeding goals.searches.total: Seeker searches triggered.jobs.byType.<JobType>.failed: Failed job runs.jobs.byType.<JobType>.nextRunAt: Next scheduled run.health.downloadClients[0].isHealthy: First download client health.health.arrInstances[0].isHealthy: First arr instance health.
Deprecated: v1
GET /api/stats (deprecated)
/api/stats is deprecated in favour of /api/v2/stats. It stops responding on 1 September 2026 00:00 UTC; requests on or after that time return 404 Not Found.