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
| Version | Base URL | Status |
|---|---|---|
| v2 | https://pulspeed.ai/api/v2 | Current — recommended |
| v1 | https://pulspeed.ai/api/v1 | Stable — 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:
| Header | Description |
|---|---|
| X-Request-ID | Unique request identifier for support |
| X-RateLimit-Limit | Your plan's requests-per-minute limit |
| X-RateLimit-Remaining | Requests left in current window |
| X-RateLimit-Reset | Unix 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"
}
}
| Code | HTTP | Description |
|---|---|---|
| unauthorized | 401 | Missing or invalid token |
| email_not_verified | 403 | Email not confirmed |
| forbidden | 403 | Resource belongs to another user |
| webhook_plan_required | 403 | Outbound webhooks require Developer or Scale plan |
| not_found | 404 | Resource not found |
| validation_error | 422 | Request validation failed |
| rate_limit_exceeded | 429 | Too many requests |
| monthly_usage_limit_exceeded | 429 | Monthly scan quota exhausted |
| site_limit_exceeded | 429 | Free plan site limit reached (max 3) |
| external_service_error | 500 | PageSpeed API or OpenAI returned an error |
Rate Limits
| Plan | API req/min | Scan req/min |
|---|---|---|
| Free | 60 | 3 |
| Developer | 300 | 10 |
| Scale | 1,000 | 15 |
When rate limited, you receive 429 Too Many Requests with a Retry-After header.
Plans & Limits
| Feature | Free | Developer $29 | Scale $79 |
|---|---|---|---|
| Scans / month | 30 | 500 | 2,000 |
| Sites | 3 | Unlimited | Unlimited |
| AI Analysis | 5 / month | Included | Included |
| History | 7 days | 90 days | 1 year |
| Outbound Webhooks | — | 5 endpoints | 20 endpoints |
| MCP Server | ✓ | ✓ | ✓ |
| API Rate Limit | 60/min | 300/min | 1,000/min |
Instant Scan
/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
| Field | Type | Description | |
|---|---|---|---|
| url | string | required | Full URL to scan |
| strategy | string | optional | mobile (default) or desktop |
| wait | boolean | optional | true (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}.
Bulk Scan
/api/v2/scan/bulk/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
| Field | Type | Description | |
|---|---|---|---|
| urls | array | required | 1–10 URLs to scan |
| strategy | string | optional | mobile (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: pending → processing → done | partially_done
Structured Metrics
/api/v2/metrics?url=https://example.com&period=7dReturns Core Web Vitals, trend analysis, and historical data over a specified period.
| Param | Type | Values | |
|---|---|---|---|
| url | string | required | Site URL |
| period | string | optional | 24h, 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
Usage
/api/v2/usageReturns 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 }
}
}
Outbound Webhooks
Push events to your endpoint after scans complete or performance thresholds are breached. Requires Developer or Scale plan.
/api/v2/webhooks— list endpoints/api/v2/webhooks— create/api/v2/webhooks/{id}— get/api/v2/webhooks/{id}— update/api/v2/webhooks/{id}— delete/api/v2/webhooks/{id}/rotate-secret— new signing secretCreate 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:
| Event | When fired |
|---|---|
| scan.completed | Scan finished successfully |
| scan.failed | Scan returned an error |
| performance.regression | Score dropped ≥ regression_delta points vs previous scan |
| threshold.exceeded | Score 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.
Performance Budgets
/api/v2/sites/{id}/budgets/api/v2/sites/{id}/budgetsDefine 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 }
}
Raw Lighthouse Audit
/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.
Authentication Endpoints
/api/v1/register— create account + API token/api/v1/login— login + get token/api/v1/me— current user/api/v1/logout— revoke current tokenPOST /api/v1/login
{ "email": "you@example.com", "password": "secret" }
// Response
{ "data": { "token": "1|abc123...", "user": { "id": 1, "plan": "free", ... } } }
Sites
/api/v1/sites/api/v1/sites/api/v1/sites/{id}/api/v1/sites/{id}/api/v1/sites/{id}POST /api/v1/sites
{ "name": "My Blog", "url": "https://example.com", "frequency": "daily" }
Scanning
Prefer API v2
Use POST /api/v2/scan for new integrations — it auto-creates sites and supports synchronous results.
/api/v1/sites/{id}/scan— trigger scan, returns job_id/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
Snapshots
/api/v1/sites/{id}/snapshots/api/v1/snapshots/{id}/api/v1/snapshots/{id}Subscription
/api/v1/subscription{
"data": {
"plan": "developer",
"is_subscribed": true,
"on_trial": false,
"next_billing_date": "2026-03-22T00:00:00Z"
}
}
CI/CD Webhooks
Trigger scans from GitHub Actions or other CI systems using a webhook token (separate from your API token). Requires Scale plan.
/api/v1/webhook/scan/api/v1/webhook/scan/{job_id}/statusPOST /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 requiredget_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 requiredset_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."