Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.case.dev/llms.txt

Use this file to discover all available pages before exploring further.

This guide creates an endpoint, verifies deliveries in your receiver, and sends a test event.

Create an endpoint

curl -X POST https://api.case.dev/webhooks/v1/endpoints \
  -H "Authorization: Bearer $CASEDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/webhooks/casedev",
    "description": "Production event receiver",
    "eventTypeFilters": ["vault.ingest.completed"]
  }'
The response includes endpoint.id and signingSecret. Store the signing secret in your secrets manager before closing the response.
Response
{
  "endpoint": {
    "id": "evsub_123",
    "url": "https://your-app.example.com/webhooks/casedev",
    "eventTypeFilters": ["vault.ingest.completed"],
    "status": "active"
  },
  "signingSecret": "whsec_..."
}

Add a receiver

Verify the signature with the raw request body. Parsing and re-stringifying JSON changes the bytes and invalidates the signature.
import express from 'express'
import crypto from 'node:crypto'

const app = express()
const secret = process.env.CASEDEV_WEBHOOK_SECRET!

app.post('/webhooks/casedev', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body.toString('utf8')

  if (!verifySignature(rawBody, req.headers, secret)) {
    return res.status(400).send('invalid signature')
  }

  const event = JSON.parse(rawBody)
  console.log(event.type, event.id)

  return res.sendStatus(204)
})

function verifySignature(body: string, headers: express.Request['headers'], secret: string) {
  const id = String(headers['case-webhook-id'] ?? '')
  const timestamp = String(headers['case-webhook-timestamp'] ?? '')
  const signature = String(headers['case-webhook-signature'] ?? '')
  const timestampSeconds = Number(timestamp)

  if (!id || !Number.isFinite(timestampSeconds)) return false
  if (Math.abs(Date.now() / 1000 - timestampSeconds) > 300) return false

  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
  const expected = crypto
    .createHmac('sha256', key)
    .update(`${id}.${timestamp}.${body}`)
    .digest('base64')

  return signature.split(' ').some((part) => {
    const [version, value] = part.split(',', 2)
    return version === 'v1' && safeEqual(value, expected)
  })
}

function safeEqual(a = '', b = '') {
  const left = Buffer.from(a)
  const right = Buffer.from(b)
  return left.length === right.length && crypto.timingSafeEqual(left, right)
}

Send a test event

curl -X POST "https://api.case.dev/webhooks/v1/endpoints/$ENDPOINT_ID/test" \
  -H "Authorization: Bearer $CASEDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "vault.ingest.completed",
    "payload": {
      "vaultId": "vault_123",
      "objectId": "obj_123",
      "durationMs": 1284,
      "chunkCount": 42
    }
  }'
The test endpoint performs one synchronous delivery. Use it to confirm reachability and signature verification before broadening your filters.

Go live

Start with exact event names, then widen to patterns like vault.* after your receiver is stable. Use Event types for the generated event list and API reference for endpoint update, replay, and rotation APIs.