Pular para o conteúdo principal

Documentation Index

Fetch the complete documentation index at: https://docs.kiwify.com.br/llms.txt

Use this file to discover all available pages before exploring further.

Headers e Verificação de Webhooks

Toda entrega de webhook é um HTTP POST com corpo JSON e três headers relevantes.

Headers HTTP

HeaderValorDescrição
Content-Typeapplication/jsonTipo do corpo
x-kiwify-digital-signatureBase64url (sem padding)Assinatura EdDSA-Ed25519
x-kiwify-timestampUnix ms (ex: 1705423200000)Timestamp usado na mensagem assinada
Exemplo:
POST /webhooks/kiwibank HTTP/1.1
Content-Type: application/json
x-kiwify-digital-signature: gnfHkqEfBhKN3lYmGDF_J9fO3jSQtq8d1-agAZPMn4u3huef0kg5XqbnxSj5SCb_
x-kiwify-timestamp: 1705423200000

{"id":"550e8400-e29b-41d4-a716-446655440000","type":"CASHOUT.PIX.TRANSFERS.COMPLETED",...}

Obter a chave pública

Use GET /v1/webhooks-keys para listar chaves públicas. Use a chave com is_active: true. Cache a chave por até 24 horas e atualize periodicamente.

Processo de verificação

Passo 1: Validar timestamp

Rejeite entregas com timestamp fora de uma janela de 5 minutos do horário atual (proteção contra replay).

Passo 2: Reconstruir a mensagem assinada

Formato PoP (mesmo padrão da autenticação da API, com diferença no uri):
{url_path}:POST:{raw_body}:{timestamp}
ComponenteDescrição
url_pathSomente o path da URL registrada (ex: /webhooks/kiwibank), não a URL completa
POSTMétodo HTTP (sempre POST)
raw_bodyCorpo JSON exatamente como recebido — não re-serialize
timestampValor do header x-kiwify-timestamp

Passo 3: Verificar a assinatura

  1. Calcule SHA-256 dos bytes UTF-8 da mensagem
  2. Decodifique x-kiwify-digital-signature de base64url (sem padding)
  3. Verifique com EdDSA-Ed25519 usando a chave pública ativa
A assinatura de webhook usa Ed25519 prehashed (SHA-256 da mensagem antes da verificação). Isso difere ligeiramente de alguns exemplos genéricos de Ed25519 que assinam a mensagem diretamente.

Exemplo (Python)

import base64
import hashlib
import time
from urllib.parse import urlparse
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization

def verify_webhook(endpoint_url: str, body_bytes: bytes, signature_b64: str, timestamp_ms: int, public_key_pem: str) -> bool:
    if abs(int(time.time() * 1000) - timestamp_ms) > 300_000:
        return False

    path = urlparse(endpoint_url).path or "/"
    message = f"{path}:POST:{body_bytes.decode('utf-8')}:{timestamp_ms}"
    digest = hashlib.sha256(message.encode("utf-8")).digest()

    public_key = serialization.load_pem_public_key(public_key_pem.encode())
    padding = (4 - len(signature_b64) % 4) % 4
    signature = base64.urlsafe_b64decode(signature_b64 + "=" * padding)

    try:
        public_key.verify(signature, digest)
        return True
    except Exception:
        return False

Erros comuns

  • Usar a URL completa em vez de apenas o path na mensagem assinada
  • Re-serializar o JSON (espaços/ordem de chaves alteram a assinatura)
  • Verificar Ed25519 diretamente sobre a mensagem sem SHA-256 prévio