Slatis Public API v1 is open for beta testers Register on the waitlist →
Slatis

Signature Verification

Verify that webhook payloads are genuinely from Slatis using HMAC-SHA256 signatures.

How it works

Every delivery includes an X-Slatis-Signature header containing an HMAC-SHA256 signature of the raw request body, signed with your webhook's secret:

X-Slatis-Signature: sha256=a4d2e5f8...

The secret is AES-256-GCM encrypted at rest — it is only returned in plaintext on webhook create and secret rotation.

Verify the signature

Always use the raw body bytes for HMAC computation. Never parse, re-serialize, or modify the payload before computing the hash — any whitespace difference will invalidate the signature.

import crypto from 'crypto'
 
function verifySignature(
  rawBody: string,
  signature: string,
  secret: string
): boolean {
  const expected =
    'sha256=' +
    crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex')
 
  return (
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  )
}

Next.js route handler example

app/api/webhooks/slatis/route.ts
import crypto from 'crypto'
import { NextRequest, NextResponse } from 'next/server'
 
const WEBHOOK_SECRET = process.env.SLATIS_WEBHOOK_SECRET!
 
export async function POST(request: NextRequest) {
  const rawBody = await request.text()
  const signature = request.headers.get('x-slatis-signature') ?? ''
 
  const expected =
    'sha256=' +
    crypto.createHmac('sha256', WEBHOOK_SECRET).update(rawBody).digest('hex')
 
  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
  }
 
  const event = JSON.parse(rawBody)
  // handle event...
 
  return NextResponse.json({ received: true })
}

Important notes

  • Use crypto.timingSafeEqual (or equivalent) to prevent timing attacks — never use === for signature comparison.
  • The secret is returned only on POST /v1/webhooks (create) and POST /v1/webhooks/{id}/rotate-secret. It is AES-256-GCM encrypted at rest and never re-exposed otherwise.
  • After rotating the secret, update your verification code before rotating — not after — to avoid a gap where deliveries fail.

On this page