Documentation

REST API

Token-based API access for scans, domains, alerts and live data (RUM).

turbometrics offers a REST API for programmatic access to scans, domains and account data. Authentication works via bearer tokens, which you manage under Profile → API & apps.

Authentication

Add your API token to every request as an Authorization header:

Authorization: Bearer {your-token}

The API always responds with JSON. Without a valid token you get 401 Unauthenticated.

Base URL

https://turbometrics.io/api/v1

Rate limiting

The daily request limit depends on your plan:

Plan Daily limit
Starter 500
Pro 5,000
Agency Unlimited

If you exceed it, you get 429 Too Many Requests:

{
  "error": "Daily API limit reached",
  "limit": 500,
  "reset_at": "2026-04-01T23:59:59+02:00"
}

The counter is reset daily at midnight.


Endpoints

GET /me

Returns account information and the current API usage.

Example:

curl -H "Authorization: Bearer {token}" \
  https://turbometrics.io/api/v1/me

Response:

{
  "data": {
    "id": 42,
    "name": "John Doe",
    "email": "[email protected]",
    "plan": {
      "key": "starter",
      "label": "Starter",
      "api_enabled": true,
      "api_daily_limit": 500
    },
    "api_usage": {
      "used_today": 12,
      "limit_today": 500,
      "reset_at": "2026-04-01T23:59:59+02:00"
    }
  }
}

GET /scans

Returns a paginated list of your scans.

Parameters:

Parameter Type Description
limit int Results per page, max. 50 (default: 20)
domain string Filters by URL content
status string queued, running, finished, failed
page int Page number

Example:

curl -H "Authorization: Bearer {token}" \
  "https://turbometrics.io/api/v1/scans?limit=10&status=finished"

Response:

{
  "data": [
    {
      "public_id": "01KN3X...",
      "report_url": "https://turbometrics.io/scan/01KN3X...",
      "status": "finished",
      "submitted_url": "https://example.com/",
      "region": "de-fsn1",
      "auth_type": null,
      "requested_at": "2026-03-31T00:29:00+02:00",
      "finished_at": "2026-03-31T00:29:45+02:00",
      "result": {
        "scores": {
          "overall": 87,
          "speed": 100,
          "images": 93,
          "caching": 89,
          "wordpress": 100,
          "technical": 70
        }
      }
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 10,
    "total": 143,
    "last_page": 15
  }
}

POST /scans

Starts a new scan.

Body (JSON):

Field Type Required Description
url string yes The URL to scan
region string no Scan location (e.g. de-fsn1, de-nbg1)
public bool no Publicly visible (default: false)
force bool no Force a fresh scan even if a recent result exists (default: false)
auth object no Authentication for protected pages (requires the Pro plan, see below)

Note on caching: without force: true, an existing scan result for the same URL is returned if it is less than 24 hours old. The response then contains "cached": true and HTTP 200 instead of 202.

Example (normal scan):

curl -X POST \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}' \
  https://turbometrics.io/api/v1/scans

Example (force a fresh scan):

curl -X POST \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "force": true}' \
  https://turbometrics.io/api/v1/scans

Response — new scan (202 Accepted):

{
  "data": {
    "id": "01KN3X...",
    "status": "queued",
    "url": "https://example.com/",
    "cached": false,
    "auth_type": null
  }
}

Response — cache hit (200 OK):

{
  "data": {
    "id": "01KN3X...",
    "status": "finished",
    "url": "https://example.com/",
    "cached": true,
    "auth_type": null
  }
}

The scan is processed asynchronously. Call GET /scans/{id} to check the status.

Scan with authentication (Pro plan and above)

Pages protected by HTTP basic auth or by a custom HTTP header can be scanned with the optional auth object. Requires the Pro plan.

Fields in the auth object:

Field Type Description
type string "basic" or "header"
username string User name (only with type: "basic")
password string Password (only with type: "basic")
header_name string Name of the HTTP header (only with type: "header")
header_value string Value of the HTTP header (only with type: "header")

Example — HTTP basic auth:

curl -X POST \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://staging.example.com",
    "auth": {
      "type": "basic",
      "username": "admin",
      "password": "secret"
    }
  }' \
  https://turbometrics.io/api/v1/scans

Example — HTTP header:

curl -X POST \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://staging.example.com",
    "auth": {
      "type": "header",
      "header_name": "X-Preview-Key",
      "header_value": "abc123"
    }
  }' \
  https://turbometrics.io/api/v1/scans

Response with authentication:

{
  "data": {
    "id": "01KN3X...",
    "status": "queued",
    "url": "https://staging.example.com/",
    "cached": false,
    "auth_type": "basic"
  }
}

The response contains auth_type as an indicator — credentials are never returned. Scans with authentication are automatically treated as private.

Without the scan_auth feature (Free / Starter), the auth object is rejected with 403 Forbidden.


GET /scans/{id}

Returns the details of a single scan.

{id} is the public_id (e.g. 01KN3X...).

report_url points to the report in the web interface. For private scans the link only works for the signed-in owner — to pass a report on to someone else, use share_url (see Share links).

Example:

curl -H "Authorization: Bearer {token}" \
  https://turbometrics.io/api/v1/scans/01KN3X...

Response (running scan):

{
  "data": {
    "public_id": "01KN3X...",
    "report_url": "https://turbometrics.io/scan/01KN3X...",
    "status": "running",
    "submitted_url": "https://example.com/",
    "region": "de-fsn1",
    "auth_type": null
  }
}

Response (finished scan):

{
  "data": {
    "public_id": "01KN3X...",
    "report_url": "https://turbometrics.io/scan/01KN3X...",
    "status": "finished",
    "is_public": false,
    "submitted_url": "https://example.com/",
    "region": "de-fsn1",
    "auth_type": null,
    "share_enabled": false,
    "share_url": null,
    "requested_at": "2026-03-31T00:29:00+02:00",
    "finished_at": "2026-03-31T00:29:45+02:00",
    "result": {
      "final_url": "https://example.com/",
      "final_host": "example.com",
      "http_status": 200,
      "scores": {
        "overall": 87,
        "speed": 100,
        "images": 93,
        "caching": 89,
        "wordpress": 100,
        "technical": 70
      },
      "summary_short": "The page loads quickly, but has room for improvement in technical metrics.",
      "summary_long": "...",
      "metrics": {
        "ttfb_ms": 182,
        "desktop": {
          "fcp_ms": 412,
          "lcp_ms": 830,
          "cls": 0.02,
          "tbt_ms": 0,
          "request_count": 34,
          "total_bytes": 512000
        },
        "mobile": {
          "fcp_ms": 980,
          "lcp_ms": 2100,
          "cls": 0.04,
          "tbt_ms": 120,
          "request_count": 34,
          "total_bytes": 512000
        }
      },
      "screenshots": {
        "desktop_url": "https://turbometrics.io/scan/01KN3X.../screenshot?profile=desktop",
        "mobile_url": "https://turbometrics.io/scan/01KN3X.../screenshot?profile=mobile",
        "filmstrip": {
          "desktop": [
            "https://turbometrics.io/scan/01KN3X.../strip/desktop/0?...",
            "https://turbometrics.io/scan/01KN3X.../strip/desktop/1?..."
          ],
          "mobile": [
            "https://turbometrics.io/scan/01KN3X.../strip/mobile/0?..."
          ]
        },
        "filmstrip_frames": {
          "desktop": [
            { "url": "https://turbometrics.io/scan/01KN3X.../strip/desktop/0?...", "ms": 0, "event": null },
            { "url": "https://turbometrics.io/scan/01KN3X.../strip/desktop/1?...", "ms": 850, "event": "lcp" }
          ],
          "mobile": [
            { "url": "https://turbometrics.io/scan/01KN3X.../strip/mobile/0?...", "ms": 0, "event": "fcp" }
          ]
        }
      },
      "findings": [
        {
          "category": "images",
          "code": "unoptimized_images",
          "severity": "warning",
          "title": "Images not optimised",
          "message": "3 images could be smaller.",
          "recommendation": "Use WebP and compress images before upload."
        }
      ]
    }
  }
}

POST /scans/{id}/share/enable

Enables the share link for a scan and returns the public share URL. Requires at least the Starter plan.

{id} is the public_id of the scan (e.g. 01KN3X...).

Example:

curl -X POST \
  -H "Authorization: Bearer {token}" \
  https://turbometrics.io/api/v1/scans/01KN3X.../share/enable

Response:

{
  "data": {
    "share_enabled": true,
    "share_url": "https://turbometrics.io/s/abc123xyz012"
  }
}

The token stays stable — enabling it again after disabling it uses the same URL.


POST /scans/{id}/share/disable

Disables the share link. The share token is kept internally, so that reactivating it later delivers the same URL.

Example:

curl -X POST \
  -H "Authorization: Bearer {token}" \
  https://turbometrics.io/api/v1/scans/01KN3X.../share/disable

Response:

{
  "data": {
    "share_enabled": false
  }
}

GET /domains

Returns a paginated list of your monitored scan targets.

Example:

curl -H "Authorization: Bearer {token}" \
  https://turbometrics.io/api/v1/domains

Response:

{
  "data": [
    {
      "id": 7,
      "host": "example.com",
      "url": "https://example.com/",
      "schedule": "daily",
      "is_active": true,
      "last_dispatched_at": "2026-03-31T08:00:00+02:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 20,
    "total": 5,
    "last_page": 1
  }
}

GET /domains/{id}/history

Returns the last 30 finished scans for a scan target.

{id} is the numeric ID from GET /domains.

Example:

curl -H "Authorization: Bearer {token}" \
  https://turbometrics.io/api/v1/domains/7/history

Response:

{
  "data": [
    {
      "score": 87,
      "created_at": "2026-03-31T08:01:23+02:00",
      "region": "de-fsn1"
    },
    {
      "score": 84,
      "created_at": "2026-03-30T08:00:55+02:00",
      "region": "de-fsn1"
    }
  ]
}

GET /alerts

Returns a paginated list of your alerts.

Parameters:

Parameter Type Description
status string open (open, not dismissed), resolved, unread
severity string critical, warning
limit int Results per page, max. 50 (default: 20)
page int Page number

Example:

curl -H "Authorization: Bearer {token}" \
  "https://turbometrics.io/api/v1/alerts?status=open&severity=critical"

Response:

{
  "data": [
    {
      "id": 123,
      "type": "score_below_threshold",
      "severity": "critical",
      "title": "Score below threshold",
      "message": "Score 34 below the critical threshold of 60.",
      "is_read": false,
      "is_dismissed": false,
      "url": "https://example.com/",
      "host": "example.com",
      "scan_id": "01KN3X...",
      "created_at": "2026-03-31T08:01:23+02:00",
      "resolved_at": null
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 20,
    "total": 3,
    "last_page": 1
  }
}

GET /alerts/{id}

Returns a single alert.

Example:

curl -H "Authorization: Bearer {token}" \
  https://turbometrics.io/api/v1/alerts/123

Response: the same structure as a single element from GET /alerts.


POST /alerts/mark-read

Marks alerts as read. Without ids, all unread alerts of the user are marked.

Body (JSON):

Field Type Required Description
ids array no Array of alert IDs; missing → all

Example (specific alerts):

curl -X POST \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"ids": [123, 124]}' \
  https://turbometrics.io/api/v1/alerts/mark-read

Example (mark all as read):

curl -X POST \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{}' \
  https://turbometrics.io/api/v1/alerts/mark-read

Response:

{
  "data": {
    "marked_read": 5
  }
}

GET /rum/sites

Returns a paginated list of your live-data websites. Starter plan and above only.

Parameters:

Parameter Type Description
limit int Results per page, max. 50 (default: 20)
page int Page number

Example:

curl -H "Authorization: Bearer {token}" \
  https://turbometrics.io/api/v1/rum/sites

Response:

{
  "data": [
    {
      "id": 1,
      "domain": "turbometrics.io",
      "active": true,
      "embed_snippet": "<script src=\"https://turbometrics.io/tm.min.js\" data-site-id=\"...\" async></script>",
      "pageviews_this_month": 988,
      "monthly_limit": 1000000,
      "created_at": "2026-04-01T00:00:00+02:00"
    }
  ],
  "meta": { "current_page": 1, "per_page": 20, "total": 2, "last_page": 1 }
}

GET /rum/sites/{id}

Returns the details of a single live-data website.

Example:

curl -H "Authorization: Bearer {token}" \
  https://turbometrics.io/api/v1/rum/sites/1

GET /rum/sites/{id}/summary

Returns p75 values and the Core Web Vitals status for a website.

Parameters:

Parameter Type Description
period string 24h, 7d, 30d (default: 24h)
device string all, desktop, mobile, tablet (default: all)

Example:

curl -H "Authorization: Bearer {token}" \
  "https://turbometrics.io/api/v1/rum/sites/1/summary?period=24h&device=all"

Response:

{
  "data": {
    "domain": "turbometrics.io",
    "period": "24h",
    "device": "all",
    "cwv_pass": true,
    "cwv_insufficient_data": false,
    "metrics": {
      "LCP":  { "p75": 226,   "rating": "good", "samples": 127 },
      "CLS":  { "p75": 0.0,   "rating": "good", "samples": 12 },
      "INP":  { "p75": 112,   "rating": "good", "samples": 16 },
      "FCP":  { "p75": 201,   "rating": "good", "samples": 26 },
      "TTFB": { "p75": 143,   "rating": "good", "samples": 27 }
    }
  }
}

cwv_pass is true when all three Core Web Vitals (LCP, CLS, INP) are within the target range and each has at least 20 measurements. With too little data, cwv_pass is null and cwv_insufficient_data is true.


GET /rum/sites/{id}/history

Returns daily p75/p50 values for one metric as a time series.

Parameters:

Parameter Type Description
metric string Required: LCP, CLS, INP, FCP, TTFB
days int 7, 30, 90 (default: 30)
device string all, desktop, mobile, tablet (default: all)

Example:

curl -H "Authorization: Bearer {token}" \
  "https://turbometrics.io/api/v1/rum/sites/1/history?metric=LCP&days=30"

Response:

{
  "data": [
    { "date": "2026-04-04", "p75": 226, "p50": 180, "samples": 127 },
    { "date": "2026-04-03", "p75": 240, "p50": 195, "samples": 98 }
  ],
  "meta": { "metric": "LCP", "days": 30, "device": "all" }
}

GET /rum/sites/{id}/pages

Returns the slowest pages by p75 for one metric.

Parameters:

Parameter Type Description
metric string LCP, FCP, TTFB (default: LCP)
period string 24h, 7d (default: 24h)
limit int 1–100 (default: 25)

Example:

curl -H "Authorization: Bearer {token}" \
  "https://turbometrics.io/api/v1/rum/sites/1/pages?metric=LCP&limit=10"

Response:

{
  "data": [
    { "path": "/", "p75": 1437, "samples": 3 },
    { "path": "/preise", "p75": 98, "samples": 12 }
  ],
  "meta": { "metric": "LCP", "period": "24h" }
}

GET /rum/sites/{id}/alerts

Returns the configured alerts for a live-data website.

Example:

curl -H "Authorization: Bearer {token}" \
  https://turbometrics.io/api/v1/rum/sites/1/alerts

Response:

{
  "data": [
    {
      "id": 209,
      "title": "turbometrics.io — Auto-Alert",
      "mode": "auto",
      "metric": null,
      "threshold": null,
      "window_minutes": 60,
      "min_samples": 20,
      "consecutive_threshold": 2,
      "status": "active",
      "emailed_at": null,
      "resolved_at": null,
      "created_at": "2026-04-05T10:00:00+02:00"
    }
  ]
}

Possible values for status: active (configured, never triggered), triggered (currently triggered), resolved (resolved).


Error codes

Code Meaning
401 No token or an invalid one
403 The plan does not support the API (Starter or higher needed) — or an auth object was passed without a Pro plan
404 Resource not found
422 Validation error (e.g. an invalid URL)
429 Hourly scan limit or daily API limit exceeded
500 Internal error

Example 422:

{
  "message": "The url field is required.",
  "errors": {
    "url": ["The url field is required."]
  }
}

Code examples

curl

# Start a scan
curl -X POST \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "region": "de-fsn1"}' \
  https://turbometrics.io/api/v1/scans

# Scan with HTTP basic auth (Pro plan and above)
curl -X POST \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://staging.example.com", "auth": {"type": "basic", "username": "admin", "password": "secret"}}' \
  https://turbometrics.io/api/v1/scans

# Scan with an HTTP header (Pro plan and above)
curl -X POST \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://staging.example.com", "auth": {"type": "header", "header_name": "X-Preview-Key", "header_value": "abc123"}}' \
  https://turbometrics.io/api/v1/scans

# Check the status
curl -H "Authorization: Bearer {token}" \
  https://turbometrics.io/api/v1/scans/01KN3X...

# The last 5 scans
curl -H "Authorization: Bearer {token}" \
  "https://turbometrics.io/api/v1/scans?limit=5&status=finished"

PHP

$token = 'your-api-token';
$base  = 'https://turbometrics.io/api/v1';

// Start a scan
$ch = curl_init("$base/scans");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json',
        'Accept: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['url' => 'https://example.com']),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

$scanId = $response['data']['id']; // e.g. "01KN3X..."

// Start a scan with HTTP basic auth (Pro plan and above)
$ch = curl_init("$base/scans");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json',
        'Accept: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'url'  => 'https://staging.example.com',
        'auth' => [
            'type'     => 'basic',
            'username' => 'admin',
            'password' => 'secret',
        ],
    ]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

// auth_type in the response shows "basic"; credentials are never returned
echo $response['data']['auth_type']; // "basic"

// Check the status
$ch = curl_init("$base/scans/$scanId");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $token,
        'Accept: application/json',
    ],
]);
$scan = json_decode(curl_exec($ch), true);
curl_close($ch);

echo $scan['data']['status']; // queued | running | finished | failed

Rather not write code? Use the MCP server

If you prefer to query scans, alerts and live data in natural language, you can connect turbometrics directly to Claude Desktop and other AI tools — without writing your own API calls.

Set up the MCP server


Connecting apps (OAuth 2.1)

The sections above describe access to your own account using a token you created yourself. If you are building an application that acts on behalf of other turbometrics users, there has been a second route since August 2026: OAuth 2.1. The user clicks "Allow" once and never has to copy a token.

The access token issued is an ordinary bearer token and works against all endpoints described above — not just the MCP server.

Document Address
Authorization server https://turbometrics.io/.well-known/oauth-authorization-server
Registration POST https://turbometrics.io/oauth/register
Authorisation https://turbometrics.io/oauth/authorize
Token POST https://turbometrics.io/oauth/token

What you need to know:

  • PKCE with S256 is mandatory. Requests without a code_challenge are refused.
  • Public clients, no secret. Your app registers itself via the registration endpoint (RFC 7591) or identifies itself through a Client ID Metadata Document.
  • Redirect URIs must use https — or http on a loopback address, for applications that open a local port.
  • Access tokens are valid for one hour, refresh tokens for 30 days. Refresh tokens rotate: each renewal gives you a new one and retires the old one.
  • The user needs at least the Starter plan and will see your app under Profile → API & apps, where access can be revoked at any time.

No OpenID Connect: there is no id_token and no userinfo endpoint. For "who is this?", call GET /api/v1/me after signing in.

Token management

You create and manage API tokens under Profile → API & apps:

  • Give every token a descriptive name (e.g. Monitoring-Script, CI/CD)
  • Set an expiry date for security-critical environments
  • The token value is shown only once after creation
  • Delete compromised tokens immediately and create new ones