Response Format

WebPeek API responses are returned as JSON with data fields at the root level. This guide explains the response format for each endpoint and how to work with the data effectively.

Response Structure Overview

Unlike many APIs that wrap data in a data object, WebPeek returns endpoint-specific fields directly at the root level. All responses include common fields for caching and processing metadata.

{
  // Endpoint-specific data fields (varies by endpoint)
  "url": "https://example.com",
  "title": "Example Domain",
  // ... more endpoint-specific fields

  // Caching information (all endpoints)
  "cache": {
    "cached": true,
    "cache_key": "abc123...",
    "cached_at": "2025-11-06T19:24:11.97789+00:00",
    "ttl_seconds": 85424,
    "expires_at": "2025-11-07T19:24:11.977Z"
  },

  // Processing metadata (all endpoints)
  "metadata": {
    "processing_time_ms": 245,
    "page_load_time_ms": 1234
  }
}

Common Fields

FieldTypeDescription
cacheobjectCaching information (all endpoints)
cache.cachedbooleanWhether this response was served from cache
cache.cache_keystringCache key for this request
cache.ttl_secondsnumberTime-to-live in seconds
metadataobjectProcessing information
metadata.processing_time_msnumberTotal processing time in milliseconds

Metadata Endpoint Response

The metadata endpoint returns comprehensive page metadata including Open Graph, Twitter Cards, structured data, and more:

{
  "url": "https://github.com",
  "final_url": "https://github.com/",
  "fetched_at": "2025-11-06T19:27:12.487Z",
  "title": "GitHub · Change is constant. GitHub keeps you ahead. · GitHub",
  "description": "Join the world's most widely adopted, AI-powered developer platform...",
  "site_name": "GitHub",
  "language": "en",
  "charset": "utf-8",
  "canonical": {
    "href": "https://github.com/",
    "is_self_canonical": true,
    "resolves": true,
    "status": 200
  },
  "viewport": "width=device-width",
  "theme_color": "#1e2327",
  "og": {
    "title": "GitHub · Change is constant. GitHub keeps you ahead.",
    "description": "Join the world's most widely adopted, AI-powered developer platform...",
    "image": "https://images.ctfassets.net/8aevphvgewt8/4pe4eOtUJ0ARpZRE4fNekf/f52b1f9c52f059a33170229883731ed0/GH-Homepage-Universe-img.png",
    "url": "https://github.com/",
    "type": "object",
    "site_name": "GitHub"
  },
  "twitter": {
    "card": "summary_large_image",
    "title": "GitHub · Change is constant. GitHub keeps you ahead.",
    "description": "Join the world's most widely adopted, AI-powered developer platform...",
    "image": "https://images.ctfassets.net/8aevphvgewt8/4pe4eOtUJ0ARpZRE4fNekf/f52b1f9c52f059a33170229883731ed0/GH-Homepage-Universe-img.png",
    "site": "github"
  },
  "images": [
    {
      "url": "https://images.ctfassets.net/8aevphvgewt8/4IfncsgGkGPFESlWXlAWfU/6f671f0ff761cb276c2effced9dca773/eyebrow-banner-duck.png",
      "alt": "Duck mascot"
    }
  ],
  "links": [
    {
      "href": "https://github.com/features",
      "text": "Features",
      "rel": null
    }
  ],
  "dominant_color": "#1e2327",
  "social_handles": {
    "twitter": "github",
    "instagram": "github",
    "linkedin": "github",
    "github": "features"
  },
  "tech_stack": ["React"],
  "http": {
    "final_url": "https://github.com/",
    "status": 200,
    "content_type": "text/html; charset=utf-8",
    "response_size_bytes": 565552,
    "etag": "W/\"e9244edc36b5ba0c811237312ee5d19a\"",
    "cache_control": "max-age=0, private, must-revalidate",
    "redirect_chain": []
  },
  "icons": [
    {
      "url": "https://github.githubassets.com/favicons/favicon.svg",
      "type": "image/svg+xml",
      "sizes": null
    }
  ],
  "indexability": {
    "robots_meta": null,
    "x_robots_tag": null,
    "robots_txt_allowed": true,
    "canonical_valid": true,
    "robots_effective": "index, follow"
  },
  "content": {
    "content_type": "documentation",
    "word_count": 2421,
    "read_time_min": 13,
    "heading_counts": {
      "h1": 4,
      "h2": 10,
      "h3": 16,
      "h4": 0,
      "h5": 0,
      "h6": 0
    }
  },
  "truncated": {
    "images": false,
    "links": true
  }
}

SEO Audit Endpoint Response

The SEO audit endpoint returns comprehensive SEO analysis with scoring, issues, and recommendations:

{
  "schema_version": "2025.11.0",
  "url": "https://www.webpeek.dev/",
  "scoring": {
    "overall": 93,
    "grade": "A",
    "by_category": {
      "metadata": 80,
      "content": 100,
      "technical": 95,
      "accessibility": 100,
      "performance": 90
    },
    "deductions": [
      {
        "category": "metadata",
        "reason": "Title is too long (63 characters)",
        "points": 10,
        "rule_id": "META_TITLE_TOO_LONG"
      }
    ]
  },
  "issues": [
    {
      "id": "meta-title-too-long",
      "code": "META_TITLE_TOO_LONG",
      "severity": "warning",
      "category": "metadata",
      "message": "Title tag is 63 characters (recommended: 50-60)",
      "recommendation": "Shorten title to 50-60 characters for optimal display in search results",
      "impact": "medium",
      "priority": 2,
      "fixable": true
    }
  ],
  "metrics": {
    "title_length": 63,
    "description_length": 219,
    "headings": {
      "h1": 1,
      "h2": 7,
      "h3": 23,
      "h4": 3,
      "h5": 0,
      "h6": 0,
      "total": 34
    },
    "links": {
      "internal": 17,
      "external": 0,
      "broken": 0,
      "total": 17
    },
    "images": {
      "total": 5,
      "with_alt": 5,
      "without_alt": 0
    },
    "word_count": 544
  },
  "indexability": {
    "robots_meta": null,
    "x_robots_tag": null,
    "canonical": "https://www.webpeek.dev/",
    "robots_txt_status": "allowed",
    "status": "indexable"
  },
  "summary": {
    "passed": [
      "No critical issues found",
      "HTTPS enabled",
      "Title tag present",
      "Meta description present",
      "Viewport meta tag present"
    ],
    "top_fixes": [
      {
        "id": "meta-title-too-long",
        "impact": "medium",
        "why_it_matters": "Long titles get truncated in search results",
        "quick_fix": "Trim title to ≤60 chars",
        "fixable": true
      }
    ],
    "critical_count": 0,
    "warning_count": 3,
    "info_count": 1,
    "rules": {
      "total": 36,
      "passed": 32,
      "failed": 4
    }
  },
  "scannedAt": "2025-11-06T19:25:26.426Z"
}

Performance Endpoint Response

The performance endpoint returns Core Web Vitals and resource analysis:

{
  "schema_version": "1.0.0",
  "run_id": "perf_2025-01-06T12-00-00_a1b2c3d4",
  "url": "https://example.com/page",
  "mode": "lab",
  "environment": {
    "device_profile": "desktop-4xCPU",
    "network_profile": "no-throttling",
    "collector_version": "webpeek-perf@1.0.0",
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
    "page_context": {
      "viewport": "1920x1080",
      "timezone": "America/New_York",
      "language": "en-US",
      "cookies_enabled": true
    }
  },
  "summary": {
    "overall": "good",
    "warnings": 1,
    "errors": 0,
    "suggestions": 3,
    "main_issue": "Optimize images to reduce LCP"
  },
  "core_web_vitals": {
    "lcp": {
      "value_ms": 2800,
      "rating": "needs-improvement",
      "budget_ms": 2500,
      "status": "warn"
    },
    "inp": {
      "value_ms": 150,
      "rating": "good",
      "budget_ms": 200,
      "status": "pass"
    },
    "cls": {
      "value": 0.08,
      "rating": "good",
      "budget": 0.1,
      "status": "pass"
    },
    "fcp": {
      "value_ms": 1200,
      "rating": "good",
      "budget_ms": 1800,
      "status": "pass"
    },
    "ttfb": {
      "value_ms": 450,
      "rating": "good",
      "budget_ms": 800,
      "status": "pass"
    }
  },
  "performance_score": {
    "schema_version": "1.0.0",
    "rule_version": "2025.11.0",
    "value": 78,
    "scale": "0-100",
    "method": "weighted-metrics",
    "weights": {
      "lcp": 0.35,
      "inp": 0.35,
      "cls": 0.15,
      "fcp": 0.1,
      "ttfb": 0.05
    }
  },
  "resources": {
    "total_requests": 45,
    "transfer_kb": 2345.6,
    "decoded_kb": 3456.7,
    "first_party": {
      "requests": 30,
      "transfer_kb": 1500.2,
      "decoded_kb": 2100.5
    },
    "third_party": {
      "requests": 15,
      "transfer_kb": 845.4,
      "decoded_kb": 1356.2
    },
    "by_type": {
      "javascript": {
        "count": 12,
        "transfer_kb": 890.1,
        "decoded_kb": 2345.6,
        "main_thread_ms": 1250
      },
      "image": {
        "count": 18,
        "transfer_kb": 1234.5,
        "decoded_kb": 1456.8,
        "unoptimized": 5
      },
      "stylesheet": {
        "count": 5,
        "transfer_kb": 123.4,
        "decoded_kb": 456.7
      }
    },
    "top_hosts": [
      {
        "host": "example.com",
        "requests": 30,
        "transfer_kb": 1500.2
      }
    ],
    "caching": {
      "cacheable_requests": 40,
      "missing_cache_headers": 5,
      "cache_hit_ratio": 0.75,
      "compression": {
        "gzip_or_br": 38,
        "uncompressed": 7,
        "estimated_savings_kb": 567.8
      }
    }
  },
  "tips": [
    {
      "tip_id": "optimize-images",
      "severity": "high",
      "message": "Optimize images to reduce LCP",
      "learn_more": "https://web.dev/optimize-lcp/"
    }
  ],
  "cache": {
    "cached": false,
    "cache_key": "perf:v1:https://example.com/page:desktop:networkidle2:30000",
    "ttl_seconds": 86400,
    "expires_at": "2025-01-07T12:00:00.000Z"
  },
  "created_at": "2025-01-06T12:00:00.000Z",
  "metadata": {
    "processing_time_ms": 4523,
    "page_load_time_ms": 3456,
    "page_load_rating": "average"
  }
}

Snapshot Endpoint Response

The snapshot endpoint returns screenshot data and metadata:

{
  "url": "https://example.com",
  "screenshot_url": "https://cdn.webpeek.dev/screenshots/abc123.webp",
  "width": 1920,
  "height": 1080,
  "format": "webp",
  "size_bytes": 145678,
  "checksum": "7eb6a602f113a8b6640f9925271d49eba914722fc63c2d8e1b3795b5e0dd49be",
  "viewport": {
    "width": 1920,
    "height": 1080,
    "deviceScaleFactor": 1
  },
  "cache_ttl_seconds": 86400,
  "created_at": "2025-11-06T19:56:25.284Z",
  "cache": {
    "cached": false,
    "cache_key": "def456...",
    "ttl_seconds": 86400
  },
  "metadata": {
    "processing_time_ms": 5615,
    "page_load_time_ms": 5548,
    "requests_blocked": 12
  }
}

Error Response Format

When errors occur, the API returns a different structure with error details:

{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error message",
    "details": {
      // Additional error context
    }
  }
}
FieldTypeDescription
successbooleanAlways false for error responses
error.codestringMachine-readable error code
error.messagestringHuman-readable error description
error.detailsobject?Additional context about the error

Working with Responses

Parsing Responses Safely

parse-response.js
async function fetchMetadata(url) {
  try {
    const response = await fetch(
      `https://api.webpeek.dev/metadata?url=${encodeURIComponent(url)}`,
      {
        headers: {
          'X-API-Key': process.env.WEBPEEK_API_KEY
        }
      }
    );

    // Parse JSON
    const data = await response.json();

    // Check for error response
    if (!response.ok || data.success === false) {
      throw new Error(data.error?.message || 'Request failed');
    }

    // WebPeek returns data at root level
    return data;
  } catch (error) {
    console.error('Failed to fetch metadata:', error);
    throw error;
  }
}

// Usage with safe access
try {
  const data = await fetchMetadata('https://github.com');

  // Access fields directly
  const title = data.title || data.og?.title || 'Untitled';
  const description = data.description || data.og?.description || '';
  const image = data.og?.image || data.twitter?.image;

  console.log({ title, description, image });

  // Check cache status
  if (data.cache?.cached) {
    console.log('Response served from cache');
  }
} catch (error) {
  console.error('Error:', error.message);
}

TypeScript Interfaces

Use these TypeScript interfaces for type safety:

webpeek-types.ts
// Common fields across all responses
interface CacheInfo {
  cached: boolean;
  cache_key: string;
  cached_at?: string;
  ttl_seconds: number;
  expires_at?: string;
}

interface ProcessingMetadata {
  processing_time_ms: number;
  page_load_time_ms?: number;
  [key: string]: any;
}

// Metadata endpoint response
interface MetadataResponse {
  url: string;
  final_url?: string;
  fetched_at?: string;
  title?: string;
  description?: string;
  site_name?: string;
  language?: string;
  charset?: string;
  canonical?: {
    href: string;
    is_self_canonical: boolean;
    resolves: boolean;
    status: number;
  };
  viewport?: string;
  theme_color?: string;
  og?: {
    title?: string;
    description?: string;
    image?: string;
    url?: string;
    type?: string;
    site_name?: string;
  };
  twitter?: {
    card?: string;
    site?: string;
    title?: string;
    description?: string;
    image?: string;
  };
  images?: Array<{
    url: string;
    alt?: string;
    width?: number;
    height?: number;
  }>;
  links?: Array<{
    href: string;
    text?: string;
    rel?: string | null;
  }>;
  dominant_color?: string;
  social_handles?: Record<string, string>;
  tech_stack?: string[];
  content?: {
    content_type?: string;
    word_count?: number;
    read_time_min?: number;
    heading_counts?: Record<string, number>;
  };
  cache?: CacheInfo;
  metadata?: ProcessingMetadata;
}

// SEO Audit response
interface SEOAuditResponse {
  schema_version: string;
  url: string;
  scoring: {
    overall: number;
    grade: string;
    by_category: Record<string, number>;
    deductions: Array<{
      category: string;
      reason: string;
      points: number;
      rule_id: string;
    }>;
  };
  issues: Array<{
    id: string;
    code: string;
    severity: 'critical' | 'warning' | 'info';
    category: string;
    message: string;
    recommendation: string;
    impact: string;
    priority: number;
    fixable: boolean;
  }>;
  metrics: {
    title_length: number;
    description_length: number;
    headings: Record<string, number>;
    links: {
      internal: number;
      external: number;
      broken: number;
      total: number;
    };
    images: {
      total: number;
      with_alt: number;
      without_alt: number;
    };
    word_count: number;
  };
  summary: {
    passed: string[];
    top_fixes: Array<{
      id: string;
      impact: string;
      why_it_matters: string;
      quick_fix: string;
      fixable: boolean;
    }>;
    critical_count: number;
    warning_count: number;
    info_count: number;
  };
  scannedAt: string;
  cache?: CacheInfo;
  metadata?: ProcessingMetadata;
}

// Performance response
interface PerformanceResponse {
  schema_version: string;
  run_id: string;
  url: string;
  mode: 'lab' | 'field';
  environment: {
    device_profile: string;
    network_profile: string;
    collector_version: string;
    user_agent: string;
    page_context: {
      viewport: string;
      timezone: string;
      language: string;
      cookies_enabled: boolean;
    };
  };
  summary: {
    overall: string;
    warnings: number;
    errors: number;
    suggestions: number;
    main_issue?: string;
  };
  core_web_vitals: {
    lcp: { value_ms: number; rating: string; budget_ms: number; status: string };
    inp: { value_ms: number; rating: string; budget_ms: number; status: string };
    cls: { value: number; rating: string; budget: number; status: string };
    fcp: { value_ms: number; rating: string; budget_ms: number; status: string };
    ttfb: { value_ms: number; rating: string; budget_ms: number; status: string };
  };
  performance_score: {
    schema_version: string;
    rule_version: string;
    value: number;
    scale: string;
    method: string;
    weights: {
      lcp: number;
      inp: number;
      cls: number;
      fcp: number;
      ttfb: number;
    };
  };
  resources: {
    total_requests: number;
    transfer_kb: number;
    decoded_kb: number;
    first_party: {
      requests: number;
      transfer_kb: number;
      decoded_kb: number;
    };
    third_party: {
      requests: number;
      transfer_kb: number;
      decoded_kb: number;
    };
    by_type: Record<string, {
      count: number;
      transfer_kb: number;
      decoded_kb: number;
      main_thread_ms?: number;
      unoptimized?: number;
    }>;
    top_hosts: Array<{
      host: string;
      requests: number;
      transfer_kb: number;
    }>;
    caching: {
      cacheable_requests: number;
      missing_cache_headers: number;
      cache_hit_ratio: number;
      compression: {
        gzip_or_br: number;
        uncompressed: number;
        estimated_savings_kb: number;
      };
    };
  };
  tips: Array<{
    tip_id: string;
    severity: string;
    message: string;
    learn_more: string;
  }>;
  cache?: CacheInfo;
  created_at: string;
  metadata?: ProcessingMetadata & {
    page_load_rating?: string;
  };
}

// Snapshot response
interface SnapshotResponse {
  url: string;
  screenshot_url: string;
  width: number;
  height: number;
  format: string;
  size_bytes: number;
  checksum: string;
  viewport: {
    width: number;
    height: number;
    deviceScaleFactor: number;
  };
  cache_ttl_seconds: number;
  created_at: string;
  cache?: CacheInfo;
  metadata?: ProcessingMetadata;
}

// Error response
interface ErrorResponse {
  success: false;
  error: {
    code: string;
    message: string;
    details?: Record<string, any>;
  };
}

Handling Optional Fields

normalize-data.js
function normalizeMetadata(data) {
  // Provide defaults for all optional fields
  return {
    url: data.url,
    title: data.title || data.og?.title || extractTitleFromUrl(data.url),
    description: data.description || data.og?.description || '',
    image: data.og?.image || data.twitter?.image || null,
    siteName: data.site_name || data.og?.site_name || extractDomain(data.url),
    type: data.og?.type || 'website',
    language: data.language || 'en',

    // Social media metadata
    social: {
      og: data.og || {},
      twitter: data.twitter || {}
    },

    // Links
    canonical: data.canonical?.href || data.url,

    // Check if cached
    fromCache: data.cache?.cached || false,
    processingTime: data.metadata?.processing_time_ms
  };
}

function extractDomain(url) {
  try {
    const hostname = new URL(url).hostname;
    return hostname.replace('www.', '');
  } catch {
    return 'Unknown';
  }
}

function extractTitleFromUrl(url) {
  try {
    const hostname = new URL(url).hostname;
    const domain = hostname.replace('www.', '').split('.')[0];
    return domain.charAt(0).toUpperCase() + domain.slice(1);
  } catch {
    return 'Unknown';
  }
}

// Usage
const rawData = await fetchMetadata('https://github.com');
const normalized = normalizeMetadata(rawData);

// Now safe to use without checking for undefined
console.log(normalized.title); // Always defined

Best Practices

Check HTTP Status First

Always check the HTTP status code before parsing JSON. Error responses include a success: false field.

Use Optional Chaining

Many fields are optional. Use optional chaining (?.) and nullish coalescing (??) to safely access nested properties.

Check Cache Status

Use cache.cached to determine if data is fresh. Cached responses may be stale for rapidly changing content.

Monitor Processing Times

Use metadata.processing_time_ms to track performance and identify slow endpoints or URLs.

Handle Truncated Data

The metadata endpoint may truncate large arrays (images, links). Check the truncated field to know if data was cut off.

Use TypeScript

TypeScript interfaces help catch type errors early and provide autocomplete in your IDE for better developer experience.

Related Documentation

API Endpoints

Detailed documentation for each endpoint and their specific response formats.

View Endpoints →

Error Handling

Learn about error response formats and how to handle them.

View Errors →

Best Practices

Best practices for working with API responses safely and efficiently.

Read Guide →

Code Examples

See complete examples of parsing and using API responses.

View Examples →