## Preserve os bytes antes de interpretar JSON

A assinatura usa exatamente `timestamp + '.' + raw_body`, com HMAC SHA-256 e o segredo de webhook do seu ambiente. Leia o timestamp de `X-Face-Auth-Timestamp` e a assinatura de `X-Face-Auth-Signature`, no formato `sha256=<hex>`.

Não use `JSON.stringify` ou outra serialização do objeto recebido para reconstruir a mensagem. Espaços, quebras de linha ou ordem de propriedades alteram os bytes. Configure seu framework para disponibilizar o corpo bruto antes do parser JSON.

## Verificação no backend

O exemplo recebe uma tolerância temporal definida pela sua política; ela não é uma promessa de prazo de entrega. Mantenha o relógio do servidor sincronizado e avalie reentregas junto à política de replay.

Verificar assinatura do webhook

### JavaScript / Node.js

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";
// Read X-Face-Auth-Timestamp / X-Face-Auth-Signature. rawBody is the exact received Buffer.
export function verifyWebhook(rawBody, timestamp, signature, secret, maxAgeSeconds, nowSeconds = Date.now() / 1000) {
  if (!Buffer.isBuffer(rawBody) || !secret || !Number.isFinite(maxAgeSeconds) || maxAgeSeconds <= 0 || !Number.isFinite(nowSeconds)) return false;
  if (typeof timestamp !== "string" || !/^[0-9]{1,16}$/.test(timestamp) || typeof signature !== "string" || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
  const sentAt = Number(timestamp);
  if (!Number.isSafeInteger(sentAt) || Math.abs(nowSeconds - sentAt) > maxAgeSeconds) return false;
  const expected = createHmac("sha256", secret).update(Buffer.from(timestamp + ".", "utf8")).update(rawBody).digest();
  const received = Buffer.from(signature.slice(7), "hex");
  return expected.length === received.length && timingSafeEqual(expected, received);
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

### TypeScript / Node.js

```typescript
import { createHmac, timingSafeEqual } from "node:crypto";
// Read X-Face-Auth-Timestamp / X-Face-Auth-Signature. rawBody is the exact received Buffer.
export function verifyWebhook(rawBody: Buffer, timestamp: string, signature: string, secret: string, maxAgeSeconds: number, nowSeconds = Date.now() / 1000): boolean {
  if (!Buffer.isBuffer(rawBody) || !secret || !Number.isFinite(maxAgeSeconds) || maxAgeSeconds <= 0 || !Number.isFinite(nowSeconds)) return false;
  if (typeof timestamp !== "string" || !/^[0-9]{1,16}$/.test(timestamp) || typeof signature !== "string" || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
  const sentAt = Number(timestamp);
  if (!Number.isSafeInteger(sentAt) || Math.abs(nowSeconds - sentAt) > maxAgeSeconds) return false;
  const expected = createHmac("sha256", secret).update(Buffer.from(timestamp + ".", "utf8")).update(rawBody).digest();
  const received = Buffer.from(signature.slice(7), "hex");
  return expected.length === received.length && timingSafeEqual(expected, received);
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

### Python

```python
from __future__ import annotations
import hashlib
import hmac
import math
import re
import time

def verify_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str,
                   max_age_seconds: float, now_seconds: float | None = None) -> bool:
    now = time.time() if now_seconds is None else now_seconds
    if not isinstance(raw_body, bytes) or not secret or not math.isfinite(max_age_seconds) or max_age_seconds <= 0 or not math.isfinite(now):
        return False
    if not isinstance(timestamp, str) or not re.fullmatch(r"[0-9]{1,16}", timestamp):
        return False
    if not isinstance(signature, str) or not re.fullmatch(r"sha256=[a-f0-9]{64}", signature):
        return False
    sent_at = int(timestamp)
    if sent_at > 9007199254740991 or abs(now - sent_at) > max_age_seconds:
        return False
    expected = "sha256=" + hmac.new(secret.encode("utf-8"), timestamp.encode("utf-8") + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

### PHP

```php
<?php
// rawBody is the unchanged byte string from php://input, before json_decode.
function verifyWebhook(string $rawBody, string $timestamp, string $signature, string $secret, float $maxAgeSeconds, ?float $nowSeconds = null): bool {
    $now = $nowSeconds ?? microtime(true);
    if ($secret === '' || !is_finite($maxAgeSeconds) || $maxAgeSeconds <= 0 || !is_finite($now)) return false;
    if (!preg_match('/^[0-9]{1,16}$/D', $timestamp) || !preg_match('/^sha256=[a-f0-9]{64}$/D', $signature)) return false;
    $sentAt = (float) $timestamp;
    if ($sentAt > 9007199254740991 || abs($now - $sentAt) > $maxAgeSeconds) return false;
    $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    return hash_equals($expected, $signature);
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

### C# / .NET

```csharp
using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
// rawBody must be the unchanged request bytes, before JSON deserialization.
public static class Webhook {
  public static bool Verify(byte[] rawBody, string timestamp, string signature, string secret, double maxAgeSeconds, double? nowSeconds = null) {
    double now = nowSeconds ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0;
    if (rawBody is null || string.IsNullOrEmpty(secret) || !double.IsFinite(maxAgeSeconds) || maxAgeSeconds <= 0 || !double.IsFinite(now)) return false;
    if (timestamp is null || signature is null || !Regex.IsMatch(timestamp, @"\A[0-9]{1,16}\z") || !Regex.IsMatch(signature, @"\Asha256=[a-f0-9]{64}\z")) return false;
    if (!long.TryParse(timestamp, out long sentAt) || sentAt > 9007199254740991L || Math.Abs(now - sentAt) > maxAgeSeconds) return false;
    byte[] prefix = Encoding.UTF8.GetBytes(timestamp + ".");
    byte[] signed = new byte[prefix.Length + rawBody.Length];
    Buffer.BlockCopy(prefix, 0, signed, 0, prefix.Length);
    Buffer.BlockCopy(rawBody, 0, signed, prefix.Length, rawBody.Length);
    byte[] expected = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), signed);
    return CryptographicOperations.FixedTimeEquals(expected, Convert.FromHexString(signature[7..]));
  }
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

### Java

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
// Java 17+. rawBody is the original request byte[], before JSON parsing.
class Webhook {
  static boolean verify(byte[] rawBody, String timestamp, String signature, String secret, double maxAgeSeconds, double nowSeconds) throws Exception {
    if (rawBody == null || secret == null || secret.isEmpty() || !Double.isFinite(maxAgeSeconds) || maxAgeSeconds <= 0 || !Double.isFinite(nowSeconds)) return false;
    if (timestamp == null || signature == null || !timestamp.matches("[0-9]{1,16}") || !signature.matches("sha256=[a-f0-9]{64}")) return false;
    long sentAt = Long.parseLong(timestamp);
    if (sentAt > 9007199254740991L || Math.abs(nowSeconds - sentAt) > maxAgeSeconds) return false;
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    mac.update((timestamp + ".").getBytes(StandardCharsets.UTF_8));
    byte[] expected = mac.doFinal(rawBody);
    return MessageDigest.isEqual(expected, HexFormat.of().parseHex(signature.substring(7)));
  }
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

### Go

```go
package main
import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "math"
  "regexp"
  "strconv"
)
// rawBody contains the exact request bytes, before json.Unmarshal. nowSeconds comes from your clock.
func verifyWebhook(rawBody []byte, timestamp, signature, secret string, maxAgeSeconds, nowSeconds float64) bool {
  if len(secret) == 0 || math.IsNaN(maxAgeSeconds) || math.IsInf(maxAgeSeconds, 0) || maxAgeSeconds <= 0 || math.IsNaN(nowSeconds) || math.IsInf(nowSeconds, 0) { return false }
  if !regexp.MustCompile("^[0-9]{1,16}$").MatchString(timestamp) || !regexp.MustCompile("^sha256=[a-f0-9]{64}$").MatchString(signature) { return false }
  sentAt, err := strconv.ParseInt(timestamp, 10, 64)
  if err != nil || sentAt > 9007199254740991 || math.Abs(nowSeconds-float64(sentAt)) > maxAgeSeconds { return false }
  mac := hmac.New(sha256.New, []byte(secret)); mac.Write([]byte(timestamp + ".")); mac.Write(rawBody)
  received, err := hex.DecodeString(signature[7:]); if err != nil { return false }
  return hmac.Equal(mac.Sum(nil), received)
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

## Depois da validação

Somente após validar o HMAC, interprete JSON, valide o envelope e processe o evento. Confira o identificador de evento do header e do corpo. Deduplicate por `event_id` em armazenamento durável; a verificação criptográfica sozinha não impede repetir o mesmo efeito de negócio.

Os exemplos isolam a verificação e não implementam um receptor completo. Adicione limites de corpo, tratamento de exceções, persistência, fila, proteção de logs e [reconciliação](/pt-BR/desenvolvedores/kyc/reentrega-e-reconciliacao). Nunca registre o segredo, o corpo com dados pessoais ou links de jornada em ferramentas públicas.
