Entrega e autenticação¶
Requisição HTTP¶
A Moonu envia o corpo do evento por HTTP POST para o endpoint configurado.
| Item | Valor |
|---|---|
| Método | POST |
| Corpo | JSON UTF-8 |
| Sucesso | Qualquer resposta 2xx |
| Redirecionamentos | Não são seguidos |
| Timeout de conexão | 3 segundos |
| Timeout da requisição | 10 segundos |
Headers enviados¶
| Header | Descrição |
|---|---|
Content-Type |
application/json |
User-Agent |
Moonu-Webhooks/1.0 |
X-Moonu-Event |
Tipo do evento, como call.completed |
X-Moonu-Event-Id |
Mesmo identificador presente em eventId no corpo |
X-Moonu-Timestamp |
Unix timestamp em segundos usado na assinatura |
X-Moonu-Signature |
Assinatura HMAC, prefixada por v1= |
Validação da assinatura¶
A assinatura é HMAC-SHA256 sobre a sequência exata abaixo, usando o segredo do endpoint:
<X-Moonu-Timestamp>.<corpo HTTP bruto>
O valor do header é v1=<hex-hmac-sha256>. Valide o corpo bruto, antes de reformatar ou desserializar/re-serializar o JSON, e compare a assinatura em tempo constante.
Exemplo em Node.js. rawBody deve ser o corpo recebido antes do parse de JSON:
import crypto from "node:crypto";
function isValidMoonuSignature({ secret, timestamp, rawBody, signature }) {
const signed = `${timestamp}.${rawBody}`;
const expected = `v1=${crypto
.createHmac("sha256", secret)
.update(signed, "utf8")
.digest("hex")}`;
const actual = Buffer.from(signature || "");
const expectedBuffer = Buffer.from(expected);
return actual.length === expectedBuffer.length && crypto.timingSafeEqual(expectedBuffer, actual);
}
Valide também o timestamp
A assinatura confirma a origem do corpo, mas sua aplicação deve rejeitar timestamps muito antigos conforme a janela de tolerância definida pela sua própria política de segurança.
Retentativas e idempotência¶
Falhas de rede e respostas 408, 425, 429 ou 5xx podem ser tentadas novamente. Os intervalos são: 1 minuto, 5 minutos, 30 minutos, 2 horas, 8 horas, 24 horas e 37 horas.
Use eventId como chave de deduplicação. O mesmo evento mantém o mesmo eventId nas tentativas de entrega. Em geral, o corpo é preservado; no evento recording.available, a URL temporária e expiresAt podem ser renovados em uma nova tentativa. Não assuma entrega exatamente uma vez nem ordem entre eventos.
Fluxo recomendado
Valide a assinatura do corpo bruto, registre eventId de forma idempotente, processe a chamada e responda 2xx. Se o mesmo eventId chegar novamente, responda 2xx sem repetir o efeito de negócio.