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 up and running in 5 minutes
- API Reference β Full endpoint documentation
- Guides β Step-by-step tutorials
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
- Go to your Dashboard
- Navigate to Settings β API Keys
- Click Create API Key
- 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 β Self-host StyleGrab
- Configuration β Environment variables and settings
- Your First Extraction β Detailed walkthrough
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
Docker Compose (Recommended)
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 β All environment variables
- Quick Start β Make your first extraction
Configuration
StyleGrab is configured via environment variables. Copy .env.example to .env and customize.
Required Variables
| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL connection string |
JWT_SECRET | Secret key for JWT token signing (min 32 chars) |
STRIPE_SECRET_KEY | Stripe API secret key for payments |
STRIPE_WEBHOOK_SECRET | Stripe webhook signing secret |
Optional Variables
| Variable | Default | Description |
|---|---|---|
HOST | 0.0.0.0 | Server bind address |
PORT | 3000 | Server port |
BROWSER_WS_URL | ws://127.0.0.1:9222 | CDP WebSocket URL for headless browser |
REDIS_URL | redis://127.0.0.1:6379 | Redis connection URL |
LOG_LEVEL | info | Logging 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
- Quick Start β Make your first extraction
- Authentication β API authentication details
Authentication
StyleGrab uses JWT tokens and API keys for authentication.
Authentication Methods
API Keys (Recommended)
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
keyis only shown once. Store it securely.
Revoke API Key
Delete an API key.
DELETE /api/auth/api-keys/:id
Response:
{
"success": true
}
Error Responses
| Status | Code | Description |
|---|---|---|
| 401 | unauthorized | Missing or invalid token |
| 401 | token_expired | JWT token has expired |
| 403 | forbidden | Valid token but insufficient permissions |
| 422 | validation_error | Invalid request body |
Example error response:
{
"error": {
"code": "unauthorized",
"message": "Invalid or expired token"
}
}
Best Practices
- Use API keys for server-to-server communication
- Set expiration dates on API keys
- Rotate keys regularly and revoke unused ones
- Never commit keys to version control
- 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"]
}
}
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | URL to extract from |
options.wait_for | string | No | Wait condition: load, domcontentloaded, networkidle |
options.viewport | object | No | Browser viewport size |
options.selectors | array | No | Limit extraction to specific elements |
options.exclude_selectors | array | No | Exclude 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:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
per_page | integer | 20 | Results per page (max 100) |
status | string | β | 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
| Status | Description |
|---|---|
pending | Extraction queued, waiting to start |
running | Browser is loading the page |
completed | Extraction finished successfully |
failed | Extraction 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
| Status | Code | Description |
|---|---|---|
| 400 | invalid_url | The provided URL is malformed |
| 404 | not_found | Extraction not found |
| 422 | extraction_failed | Browser failed to load the page |
| 429 | rate_limited | Too 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Source snapshot ID |
to | string | Yes | Target 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:
- Extract tokens from your design system documentation
- Extract tokens from production
- 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"
}
| Role | Permissions |
|---|---|
owner | Full access, can delete team |
admin | Manage members, create extractions |
member | View 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
| Status | Code | Description |
|---|---|---|
| 403 | not_team_member | Youβre not a member of this team |
| 403 | insufficient_role | Your role doesnβt allow this action |
| 404 | team_not_found | Team doesnβt exist |
| 409 | already_member | User 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"
}
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS endpoint to receive events |
events | array | Yes | Events to subscribe to |
secret | string | No | Secret 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
secretsecurely. 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
| Header | Description |
|---|---|
X-StyleGrab-Signature | HMAC-SHA256 signature |
X-StyleGrab-Timestamp | Unix 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:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
After 3 failed attempts, the webhook is marked as failed. Fix your endpoint and the next event will attempt delivery again.
Best Practices
- Respond quickly β Return 2xx within 5 seconds
- Process async β Queue the webhook for background processing
- Verify signatures β Always verify the HMAC signature
- Handle duplicates β Webhooks may be delivered more than once
- Use HTTPS β Webhook URLs must use HTTPS
Payments
StyleGrab uses Stripe for subscription billing.
Plans
| Plan | Price | Extractions | Teams | Features |
|---|---|---|---|---|
| Free | $0/mo | 50/month | 1 | Basic extraction |
| Pro | $29/mo | 1,000/month | 5 | Webhooks, diffing, priority |
| Enterprise | Custom | Unlimited | Unlimited | SLA, 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
| Status | Description |
|---|---|
active | Subscription is active |
past_due | Payment failed, retrying |
canceled | Subscription ended |
trialing | In trial period |
Stripe Webhooks
StyleGrab processes Stripe webhooks automatically. No action needed, but for reference:
| Event | Action |
|---|---|
checkout.session.completed | Activate subscription |
invoice.paid | Renew subscription |
invoice.payment_failed | Mark as past due |
customer.subscription.deleted | Deactivate 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
- A StyleGrab account (sign up here)
- An API key (create one in Dashboard β Settings β API Keys)
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?
- Set up webhooks to get notified when extractions complete
- Monitor for design drift by comparing snapshots over time
- Integrate with CI/CD to catch design changes before they ship
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 asSTYLEGRAB_WEBHOOK_SECRETin 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
- Check your endpoint is publicly accessible
- Verify HTTPS is working (webhooks require HTTPS)
- Check the webhook is
activein the API
Signature verification failing
- Ensure youβre using the raw request body (not parsed JSON)
- Check the secret matches what was returned when creating the webhook
- 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
- Integrate with CI/CD to block deploys with design drift
- Webhook setup for detailed integration options
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 keyPROD_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
- Webhook setup for async processing
- Design drift monitoring for ongoing tracking
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:
- Download the extension ZIP from GitHub Releases
- Extract the ZIP file
- Open
chrome://extensions/in Chrome - Enable βDeveloper modeβ (toggle in top right)
- Click βLoad unpackedβ
- 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
- Navigate to any website
- Click the StyleGrab icon
- Click βExtract Tokensβ
- 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://pageschrome-extension://pages- Local files (unless enabled in extension settings)
βPermission deniedβ errors
- Go to
chrome://extensions/ - Find StyleGrab and click βDetailsβ
- Ensure βSite accessβ is set to βOn all sitesβ
Extraction not working
Some sites block content scripts. Try:
- Refresh the page
- Disable other extensions that might interfere
- Check the browser console for errors
Next Steps
- Using the extension
- API reference for programmatic access
Using the Browser Extension
Extract and export design tokens directly from any webpage.
Quick Start
- Navigate to the page you want to extract from
- Click the StyleGrab icon in your toolbar
- Click Extract Tokens
- 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:
- Click Pick Element in the toolbar
- Hover over elements on the page (theyβll highlight)
- Click to select an element
- Only tokens from that element tree are extracted
Compare Mode
Compare tokens from different pages:
- Extract from Page A
- Click Save as Baseline
- Navigate to Page B
- Extract and click Compare
- 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
| Shortcut | Action |
|---|---|
Ctrl+Shift+E | Quick extract current page |
Ctrl+Shift+P | Toggle element picker |
Escape | Cancel 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:
- Right-click the iframe
- Select βOpen frame in new tabβ
- 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
| Code | HTTP Status | Description |
|---|---|---|
unauthorized | 401 | Missing or invalid authentication |
token_expired | 401 | JWT token has expired |
invalid_api_key | 401 | API key is invalid or revoked |
forbidden | 403 | Valid auth but insufficient permissions |
Validation Errors
| Code | HTTP Status | Description |
|---|---|---|
validation_error | 422 | Request body failed validation |
invalid_url | 400 | URL is malformed or unreachable |
invalid_webhook_url | 400 | Webhook URL must be HTTPS |
missing_field | 400 | Required 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
| Code | HTTP Status | Description |
|---|---|---|
not_found | 404 | Resource does not exist |
already_exists | 409 | Resource already exists |
conflict | 409 | Operation conflicts with current state |
Extraction Errors
| Code | HTTP Status | Description |
|---|---|---|
extraction_failed | 422 | Browser failed to load the page |
extraction_timeout | 422 | Page load timed out |
page_blocked | 422 | Page blocked the extraction |
invalid_content | 422 | Page 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
| Code | HTTP Status | Description |
|---|---|---|
rate_limited | 429 | Too many requests |
usage_limit_exceeded | 429 | Monthly quota exceeded |
Headers included:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705312800
Retry-After: 60
Billing Errors
| Code | HTTP Status | Description |
|---|---|---|
subscription_required | 402 | Feature requires paid plan |
subscription_expired | 402 | Subscription has ended |
payment_failed | 402 | Payment processing failed |
Webhook Errors
| Code | HTTP Status | Description |
|---|---|---|
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
| Code | HTTP Status | Description |
|---|---|---|
internal_error | 500 | Unexpected server error |
service_unavailable | 503 | Service 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
| Plan | Requests/min | Extractions/day | Extractions/month |
|---|---|---|---|
| Free | 20 | 10 | 50 |
| Pro | 100 | 100 | 1,000 |
| Enterprise | Custom | Custom | Unlimited |
Rate Limit Headers
Every response includes rate limit information:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705312800
| Header | Description |
|---|---|
X-RateLimit-Limit | Max requests allowed per window |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix 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:
| Endpoint | Limit |
|---|---|
POST /api/extractions | 10/min (Free), 50/min (Pro) |
GET /api/extractions/:id/diff | 30/min |
POST /api/auth/login | 5/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
- Cache responses β Donβt re-fetch data that hasnβt changed
- Batch operations β Combine multiple operations when possible
- Use webhooks β Avoid polling by using webhook notifications
- Respect Retry-After β Always honor the Retry-After header
- 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.detectedevent 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:
- GitHub Releases
- RSS Feed
- Email: Enable βProduct updatesβ in account settings