JSON API
A read-only, token-authenticated JSON API for scripts, CI and coding agents, covering routes, requests, queries, jobs, exceptions and deployments.
Overview
Rails Pulse serves a read-only JSON API under /rails_pulse/api/v1 (or under your mount_path). The CLI and the MCP server are both built on it, and you can call it from your own scripts.
The API is served by whichever process serves the dashboard. If your main app runs with mount_dashboard = false, call the dashboard host instead. See Deployment Modes.
Authentication
Set a token in the initializer:
# config/initializers/rails_pulse.rb
RailsPulse.configure do |config|
config.api_token = ENV["RAILS_PULSE_API_TOKEN"]
end
Send it in an X-Rails-Pulse-Token header:
curl https://yourapp.com/rails_pulse/api/v1/routes \
-H "X-Rails-Pulse-Token: $RAILS_PULSE_API_TOKEN"
The API never accepts a dashboard login. With no api_token set, every request gets a 401.
api_token only reads. Recording a deployment needs the separate config.deployment_token, and the API token is refused there. If you would rather have one credential for both, set them to the same value. See Deployment Tracking.
Endpoints
| Endpoint | Returns |
|---|---|
GET /api/v1/capabilities | The Rails Pulse version, and the application name and environment that answered |
GET /api/v1/coverage | What has been recorded, how recently, and any collection gaps |
GET /api/v1/routes | Tracked routes |
GET /api/v1/requests | Individual requests |
GET /api/v1/queries | Tracked SQL queries |
GET /api/v1/jobs | Background jobs |
GET /api/v1/job_runs | Individual job runs |
GET /api/v1/exceptions | Exception groups |
GET /api/v1/exceptions/:id | One exception group with its recent occurrences |
GET /api/v1/deployments | Recorded deployments, each with a before-and-after comparison |
GET /api/v1/insights | What needs attention in one summary period, and whether the thresholds fit |
Paging and time windows
Every list endpoint takes:
| Parameter | Description |
|---|---|
limit | Records to return, default 25, max 500 |
offset | Records to skip |
since | Start of the window, ISO 8601 |
until | End of the window, ISO 8601 |
since and until accept a date or a date and time; one with no zone is read as UTC. Listings break ties on id, so paging with offset is stable.
Responses share one shape:
{
"data": [ ... ],
"meta": { "total": 142, "limit": 25, "offset": 0 }
}
Filters
| Endpoint | Filters |
|---|---|
routes | search, sort (request_count, avg_duration, error_count), path and controller_action (exact matches), and with a window, min_requests to leave out routes with little traffic |
requests | route (substring of the controller action or path), status (500 or 5xx), path_prefix (start of the route path) |
queries | sort, hashed_sql (exact lookup), and route (a route id, controller action or path) to return only the SQL issued while serving that endpoint |
jobs | job (exact class name), status=failed |
job_runs | job, status |
exceptions | status (open, resolved, ignored), search (class or location), sort (last_seen_at, first_seen_at, occurrence_count), fingerprint (exact lookup); since/until apply to when the group was last seen |
exceptions/:id | occurrences (default 5, max 20) |
deployments | revision (the full revision or the start of one) |
Substring filters match _, % and ! literally.
Exact lookups
Every stable key a record carries has an exact lookup, so an identifier copied from an alert or a log leads straight to the detail: queries takes hashed_sql, exceptions takes fingerprint (combine it with no status or window to find the group however old or resolved it is), routes takes path and controller_action as exact matches, and deployments takes revision, matching the full revision or the start of one.
requests takes path_prefix, matched against the start of the route path; a trailing * is dropped, so a pattern like /wp-admin/* can be passed as it is, and /* or * match every path. A request no route matched is stored under its raw path, so path_prefix with status=404 lists the paths behind an unmatched-traffic spike.
Queries inside an endpoint
queries?route=12 returns only the SQL that ran while serving route 12. Each query’s stats.source_locations lists the files and lines it was issued from, most frequent first. This is how you get from “this endpoint is slow” to “this is the query, and here is where it comes from”.
Job windows
With since/until, jobs reports figures for that window in each row’s stats, built from the job summaries plus any runs since the last summary. The top-level counters stay lifetime totals. meta.window reports the bounds actually read, which are widened to the summary periods they touch (in config.time_zone). Percentiles are only included when one summary period holds every run in the window, because they can’t be combined across periods; stats.percentiles_note says when they are left out.
Deployment comparison
Every deployments row carries a comparison: the hour before the hour the deploy started in, against the hour after the hour it finished in, read from the hourly summaries (every hour the deploy ran in mixes both versions and is skipped). Each of avg_response_time, p95_response_time and error_rate is degraded when it is materially worse afterwards — worse by the ratio and the absolute floor in config.regression_thresholds — clean otherwise, and insufficient_data when either hour had fewer than 10 requests.
The overall outcome is degraded if any metric degraded, pending until the hour after has been summarized, unavailable when that hour was never summarized (check that SummaryJob is scheduled), and insufficient_data when the hours were too quiet or have aged out of config.hourly_summary_retention; note says which. The comparison is computed on each request and never stored.
Insights
GET /api/v1/insights reads one summary period — period (hour, day, week or month, default week), and at (ISO 8601) picks the period containing that time; without it, the last complete period. It reports one period rather than a window because a P95 cannot be combined across periods.
needs_attentionlists the routes, queries and jobs past their slow or critical thresholds, by the same rules as the dashboard’s Needs Attention panel —criticalfirst, ten at most, each with its recordidfor a follow-up call.threshold_recommendationschecksroute_thresholdsandquery_thresholdsagainst the period’s slowest routes and most expensive queries, and suggests a ready-to-pasteconfig_snippetwhen a threshold is too noisy or never fires.thresholdsechoes the current settings, andperiod.summarizedsays whetherSummaryJobhas written the period yet.
Coverage
GET /api/v1/coverage answers “is the data there?” rather than a question about the data:
- The oldest and newest request, job run and exception held, with counts
- How far hourly summaries have been generated, and whether they are stale
- What retention keeps
- The background writers: how many are live, their queue depth, and requests dropped in the last hour, with
collection.gap_suspectedset when anything was dropped. A writer that has gone quiet means nothing was queued, and the note says so rather than reporting a gap; withconfig.async = falsethe collection section says there is no writer to report.
If a kind of data isn’t being tracked (config.enabled, track_jobs, track_exceptions), coverage says so instead of reporting zero. Check it before treating an empty result as good news.
Errors
| Status | When |
|---|---|
400 | An unknown sort, status or period, an invalid at, a since/until that is not ISO 8601, an until at or before since, a repeated parameter, or a paging value that is not a whole number. The body lists the accepted values. |
401 | A missing or wrong token, or no api_token configured |
404 | An unknown exception group id |
Stability
The endpoints and parameters on this page are part of Rails Pulse’s public API and follow semantic versioning from 1.0. The full contract is in docs/api.md in the gem.
Next Steps
- CLI — the same endpoints from a terminal
- Coding Agents (MCP) — give an AI agent read access