Kevo Docs

Webhooks

Kevo sends webhook events to your server when users are created or authenticate. All events are signed with HMAC-SHA256 for verification.

Setup

Configure your webhook endpoint URL in the Kevo Portal under Project Settings → Webhooks. You'll also receive a webhook secret used to verify the HMAC signature on incoming requests.

Kevo delivers webhooks with a POST request containing a JSON body. Your endpoint must respond with a 2xx status within 10 seconds. Failed deliveries are retried up to 3 times with exponential backoff. Webhooks are delivered at least once — your handler must be idempotent.

Events

user.created

Fired when a user signs up for the first time (first auth with this project).

json
{
  "event": "user.created",
  "projectId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "timestamp": 1743588000000,
  "data": {
    "userId": "uuid",
    "method": "email",        // auth method used to sign up
    "address": "0x..."        // only present for wallet methods
  }
}

Possible method values:

emailgoogleapplexpasskeywalletsol_wallet

user.authenticated

Fired on every successful sign-in, including returning users.

json
{
  "event": "user.authenticated",
  "projectId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "timestamp": 1743588000000,
  "data": {
    "userId": "uuid",
    "method": "google"
  }
}

user.email_linked

Fired when an already authenticated user verifies and links an email to their account, usually before OTP-gated private key export.

json
{
  "event": "user.email_linked",
  "projectId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "timestamp": 1743588000000,
  "data": {
    "userId": "uuid",
    "email": "[email protected]"
  }
}

Signature Verification

Each webhook request includes an X-Kevo-Signature header containing an HMAC-SHA256 signature of the raw request body, computed using your webhook secret.

Always verify this signature before processing the event.

The verifyWebhookSignature helper from @kevo-ws/sdk/server handles HMAC computation and constant-time comparison in one call:

typescript
import { verifyWebhookSignature } from '@kevo-ws/sdk/server'
import type { WebhookPayload } from '@kevo-ws/sdk/server'

// Express
app.post('/webhooks/kevo', express.raw({ type: 'application/json' }), (req, res) => {
  const isValid = verifyWebhookSignature(
    req.body.toString(),                      // raw body string
    req.headers['x-kevo-signature'] as string, // 'sha256=<hex>'
    process.env.KEVO_WEBHOOK_SECRET!,
  )
  if (!isValid) return res.status(401).json({ error: 'Invalid signature' })

  const payload: WebhookPayload = JSON.parse(req.body.toString())
  console.log(payload.event, payload.data)
  res.json({ received: true })
})

// Next.js App Router
export async function POST(req: Request) {
  const rawBody = await req.text()
  const isValid = verifyWebhookSignature(
    rawBody,
    req.headers.get('x-kevo-signature'),
    process.env.KEVO_WEBHOOK_SECRET!,
  )
  if (!isValid) {
    return Response.json({ error: 'Invalid signature' }, { status: 401 })
  }
  const payload: WebhookPayload = JSON.parse(rawBody)
  // handle event...
  return Response.json({ received: true })
}

// Fastify (parse body as buffer)
app.post('/webhooks/kevo', (req, reply) => {
  const isValid = verifyWebhookSignature(
    (req.body as Buffer).toString(),
    req.headers['x-kevo-signature'] as string,
    process.env.KEVO_WEBHOOK_SECRET!,
  )
  if (!isValid) return reply.code(401).send({ error: 'Invalid signature' })

  const payload: WebhookPayload = JSON.parse((req.body as Buffer).toString())
  // handle event...
  reply.send({ received: true })
})

Manual verification (without SDK)

If you prefer not to use the SDK helper, compute the HMAC-SHA256 yourself. The signature header format is sha256=<hex>. Strip the prefix, then compare using constant-time comparison.

typescript
import crypto from 'crypto'

const signature = req.headers['x-kevo-signature'] as string  // 'sha256=abc123...'
const secret = process.env.KEVO_WEBHOOK_SECRET!

const expected = 'sha256=' + crypto
  .createHmac('sha256', secret)
  .update(rawBody)          // raw body string or Buffer
  .digest('hex')

const valid = crypto.timingSafeEqual(
  Buffer.from(signature),
  Buffer.from(expected),
)
if (!valid) return res.status(401).json({ error: 'Invalid signature' })

Retry Policy

If your endpoint returns a non-2xx status or times out, Kevo will retry the delivery:

AttemptDelay
1st retry30 seconds
2nd retry5 minutes
3rd retry30 minutes
Make your webhook handler idempotent. Due to retries, you may receive the same event multiple times. Use timestamp + event + data.userId as a deduplication key.