Pulspeed Documentation

Performance monitoring API for developers and AI agents. One API call returns Core Web Vitals, AI analysis, and improvement recommendations.

Quick Start 30 seconds to first scan

1. Get your API token from Settings → API Tokens.

2. Run your first scan:

curl -X POST https://pulspeed.ai/api/v2/scan \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'

3. Response (synchronous — ~30s):

{
  "data": {
    "id": 42,
    "performance_score": 84,
    "metrics": {
      "fcp": 1240, "lcp": 2450, "tti": 3100,
      "tbt": 180,  "ttfb": 420, "cls": 0.04
    },
    "analysis": "Your LCP of 2.45s is above the 2.5s threshold...",
    "strategy": "mobile",
    "taken_at": "2026-02-22T10:00:00Z"
  },
  "meta": { "message": "Scan completed", "site_id": 7 }
}

Authentication

All API endpoints (except /register and /login) require a Bearer token:

Authorization: Bearer <your_api_token>

Tokens are created in Settings → API Tokens. Tokens never expire unless manually revoked.

Keep tokens secret.

Tokens have full account access. Never commit them to source code — use environment variables or a secrets manager.

Base URLs

VersionBase URLStatus
v2https://pulspeed.ai/api/v2Current — recommended
v1https://pulspeed.ai/api/v1Stable — maintained

All responses include an X-API-Version header with the served version.

Interactive API Reference

Full OpenAPI 3.1 spec with Try-It console at /docs/api. JSON spec at /docs/api.json.

Response Format

All responses are JSON. Successful responses wrap data in a data key. Collection responses include pagination metadata.

// Single resource
{ "data": { "id": 1, ... } }

// Collection
{ "data": [ {...}, {...} ] }

// With metadata
{
  "data": { ... },
  "meta": { "request_id": "req_abc", "message": "..." }
}

Every response includes these headers:

HeaderDescription
X-Request-IDUnique request identifier for support
X-RateLimit-LimitYour plan's requests-per-minute limit
X-RateLimit-RemainingRequests left in current window
X-RateLimit-ResetUnix timestamp when window resets

Error Codes

All errors follow a consistent structure:

{
  "error": {
    "code": "scan_limit_exceeded",
    "message": "Monthly scan limit of 30 reached for your free plan.",
    "request_id": "req_01abc123",
    "docs_url": "https://pulspeed.ai/docs#errors"
  }
}
CodeHTTPDescription
unauthorized401Missing or invalid token
email_not_verified403Email not confirmed
forbidden403Resource belongs to another user
webhook_plan_required403Outbound webhooks require Developer or Scale plan
not_found404Resource not found
validation_error422Request validation failed
rate_limit_exceeded429Too many requests
monthly_usage_limit_exceeded429Monthly scan quota exhausted
site_limit_exceeded429Free plan site limit reached (max 3)
external_service_error500PageSpeed API or OpenAI returned an error

Rate Limits

PlanAPI req/minScan req/min
Free603
Developer30010
Scale1,00015

When rate limited, you receive 429 Too Many Requests with a Retry-After header.

Plans & Limits

FeatureFreeDeveloper $29Scale $79
Scans / month305002,000
Sites3UnlimitedUnlimited
AI Analysis5 / monthIncludedIncluded
History7 days90 days1 year
Outbound Webhooks5 endpoints20 endpoints
MCP Server
API Rate Limit60/min300/min1,000/min
v2

Instant Scan

POST /api/v2/scan

Scan a URL and get results in one call. Auto-creates the site if it doesn't exist. By default blocks until the scan completes (~30–60s).

Request body

FieldTypeDescription
urlstringrequiredFull URL to scan
strategystringoptionalmobile (default) or desktop
waitbooleanoptionaltrue (default) — block until done. false — async, returns job_id

Sync response (wait=true) — 200

{
  "data": {
    "id": 42,
    "performance_score": 84,
    "metrics": {
      "fcp": 1240, "lcp": 2450,
      "tti": 3100, "tbt": 180,
      "ttfb": 420, "cls": 0.04
    },
    "analysis": "...",
    "strategy": "mobile",
    "taken_at": "2026-02-22T10:00:00Z"
  }
}

Async response (wait=false) — 202

{
  "data": {
    "id": 99,
    "site_id": 7,
    "status": "pending",
    "source": "api_v2"
  },
  "meta": {
    "message": "Scan job dispatched"
  }
}

Poll async job status via GET /api/v2/scan-jobs/{id}.

v2

Bulk Scan

POST/api/v2/scan/bulk
GET/api/v2/scan/bulk/{batch_id}

Dispatch scans for up to 10 URLs at once. All jobs are queued immediately. Poll the batch endpoint to track overall progress.

POST request body

FieldTypeDescription
urlsarrayrequired1–10 URLs to scan
strategystringoptionalmobile (default) or desktop

POST response — 202

{
  "data": {
    "batch_id": "550e8400-...",
    "status": "pending",
    "total": 3,
    "jobs": [
      {"url":"https://example.com","job_id":10,"status":"queued"},
      {"url":"https://example.com/about","job_id":11,"status":"queued"},
      {"url":"https://example.com/blog","job_id":12,"status":"queued"}
    ]
  },
  "meta": { "poll_url": "/api/v2/scan/bulk/550e8400-..." }
}

GET batch status

{
  "data": {
    "batch_id": "550e8400-...",
    "status": "done",
    "strategy": "mobile",
    "total": 3,
    "completed": 2,
    "failed": 1,
    "pending": 0,
    "jobs": [
      {"job_id":10,"url":"https://example.com",
       "status":"done","finished_at":"..."},
      ...
    ]
  }
}

Batch statuses: pendingprocessingdone | partially_done

v2

Structured Metrics

GET/api/v2/metrics?url=https://example.com&period=7d

Returns Core Web Vitals, trend analysis, and historical data over a specified period.

ParamTypeValues
urlstringrequiredSite URL
periodstringoptional24h, 7d (default), 30d, 90d
{
  "data": {
    "url": "https://example.com",
    "site_id": 7,
    "current": { "performance_score": 84, "fcp": 1240, "lcp": 2450, ... },
    "trend": "improving",
    "history": [ { "performance_score": 80, "taken_at": "..." }, ... ],
    "period": "7d"
  }
}

Trend values: improving | stable | declining | insufficient_data

v2

Usage

GET/api/v2/usage

Returns current billing-period consumption, quota, and plan limits.

{
  "data": {
    "plan": "developer",
    "period": { "start": "2026-02-01", "end": "2026-02-28" },
    "scans":       { "used": 42, "limit": 500, "remaining": 458 },
    "ai_analyses": { "used": 15, "limit": 500, "remaining": 485 }
  }
}
v2

Outbound Webhooks

Push events to your endpoint after scans complete or performance thresholds are breached. Requires Developer or Scale plan.

GET/api/v2/webhooks— list endpoints
POST/api/v2/webhooks— create
GET/api/v2/webhooks/{id}— get
PUT/api/v2/webhooks/{id}— update
DELETE/api/v2/webhooks/{id}— delete
POST/api/v2/webhooks/{id}/rotate-secret— new signing secret

Create endpoint

POST /api/v2/webhooks
{
  "url": "https://myapp.com/hooks/pulspeed",
  "events": ["scan.completed", "performance.regression"],
  "config": {
    "regression_delta": 5,   // trigger if score drops ≥5 pts
    "threshold_score": 70    // trigger if score < 70
  }
}

Available events:

EventWhen fired
scan.completedScan finished successfully
scan.failedScan returned an error
performance.regressionScore dropped ≥ regression_delta points vs previous scan
threshold.exceededScore fell below threshold_score

Payload structure

{
  "id": "evt_550e8400-e29b-...",
  "event": "scan.completed",
  "created_at": "2026-02-22T10:00:00Z",
  "data": {
    "site_id": 7,
    "site_url": "https://example.com",
    "scan_job_id": 42,
    "snapshot": { "performance_score": 84, "fcp": 1240, ... }
  }
}

Verifying signatures

Every delivery includes X-Pulspeed-Signature: sha256=<hmac>. Verify it to reject spoofed requests:

# Python
import hmac, hashlib

def verify(body: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# Node.js
const crypto = require('crypto');
function verify(body, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret).update(body).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(signature)
  );
}

Deliveries are retried 3 times on failure: after 60s, 5 min, 30 min.

v2

Performance Budgets

PUT/api/v2/sites/{id}/budgets
DELETE/api/v2/sites/{id}/budgets

Define acceptable thresholds. When a scan violates a budget, the threshold.exceeded webhook fires.

PUT /api/v2/sites/7/budgets
{
  "performance_score": { "min": 80 },
  "lcp":  { "max": 2500 },
  "tbt":  { "max": 200 },
  "cls":  { "max": 0.1 }
}
v2

Raw Lighthouse Audit

GET/api/v2/snapshots/{id}/raw-audit/{key}

Returns the full Lighthouse audit object for a specific audit key. Useful for debugging detailed resource issues.

GET /api/v2/snapshots/42/raw-audit/render-blocking-resources

# Response
{
  "data": {
    "snapshot_id": 42,
    "audit_key": "render-blocking-resources",
    "audit": {
      "title": "Eliminate render-blocking resources",
      "score": 0.4,
      "displayValue": "Potential savings of 540 ms",
      "details": {
        "items": [
          { "url": "https://example.com/style.css", "wastedMs": 320 }
        ]
      }
    }
  }
}

Common audit keys: render-blocking-resources, bootup-time, unused-javascript, uses-optimized-images, network-requests.

v1

Authentication Endpoints

POST/api/v1/register— create account + API token
POST/api/v1/login— login + get token
GET/api/v1/me— current user
POST/api/v1/logout— revoke current token
POST /api/v1/login
{ "email": "you@example.com", "password": "secret" }

// Response
{ "data": { "token": "1|abc123...", "user": { "id": 1, "plan": "free", ... } } }
v1

Sites

GET/api/v1/sites
POST/api/v1/sites
GET/api/v1/sites/{id}
PUT/api/v1/sites/{id}
DELETE/api/v1/sites/{id}
POST /api/v1/sites
{ "name": "My Blog", "url": "https://example.com", "frequency": "daily" }
v1

Scanning

Prefer API v2

Use POST /api/v2/scan for new integrations — it auto-creates sites and supports synchronous results.

POST/api/v1/sites/{id}/scan— trigger scan, returns job_id
GET/api/v1/scan-jobs/{id}— poll status
// 1. Trigger
POST /api/v1/sites/7/scan
// → { "data": { "id": 42, "status": "pending", ... } }

// 2. Poll until status = "done"
GET /api/v1/scan-jobs/42
// → { "data": { "id": 42, "status": "done", "finished_at": "..." } }

// 3. Get snapshot
GET /api/v1/sites/7  // includes latest_snapshot
v1

Snapshots

GET/api/v1/sites/{id}/snapshots
GET/api/v1/snapshots/{id}
DELETE/api/v1/snapshots/{id}
v1

Subscription

GET/api/v1/subscription
{
  "data": {
    "plan": "developer",
    "is_subscribed": true,
    "on_trial": false,
    "next_billing_date": "2026-03-22T00:00:00Z"
  }
}
v1

CI/CD Webhooks

Trigger scans from GitHub Actions or other CI systems using a webhook token (separate from your API token). Requires Scale plan.

POST/api/v1/webhook/scan
GET/api/v1/webhook/scan/{job_id}/status
POST /api/v1/webhook/scan
Authorization: Bearer <webhook_token>
{
  "repository": { "full_name": "myorg/myrepo" }
}

MCP Server

The @pulspeed/mcp-server exposes Pulspeed as a Model Context Protocol (MCP) tool, letting AI agents — Claude, Cursor, Windsurf — directly trigger scans, read metrics, and get recommendations.

Available on all plans including Free.

The MCP server uses the same API token. The tools respect your plan's rate limits and scan quotas.

Installation

Claude Code (CLI)

claude mcp add pulspeed -- npx -y @pulspeed/mcp-server

# Set your API key
export PULSPEED_API_KEY="your_token_here"

Claude Desktop / Cursor / Windsurf

Add to your MCP config file (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):

{
  "mcpServers": {
    "pulspeed": {
      "command": "npx",
      "args": ["-y", "@pulspeed/mcp-server"],
      "env": { "PULSPEED_API_KEY": "your_token_here" }
    }
  }
}

Custom API endpoint

"env": {
  "PULSPEED_API_KEY": "your_token",
  "PULSPEED_BASE_URL": "https://pulspeed.ai/api/v1"
}

Tools Reference

10 tools available. All tools accept url or site_id to identify a site.

scan_site — trigger a scan and get results
// Params
{ "url": "https://example.com", "strategy": "mobile", "wait": true }
// strategy: "mobile" | "desktop" (default: mobile)
// wait: true = block until done (default), false = async + job_id
bulk_scan — scan multiple URLs at once
{ "urls": ["https://example.com", "https://example.com/about"],
  "strategy": "mobile",
  "wait": false }  // false (default) = async, true = sequential+results
list_sites — list all monitored sites
// No parameters required
get_site_metrics — Core Web Vitals + history
{ "url": "https://example.com", "period": "7d" }
// period: "24h" | "7d" | "30d" | "90d" (default: 7d)
get_recommendations — AI-powered optimisation tips
{ "url": "https://example.com" }
compare_snapshots — diff two scans with % deltas
{ "url": "https://example.com" }  // compares latest two
// or specify: { "snapshot_id_a": 42, "snapshot_id_b": 38 }
list_regressions — find recent score drops
{ "url": "https://example.com",
  "threshold": 3,   // min score drop to count (default: 3)
  "limit": 20 }     // scan history to analyse (default: 20)
get_usage — current quota consumption
// No parameters required
set_performance_budget — set thresholds for webhook alerts
{ "url": "https://example.com",
  "performance_score": { "min": 80 },
  "lcp": { "max": 2500 },
  "cls": { "max": 0.1 } }
get_raw_audit — Lighthouse audit detail
{ "url": "https://example.com",
  "audit_key": "render-blocking-resources" }
// Common keys: render-blocking-resources, bootup-time,
// unused-javascript, uses-optimized-images, network-requests

Example: Diagnose a performance regression

User: "My site score dropped today. What happened?"

Claude:
  → list_regressions(url="https://mysite.com", threshold=5)
     "🔴 CRITICAL −12 pts: 78 → 66 on 2026-02-22"
  → compare_snapshots(url="https://mysite.com")
     "LCP regressed: 2100ms → 3800ms (+81%)"
  → get_raw_audit(url="https://mysite.com", audit_key="bootup-time")
     "analytics.js: 1840ms — saves 1.2s if deferred"

Claude: "Your LCP jumped 1.7s due to a new analytics script
blocking the main thread for 1.84s. Defer or async-load it."