Webhooks
Deliver Spanlens events to your own server as HTTP POST payloads in real time. Three event types are supported — request created, trace completed, and alert triggered — all signed with HMAC-SHA256 so you can verify authenticity. Use webhooks to build custom Slack bots, data pipelines, CI/CD triggers, or any other automation beyond the dashboard.
Supported events
| Event | When it fires |
|---|---|
request.created | After the proxy receives an LLM response and inserts a row into requests |
trace.completed | When the last span in an agent trace closes |
alert.triggered | When an Alert rule exceeds its threshold and sends a notification |
Endpoints
GET /api/v1/webhooks # List all webhooks in the organization
POST /api/v1/webhooks # Register a new webhook
PATCH /api/v1/webhooks/:id # Update name, URL, events, or active status
DELETE /api/v1/webhooks/:id # Delete a webhook
POST /api/v1/webhooks/:id/test # Send a test payload immediately
GET /api/v1/webhooks/:id/deliveries # Last 10 delivery recordshttpAll endpoints require Authorization: Bearer <supabase-jwt>. Creating, updating, and deleting webhooks requires admin or editor role. Viewers can only list webhooks and view delivery history.
Registering a webhook
Request schema
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable label (e.g. "Slack event pipe") |
url | string | Yes | Must start with https://. Plain HTTP is rejected. |
events | string[] | Yes | Events to subscribe to. An empty array means no events will be delivered. |
is_active | boolean | Optional | Defaults to true. Set false to pause delivery. |
curl -X POST https://spanlens-server.vercel.app/api/v1/webhooks \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{
"name": "My data pipeline",
"url": "https://my-server.example.com/hooks/spanlens",
"events": ["request.created", "alert.triggered"],
"is_active": true
}'bashResponse example
{
"id": "wh_01j9abc...",
"name": "My data pipeline",
"url": "https://my-server.example.com/hooks/spanlens",
"secret": "a3f8c2d1e5b04f7a9c6e2d8b1a4f03c7",
"events": ["request.created", "alert.triggered"],
"is_active": true,
"created_at": "2026-05-15T09:00:00Z"
}jsonThe secret is a 32-character hex string returned only at registration time. Store it securely — it cannot be recovered if lost. Subsequent GET responses show only a masked value.
Payload structure
Spanlens sends the following JSON body as an HTTP POST to your endpoint when an event fires.
{
"event": "request.created",
"created_at": "2026-05-15T09:01:23Z",
"data": {
"id": "req_01j9xyz...",
"project_id": "proj_01j9...",
"model": "gpt-4o-mini-2024-07-18",
"provider": "openai",
"input_tokens": 512,
"output_tokens": 128,
"cost_usd": 0.000096,
"duration_ms": 843
}
}jsonThe shape of the data field varies by event type. request.created contains a summary of the request row; trace.completed contains trace metadata; and alert.triggered contains the triggered rule and its current value.
Signature verification
Every delivery includes an X-Spanlens-Signature header. The value is an HMAC-SHA256 digest of the raw request body using the secret issued at registration. Always verify the signature to reject forged requests.
Node.js verification example
import crypto from 'node:crypto'
export function verifySpanlensSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex')
// Use timingSafeEqual to prevent timing attacks
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(signatureHeader, 'hex')
if (a.length !== b.length) return false
return crypto.timingSafeEqual(a, b)
}
// Express example
app.post('/hooks/spanlens', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-spanlens-signature'] as string
if (!verifySpanlensSignature(req.body.toString(), sig, process.env.WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' })
}
const event = JSON.parse(req.body.toString())
// handle event
res.json({ ok: true })
})typescriptImportant: read req.body as raw bytes. Re-serializing the parsed JSON can change whitespace or key order, causing a signature mismatch. Use express.raw() or an equivalent middleware.
Delivery history
GET /api/v1/webhooks/:id/deliveries returns the last 10 delivery records. Each record includes the HTTP status code, the first 500 characters of the response body, and the delivery timestamp. If you see repeated 4xx or 5xx responses, check your server logs alongside the delivery history.
curl https://spanlens-server.vercel.app/api/v1/webhooks/wh_01j9abc.../deliveries \
-H "Authorization: Bearer <JWT>"bash[
{
"id": "del_01j9...",
"event": "request.created",
"status_code": 200,
"response_body": "{"ok":true}",
"delivered_at": "2026-05-15T09:01:24Z"
}
]jsonTest delivery
Call POST /api/v1/webhooks/:id/test to immediately send a dummy payload. Use this to verify your endpoint URL and signature verification logic without waiting for a real event.
curl -X POST https://spanlens-server.vercel.app/api/v1/webhooks/wh_01j9abc.../test \
-H "Authorization: Bearer <JWT>"bashPermissions
| Action | admin | editor | viewer |
|---|---|---|---|
| List / delivery history | ✓ | ✓ | ✓ |
| Create / update / delete | ✓ | ✓ | — |
| Test delivery | ✓ | ✓ | — |
Limitations
- 20 webhooks per organization maximum.
- No retries. If your server returns a non-2xx status or times out (10s), the delivery is recorded as failed and is not retried. Implement idempotency on your receiver side.
- Only the last 10 delivery records are kept per webhook. Store delivery logs on your server if you need a complete audit trail.
- HTTPS required. HTTP URLs are rejected at registration time.
Related: Alerts (threshold-based notifications), Audit logs (change history), Security (PII / prompt injection scanning).