Webhooks
A Velix envia notificações HTTP POST para sua URL configurada quando eventos ocorrem na plataforma.
Eventos disponíveis
| Evento | Quando |
|---|---|
checkin.success | Check-in aprovado |
checkin.denied | Biometria não reconhecida |
checkin.liveness_failed | Prova de vida reprovada |
checkin.geofence_alert | Check-in aprovado com alerta de localização |
person.enrolled | Enroll biométrico concluído |
person.deleted | Pessoa removida do tenant |
Payload
{
"event": "checkin.success",
"tenantId": "tenant_abc",
"timestamp": "2026-06-22T10:30:00.000Z",
"data": {
"personId": "person_xyz",
"eventId": "event_123",
"method": "facial",
"geofenceStatus": "ok"
}
}
Verificar assinatura
Toda requisição inclui o header X-Velix-Signature com o HMAC-SHA256 do corpo usando o secret configurado. Sempre verifique a assinatura antes de processar.
- TypeScript
- JavaScript
- Kotlin
- Ruby
- Swift
- Dart
- Elixir
- Lua
- PowerShell
- Delphi
- PHP
- C#
- Java
- Rust
- Go
import { createHmac, timingSafeEqual } from 'crypto';
function verifySignature(rawBody: string, signature: string, secret: string): boolean {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// Express:
app.post('/webhooks/velix', (req, res) => {
const sig = req.headers['x-velix-signature'] as string;
if (!verifySignature(req.rawBody, sig, process.env.VELIX_WEBHOOK_SECRET!))
return res.status(401).send('Assinatura inválida');
console.log(`Evento: ${req.body.event}`);
res.sendStatus(200);
});
const { createHmac, timingSafeEqual } = require('crypto');
function verifySignature(rawBody, signature, secret) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
app.post('/webhooks/velix', (req, res) => {
const sig = req.headers['x-velix-signature'];
if (!verifySignature(req.rawBody, sig, process.env.VELIX_WEBHOOK_SECRET))
return res.status(401).send('Assinatura inválida');
res.sendStatus(200);
});
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
fun verifySignature(rawBody: String, signature: String, secret: String): Boolean {
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(secret.toByteArray(), "HmacSHA256"))
val expected = mac.doFinal(rawBody.toByteArray()).joinToString("") { "%02x".format(it) }
return java.security.MessageDigest.isEqual(expected.toByteArray(), signature.toByteArray())
}
require "openssl"
def verify_signature(raw_body, signature, secret)
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)
Rack::Utils.secure_compare(expected, signature)
end
import CryptoKit
func verifySignature(rawBody: String, signature: String, secret: String) -> Bool {
let key = SymmetricKey(data: Data(secret.utf8))
let mac = HMAC<SHA256>.authenticationCode(for: Data(rawBody.utf8), using: key)
let expected = mac.map { String(format: "%02x", $0) }.joined()
return expected == signature
}
import 'package:crypto/crypto.dart';
import 'dart:convert';
bool verifySignature(String rawBody, String signature, String secret) {
final hmac = Hmac(sha256, utf8.encode(secret));
final expected = hmac.convert(utf8.encode(rawBody)).toString();
return expected == signature;
}
def verify_signature(raw_body, signature, secret) do
expected = :crypto.mac(:hmac, :sha256, secret, raw_body) |> Base.encode16(case: :lower)
Plug.Crypto.secure_compare(expected, signature)
end
local hmac = require("resty.hmac") -- ou openssl.hmac fora do OpenResty
local function verify_signature(raw_body, signature, secret)
local expected = hmac.new(secret, hmac.ALGOS.SHA256):final(raw_body, true)
return expected == signature
end
function Test-VelixSignature {
param([string]$RawBody, [string]$Signature, [string]$Secret)
$hmac = New-Object System.Security.Cryptography.HMACSHA256
$hmac.Key = [Text.Encoding]::UTF8.GetBytes($Secret)
$hash = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($RawBody))
$expected = ($hash | ForEach-Object { $_.ToString("x2") }) -join ""
return $expected -eq $Signature
}
uses System.Hash;
function VerifySignature(const RawBody, Signature, Secret: string): Boolean;
var
Expected: string;
begin
Expected := THashSHA2.GetHMAC(RawBody, Secret, SHA256);
Result := SameText(Expected, Signature);
end;
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_VELIX_SIGNATURE'] ?? '';
$secret = getenv('VELIX_WEBHOOK_SECRET');
$expected = hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Assinatura inválida');
}
$event = json_decode($rawBody, true);
using System.Security.Cryptography;
[HttpPost("/webhooks/velix")]
public IActionResult Receive()
{
using var reader = new StreamReader(Request.Body);
var rawBody = reader.ReadToEnd();
var signature = Request.Headers["X-Velix-Signature"].ToString();
var secret = Environment.GetEnvironmentVariable("VELIX_WEBHOOK_SECRET")!;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody))).ToLower();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signature)))
return Unauthorized();
return Ok();
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
String rawBody = new String(request.getInputStream().readAllBytes());
String signature = request.getHeader("X-Velix-Signature");
String secret = System.getenv("VELIX_WEBHOOK_SECRET");
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
String expected = HexFormat.of().formatHex(hmac.doFinal(rawBody.getBytes()));
if (!MessageDigest.isEqual(expected.getBytes(), signature.getBytes()))
throw new SecurityException("Assinatura inválida");
use hmac::{Hmac, Mac};
use sha2::Sha256;
fn verify_signature(raw_body: &str, signature: &str, secret: &str) -> bool {
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
.expect("HMAC init failed");
mac.update(raw_body.as_bytes());
let expected = hex::encode(mac.finalize().into_bytes());
// timing-safe compare
expected.as_bytes() == signature.as_bytes()
}
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
"os"
)
func verifySignature(rawBody []byte, signature string) bool {
mac := hmac.New(sha256.New, []byte(os.Getenv("VELIX_WEBHOOK_SECRET")))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
if !verifySignature(body, r.Header.Get("X-Velix-Signature")) {
http.Error(w, "assinatura inválida", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
}
Configurar webhook
:::caution Endpoint corrigido
Não existe um recurso /v1/webhooks dedicado. A URL do webhook é um campo de configurações do tenant (webhookUrl), atualizado via PATCH /v1/tenants/:id/settings. O secret usado para assinar o payload (X-Velix-Signature) é fornecido pela equipe Velix durante o onboarding do tenant — solicite a rotação em caso de suspeita de vazamento.
:::
curl -X PATCH https://api.velixbiometrics.com/v1/tenants/$TENANT_ID/settings \
-H "Authorization: Bearer $VELIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://sua-api.com/webhooks/velix"
}'
Retentativas
A Velix tenta reenviar por até 3 tentativas com backoff exponencial (1s, 5s, 30s) caso seu endpoint retorne status diferente de 2xx ou timeout acima de 10s.
:::tip Teste local
Use ngrok ou similar para expor seu endpoint local durante o desenvolvimento:
ngrok http 3000
Então configure o webhook com a URL do ngrok. :::