Skip to content

Analytics API

Read delivery totals, zero-filled time series, top files, traffic sources, workspace activity, and storage ownership for one workspace.

On this page

Use these routes to build a usage dashboard, alert on error spikes, report which sites send traffic to your files, or find the files that take up the most storage. Every route is read-only and scoped to the workspace that the API key belongs to (or the workspace selected with X-Workspace-Id for a signed-in session).

Before you start#

  • Scope. API keys need analytics:read for every /api/analytics/* route. Any workspace member, including a viewer, can read analytics from a signed-in session.
  • Where the numbers come from. Delivery figures are aggregated from hourly rollups of requests to https://cdn.steadylink.io/a/{asset_id} and your delivery domains. A request that has not been rolled up yet does not appear, so expect the current hour to fill in gradually.
  • Time windows are UTC. range accepts 24h, 7d, or 30d. Any other value returns 422.
rangeWindow startBucket sizePoints in a series
24hStart of the current UTC hour, minus 23 hours1 hour24
7dMidnight UTC today, minus 6 days1 day7
30dMidnight UTC today, minus 29 days1 day30

The current hour or day is always the last bucket and is still accumulating.

Percentages in these responses (cacheHitRate, errorRate, share) are expressed from 0 to 100, not as fractions. When there were no requests in the window they are 0.

Summary#

Get delivery and storage totals#

GET/api/analytics/summary
Requiresanalytics:read

Returns one set of totals for the selected window, plus the storage the workspace holds right now. Use it for headline numbers; use the series when you need a chart.

Query parameters

rangestringDefault 7d
24h, 7d, or 30d.
curl "https://api.steadylink.io/api/analytics/summary?range=7d" \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "range": "7d",
  "requests": 3840000,
  "bandwidthBytes": 1840000000000,
  "cacheHitRate": 96.8,
  "errors": 112,
  "actionableDeliveryIssues": {
    "missingFileOrRevision": 37
  },
  "errorRate": 0.0029,
  "p95Ms": 18,
  "storedBytes": 30494267801,
  "objectCount": 128
}

Response fields

requestsinteger
Delivery requests in the window.
bandwidthBytesinteger
Bytes sent to clients in the window.
cacheHitRatenumber
Percentage of requests served from cache, rounded to two decimals.
errorsinteger
Requests that returned a status of 400 or higher. This includes expected refusals, such as an expired signed link or a private file requested without one.
actionableDeliveryIssues.missingFileOrRevisioninteger
Requests for a file or revision that does not exist. This is the error class you can usually fix yourself, for example a link to a deleted file or a pinned revision that was removed.
errorRatenumber
errors as a percentage of requests, rounded to four decimals.
p95Msinteger
The highest hourly 95th-percentile response time in the window, in milliseconds. It is a worst-hour figure, not a percentile computed across the whole window.
storedBytesinteger
Bytes held by every revision of every file in the workspace, including retained older revisions. Not limited by range.
objectCountinteger
Number of files in the workspace. Not limited by range.

Time series#

Get a time series#

GET/api/analytics/series
Requiresanalytics:read

Returns one point per hour (24h) or per day (7d, 30d). The series is always complete: buckets without traffic are filled with zeros, so you can plot points directly without checking for gaps. Each at value is the start of its bucket in UTC, without a timezone suffix.

Query parameters

rangestringDefault 7d
24h, 7d, or 30d.
curl
curl "https://api.steadylink.io/api/analytics/series?range=24h" \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "range": "24h",
  "interval": "hour",
  "points": [
    {
      "at": "2026-10-07T15:00:00",
      "requests": 18240,
      "bandwidthBytes": 9120044312,
      "cacheHits": 17702,
      "cacheHitRate": 97.05,
      "errors": 3,
      "missingFileOrRevision": 1,
      "p95Ms": 21
    },
    {
      "at": "2026-10-07T16:00:00",
      "requests": 0,
      "bandwidthBytes": 0,
      "cacheHits": 0,
      "cacheHitRate": 0,
      "errors": 0,
      "missingFileOrRevision": 0,
      "p95Ms": 0
    }
  ]
}

The example shows two of the 24 points. interval is hour for 24h and day otherwise.

Top files#

List the most requested files#

GET/api/analytics/objects
Requiresanalytics:read

Returns files ordered by request count in the window, most requested first. Files with no delivery requests in the window are not listed.

Query parameters

rangestringDefault 24h
24h, 7d, or 30d. Note that the default here is 24h, unlike the other routes.
limitintegerDefault 25
Number of files, from 1 to 25. Values outside that range return 422 rather than being clamped.
curl
curl "https://api.steadylink.io/api/analytics/objects?range=7d&limit=10" \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "range": "7d",
  "limit": 10,
  "items": [
    {
      "assetId": "3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44",
      "bucketId": "9b1c7e52-0f3a-4c6d-8e2b-5a7d1c3e9f10",
      "name": "campaign-hero.webp",
      "path": "campaign/campaign-hero.webp",
      "visibility": "public",
      "requests": 412880,
      "bandwidthBytes": 98211400192,
      "cacheHitRate": 98.41,
      "errors": 0,
      "missingFileOrRevision": 0,
      "p95Ms": 16
    }
  ]
}

visibility is the file's own setting (public, private, or inherit when it follows the bucket). bucketId and path are null for a file that is no longer in a bucket.

Traffic sources#

List traffic sources#

GET/api/analytics/sources
Requiresanalytics:read

Returns where delivery requests came from, grouped by source and ordered by request count. Pass assetId to see the sources for one file, for example to find out which site embeds an image.

Each request is classified once, in this order:

  1. A tag on the link. ?ref=newsletter or ?utm_source=newsletter on a delivery URL makes the source newsletter with kind tagged. Tags are the only signal that survives apps and email clients that strip the referrer, so add one to links you place in those channels. These parameters never change the bytes that are delivered.
  2. A link-preview crawler, such as Discord or Slack fetching a preview card. Kind preview.
  3. The referring site's host, never its path or query. Well-known platforms are grouped, so t.co and twitter.com both count as x.com. Kinds search, social, site, or steadylink.
  4. direct when nothing identifies the source.

Query parameters

rangestringDefault 7d
24h, 7d, or 30d.
assetIduuid
Limit the result to one file.
limitintegerDefault 10
Number of sources to return, from 1 to 50. totalRequests and sourceCount always cover every source, not just the returned ones.
curl
curl "https://api.steadylink.io/api/analytics/sources?range=30d&assetId=3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44" \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "range": "30d",
  "assetId": "3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44",
  "totalRequests": 52310,
  "sourceCount": 14,
  "items": [
    { "source": "direct", "kind": "direct", "requests": 30112, "bandwidthBytes": 7180223004, "files": 1, "share": 57.56 },
    { "source": "reddit.com", "kind": "social", "requests": 9844, "bandwidthBytes": 2350118400, "files": 1, "share": 18.82 },
    { "source": "newsletter", "kind": "tagged", "requests": 6020, "bandwidthBytes": 1437312000, "files": 1, "share": 11.51 }
  ]
}

Item fields

sourcestring
Canonical source: a tag, a platform host such as google.com, a referring host, or direct.
kindstring
direct, search, social, site, tagged, preview, or steadylink.
filesinteger
Number of distinct files this source requested.
sharenumber
This source's percentage of totalRequests.

Activity#

List recent activity#

GET/api/analytics/activity
Requiresanalytics:read

Returns the workspace audit trail, newest first: uploads, new revisions, restores, renames, visibility changes, blocked uploads, and signed-link changes. range does not apply here.

Query parameters

limitintegerDefault 25
Number of events, from 1 to 100.
curl
curl "https://api.steadylink.io/api/analytics/activity?limit=50" \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "items": [
    {
      "id": "c41e8a20-77b5-4f0e-9a61-0e2d4b8c3f19",
      "title": "New revision uploaded",
      "action": "asset.object_version_create",
      "assetId": "3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44",
      "assetName": "campaign-hero.webp",
      "actor": null,
      "success": true,
      "metadata": { "version": 4 },
      "createdAt": "2026-10-08T09:12:44.381022"
    }
  ]
}

title is a readable label for known actions and a title-cased form of action otherwise, so match on action in code. actor is "You" when the signed-in user performed the action and null in every other case, including every request made with an API key. metadata varies by action.

Storage#

Get storage by bucket and file#

GET/api/analytics/storage
Requiresanalytics:read

Returns how much storage each bucket holds, down to each file's current revision and all of its retained revisions. Use it to find files whose revision history is worth trimming. Buckets are sorted largest first; files inside a bucket are sorted by allVersionsBytes, largest first.

curl
curl "https://api.steadylink.io/api/analytics/storage" \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "usedBytes": 30494267801,
  "limitBytes": 107374182400,
  "remainingBytes": 76879914599,
  "buckets": [
    {
      "bucketId": "9b1c7e52-0f3a-4c6d-8e2b-5a7d1c3e9f10",
      "name": "Marketing",
      "bytes": 21877391360,
      "fileCount": 84,
      "files": [
        {
          "assetId": "3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44",
          "name": "launch-video.mp4",
          "path": "video/launch-video.mp4",
          "currentBytes": 412090368,
          "allVersionsBytes": 1648361472,
          "versionCount": 4
        }
      ]
    }
  ]
}

Response fields

limitBytesinteger
The storage allowance of the workspace's active plan. A paid plan whose billing is not active counts as the free plan here.
buckets[].bytesinteger
Sum of allVersionsBytes for the files in the bucket.
files[].currentBytesinteger
Size of the revision the link serves now.
files[].allVersionsBytesinteger
Size of every retained revision of the file, current included.

The response lists every file in every bucket, so it can be large for big workspaces. Cache it rather than polling it.

Next steps#