> ## Documentation Index
> Fetch the complete documentation index at: https://help.ciarem.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Verificar assinaturas

> Confira que uma solicitação de webhook vem mesmo da Ciarem e não foi alterada, com código que você pode copiar.

A URL do seu webhook é pública, então qualquer pessoa que a descubra pode enviar solicitações para ela. A Ciarem assina cada solicitação com o **segredo de assinatura** do seu endpoint. Conferir essa assinatura é como o seu servidor distingue um evento real da Ciarem de um forjado. **Verifique antes de confiar no corpo.**

## O segredo de assinatura

Cada endpoint tem seu próprio segredo. Ele começa com `whsec_`.

* **No painel:** aparece quando você adiciona o endpoint, e de novo a qualquer momento na página do endpoint com **Mostrar segredo**. Só os administradores podem vê-lo.
* **Com a API:** retornado uma única vez, na resposta de `POST /v1/webhook-endpoints`.

Trate-o como uma senha: guarde-o no seu gerenciador de segredos ou em uma variável de ambiente, nunca no código nem em um navegador.

## O cabeçalho de assinatura

Cada solicitação carrega um cabeçalho `Ciarem-Signature`:

```
Ciarem-Signature: t=1760022245,v1=5f8a0c…c2e1
```

| Parte | |
| - | - |
| `t` | Quando a Ciarem enviou esta solicitação, como timestamp Unix em segundos. |
| `v1` | A assinatura: um HMAC-SHA256, escrito em hexadecimal minúsculo. |

A assinatura é calculada sobre o timestamp, um ponto e o **corpo bruto da solicitação**, usando o seu segredo de assinatura como chave:

```
v1 = HMAC_SHA256(key = signing secret, message = t + "." + raw body)
```

## Como verificar

**1. Leia o corpo bruto**

Pegue o corpo da solicitação **exatamente como os bytes chegaram**, antes de qualquer parsing de JSON. Fazer o parsing e serializar de novo muda o espaçamento e o escape de caracteres, e a assinatura deixa de bater.

**2. Separe o cabeçalho**

Separe `Ciarem-Signature` por `,` e depois por `=` para obter `t` e `v1`.

**3. Calcule a assinatura esperada**

Monte a mensagem `t + "." + raw body` e calcule o HMAC-SHA256 dela com o seu segredo de assinatura. Use o segredo inteiro, incluindo o prefixo `whsec_`, como ele é.

**4. Compare com segurança**

Compare o seu resultado com `v1` usando uma função de comparação em tempo constante, não `==`.

**5. Confira o timestamp**

Rejeite a solicitação se `t` estiver a mais de **5 minutos** do relógio do seu servidor. Isso impede que alguém capture uma solicitação real e a reenvie depois.

Se algum passo falhar, responda `400` e não faça nada com o corpo.

## Código que você pode copiar

```javascript Node.js (Express) theme={null}
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.CIAREM_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_SECONDS = 300;

function verify(rawBody, header) {
  const parts = Object.fromEntries(
    (header ?? "").split(",").map((p) => p.split("=", 2)),
  );
  const timestamp = Number(parts.t);
  if (!timestamp || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// express.raw mantém o corpo como bytes. Não use express.json() nesta rota.
app.post("/hooks/ciarem", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.get("ciarem-signature"))) {
    return res.status(400).send("invalid signature");
  }
  const event = JSON.parse(req.body);
  // Coloque o evento em uma fila para processar e responda na hora.
  res.sendStatus(200);
});

app.listen(3000);
```

```python Python (FastAPI) theme={null}
import hashlib
import hmac
import json
import os
import time

from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
SECRET = os.environ["CIAREM_WEBHOOK_SECRET"]  # whsec_…
TOLERANCE_SECONDS = 300


def verify(raw_body: bytes, header: str | None) -> bool:
    parts = dict(p.split("=", 1) for p in (header or "").split(",") if "=" in p)
    try:
        timestamp = int(parts["t"])
        signature = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > TOLERANCE_SECONDS:
        return False
    expected = hmac.new(
        SECRET.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)


@app.post("/hooks/ciarem")
async def ciarem_webhook(request: Request):
    raw_body = await request.body()  # os bytes, não request.json()
    if not verify(raw_body, request.headers.get("ciarem-signature")):
        raise HTTPException(status_code=400, detail="invalid signature")
    event = json.loads(raw_body)
    # Coloque o evento em uma fila para processar e responda na hora.
    return {"ok": True}
```

```php PHP theme={null}
<?php
$secret = getenv('CIAREM_WEBHOOK_SECRET'); // whsec_…
$tolerance = 300;

$rawBody = file_get_contents('php://input'); // os bytes, antes do json_decode
$header = $_SERVER['HTTP_CIAREM_SIGNATURE'] ?? '';

$parts = [];
foreach (explode(',', $header) as $pair) {
    [$key, $value] = array_pad(explode('=', $pair, 2), 2, '');
    $parts[$key] = $value;
}
$timestamp = (int) ($parts['t'] ?? 0);
$signature = $parts['v1'] ?? '';

$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);

if (
    $timestamp === 0
    || abs(time() - $timestamp) > $tolerance
    || !hash_equals($expected, $signature)
) {
    http_response_code(400);
    exit('invalid signature');
}

$event = json_decode($rawBody, true);
// Coloque o evento em uma fila para processar e responda na hora.
http_response_code(200);
```

```ruby Ruby (Sinatra) theme={null}
require "json"
require "openssl"
require "sinatra"

SECRET = ENV.fetch("CIAREM_WEBHOOK_SECRET") # whsec_…
TOLERANCE_SECONDS = 300

def verify(raw_body, header)
  parts = header.to_s.scan(/(\w+)=([^,]+)/).to_h
  timestamp = parts["t"].to_i
  signature = parts["v1"].to_s
  return false if timestamp.zero? || signature.empty?
  return false if (Time.now.to_i - timestamp).abs > TOLERANCE_SECONDS

  expected = OpenSSL::HMAC.hexdigest("SHA256", SECRET, "#{timestamp}.#{raw_body}")
  Rack::Utils.secure_compare(expected, signature)
end

post "/hooks/ciarem" do
  raw_body = request.body.read # os bytes, antes do JSON.parse
  halt 400, "invalid signature" unless verify(raw_body, request.env["HTTP_CIAREM_SIGNATURE"])
  event = JSON.parse(raw_body)
  # Coloque o evento em uma fila para processar e responda na hora.
  status 200
end
```

## Confira seu código com um exemplo conhecido

Use estes valores para confirmar que o seu código de verificação calcula a assinatura certa, sem esperar por um evento real. Pule a checagem do timestamp enquanto roda isto, já que o timestamp do exemplo está no passado.

| | |
| - | - |
| Segredo de assinatura | `whsec_test` |
| Timestamp `t` | `1760000000` |
| Corpo bruto | `{"id":"evt_test_sample00000000000","type":"test","api_version":"v1","created_at":"2026-01-15T14:30:00Z","data":{}}` |
| `v1` esperado | `057e6f478035e5f43cc6c85e722bb8871eff8baa2852afdf23a52ef6a7d739c3` |

Você pode gerar o mesmo valor em um terminal:

```bash theme={null}
printf '%s' '1760000000.{"id":"evt_test_sample00000000000","type":"test","api_version":"v1","created_at":"2026-01-15T14:30:00Z","data":{}}' \
  | openssl dgst -sha256 -hmac 'whsec_test'
```

## Quando a assinatura falha

| O que você vê | Causa provável |
| - | - |
| A assinatura nunca bate | Você está calculando o hash de um corpo serializado de novo. Calcule o hash dos bytes brutos como chegaram. No Express, um `express.json()` global roda antes da sua rota e substitui o corpo; use `express.raw()` na rota do webhook. |
| Bate para alguns eventos e não para outros | O corpo contém caracteres acentuados ou emoji e o seu framework mudou a codificação deles. A Ciarem envia o corpo em UTF-8 com esses caracteres escritos como são, não como escapes `\u`. De novo, calcule o hash dos bytes brutos. |
| Nunca bate, e você calcula o hash do corpo bruto | O segredo errado. Cada endpoint tem o seu, e ele deve incluir o prefixo `whsec_`. Copie-o de novo na página do endpoint. |
| Parou de bater depois que você recriou o endpoint | Um endpoint novo recebe um segredo novo. Atualize o seu servidor. |
| Solicitações reais são rejeitadas como antigas demais | O relógio do seu servidor está errado. Sincronize-o com NTP, ou aumente um pouco a tolerância. |

## Boas práticas

* **Um segredo por endpoint.** Se você tem servidores de staging e de produção, dê a cada um o seu próprio endpoint, para que um segredo de staging vazado não possa forjar eventos de produção.
* **Para trocar o segredo, substitua o endpoint.** Um segredo não pode ser alterado no lugar. Para trocar, adicione um novo endpoint com a mesma URL e os mesmos eventos, atualize o seu servidor para aceitar o novo segredo e depois exclua o endpoint antigo. Enquanto os dois existem, você recebe cada evento duas vezes, uma por endpoint, com o mesmo `id` de evento, então [remover duplicados pelo `id`](https://help.ciarem.ai/pt/webhooks/deliveries#duplicados-e-ordem) deixa isso inofensivo.
* **Cada nova tentativa é assinada de novo.** Uma entrega tentada de novo tem o mesmo corpo e o mesmo `id` de evento, mas um novo `t` e uma nova assinatura, então a checagem de 5 minutos também funciona para as novas tentativas.
* **A verificação prova a origem, não a unicidade.** Um evento verificado ainda pode ser um duplicado. Remova duplicados pelo `id` também.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.