Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

StyleGrab

Extract design tokens from any URL. Track changes. Get notified.

StyleGrab is a high-performance design token extraction and monitoring API built with Rust. Connect to any webpage via a headless browser, extract structured design tokens, and monitor for design drift over time.

Key Features

🎨 Live CSS Extraction

Connect to any URL via CDP protocol, extract computed styles, and produce structured design tokens β€” colors, typography, spacing, and assets β€” in a single API call.

πŸ“Έ Snapshot Diffing

Every extraction creates a versioned snapshot. Compare any two snapshots to get a precise diff of what changed. Catch design drift before it ships.

πŸ”” Webhook Notifications

Subscribe to events and receive HMAC-SHA256 signed payloads. Integrate with Slack, CI pipelines, or any automation tool.

Quick Example

# Extract design tokens from a URL
curl -X POST https://api.stylegrab.dev/api/extractions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'

Response:

{
  "id": "ext_abc123",
  "status": "completed",
  "tokens": {
    "colors": [
      {"name": "primary", "value": "#3b82f6", "usage": 42},
      {"name": "background", "value": "#ffffff", "usage": 156}
    ],
    "typography": [
      {"family": "Inter", "weights": ["400", "600", "700"]}
    ],
    "spacing": ["4px", "8px", "16px", "24px", "32px"]
  }
}

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Clients                             β”‚
β”‚              (Dashboard / CLI / CI Pipelines)               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚ HTTPS
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Axum API Server                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   Auth   β”‚ β”‚ Extraction β”‚ β”‚ Teams β”‚ β”‚    Payments    β”‚  β”‚
β”‚  β”‚  Module  β”‚ β”‚   Engine   β”‚ β”‚Module β”‚ β”‚   (Stripe)     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                     β”‚                                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚              Webhooks Dispatch                        β”‚  β”‚
β”‚  β”‚   HMAC-SHA256 signing Β· 3x retry Β· exp backoff       β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β–Ό                β–Ό                β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ PostgreSQL β”‚  β”‚   Redis    β”‚  β”‚  Headless  β”‚
   β”‚  (Data)    β”‚  β”‚  (Cache)   β”‚  β”‚  Browser   β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Next Steps

Quick Start

Get started with StyleGrab in under 5 minutes.

1. Create an Account

Sign up at stylegrab.dev to create your account.

2. Get Your API Key

  1. Go to your Dashboard
  2. Navigate to Settings β†’ API Keys
  3. Click Create API Key
  4. Copy your key (you won’t see it again)

3. Make Your First Extraction

curl -X POST https://api.stylegrab.dev/api/extractions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://stripe.com"}'

4. View the Results

The response includes extracted design tokens:

{
  "id": "ext_abc123",
  "url": "https://stripe.com",
  "status": "completed",
  "tokens": {
    "colors": [
      {"name": "primary", "value": "#635bff", "usage": 89},
      {"name": "text", "value": "#425466", "usage": 234}
    ],
    "typography": [
      {"family": "system-ui", "weights": ["400", "500", "600"]}
    ],
    "spacing": ["4px", "8px", "12px", "16px", "24px", "32px", "48px"]
  },
  "created_at": "2024-01-15T10:30:00Z"
}

5. Compare Changes Over Time

Run another extraction later and compare snapshots:

# Get snapshots for an extraction
curl https://api.stylegrab.dev/api/extractions/ext_abc123/snapshots \
  -H "Authorization: Bearer YOUR_API_KEY"

# Compare two snapshots
curl "https://api.stylegrab.dev/api/extractions/ext_abc123/diff?from=snap_1&to=snap_2" \
  -H "Authorization: Bearer YOUR_API_KEY"

Next Steps

Installation

StyleGrab can be self-hosted using Docker or built from source.

Prerequisites

  • Docker and Docker Compose (recommended)
  • Or: Rust 1.75+, PostgreSQL 16+, and a headless Chromium instance

The fastest way to get StyleGrab running locally:

# Clone the repository
git clone https://github.com/stylegrab/stylegrab.git
cd stylegrab

# Copy environment template
cp .env.example .env

# Edit .env with your values (see Configuration)
nano .env

# Start all services
docker compose up -d

This starts:

  • app β€” StyleGrab API server on port 3000
  • postgres β€” PostgreSQL database
  • redis β€” Redis cache
  • browser β€” Headless Chromium for extractions

From Source

# Clone the repository
git clone https://github.com/stylegrab/stylegrab.git
cd stylegrab

# Copy environment template
cp .env.example .env

# Set up PostgreSQL and run migrations
# (ensure DATABASE_URL is set in .env)
cargo install sqlx-cli
sqlx database create
sqlx migrate run

# Build and run
cargo build --release
./target/release/stylegrab

Verify Installation

# Check health endpoint
curl http://localhost:3000/health

# Expected response
{"status": "ok"}

Headless Browser Setup

StyleGrab requires a Chromium instance with CDP (Chrome DevTools Protocol) enabled:

# Run Chromium with CDP enabled
chromium --headless --disable-gpu --remote-debugging-port=9222

# Or use the provided Docker image
docker run -d --name chrome \
  -p 9222:9222 \
  zenika/alpine-chrome \
  --no-sandbox --headless --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222

Set BROWSER_WS_URL=ws://localhost:9222 in your .env file.

Next Steps

Configuration

StyleGrab is configured via environment variables. Copy .env.example to .env and customize.

Required Variables

VariableDescription
DATABASE_URLPostgreSQL connection string
JWT_SECRETSecret key for JWT token signing (min 32 chars)
STRIPE_SECRET_KEYStripe API secret key for payments
STRIPE_WEBHOOK_SECRETStripe webhook signing secret

Optional Variables

VariableDefaultDescription
HOST0.0.0.0Server bind address
PORT3000Server port
BROWSER_WS_URLws://127.0.0.1:9222CDP WebSocket URL for headless browser
REDIS_URLredis://127.0.0.1:6379Redis connection URL
LOG_LEVELinfoLogging level (trace, debug, info, warn, error)

Example Configuration

# Database
DATABASE_URL=postgres://stylegrab:password@localhost:5432/stylegrab

# Authentication
JWT_SECRET=your-super-secret-jwt-key-min-32-characters

# Stripe (for payments)
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Browser
BROWSER_WS_URL=ws://chrome:9222

# Server
HOST=0.0.0.0
PORT=3000

# Redis (optional, for caching)
REDIS_URL=redis://localhost:6379

# Logging
LOG_LEVEL=info

Database Migrations

Migrations run automatically on startup. To run them manually:

# Using sqlx-cli
cargo install sqlx-cli
sqlx migrate run

# Or via Docker
docker compose exec app sqlx migrate run

Production Considerations

Security

  • Use strong, unique values for JWT_SECRET
  • Enable HTTPS via a reverse proxy (nginx, Caddy, etc.)
  • Restrict database access to the application server only
  • Use environment-specific Stripe keys (live vs test)

Performance

  • Enable Redis caching for frequently accessed data
  • Use connection pooling for PostgreSQL
  • Run multiple app instances behind a load balancer

Monitoring

Set LOG_LEVEL=debug for troubleshooting. In production, use info or warn.

Next Steps

Authentication

StyleGrab uses JWT tokens and API keys for authentication.

Authentication Methods

API keys are the preferred method for programmatic access:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://api.stylegrab.dev/api/extractions

JWT Tokens

Used by the web dashboard. Obtained via login:

curl -X POST https://api.stylegrab.dev/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "your-password"}'

Endpoints

Register

Create a new account.

POST /api/auth/register

Request Body:

{
  "email": "you@example.com",
  "password": "min-8-characters",
  "name": "Your Name"
}

Response:

{
  "id": "user_abc123",
  "email": "you@example.com",
  "name": "Your Name",
  "token": "eyJhbGciOiJIUzI1NiIs..."
}

Login

Authenticate and receive a JWT token.

POST /api/auth/login

Request Body:

{
  "email": "you@example.com",
  "password": "your-password"
}

Response:

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_at": "2024-01-22T10:30:00Z"
}

Get Current User

Retrieve the authenticated user’s profile.

GET /api/auth/me

Headers:

Authorization: Bearer YOUR_TOKEN

Response:

{
  "id": "user_abc123",
  "email": "you@example.com",
  "name": "Your Name",
  "created_at": "2024-01-15T10:30:00Z"
}

Create API Key

Generate a new API key for programmatic access.

POST /api/auth/api-keys

Request Body:

{
  "name": "CI Pipeline Key",
  "expires_at": "2025-01-15T00:00:00Z"
}

Response:

{
  "id": "key_abc123",
  "name": "CI Pipeline Key",
  "key": "sg_live_abc123xyz...",
  "expires_at": "2025-01-15T00:00:00Z",
  "created_at": "2024-01-15T10:30:00Z"
}

⚠️ Important: The key is only shown once. Store it securely.


Revoke API Key

Delete an API key.

DELETE /api/auth/api-keys/:id

Response:

{
  "success": true
}

Error Responses

StatusCodeDescription
401unauthorizedMissing or invalid token
401token_expiredJWT token has expired
403forbiddenValid token but insufficient permissions
422validation_errorInvalid request body

Example error response:

{
  "error": {
    "code": "unauthorized",
    "message": "Invalid or expired token"
  }
}

Best Practices

  1. Use API keys for server-to-server communication
  2. Set expiration dates on API keys
  3. Rotate keys regularly and revoke unused ones
  4. Never commit keys to version control
  5. Use environment variables to store keys

Extractions

The extractions API lets you extract design tokens from any URL.

Endpoints

Create Extraction

Start a new CSS extraction job.

POST /api/extractions

Request Body:

{
  "url": "https://example.com",
  "options": {
    "wait_for": "networkidle",
    "viewport": { "width": 1920, "height": 1080 },
    "selectors": ["#header", ".main-content"],
    "exclude_selectors": [".ad-banner"]
  }
}
FieldTypeRequiredDescription
urlstringYesURL to extract from
options.wait_forstringNoWait condition: load, domcontentloaded, networkidle
options.viewportobjectNoBrowser viewport size
options.selectorsarrayNoLimit extraction to specific elements
options.exclude_selectorsarrayNoExclude elements from extraction

Response:

{
  "id": "ext_abc123",
  "url": "https://example.com",
  "status": "pending",
  "created_at": "2024-01-15T10:30:00Z"
}

The extraction runs asynchronously. Poll the status endpoint or use webhooks to know when it’s complete.


List Extractions

Get all extractions for your account.

GET /api/extractions

Query Parameters:

ParameterTypeDefaultDescription
pageinteger1Page number
per_pageinteger20Results per page (max 100)
statusstringβ€”Filter by status

Response:

{
  "data": [
    {
      "id": "ext_abc123",
      "url": "https://example.com",
      "status": "completed",
      "created_at": "2024-01-15T10:30:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 42
  }
}

Get Extraction

Retrieve a specific extraction with its tokens.

GET /api/extractions/:id

Response:

{
  "id": "ext_abc123",
  "url": "https://example.com",
  "status": "completed",
  "tokens": {
    "colors": [
      {
        "name": "primary",
        "value": "#3b82f6",
        "format": "hex",
        "usage": 42
      },
      {
        "name": "background",
        "value": "#ffffff",
        "format": "hex",
        "usage": 156
      }
    ],
    "typography": [
      {
        "family": "Inter",
        "weights": ["400", "600", "700"],
        "sizes": ["14px", "16px", "24px", "32px"]
      }
    ],
    "spacing": [
      "4px", "8px", "12px", "16px", "24px", "32px", "48px", "64px"
    ],
    "assets": [
      {
        "type": "image",
        "url": "https://example.com/logo.svg",
        "dimensions": { "width": 120, "height": 40 }
      }
    ]
  },
  "metadata": {
    "title": "Example Domain",
    "duration_ms": 2340,
    "elements_analyzed": 847
  },
  "created_at": "2024-01-15T10:30:00Z",
  "completed_at": "2024-01-15T10:30:02Z"
}

Extraction Status

StatusDescription
pendingExtraction queued, waiting to start
runningBrowser is loading the page
completedExtraction finished successfully
failedExtraction encountered an error

Token Types

Colors

{
  "name": "primary",
  "value": "#3b82f6",
  "format": "hex",
  "usage": 42,
  "contexts": ["background-color", "border-color"]
}

Typography

{
  "family": "Inter",
  "weights": ["400", "600", "700"],
  "sizes": ["14px", "16px", "24px"],
  "line_heights": ["1.4", "1.5", "1.6"]
}

Spacing

An array of unique spacing values found in margins, paddings, and gaps:

["4px", "8px", "12px", "16px", "24px", "32px"]

Assets

{
  "type": "image",
  "url": "https://example.com/logo.svg",
  "dimensions": { "width": 120, "height": 40 },
  "format": "svg"
}

Error Responses

StatusCodeDescription
400invalid_urlThe provided URL is malformed
404not_foundExtraction not found
422extraction_failedBrowser failed to load the page
429rate_limitedToo many requests
{
  "error": {
    "code": "extraction_failed",
    "message": "Failed to load page: timeout after 30s"
  }
}

Snapshots & Diffing

Every extraction creates a snapshot. Compare snapshots to detect design changes over time.

Endpoints

List Snapshots

Get all snapshots for an extraction.

GET /api/extractions/:id/snapshots

Response:

{
  "data": [
    {
      "id": "snap_abc123",
      "extraction_id": "ext_abc123",
      "version": 3,
      "created_at": "2024-01-17T10:30:00Z"
    },
    {
      "id": "snap_xyz789",
      "extraction_id": "ext_abc123",
      "version": 2,
      "created_at": "2024-01-16T10:30:00Z"
    },
    {
      "id": "snap_def456",
      "extraction_id": "ext_abc123",
      "version": 1,
      "created_at": "2024-01-15T10:30:00Z"
    }
  ]
}

Compare Snapshots

Get a diff between two snapshots.

GET /api/extractions/:id/diff

Query Parameters:

ParameterTypeRequiredDescription
fromstringYesSource snapshot ID
tostringYesTarget snapshot ID

Example:

curl "https://api.stylegrab.dev/api/extractions/ext_abc123/diff?from=snap_def456&to=snap_abc123" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response:

{
  "from": {
    "id": "snap_def456",
    "version": 1,
    "created_at": "2024-01-15T10:30:00Z"
  },
  "to": {
    "id": "snap_abc123",
    "version": 3,
    "created_at": "2024-01-17T10:30:00Z"
  },
  "changes": {
    "colors": {
      "added": [
        {"name": "accent", "value": "#10b981"}
      ],
      "removed": [
        {"name": "secondary", "value": "#6b7280"}
      ],
      "modified": [
        {
          "name": "primary",
          "from": "#3b82f6",
          "to": "#2563eb"
        }
      ]
    },
    "typography": {
      "added": [],
      "removed": [],
      "modified": [
        {
          "family": "Inter",
          "changes": {
            "weights": {
              "added": ["800"],
              "removed": []
            }
          }
        }
      ]
    },
    "spacing": {
      "added": ["40px", "56px"],
      "removed": ["36px"]
    }
  },
  "summary": {
    "total_changes": 7,
    "colors_changed": 3,
    "typography_changed": 1,
    "spacing_changed": 3
  }
}

Diff Structure

Added

Tokens that exist in to but not in from:

{
  "added": [
    {"name": "accent", "value": "#10b981"}
  ]
}

Removed

Tokens that exist in from but not in to:

{
  "removed": [
    {"name": "secondary", "value": "#6b7280"}
  ]
}

Modified

Tokens that exist in both but with different values:

{
  "modified": [
    {
      "name": "primary",
      "from": "#3b82f6",
      "to": "#2563eb"
    }
  ]
}

Use Cases

Scheduled Monitoring

Run extractions on a schedule to detect drift:

# Cron job: Extract daily at midnight
0 0 * * * curl -X POST https://api.stylegrab.dev/api/extractions \
  -H "Authorization: Bearer $API_KEY" \
  -d '{"url": "https://yoursite.com"}'

Pre-Deploy Checks

Compare staging vs production before deploying:

# Extract from staging
STAGING=$(curl -X POST .../extractions -d '{"url": "https://staging.yoursite.com"}')
STAGING_SNAP=$(echo $STAGING | jq -r '.id')

# Compare with latest production snapshot
curl ".../extractions/$PROD_EXT_ID/diff?from=$PROD_SNAP&to=$STAGING_SNAP"

Design System Audits

Track when components drift from the design system:

  1. Extract tokens from your design system documentation
  2. Extract tokens from production
  3. Compare to find inconsistencies

Webhooks

Get notified when diffs are detected:

{
  "event": "diff.detected",
  "url": "https://yoursite.com/webhooks/stylegrab"
}

See Webhooks for setup details.

Teams

Collaborate with team members on extractions and share API access.

Endpoints

Create Team

Create a new team.

POST /api/teams

Request Body:

{
  "name": "Design System Team",
  "description": "Monitoring our component library"
}

Response:

{
  "id": "team_abc123",
  "name": "Design System Team",
  "description": "Monitoring our component library",
  "owner_id": "user_xyz789",
  "created_at": "2024-01-15T10:30:00Z"
}

List Teams

Get all teams you belong to.

GET /api/teams

Response:

{
  "data": [
    {
      "id": "team_abc123",
      "name": "Design System Team",
      "role": "owner",
      "member_count": 5,
      "created_at": "2024-01-15T10:30:00Z"
    },
    {
      "id": "team_def456",
      "name": "Marketing Site",
      "role": "member",
      "member_count": 3,
      "created_at": "2024-01-10T10:30:00Z"
    }
  ]
}

Add Team Member

Invite a user to your team.

POST /api/teams/:id/members

Request Body:

{
  "email": "teammate@example.com",
  "role": "member"
}
RolePermissions
ownerFull access, can delete team
adminManage members, create extractions
memberView and create extractions

Response:

{
  "id": "member_abc123",
  "user_id": "user_xyz789",
  "email": "teammate@example.com",
  "role": "member",
  "status": "pending",
  "invited_at": "2024-01-15T10:30:00Z"
}

Remove Team Member

Remove a user from the team.

DELETE /api/teams/:id/members/:user_id

Response:

{
  "success": true
}

Team Resources

When you create a resource (extraction, webhook, etc.) within a team context, it’s shared with all team members.

Creating Team Resources

Include the team_id header:

curl -X POST https://api.stylegrab.dev/api/extractions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Team-ID: team_abc123" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'

Listing Team Resources

Filter by team:

curl "https://api.stylegrab.dev/api/extractions?team_id=team_abc123" \
  -H "Authorization: Bearer YOUR_API_KEY"

Billing

Teams share a single subscription. The team owner manages billing.

  • Free: 1 team, 3 members max
  • Pro: Unlimited teams, 10 members each
  • Enterprise: Unlimited teams and members

See Payments for subscription details.

Error Responses

StatusCodeDescription
403not_team_memberYou’re not a member of this team
403insufficient_roleYour role doesn’t allow this action
404team_not_foundTeam doesn’t exist
409already_memberUser is already a team member

Webhooks

Receive real-time notifications when events occur in your StyleGrab account.

Endpoints

Register Webhook

Create a new webhook subscription.

POST /api/webhooks

Request Body:

{
  "url": "https://yoursite.com/webhooks/stylegrab",
  "events": ["extraction.completed", "diff.detected"],
  "secret": "your-webhook-secret"
}
FieldTypeRequiredDescription
urlstringYesHTTPS endpoint to receive events
eventsarrayYesEvents to subscribe to
secretstringNoSecret for HMAC signature (auto-generated if not provided)

Response:

{
  "id": "wh_abc123",
  "url": "https://yoursite.com/webhooks/stylegrab",
  "events": ["extraction.completed", "diff.detected"],
  "secret": "whsec_abc123...",
  "active": true,
  "created_at": "2024-01-15T10:30:00Z"
}

⚠️ Important: Store the secret securely. You’ll need it to verify webhook signatures.


List Webhooks

Get all registered webhooks.

GET /api/webhooks

Response:

{
  "data": [
    {
      "id": "wh_abc123",
      "url": "https://yoursite.com/webhooks/stylegrab",
      "events": ["extraction.completed", "diff.detected"],
      "active": true,
      "created_at": "2024-01-15T10:30:00Z"
    }
  ]
}

Update Webhook

Modify a webhook subscription.

PUT /api/webhooks/:id

Request Body:

{
  "url": "https://newsite.com/webhooks",
  "events": ["extraction.completed"],
  "active": false
}

Delete Webhook

Remove a webhook subscription.

DELETE /api/webhooks/:id

Events

extraction.completed

Fired when a CSS extraction finishes successfully.

{
  "event": "extraction.completed",
  "timestamp": "2024-01-15T10:30:02Z",
  "data": {
    "extraction_id": "ext_abc123",
    "url": "https://example.com",
    "snapshot_id": "snap_xyz789",
    "token_count": {
      "colors": 12,
      "typography": 3,
      "spacing": 8
    }
  }
}

diff.detected

Fired when changes are detected between snapshots.

{
  "event": "diff.detected",
  "timestamp": "2024-01-15T10:30:02Z",
  "data": {
    "extraction_id": "ext_abc123",
    "url": "https://example.com",
    "from_snapshot": "snap_abc123",
    "to_snapshot": "snap_xyz789",
    "changes": {
      "colors_changed": 3,
      "typography_changed": 1,
      "spacing_changed": 2,
      "total_changes": 6
    }
  }
}

snapshot.created

Fired when a new snapshot is stored.

{
  "event": "snapshot.created",
  "timestamp": "2024-01-15T10:30:02Z",
  "data": {
    "snapshot_id": "snap_xyz789",
    "extraction_id": "ext_abc123",
    "version": 3
  }
}

Signature Verification

All webhook payloads are signed using HMAC-SHA256. Verify the signature to ensure the request came from StyleGrab.

Headers

HeaderDescription
X-StyleGrab-SignatureHMAC-SHA256 signature
X-StyleGrab-TimestampUnix timestamp of the request

Verification Example (Node.js)

const crypto = require('crypto');

function verifyWebhook(payload, signature, timestamp, secret) {
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  
  // Use timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Express middleware
app.post('/webhooks/stylegrab', (req, res) => {
  const signature = req.headers['x-stylegrab-signature'];
  const timestamp = req.headers['x-stylegrab-timestamp'];
  
  if (!verifyWebhook(req.rawBody, signature, timestamp, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process the webhook
  const event = req.body;
  console.log(`Received ${event.event}`);
  
  res.status(200).send('OK');
});

Verification Example (Python)

import hmac
import hashlib

def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
    signed_payload = f"{timestamp}.{payload.decode()}"
    expected = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

Retry Policy

StyleGrab retries failed webhook deliveries with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes

After 3 failed attempts, the webhook is marked as failed. Fix your endpoint and the next event will attempt delivery again.

Best Practices

  1. Respond quickly β€” Return 2xx within 5 seconds
  2. Process async β€” Queue the webhook for background processing
  3. Verify signatures β€” Always verify the HMAC signature
  4. Handle duplicates β€” Webhooks may be delivered more than once
  5. Use HTTPS β€” Webhook URLs must use HTTPS

Payments

StyleGrab uses Stripe for subscription billing.

Plans

PlanPriceExtractionsTeamsFeatures
Free$0/mo50/month1Basic extraction
Pro$29/mo1,000/month5Webhooks, diffing, priority
EnterpriseCustomUnlimitedUnlimitedSLA, dedicated support

Endpoints

Create Checkout Session

Start a Stripe checkout flow.

POST /api/payments/checkout

Request Body:

{
  "plan": "pro",
  "success_url": "https://yoursite.com/success",
  "cancel_url": "https://yoursite.com/cancel"
}

Response:

{
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_..."
}

Redirect the user to checkout_url to complete payment.


Customer Portal

Get a link to the Stripe customer portal for managing subscriptions.

POST /api/payments/portal

Request Body:

{
  "return_url": "https://yoursite.com/dashboard"
}

Response:

{
  "portal_url": "https://billing.stripe.com/p/session/..."
}

The customer portal allows users to:

  • Update payment method
  • View invoices
  • Cancel subscription
  • Change plan

Subscription Status

Get the current subscription status.

GET /api/payments/status

Response:

{
  "plan": "pro",
  "status": "active",
  "current_period_end": "2024-02-15T00:00:00Z",
  "cancel_at_period_end": false,
  "usage": {
    "extractions_used": 342,
    "extractions_limit": 1000,
    "reset_date": "2024-02-01T00:00:00Z"
  }
}

Status Values

StatusDescription
activeSubscription is active
past_duePayment failed, retrying
canceledSubscription ended
trialingIn trial period

Stripe Webhooks

StyleGrab processes Stripe webhooks automatically. No action needed, but for reference:

EventAction
checkout.session.completedActivate subscription
invoice.paidRenew subscription
invoice.payment_failedMark as past due
customer.subscription.deletedDeactivate subscription

Usage Limits

When you exceed your plan’s extraction limit:

{
  "error": {
    "code": "usage_limit_exceeded",
    "message": "Monthly extraction limit reached (50/50). Upgrade to Pro for more.",
    "upgrade_url": "https://stylegrab.dev/pricing"
  }
}

Enterprise

For custom plans, volume discounts, or SLAs, contact sales@stylegrab.dev.

Enterprise features:

  • Unlimited extractions
  • Dedicated infrastructure
  • Custom SLA (99.9% uptime)
  • Priority support
  • SSO/SAML authentication
  • Audit logs

Your First Extraction

This guide walks through extracting design tokens from a real website.

Prerequisites

Step 1: Pick a Target URL

For this example, we’ll extract tokens from Stripe’s websiteβ€”they have a well-designed system with clear color and typography choices.

https://stripe.com

Step 2: Start the Extraction

curl -X POST https://api.stylegrab.dev/api/extractions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://stripe.com",
    "options": {
      "wait_for": "networkidle"
    }
  }'

Response:

{
  "id": "ext_h3k9x2m1",
  "url": "https://stripe.com",
  "status": "pending",
  "created_at": "2024-01-15T10:30:00Z"
}

Save the idβ€”you’ll need it to fetch results.

Step 3: Check Status

Extractions run asynchronously. Poll until status is completed:

curl https://api.stylegrab.dev/api/extractions/ext_h3k9x2m1 \
  -H "Authorization: Bearer YOUR_API_KEY"

Or set up a webhook to get notified automatically.

Step 4: Review the Tokens

Once complete, the response includes extracted tokens:

{
  "id": "ext_h3k9x2m1",
  "url": "https://stripe.com",
  "status": "completed",
  "tokens": {
    "colors": [
      {"name": "stripe-purple", "value": "#635bff", "usage": 89},
      {"name": "text-dark", "value": "#0a2540", "usage": 342},
      {"name": "text-gray", "value": "#425466", "usage": 156},
      {"name": "background", "value": "#ffffff", "usage": 78}
    ],
    "typography": [
      {
        "family": "system-ui, -apple-system, BlinkMacSystemFont",
        "weights": ["400", "500", "600", "700"],
        "sizes": ["14px", "16px", "18px", "24px", "32px", "48px"]
      }
    ],
    "spacing": [
      "4px", "8px", "12px", "16px", "24px", "32px", "48px", "64px", "96px"
    ]
  },
  "metadata": {
    "title": "Stripe | Financial Infrastructure for the Internet",
    "duration_ms": 3240,
    "elements_analyzed": 1247
  }
}

Step 5: Export to Your Format

Use the extracted tokens in your project:

CSS Custom Properties

:root {
  --color-primary: #635bff;
  --color-text: #0a2540;
  --color-text-muted: #425466;
  
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 32px;
  --spacing-xl: 64px;
}

Tailwind Config

module.exports = {
  theme: {
    extend: {
      colors: {
        primary: '#635bff',
        text: '#0a2540',
        'text-muted': '#425466',
      }
    }
  }
}

Design Tokens JSON

{
  "color": {
    "primary": { "value": "#635bff" },
    "text": { "value": "#0a2540" }
  },
  "spacing": {
    "xs": { "value": "4px" },
    "sm": { "value": "8px" }
  }
}

What’s Next?

Setting Up Webhooks

Get real-time notifications when extractions complete or design changes are detected.

Why Webhooks?

Instead of polling the API for status updates, webhooks push events to your server as they happen. This enables:

  • Instant notifications β€” No delay waiting for poll intervals
  • Reduced API calls β€” Don’t waste requests checking status
  • Automation β€” Trigger workflows automatically

Step 1: Create an Endpoint

Your webhook endpoint must:

  • Accept POST requests
  • Return 2xx status within 5 seconds
  • Verify the HMAC signature

Example (Node.js/Express)

const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = process.env.STYLEGRAB_WEBHOOK_SECRET;

// Parse raw body for signature verification
app.use('/webhooks/stylegrab', express.raw({ type: '*/*' }));

app.post('/webhooks/stylegrab', (req, res) => {
  // Verify signature
  const signature = req.headers['x-stylegrab-signature'];
  const timestamp = req.headers['x-stylegrab-timestamp'];
  
  const signedPayload = `${timestamp}.${req.body.toString()}`;
  const expectedSig = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(signedPayload)
    .digest('hex');
  
  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSig))) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process the event
  const event = JSON.parse(req.body);
  console.log(`Received: ${event.event}`);
  
  switch (event.event) {
    case 'extraction.completed':
      handleExtractionComplete(event.data);
      break;
    case 'diff.detected':
      handleDiffDetected(event.data);
      break;
  }
  
  res.status(200).send('OK');
});

function handleExtractionComplete(data) {
  console.log(`Extraction ${data.extraction_id} finished`);
  // Send to Slack, update database, etc.
}

function handleDiffDetected(data) {
  console.log(`${data.changes.total_changes} changes detected!`);
  // Alert the team, block deployment, etc.
}

app.listen(3000);

Step 2: Register the Webhook

curl -X POST https://api.stylegrab.dev/api/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yoursite.com/webhooks/stylegrab",
    "events": ["extraction.completed", "diff.detected", "snapshot.created"]
  }'

Response:

{
  "id": "wh_abc123",
  "url": "https://yoursite.com/webhooks/stylegrab",
  "events": ["extraction.completed", "diff.detected", "snapshot.created"],
  "secret": "whsec_a1b2c3d4e5f6...",
  "active": true
}

⚠️ Save the secret! Store it as STYLEGRAB_WEBHOOK_SECRET in your environment. You won’t see it again.

Step 3: Test the Webhook

Run an extraction and watch for the webhook:

# Trigger an extraction
curl -X POST https://api.stylegrab.dev/api/extractions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"url": "https://example.com"}'

Your endpoint should receive:

{
  "event": "extraction.completed",
  "timestamp": "2024-01-15T10:30:02Z",
  "data": {
    "extraction_id": "ext_abc123",
    "url": "https://example.com",
    "snapshot_id": "snap_xyz789"
  }
}

Integrations

Slack Notifications

async function handleDiffDetected(data) {
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `🎨 Design drift detected on ${data.url}`,
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `*${data.changes.total_changes} changes* detected between snapshots`
          }
        },
        {
          type: 'section',
          fields: [
            { type: 'mrkdwn', text: `*Colors:* ${data.changes.colors_changed}` },
            { type: 'mrkdwn', text: `*Typography:* ${data.changes.typography_changed}` }
          ]
        }
      ]
    })
  });
}

GitHub Actions

Trigger a workflow when design changes are detected:

# .github/workflows/design-check.yml
on:
  repository_dispatch:
    types: [design_drift]

jobs:
  alert:
    runs-on: ubuntu-latest
    steps:
      - name: Create Issue
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: 'Design drift detected',
              body: 'StyleGrab detected changes. Review the diff at...'
            })

Troubleshooting

Webhook not receiving events

  1. Check your endpoint is publicly accessible
  2. Verify HTTPS is working (webhooks require HTTPS)
  3. Check the webhook is active in the API

Signature verification failing

  1. Ensure you’re using the raw request body (not parsed JSON)
  2. Check the secret matches what was returned when creating the webhook
  3. Verify timing β€” reject requests with timestamps > 5 minutes old

Webhook marked as failed

After 3 failed delivery attempts, the webhook is marked failed. Fix your endpoint and the next event will retry delivery.

Next Steps

Monitoring Design Drift

Track how your site’s design changes over time and catch unintended modifications before they reach production.

What is Design Drift?

Design drift happens when:

  • A developer changes a color that should match the design system
  • A CSS update unintentionally affects other components
  • Third-party scripts inject styles
  • Responsive breakpoints break existing layouts

StyleGrab detects these changes by comparing snapshots of extracted design tokens.

Setting Up Monitoring

1. Create a Scheduled Extraction

Set up a cron job or scheduled task to extract tokens regularly:

# Daily extraction at midnight UTC
0 0 * * * curl -X POST https://api.stylegrab.dev/api/extractions \
  -H "Authorization: Bearer $STYLEGRAB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://yoursite.com"}'

2. Subscribe to Diff Events

Register a webhook to receive notifications when changes are detected:

curl -X POST https://api.stylegrab.dev/api/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yoursite.com/webhooks/design-drift",
    "events": ["diff.detected"]
  }'

3. Review Changes

When drift is detected, you’ll receive:

{
  "event": "diff.detected",
  "data": {
    "extraction_id": "ext_abc123",
    "url": "https://yoursite.com",
    "from_snapshot": "snap_abc123",
    "to_snapshot": "snap_xyz789",
    "changes": {
      "colors_changed": 2,
      "typography_changed": 0,
      "spacing_changed": 1,
      "total_changes": 3
    }
  }
}

Get the full diff:

curl "https://api.stylegrab.dev/api/extractions/ext_abc123/diff?from=snap_abc123&to=snap_xyz789" \
  -H "Authorization: Bearer YOUR_API_KEY"

Setting Thresholds

Not all changes are problems. Set up smart alerting:

async function handleDiffDetected(data) {
  const { changes } = data;
  
  // Only alert on significant changes
  if (changes.total_changes < 3) {
    console.log('Minor change, skipping alert');
    return;
  }
  
  // High priority: color changes
  if (changes.colors_changed > 0) {
    await sendSlackAlert('🚨 Color change detected', 'high');
  }
  
  // Medium priority: typography
  if (changes.typography_changed > 0) {
    await sendSlackAlert('⚠️ Typography change detected', 'medium');
  }
  
  // Low priority: spacing only
  if (changes.colors_changed === 0 && changes.typography_changed === 0) {
    await sendSlackAlert('ℹ️ Spacing change detected', 'low');
  }
}

Comparing Environments

Monitor differences between environments:

#!/bin/bash
# compare-envs.sh

# Extract from staging
STAGING=$(curl -s -X POST https://api.stylegrab.dev/api/extractions \
  -H "Authorization: Bearer $API_KEY" \
  -d '{"url": "https://staging.yoursite.com"}' | jq -r '.id')

# Extract from production
PROD=$(curl -s -X POST https://api.stylegrab.dev/api/extractions \
  -H "Authorization: Bearer $API_KEY" \
  -d '{"url": "https://yoursite.com"}' | jq -r '.id')

# Wait for completion
sleep 10

# Get latest snapshots
STAGING_SNAP=$(curl -s "https://api.stylegrab.dev/api/extractions/$STAGING/snapshots" \
  -H "Authorization: Bearer $API_KEY" | jq -r '.data[0].id')
  
PROD_SNAP=$(curl -s "https://api.stylegrab.dev/api/extractions/$PROD/snapshots" \
  -H "Authorization: Bearer $API_KEY" | jq -r '.data[0].id')

# Compare
curl "https://api.stylegrab.dev/api/extractions/$STAGING/diff?from=$PROD_SNAP&to=$STAGING_SNAP" \
  -H "Authorization: Bearer $API_KEY"

Best Practices

1. Baseline Your Design System

Create an extraction from your design system documentation or Storybook:

curl -X POST https://api.stylegrab.dev/api/extractions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"url": "https://design.yoursite.com"}'

Compare production against this baseline to find inconsistencies.

2. Monitor Critical Pages

Focus monitoring on high-value pages:

  • Homepage
  • Pricing page
  • Checkout flow
  • Login/signup

3. Ignore Expected Changes

Some changes are intentional (A/B tests, promotions). Track these separately:

# Tag extractions for context
curl -X POST https://api.stylegrab.dev/api/extractions \
  -d '{
    "url": "https://yoursite.com",
    "metadata": {
      "context": "holiday-promo",
      "expected_changes": ["colors"]
    }
  }'

4. Set Up Escalation

// Escalate if changes persist across multiple snapshots
const recentDiffs = await getRecentDiffs(extractionId, { limit: 3 });

if (recentDiffs.every(d => d.changes.colors_changed > 0)) {
  await escalateToDesignTeam('Persistent color drift detected');
}

Next Steps

CI/CD Integration

Catch design drift before it ships by integrating StyleGrab into your deployment pipeline.

Overview

Add StyleGrab checks to your CI/CD pipeline to:

  • Compare staging against production before deploying
  • Fail builds when unexpected design changes are detected
  • Generate design diff reports on pull requests

GitHub Actions

Basic Check

# .github/workflows/design-check.yml
name: Design Check

on:
  pull_request:
    branches: [main]

jobs:
  design-diff:
    runs-on: ubuntu-latest
    steps:
      - name: Extract from Preview
        id: preview
        run: |
          RESPONSE=$(curl -s -X POST https://api.stylegrab.dev/api/extractions \
            -H "Authorization: Bearer ${{ secrets.STYLEGRAB_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d '{"url": "${{ env.PREVIEW_URL }}"}')
          echo "extraction_id=$(echo $RESPONSE | jq -r '.id')" >> $GITHUB_OUTPUT
      
      - name: Wait for Extraction
        run: sleep 15
      
      - name: Compare with Production
        id: diff
        run: |
          # Get preview snapshot
          PREVIEW_SNAP=$(curl -s "https://api.stylegrab.dev/api/extractions/${{ steps.preview.outputs.extraction_id }}/snapshots" \
            -H "Authorization: Bearer ${{ secrets.STYLEGRAB_API_KEY }}" | jq -r '.data[0].id')
          
          # Compare against production baseline
          DIFF=$(curl -s "https://api.stylegrab.dev/api/extractions/${{ steps.preview.outputs.extraction_id }}/diff?from=${{ vars.PROD_BASELINE_SNAP }}&to=$PREVIEW_SNAP" \
            -H "Authorization: Bearer ${{ secrets.STYLEGRAB_API_KEY }}")
          
          TOTAL_CHANGES=$(echo $DIFF | jq '.summary.total_changes')
          echo "changes=$TOTAL_CHANGES" >> $GITHUB_OUTPUT
          echo "$DIFF" > diff.json
      
      - name: Comment on PR
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require('fs');
            const diff = JSON.parse(fs.readFileSync('diff.json', 'utf8'));
            
            let body = '## 🎨 Design Token Changes\n\n';
            
            if (diff.summary.total_changes === 0) {
              body += 'βœ… No design token changes detected.';
            } else {
              body += `⚠️ **${diff.summary.total_changes} changes detected**\n\n`;
              body += `| Type | Changes |\n|------|--------|\n`;
              body += `| Colors | ${diff.summary.colors_changed} |\n`;
              body += `| Typography | ${diff.summary.typography_changed} |\n`;
              body += `| Spacing | ${diff.summary.spacing_changed} |\n`;
            }
            
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: body
            });
      
      - name: Fail on Unexpected Changes
        if: steps.diff.outputs.changes > 5
        run: |
          echo "Too many design changes detected (${{ steps.diff.outputs.changes }})"
          exit 1

Required Secrets

Add these to your repository settings:

  • STYLEGRAB_API_KEY β€” Your API key
  • PROD_BASELINE_SNAP β€” Snapshot ID of your production baseline

GitLab CI

# .gitlab-ci.yml
design-check:
  stage: test
  image: curlimages/curl:latest
  script:
    - |
      # Start extraction
      EXTRACTION=$(curl -s -X POST https://api.stylegrab.dev/api/extractions \
        -H "Authorization: Bearer $STYLEGRAB_API_KEY" \
        -d "{\"url\": \"$CI_ENVIRONMENT_URL\"}")
      EXTRACTION_ID=$(echo $EXTRACTION | jq -r '.id')
      
      # Wait and get snapshot
      sleep 15
      SNAPSHOT=$(curl -s "https://api.stylegrab.dev/api/extractions/$EXTRACTION_ID/snapshots" \
        -H "Authorization: Bearer $STYLEGRAB_API_KEY" | jq -r '.data[0].id')
      
      # Compare
      DIFF=$(curl -s "https://api.stylegrab.dev/api/extractions/$EXTRACTION_ID/diff?from=$PROD_BASELINE&to=$SNAPSHOT" \
        -H "Authorization: Bearer $STYLEGRAB_API_KEY")
      
      CHANGES=$(echo $DIFF | jq '.summary.total_changes')
      echo "Design changes detected: $CHANGES"
      
      if [ "$CHANGES" -gt 5 ]; then
        echo "Too many design changes. Failing pipeline."
        exit 1
      fi
  only:
    - merge_requests

CircleCI

# .circleci/config.yml
version: 2.1

jobs:
  design-check:
    docker:
      - image: cimg/base:stable
    steps:
      - run:
          name: Check Design Tokens
          command: |
            RESPONSE=$(curl -s -X POST https://api.stylegrab.dev/api/extractions \
              -H "Authorization: Bearer $STYLEGRAB_API_KEY" \
              -d '{"url": "'$PREVIEW_URL'"}')
            
            EXTRACTION_ID=$(echo $RESPONSE | jq -r '.id')
            sleep 15
            
            SNAPSHOT=$(curl -s "https://api.stylegrab.dev/api/extractions/$EXTRACTION_ID/snapshots" \
              -H "Authorization: Bearer $STYLEGRAB_API_KEY" | jq -r '.data[0].id')
            
            CHANGES=$(curl -s "https://api.stylegrab.dev/api/extractions/$EXTRACTION_ID/diff?from=$PROD_BASELINE&to=$SNAPSHOT" \
              -H "Authorization: Bearer $STYLEGRAB_API_KEY" | jq '.summary.total_changes')
            
            echo "Changes: $CHANGES"
            [ "$CHANGES" -le 5 ] || exit 1

workflows:
  version: 2
  test:
    jobs:
      - design-check

Best Practices

1. Use Preview Environments

Always compare against a preview/staging URL, not localhost:

env:
  PREVIEW_URL: https://preview-${{ github.event.pull_request.number }}.yoursite.dev

2. Set Reasonable Thresholds

Start permissive and tighten over time:

  • Warning: > 3 changes
  • Failure: > 10 changes

3. Allow Intentional Changes

Add labels to bypass checks for intentional design updates:

- name: Check for Skip Label
  if: contains(github.event.pull_request.labels.*.name, 'design-update')
  run: echo "Skipping design check - intentional update"

4. Update Baseline After Deploys

# Post-deploy job
update-baseline:
  needs: deploy
  steps:
    - run: |
        EXTRACTION=$(curl -s -X POST https://api.stylegrab.dev/api/extractions \
          -H "Authorization: Bearer $STYLEGRAB_API_KEY" \
          -d '{"url": "https://yoursite.com"}')
        
        sleep 15
        
        SNAPSHOT=$(curl -s "https://api.stylegrab.dev/api/extractions/$(echo $EXTRACTION | jq -r '.id')/snapshots" \
          -H "Authorization: Bearer $STYLEGRAB_API_KEY" | jq -r '.data[0].id')
        
        # Store as new baseline
        gh variable set PROD_BASELINE_SNAP --body "$SNAPSHOT"

Troubleshooting

Extraction timing out

Increase the wait time or use webhooks instead of polling:

- name: Wait for Webhook
  uses: lewagon/wait-on-check-action@v1.3.4
  with:
    ref: ${{ github.ref }}
    check-name: 'stylegrab-extraction'
    wait-interval: 10

Flaky diffs

Some sites have dynamic content. Use selectors to target stable areas:

curl -X POST https://api.stylegrab.dev/api/extractions \
  -d '{
    "url": "https://yoursite.com",
    "options": {
      "selectors": ["#main-content", ".design-system-components"],
      "exclude_selectors": [".ad-banner", ".dynamic-promo"]
    }
  }'

Next Steps

Browser Extension Installation

The StyleGrab browser extension lets you extract design tokens directly from any page you’re viewing.

Download

Chrome Web Store

Install from the Chrome Web Store (recommended).

Manual Installation

For development or testing:

  1. Download the extension ZIP from GitHub Releases
  2. Extract the ZIP file
  3. Open chrome://extensions/ in Chrome
  4. Enable β€œDeveloper mode” (toggle in top right)
  5. Click β€œLoad unpacked”
  6. Select the extracted folder

Setup

1. Connect Your Account

After installation, click the StyleGrab icon in your toolbar and sign in with your StyleGrab account.

2. Grant Permissions

The extension requires permission to:

  • Read page content β€” To extract CSS and design tokens
  • Access tabs β€” To work on the active tab

Verify Installation

  1. Navigate to any website
  2. Click the StyleGrab icon
  3. Click β€œExtract Tokens”
  4. View the extracted design tokens in the side panel

Troubleshooting

Extension icon is grayed out

The extension only works on http:// and https:// pages. It won’t activate on:

  • chrome:// pages
  • chrome-extension:// pages
  • Local files (unless enabled in extension settings)

β€œPermission denied” errors

  1. Go to chrome://extensions/
  2. Find StyleGrab and click β€œDetails”
  3. Ensure β€œSite access” is set to β€œOn all sites”

Extraction not working

Some sites block content scripts. Try:

  1. Refresh the page
  2. Disable other extensions that might interfere
  3. Check the browser console for errors

Next Steps

Using the Browser Extension

Extract and export design tokens directly from any webpage.

Quick Start

  1. Navigate to the page you want to extract from
  2. Click the StyleGrab icon in your toolbar
  3. Click Extract Tokens
  4. View results in the side panel

Features

Extract Full Page

Extracts all design tokens from the current page:

  • Colors (backgrounds, text, borders)
  • Typography (fonts, sizes, weights)
  • Spacing (margins, paddings, gaps)
  • Assets (images, icons)

Element Picker

Target specific elements:

  1. Click Pick Element in the toolbar
  2. Hover over elements on the page (they’ll highlight)
  3. Click to select an element
  4. Only tokens from that element tree are extracted

Compare Mode

Compare tokens from different pages:

  1. Extract from Page A
  2. Click Save as Baseline
  3. Navigate to Page B
  4. Extract and click Compare
  5. View the diff

Export Options

Copy to Clipboard

Click any token to copy its value.

Export Formats

Click Export to download tokens as:

  • CSS Variables

    :root {
      --color-primary: #3b82f6;
      --color-text: #1f2937;
    }
    
  • Tailwind Config

    module.exports = {
      theme: {
        colors: {
          primary: '#3b82f6',
          text: '#1f2937'
        }
      }
    }
    
  • Design Tokens JSON

    {
      "color": {
        "primary": { "value": "#3b82f6" }
      }
    }
    
  • SCSS Variables

    $color-primary: #3b82f6;
    $color-text: #1f2937;
    

Save to Account

Click Save to StyleGrab to:

  • Store the extraction in your account
  • Enable snapshot comparison over time
  • Share with team members

Keyboard Shortcuts

ShortcutAction
Ctrl+Shift+EQuick extract current page
Ctrl+Shift+PToggle element picker
EscapeCancel current operation

Customize shortcuts at chrome://extensions/shortcuts.

Side Panel

The side panel shows:

Colors

Grouped by usage:

  • Backgrounds β€” Background colors
  • Text β€” Text colors
  • Borders β€” Border colors
  • Other β€” Shadows, accents, etc.

Each color shows:

  • Hex/RGB value
  • Usage count (how many times it appears)
  • Visual swatch

Typography

  • Font families
  • Font weights
  • Font sizes
  • Line heights

Spacing

Common spacing values found in margins, paddings, and gaps, sorted by frequency.

Settings

Access via the gear icon:

  • Theme β€” Light/dark mode
  • Auto-extract β€” Extract automatically when navigating
  • Default format β€” Preferred export format
  • API key β€” For saving to your account

Tips

Extracting from SPAs

Single-page apps may load content dynamically. Wait for the page to fully load before extracting.

Dealing with iframes

The extension extracts from the main frame by default. To extract from an iframe:

  1. Right-click the iframe
  2. Select β€œOpen frame in new tab”
  3. Extract from there

Ignoring elements

Some elements (ads, tracking pixels) pollute results. Use the element picker to target only the relevant parts of the page.

Error Codes

StyleGrab API returns structured error responses with machine-readable codes.

Error Response Format

{
  "error": {
    "code": "not_found",
    "message": "Extraction not found",
    "details": {}
  }
}

Authentication Errors

CodeHTTP StatusDescription
unauthorized401Missing or invalid authentication
token_expired401JWT token has expired
invalid_api_key401API key is invalid or revoked
forbidden403Valid auth but insufficient permissions

Validation Errors

CodeHTTP StatusDescription
validation_error422Request body failed validation
invalid_url400URL is malformed or unreachable
invalid_webhook_url400Webhook URL must be HTTPS
missing_field400Required field not provided

Example:

{
  "error": {
    "code": "validation_error",
    "message": "Validation failed",
    "details": {
      "fields": {
        "url": "must be a valid URL",
        "events": "must include at least one event"
      }
    }
  }
}

Resource Errors

CodeHTTP StatusDescription
not_found404Resource does not exist
already_exists409Resource already exists
conflict409Operation conflicts with current state

Extraction Errors

CodeHTTP StatusDescription
extraction_failed422Browser failed to load the page
extraction_timeout422Page load timed out
page_blocked422Page blocked the extraction
invalid_content422Page returned non-HTML content

Example:

{
  "error": {
    "code": "extraction_failed",
    "message": "Failed to load page",
    "details": {
      "reason": "SSL certificate error",
      "url": "https://example.com"
    }
  }
}

Rate Limiting

CodeHTTP StatusDescription
rate_limited429Too many requests
usage_limit_exceeded429Monthly quota exceeded

Headers included:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705312800
Retry-After: 60

Billing Errors

CodeHTTP StatusDescription
subscription_required402Feature requires paid plan
subscription_expired402Subscription has ended
payment_failed402Payment processing failed

Webhook Errors

CodeHTTP StatusDescription
webhook_delivery_failedβ€”Webhook endpoint returned non-2xx
webhook_timeoutβ€”Endpoint didn’t respond in time
webhook_disabledβ€”Webhook was disabled after failures

Internal Errors

CodeHTTP StatusDescription
internal_error500Unexpected server error
service_unavailable503Service temporarily unavailable

For 5xx errors, retry with exponential backoff. If the error persists, contact support.

Handling Errors

JavaScript

async function extractTokens(url) {
  const response = await fetch('https://api.stylegrab.dev/api/extractions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ url })
  });
  
  if (!response.ok) {
    const error = await response.json();
    
    switch (error.error.code) {
      case 'rate_limited':
        const retryAfter = response.headers.get('Retry-After');
        await sleep(retryAfter * 1000);
        return extractTokens(url); // Retry
        
      case 'unauthorized':
        throw new Error('Invalid API key');
        
      case 'extraction_failed':
        console.error('Page could not be loaded:', error.error.details);
        return null;
        
      default:
        throw new Error(error.error.message);
    }
  }
  
  return response.json();
}

Python

import requests
import time

def extract_tokens(url):
    response = requests.post(
        'https://api.stylegrab.dev/api/extractions',
        headers={'Authorization': f'Bearer {API_KEY}'},
        json={'url': url}
    )
    
    if response.status_code == 429:
        retry_after = int(response.headers.get('Retry-After', 60))
        time.sleep(retry_after)
        return extract_tokens(url)
    
    if not response.ok:
        error = response.json()['error']
        raise Exception(f"{error['code']}: {error['message']}")
    
    return response.json()

Rate Limits

StyleGrab applies rate limits to ensure fair usage and service stability.

Limits by Plan

PlanRequests/minExtractions/dayExtractions/month
Free201050
Pro1001001,000
EnterpriseCustomCustomUnlimited

Rate Limit Headers

Every response includes rate limit information:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705312800
HeaderDescription
X-RateLimit-LimitMax requests allowed per window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when the window resets

When Rate Limited

When you exceed the limit, you’ll receive:

HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705312845
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please retry after 45 seconds."
  }
}

Handling Rate Limits

Exponential Backoff

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);
    
    if (response.status !== 429) {
      return response;
    }
    
    const retryAfter = response.headers.get('Retry-After') || Math.pow(2, attempt);
    console.log(`Rate limited. Retrying in ${retryAfter}s...`);
    await new Promise(r => setTimeout(r, retryAfter * 1000));
  }
  
  throw new Error('Max retries exceeded');
}

Proactive Throttling

Check remaining quota before making requests:

class RateLimiter {
  constructor() {
    this.remaining = Infinity;
    this.resetTime = 0;
  }
  
  updateFromHeaders(headers) {
    this.remaining = parseInt(headers.get('X-RateLimit-Remaining'));
    this.resetTime = parseInt(headers.get('X-RateLimit-Reset')) * 1000;
  }
  
  async waitIfNeeded() {
    if (this.remaining <= 0) {
      const waitMs = this.resetTime - Date.now();
      if (waitMs > 0) {
        console.log(`Waiting ${waitMs}ms for rate limit reset...`);
        await new Promise(r => setTimeout(r, waitMs));
      }
    }
  }
}

Endpoint-Specific Limits

Some endpoints have stricter limits:

EndpointLimit
POST /api/extractions10/min (Free), 50/min (Pro)
GET /api/extractions/:id/diff30/min
POST /api/auth/login5/min per IP

Increasing Limits

Upgrade Your Plan

The easiest way to increase limits is to upgrade to Pro or Enterprise.

Request Higher Limits

For Pro customers needing higher limits, contact support@stylegrab.dev with:

  • Your use case
  • Expected request volume
  • Peak usage patterns

Enterprise Custom Limits

Enterprise plans include custom rate limits based on your needs, plus:

  • Dedicated infrastructure
  • Priority queue for extractions
  • No monthly extraction caps

Best Practices

  1. Cache responses β€” Don’t re-fetch data that hasn’t changed
  2. Batch operations β€” Combine multiple operations when possible
  3. Use webhooks β€” Avoid polling by using webhook notifications
  4. Respect Retry-After β€” Always honor the Retry-After header
  5. Monitor usage β€” Track your remaining quota proactively

Usage Dashboard

View your current usage at stylegrab.dev/dashboard/usage:

  • Requests this minute
  • Extractions today
  • Extractions this month
  • Usage history charts

Changelog

All notable changes to StyleGrab are documented here.

[1.2.0] - 2024-01-15

Added

  • Snapshot diffing β€” Compare any two snapshots to see exactly what changed
  • Webhook events β€” New diff.detected event for change notifications
  • Team support β€” Create teams and share extractions with collaborators
  • Element selectors β€” Target specific DOM elements for extraction

Changed

  • Improved color extraction accuracy for CSS variables
  • Faster extraction times (avg 40% improvement)
  • Better handling of web fonts

Fixed

  • Fixed timeout issues on slow-loading pages
  • Fixed duplicate colors when using shorthand properties
  • Fixed webhook retry not respecting exponential backoff

[1.1.0] - 2024-01-01

Added

  • Webhook support β€” Get notified when extractions complete
  • API keys β€” Create multiple keys with optional expiration
  • Export formats β€” CSS variables, Tailwind config, SCSS

Changed

  • Extraction results now include usage counts for each token
  • Improved typography detection for system fonts
  • Rate limits increased for Pro plan

Fixed

  • Fixed extraction failing on pages with Content-Security-Policy
  • Fixed spacing values not being deduplicated
  • Fixed API key not working after password change

[1.0.0] - 2023-12-15

Added

  • Initial release
  • CSS extraction from any URL via headless browser
  • Color, typography, and spacing token extraction
  • Asset (image) detection
  • JWT authentication
  • Stripe billing integration
  • Browser extension (Chrome)

Migration Guides

Upgrading to 1.2.0

New diff endpoint: If you were manually comparing snapshots, use the new diff endpoint:

- // Old: Fetch both snapshots and compare client-side
- const snap1 = await fetchSnapshot(id1);
- const snap2 = await fetchSnapshot(id2);
- const diff = compareTkens(snap1, snap2);

+ // New: Use the diff endpoint
+ const diff = await fetch(`/api/extractions/${id}/diff?from=${snap1}&to=${snap2}`);

Webhook secret: If you created webhooks before 1.2.0, regenerate them to get HMAC signatures:

# Delete old webhook
curl -X DELETE https://api.stylegrab.dev/api/webhooks/wh_old123 \
  -H "Authorization: Bearer YOUR_API_KEY"

# Create new webhook with signature support
curl -X POST https://api.stylegrab.dev/api/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"url": "https://...", "events": [...]}'

Upgrading to 1.1.0

Webhook format change: The webhook payload structure changed:

{
- "type": "extraction_completed",
- "extraction": { ... }
+ "event": "extraction.completed",
+ "data": { ... }
}

Versioning

StyleGrab follows Semantic Versioning:

  • Major (X.0.0) β€” Breaking changes
  • Minor (1.X.0) β€” New features, backward compatible
  • Patch (1.0.X) β€” Bug fixes, backward compatible

Subscribe to Updates

Get notified of new releases: