Webhooks
Get real-time notifications when events happen in your Skimly account. Webhooks are essential for building integrations, monitoring usage, and keeping your systems in sync.
Real-time updates: Get notified instantly when chats complete, blobs are created, or usage changes.
Build integrations: Connect Skimly to your existing systems and workflows.
Monitor usage: Track costs and token usage in real-time.
Audit trail: Keep a complete record of all activities.
1. Setup Local Development
To receive webhooks locally, you need to create a secure tunnel to your localhost. We recommend ngrok or cloudflared for development.
ngrok (Recommended)
# Install ngrok npm install -g ngrok # Create tunnel to your local server ngrok http 8000 # Use the https URL in Skimly Settings
cloudflared (Alternative)
# Install cloudflared brew install cloudflare/cloudflare/cloudflared # Create tunnel cloudflared tunnel --url http://localhost:8000
Never expose webhook endpoints publicly without proper authentication. Use ngrok/cloudflared for development only. In production, deploy to a secure HTTPS endpoint.
2. Subscribe to Webhooks
Once you have a public URL, subscribe to webhook events. You can subscribe to specific event types or receive all events.
Subscribe to All Events
curl http://localhost:8000/api/webhooks \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://abc123.ngrok.io/webhooks"}'
Subscribe to Specific Events
curl http://localhost:8000/api/webhooks \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://abc123.ngrok.io/webhooks", "events": ["chat.completed", "blob.created"] }'
SDK Examples
Use our official SDKs for easier webhook management:
Node.js
import { SkimlyClient } from "@skimly/sdk" const client = SkimlyClient.fromEnv() // Subscribe to webhooks await client.webhooks.create({ url: "https://abc123.ngrok.io/webhooks", events: ["chat.completed", "blob.created"] }) // List webhooks const webhooks = await client.webhooks.list() // Delete webhook await client.webhooks.delete("wh_123")
Python
from skimly import SkimlyClient client = SkimlyClient.from_env() # Subscribe to webhooks client.webhooks.create( url="https://abc123.ngrok.io/webhooks", events=["chat.completed", "blob.created"] ) # List webhooks webhooks = client.webhooks.list() # Delete webhook client.webhooks.delete("wh_123")
3. Handle Webhook Events
Your webhook endpoint will receive POST requests with event data. Always verify the signature to ensure the webhook came from Skimly.
Node.js Examples
Express.js
import express from "express" import { verifyWebhook } from "@skimly/sdk" const app = express() // Important: Use raw body parsing for webhook verification app.post("/webhooks", express.raw({type: 'application/json'}), (req, res) => { try { const event = verifyWebhook(req.body, req.headers['skimly-signature']) switch (event.type) { case 'chat.completed': console.log('Chat finished:', { requestId: event.data.request_id, tokensSaved: event.data.tokens_saved, costUsd: event.data.cost_usd }) break case 'blob.created': console.log('Blob created:', { blobId: event.data.blob_id, sizeBytes: event.data.size_bytes }) break case 'usage.updated': console.log('Usage updated:', { organizationId: event.data.organization_id, totalTokens: event.data.total_tokens }) break } res.sendStatus(200) } catch (error) { console.error('Webhook verification failed:', error) res.status(401).json({error: 'Invalid signature'}) } })
Fastify
import Fastify from "fastify" import { verifyWebhook } from "@skimly/sdk" const fastify = Fastify() fastify.post("/webhooks", { config: { rawBody: true } }, async (request, reply) => { try { const event = verifyWebhook(request.rawBody, request.headers['skimly-signature']) if (event.type === 'chat.completed') { console.log('Chat completed:', event.data) } return { received: true } } catch (error) { reply.code(401) return { error: 'Invalid signature' } } })
Python Examples
Flask
from flask import Flask, request from skimly import verify_webhook app = Flask(__name__) @app.route("/webhooks", methods=["POST"]) def webhook(): try: event = verify_webhook( request.get_data(), request.headers.get("skimly-signature") ) if event["type"] == "chat.completed": print("Chat completed:", { "request_id": event["data"]["request_id"], "tokens_saved": event["data"]["tokens_saved"], "cost_usd": event["data"]["cost_usd"] }) elif event["type"] == "blob.created": print("Blob created:", { "blob_id": event["data"]["blob_id"], "size_bytes": event["data"]["size_bytes"] }) return "", 200 except Exception as e: print(f"Webhook verification failed: {e}") return {"error": "Invalid signature"}, 401
FastAPI
from fastapi import FastAPI, Request, HTTPException from skimly import verify_webhook app = FastAPI() @app.post("/webhooks") async def webhook(request: Request): try: body = await request.body() signature = request.headers.get("skimly-signature") event = verify_webhook(body, signature) if event["type"] == "chat.completed": print(f"Chat completed: {event['data']}") elif event["type"] == "blob.created": print(f"Blob created: {event['data']}") return {"received": True} except Exception as e: raise HTTPException(status_code=401, detail="Invalid signature")
Always use raw body parsing for webhook endpoints. The signature verification requires the exact bytes that were sent. Express.js users should use express.raw()
, Fastify users should enable rawBody: true
.
Event Types
Skimly sends different types of events depending on what happened in your account. Each event contains relevant data and metadata.
chat.completed
Fired when a chat request finishes processing
request_id
Unique identifier for the requestprovider
Which provider was used (openai/anthropic)model
Model name (e.g., gpt-4o-mini)tokens_in
Input tokens sent to providertokens_out
Output tokens from providertokens_saved
Tokens avoided via blobbingcost_usd
Actual cost of the requestuser_id
User who made the requestblob.created
Fired when new content is uploaded as a blob
blob_id
Unique blob identifiercontent_hash
SHA256 hash of the contentsize_bytes
Size of the content in bytesmime_type
Content type (e.g., text/plain)user_id
User who created the bloborganization_id
Organization that owns the blobusage.updated
Fired when organization usage metrics change
organization_id
Affected organizationperiod
Billing period (e.g., "2024-01")total_tokens
Total tokens used in periodtotal_cost_usd
Total cost in USDtokens_saved
Tokens saved via blobbingcost_savings_usd
Cost savings in USD4. Testing Webhooks
Test your webhook endpoints locally to ensure they're working correctly before deploying to production.
Test with cURL
# Test your endpoint locally curl -X POST http://localhost:8000/webhooks \ -H "Content-Type: application/json" \ -d '{"type":"test","data":{"message":"Hello webhook!"}}'
Monitor Logs
# Watch your server logs npm run dev # Or for Python python app.py # Look for webhook requests in the console
5. Production Deployment
When deploying to production, ensure your webhook endpoint is secure and reliable.
Security Checklist
- ✅ Use HTTPS endpoints only
- ✅ Verify webhook signatures
- ✅ Implement rate limiting
- ✅ Use environment variables for secrets
- ✅ Monitor for suspicious activity
Reliability Checklist
- ✅ Return 200 status quickly
- ✅ Process events asynchronously
- ✅ Implement retry logic
- ✅ Monitor webhook delivery
- ✅ Have fallback mechanisms
Follow the steps above to set up webhooks for your Skimly integration. Start with local development using ngrok, then deploy to production when ready.
Next Steps
API Reference
Complete endpoint documentation with all available options and parameters.
View API docs →