Skip to content
Back to skills

207 Event 091a1210

ASecurity

Event handling, signature verification, and monitoring.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
developmenttypescriptpythonphpbashnextjstesting

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 207-event_091a1210 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 207 Event 091a1210?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 207 Event 091a1210
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-207-event-091a1210/badge)](https://www.skillsdirectory.com/skills/tools-only-207-event-091a1210)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# Polar Webhooks

Event handling, signature verification, and monitoring.

## Setup

1. Org Settings → Webhooks
2. Enter endpoint URL (publicly accessible)
3. Receive webhook secret (base64 encoded)
4. Select event types
5. Save configuration

**Requirements:**
- HTTPS endpoint
- Respond within 20 seconds
- Return 2xx status code

## Signature Verification

### Headers
```
webhook-id: msg_xxx
webhook-signature: v1,signature_xxx
webhook-timestamp: 1642000000
```

### TypeScript Verification
```typescript
import { validateEvent, WebhookVerificationError } from '@polar-sh/sdk/webhooks';

app.post('/webhook/polar', (req, res) => {
  try {
    const event = validateEvent(
      req.body,
      req.headers,
      process.env.POLAR_WEBHOOK_SECRET
    );

    // Event is valid, process it
    await handleEvent(event);

    res.json({ received: true });
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      console.error('Invalid webhook signature');
      return res.status(400).json({ error: 'Invalid signature' });
    }
    throw error;
  }
});
```

### Python Verification
```python
from polar_sdk.webhooks import validate_event, WebhookVerificationError

@app.route('/webhook/polar', methods=['POST'])
def polar_webhook():
    try:
        event = validate_event(
            request.get_data(),
            dict(request.headers),
            os.environ['POLAR_WEBHOOK_SECRET']
        )

        handle_event(event)
        return {'received': True}

    except WebhookVerificationError:
        return {'error': 'Invalid signature'}, 400
```

### Manual Verification
```typescript
import crypto from 'crypto';

function verifySignature(payload, headers, secret) {
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'].split(',');

  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', Buffer.from(secret, 'base64'))
    .update(signedPayload)
    .digest('base64');

  return signatures.some(sig => {
    const [version, signature] = sig.split('=');
    return version === 'v1' && signature === expectedSignature;
  });
}
```

## Event Types

### Checkout
- `checkout.created` - Checkout session created
- `checkout.updated` - Session updated

### Order
- `order.created` - Order created (check `billing_reason`)
  - `purchase` - One-time product
  - `subscription_create` - New subscription
  - `subscription_cycle` - Renewal
  - `subscription_update` - Plan change
- `order.paid` - Payment confirmed
- `order.updated` - Order updated
- `order.refunded` - Refund processed

### Subscription
- `subscription.created` - Subscription created
- `subscription.active` - Subscription activated
- `subscription.updated` - Subscription modified
- `subscription.canceled` - Cancellation scheduled
- `subscription.revoked` - Subscription terminated

**Note:** Multiple events may fire for single action

### Customer
- `customer.created` - Customer created
- `customer.updated` - Customer modified
- `customer.deleted` - Customer deleted
- `customer.state_changed` - Benefits/subscriptions changed

### Benefit Grant
- `benefit_grant.created` - Benefit granted
- `benefit_grant.updated` - Grant modified
- `benefit_grant.revoked` - Benefit revoked

### Refund
- `refund.created` - Refund initiated
- `refund.updated` - Refund status changed

### Product
- `product.created` - Product created
- `product.updated` - Product modified

## Event Structure

```typescript
{
  "type": "order.paid",
  "data": {
    "id": "order_xxx",
    "amount": 2000,
    "currency": "USD",
    "billing_reason": "purchase",
    "customer": { ... },
    "product": { ... },
    "subscription": null,
    "metadata": { ... }
  }
}
```

## Handler Implementation

### Basic Handler
```typescript
async function handleEvent(event) {
  switch (event.type) {
    case 'order.paid':
      await handleOrderPaid(event.data);
      break;

    case 'subscription.active':
      await grantAccess(event.data.customer_id);
      break;

    case 'subscription.revoked':
      await revokeAccess(event.data.customer_id);
      break;

    case 'benefit_grant.created':
      await notifyBenefitGranted(event.data);
      break;

    default:
      console.log(`Unhandled event: ${event.type}`);
  }
}
```

### Order Handler
```typescript
async function handleOrderPaid(order) {
  // Handle different billing reasons
  switch (order.billing_reason) {
    case 'purchase':
      await fulfillOneTimeOrder(order);
      break;

    case 'subscription_create':
      await handleNewSubscription(order);
      break;

    case 'subscription_cycle':
      await handleRenewal(order);
      break;

    case 'subscription_update':
      await handleUpgrade(order);
      break;
  }
}
```

### Customer State Handler
```typescript
async function handleCustomerStateChanged(customer) {
  // Customer state includes:
  // - active_subscriptions
  // - active_benefits

  const hasActiveSubscription = customer.active_subscriptions.length > 0;

  if (hasActiveSubscription) {
    await enableFeatures(customer.external_id);
  } else {
    await disableFeatures(customer.external_id);
  }
}
```

## Best Practices

### 1. Respond Immediately
```typescript
app.post('/webhook/polar', async (req, res) => {
  // Respond quickly
  res.json({ received: true });

  // Queue for background processing
  await webhookQueue.add('polar-webhook', req.body);
});
```

### 2. Idempotency
```typescript
async function handleEvent(event) {
  // Check if already processed
  const exists = await db.processedEvents.findOne({
    webhook_id: event.id
  });

  if (exists) {
    console.log('Event already processed');
    return;
  }

  // Process event
  await processEvent(event);

  // Mark as processed
  await db.processedEvents.insert({
    webhook_id: event.id,
    processed_at: new Date()
  });
}
```

### 3. Retry Logic
```typescript
async function processWithRetry(event, maxRetries = 3) {
  let attempt = 0;

  while (attempt < maxRetries) {
    try {
      await handleEvent(event);
      return;
    } catch (error) {
      attempt++;
      if (attempt >= maxRetries) throw error;
      await sleep(1000 * attempt);
    }
  }
}
```

### 4. Error Handling
```typescript
app.post('/webhook/polar', async (req, res) => {
  try {
    const event = validateEvent(req.body, req.headers, secret);
    res.json({ received: true });

    await processWithRetry(event);
  } catch (error) {
    console.error('Webhook processing failed:', error);
    // Log to error tracking service
    await logError(error, req.body);

    if (error instanceof WebhookVerificationError) {
      return res.status(400).json({ error: 'Invalid signature' });
    }

    // Return 2xx even on processing errors
    // Polar will retry if non-2xx
    res.json({ received: true });
  }
});
```

### 5. Logging
```typescript
logger.info('Webhook received', {
  event_type: event.type,
  event_id: event.id,
  customer_id: event.data.customer?.id,
  amount: event.data.amount
});
```

## Monitoring

### Dashboard Features
- View webhook attempts
- Check response status
- Review retry history
- Manual retry option
- Filter by event type
- Search by customer

### Application Monitoring
```typescript
const metrics = {
  webhooks_received: counter('polar_webhooks_received_total'),
  webhooks_processed: counter('polar_webhooks_processed_total'),
  webhooks_failed: counter('polar_webhooks_failed_total'),
  processing_time: histogram('polar_webhook_processing_seconds')
};

app.post('/webhook/polar', async (req, res) => {
  metrics.webhooks_received.inc({ type: req.body.type });

  const timer = metrics.processing_time.startTimer();

  try {
    await handleEvent(req.body);
    metrics.webhooks_processed.inc({ type: req.body.type });
  } catch (error) {
    metrics.webhooks_failed.inc({ type: req.body.type });
  } finally {
    timer();
  }

  res.json({ received: true });
});
```

## Framework Adapters

### Next.js
```typescript
import { validateEvent } from '@polar-sh/nextjs/webhooks';

export async function POST(req: Request) {
  const event = await validateEvent(req);

  await handleEvent(event);

  return Response.json({ received: true });
}
```

### Laravel
```php
use Polar\Webhooks\WebhookHandler;

Route::post('/webhook/polar', function (Request $request) {
    $event = WebhookHandler::validate(
        $request->getContent(),
        $request->headers->all(),
        config('polar.webhook_secret')
    );

    dispatch(new ProcessPolarWebhook($event));

    return response()->json(['received' => true]);
});
```

## Testing

### Manual Testing
```bash
# Use Polar dashboard to send test webhooks
# Or use webhook testing tools

curl -X POST https://your-domain.com/webhook/polar \
  -H "Content-Type: application/json" \
  -H "webhook-id: msg_test" \
  -H "webhook-timestamp: $(date +%s)" \
  -H "webhook-signature: v1,test_signature" \
  -d '{"type":"order.paid","data":{...}}'
```

### Local Testing with ngrok
```bash
# Expose local server
ngrok http 3000

# Use ngrok URL in Polar webhook settings
https://abc123.ngrok.io/webhook/polar
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.