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.
Events
user.created
Fired when a user signs up for the first time (first auth with this project).
{
"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_walletuser.authenticated
Fired on every successful sign-in, including returning users.
{
"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.
{
"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.
Using the SDK (recommended)
The verifyWebhookSignature helper from @kevo-ws/sdk/server handles HMAC computation and constant-time comparison in one call:
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.
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:
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
timestamp + event + data.userId as a deduplication key.