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

# Implementar um receptor de webhook

> Crie um endpoint, verifique assinaturas, aceite eventos de forma durável e gerencie novas tentativas e suspensões de webhooks.

Receba eventos da YCloud em um endpoint público e processe-os sem perder ou duplicar ações de negócios. Use HTTPS para produção, preserve o corpo bruto da requisição e verifique cada assinatura antes de aceitar o evento.

## Registrar seu endpoint

No [Console da YCloud](https://www.ycloud.com/console), abra **Desenvolvedores > Webhook**, selecione **Adicionar Endpoints**, insira a **URL dos Endpoints**, selecione **Eventos** e salve com **Confirmar**. Você também pode usar `POST /v2/webhookEndpoints`; consulte [Configurar webhooks](/pt/api-reference/guides/api-fundamentals/configure-webhooks) para ver a requisição e a resposta.

* Você pode configurar até 20 endpoints por conta.
* A URL deve ser acessível publicamente e não deve resolver para um endereço privado.
* A URL suporta até 500 caracteres; a descrição opcional suporta até 400.
* Guarde em segurança o segredo de assinatura `secret` retornado.

## Ler a requisição do evento

```http theme={"theme":{"light":"github-light","dark":"github-dark"}}
Content-Type: application/json
YCloud-Signature: t=1762224357,s=SIGNATURE_HEX
X-Webhook-Endpoint-ID: WEBHOOK_ENDPOINT_ID
```

| Campo | Significado |
| - | - |
| `id` | ID do evento. Use-o para desduplicar a entrega. |
| `type` | Tipo de evento, como `whatsapp.message.updated`. |
| `apiVersion` | Versão da API utilizada pelo evento. |
| `createTime` | Registro de data e hora (timestamp) de criação do evento. |
| Objeto específico do evento | Carga útil (payload) como `whatsappMessage`, `whatsappInboundMessage` ou `contactCreated`. |

Para obter exemplos completos, consulte [Cargas úteis de webhook](/pt/api-reference/guides/examples/webhook-examples/webhook-payload-examples).

## Verificar a assinatura

O cabeçalho `YCloud-Signature` tem o formato `t=TIMESTAMP,s=SIGNATURE`.
O timestamp é a hora Unix em segundos.

1. Extraia `t` e `s` do cabeçalho.
2. Junte o timestamp, um ponto final e os bytes exatos do corpo bruto da requisição.
3. Calcule o HMAC-SHA256 com o segredo de assinatura do endpoint.
4. Compare o resultado hexadecimal usando uma comparação de tempo constante.

Não serialize o JSON analisado para reconstruir o corpo. Espaços em branco, ordem das chaves ou escape de Unicode alteram a entrada da assinatura.

O exemplo abaixo também usa uma tolerância de timestamp configurável de cinco minutos para reduzir o risco de repetição (replay). Essa tolerância é uma política da aplicação, não um prazo de repetição da YCloud. Mantenha o relógio do seu servidor sincronizado.

```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import crypto from "node:crypto";

export function verifySignature(rawBody, header, secret, now = Date.now()) {
  if (!Buffer.isBuffer(rawBody) || typeof header !== "string" || !secret) return false;
  const fields = header.split(",").map(part => part.trim().split("="));
  const timestamps = fields.filter(([key]) => key === "t").map(([, value]) => value);
  const signatures = fields.filter(([key]) => key === "s").map(([, value]) => value);
  if (timestamps.length !== 1 || !/^\d+$/.test(timestamps[0])) return false;
  const timestamp = timestamps[0];
  if (Math.abs(Math.floor(now / 1000) - Number(timestamp)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret)
    .update(timestamp + ".")
    .update(rawBody)
    .digest();
  return signatures.some(signature =>
    typeof signature === "string" && /^[a-fA-F0-9]{64}$/.test(signature) &&
    crypto.timingSafeEqual(expected, Buffer.from(signature, "hex"))
  );
}
```

## Aceitar antes de confirmar

Persista o evento validado em uma fila durável ou em uma caixa de entrada transacional antes de retornar `2xx`. Se o armazenamento estiver indisponível, retorne uma falha para que a entrega possa ser repetida. Após a aceitação durável, deixe seu worker lidar com falhas de processamento de negócios com suas próprias tentativas.

Este manipulador Express usa uma operação `persistEvent` fornecida pela aplicação. Implemente-a como uma inserção atômica com chave definida pelo `id` do evento; um evento já armazenado deve contar como sucesso. Não marque um evento como processado antes que sua transação de negócio seja confirmada.

```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import express from "express";
import { verifySignature } from "./signature.js";
import { persistEvent } from "./event-inbox.js";

const app = express();
const secret = process.env.YCLOUD_WEBHOOK_SECRET;
if (!secret) throw new Error("YCLOUD_WEBHOOK_SECRET is required");

// Register the raw-body route before any JSON-parsing middleware.
app.post("/webhook", express.raw({ type: "application/json", limit: "1mb" }), async (req, res) => {
  if (!verifySignature(req.body, req.get("YCloud-Signature"), secret)) {
    return res.sendStatus(401);
  }
  let event;
  try {
    event = JSON.parse(req.body.toString("utf8"));
  } catch {
    return res.sendStatus(400);
  }
  if (!event || typeof event.id !== "string" || typeof event.type !== "string") {
    return res.sendStatus(400);
  }
  try {
    await persistEvent(event.id, event);
    return res.sendStatus(204);
  } catch {
    return res.sendStatus(503);
  }
});

app.listen(3000);
```

### Exemplo em Java e Spring

Este exemplo em Java 17 aplica a mesma ordem de verificação e aceitação durável. Forneça um bean `EventInbox` suportado por um armazenamento transacional com uma restrição de unicidade no ID do evento. `insertIfAbsent` deve confirmar o evento completo antes de retornar; IDs duplicados retornam com sucesso. O seu worker pode então processar e marcar os eventos armazenados em sua própria transação.

```java theme={"theme":{"light":"github-light","dark":"github-dark"}}
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.HexFormat;

@RestController
public class WebhookController {
    private final String secret;
    private final ObjectMapper mapper;
    private final EventInbox inbox;

    public WebhookController(@Value("${YCLOUD_WEBHOOK_SECRET}") String secret,
                             ObjectMapper mapper, EventInbox inbox) {
        if (secret.isBlank()) throw new IllegalArgumentException("Missing webhook secret");
        this.secret = secret;
        this.mapper = mapper;
        this.inbox = inbox;
    }

    @PostMapping("/webhook")
    public ResponseEntity<Void> receive(
            @RequestHeader(value = "YCloud-Signature", required = false) String header,
            @RequestBody byte[] body) {
        if (!verify(body, header)) return ResponseEntity.status(401).build();
        JsonNode event;
        try {
            event = mapper.readTree(body);
            if (event == null || !event.path("id").isTextual() || !event.path("type").isTextual()) {
                return ResponseEntity.badRequest().build();
            }
        } catch (Exception invalidJson) {
            return ResponseEntity.badRequest().build();
        }
        try {
            inbox.insertIfAbsent(event.path("id").asText(), body);
            return ResponseEntity.noContent().build();
        } catch (Exception storageFailure) {
            return ResponseEntity.status(503).build();
        }
    }

    private boolean verify(byte[] body, String header) {
        if (header == null) return false;
        String timestamp = null;
        java.util.List<String> signatures = new java.util.ArrayList<>();
        for (String part : header.split(",")) {
            String[] pair = part.trim().split("=", 2);
            if (pair.length != 2) continue;
            if (pair[0].equals("t")) {
                if (timestamp != null) return false;
                timestamp = pair[1];
            }
            if (pair[0].equals("s")) signatures.add(pair[1]);
        }
        if (timestamp == null || !timestamp.matches("[0-9]{1,12}")) return false;
        try {
            long age = Instant.now().getEpochSecond() - Long.parseLong(timestamp);
            if (Math.abs(age) > 300) 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(body);
            for (String signature : signatures) {
                if (signature.matches("[a-fA-F0-9]{64}") &&
                    MessageDigest.isEqual(expected, HexFormat.of().parseHex(signature))) return true;
            }
            return false;
        } catch (Exception invalidSignature) {
            return false;
        }
    }

    public interface EventInbox {
        void insertIfAbsent(String eventId, byte[] eventBody) throws Exception;
    }
}
```

Não use uma gravação separada “já processado” no Redis antes de enfileirar o evento: se o enfileiramento falhar após essa gravação, uma nova tentativa poderá ser descartada. Use uma caixa de entrada durável e atômica ou uma fila cuja aceitação e tratamento de duplicatas sejam atômicos.

## Tempo de resposta, tentativas e suspensão

Retorne uma resposta `2xx` prontamente; mire em menos de 6 segundos. Respostas lentas acima de 10 segundos podem reduzir a prioridade de entrega. Não execute tarefas de negócios lentas dentro do manipulador HTTP.

Para uma resposta diferente de `2xx` ou ausência de resposta, os intervalos padrão de nova tentativa são:

| Tentativa | Atraso |
| - | - |
| 1 | 10 segundos |
| 2 | 30 segundos |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 1 hora |
| 6 | 2 horas |
| 7 | 2 horas |

A YCloud interrompe as tentativas para esse evento após atingir o limite configurado. Com as configurações padrão, uma URL pode ser suspensa por 3 minutos quando atinge 200 falhas por minuto ou 10 minutos de tempo acumulado de falha em um minuto em requisições simultâneas. As requisições são pausadas durante a suspensão e retomadas em seguida.

Monitore também o `status` do endpoint. Um endpoint com status `pending` não recebe eventos; consulte [configuração de endpoints](/pt/api-reference/guides/api-fundamentals/configure-webhooks).

## Verificar o seu receptor

* Uma assinatura válida e um evento armazenado de forma durável retornam `2xx`.
* Corpos modificados, assinaturas malformadas e timestamps desatualizados são rejeitados.
* Um evento duplicado é aceito sem repetir sua ação de negócio.
* Uma falha no armazenamento retorna erro e permite uma nova tentativa de entrega.
* Tipos de eventos desconhecidos não causam falhas no receptor.
* Falhas de processamento são repetidas pelo seu worker após a aceitação.
* Segredos e payloads completos de clientes não são gravados nos logs da aplicação.


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