HomeGuiaAPI ReferenceNovidadesComunidade
API Reference

Validação de autenticidade

Confirmação de webhooks com assinatura de conteúdo

Confirmar autenticidade da notificação

Visão geral

Toda notificação deve ter sua autenticidade validada antes de qualquer atualização no sistema do integrador.

O PagBank assina o corpo da notificação com uma chave privada. O integrador consulta a chave pública correspondente e a utiliza para verificar as assinaturas recebidas no header x-payload-signature.

A validação utiliza:

  • algoritmo de hash: SHA-256;
  • algoritmo de assinatura: ECDSA;
  • identificação comum nas bibliotecas: SHA256withECDSA;
  • conteúdo validado: corpo original da requisição HTTP;
  • assinatura: valor em Base64 recebido em x-payload-signature.

A notificação é considerada autêntica quando pelo menos uma assinatura recebida for válida.

Antes de começar

Sua aplicação deve:

  • possuir um token válido para consultar a chave pública;
  • conseguir acessar o corpo bruto da requisição;
  • capturar todos os valores de x-payload-signature;
  • utilizar uma biblioteca com suporte a ECDSA e SHA-256;
  • armazenar a chave pública de forma segura e atualizada.

Valide a assinatura antes de converter o payload em objeto, alterar sua formatação ou iniciar qualquer processamento de negócio.

1. Consulte a chave pública

Obtenha a chave pública específica para autenticação de webhooks utilizando type=webhook.

curl --location \
  'https://sandbox.api.pagseguro.com/public-keys?type=webhook' \
  --header 'accept: application/json' \
  --header 'content-type: application/json' \
  --header 'Authorization: Bearer SEU_TOKEN'

Exemplo de resposta:

{
  "public_key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMgZPv1MegPbRkddk7LdfRtix4uTVs5HeAE0kjlzEJwod3aSIcG3LiKbQFOxHzYlzqrq8xXO/T+VbstgwuHPjzg==",
  "links": []
}

O campo public_key contém uma chave pública codificada em Base64 no formato X.509.

Armazene a chave em cache para evitar uma consulta a cada notificação. Atualize o valor quando houver renovação das chaves.

Antes da publicação desta documentação como padrão geral do PagBank, confirme a URL de produção do endpoint de chaves públicas do novo serviço de notificações.

2. Capture a assinatura e o payload original

O PagBank enviará uma ou mais assinaturas no header:

x-payload-signature: MEQCIA...

Durante a rotação de chaves, o header pode conter mais de um valor:

x-payload-signature = [
  MEQCIAv7vXuOvhJlFpHHln5gIiiB8mRnYX6b13tNGEMwHaajAiBhZzuWaiLzuFf4PyevOHrx6gXLRJVUrm1kR/649td+dQ==,
  XYZaxAv7vXuOvhJlFpHHln5gIiiB8mRnYX6b13tNGEMwHaajAiBhZzuWaiLzuFf4PyevOHrx6gXLRJVUrm1kR/649td+dQ==
]

Valide todos os valores recebidos. Basta que uma assinatura seja válida.

Preserve o corpo bruto

A assinatura é calculada sobre os bytes exatos enviados na requisição.

Não faça antes da validação:

  • parse e nova serialização do JSON;
  • remoção ou inclusão de espaços;
  • alteração de quebras de linha;
  • ordenação de propriedades;
  • conversão de caracteres;
  • criação de um novo JSON a partir do objeto recebido.

Use o corpo bruto recebido pelo servidor, interpretado em UTF-8.

3. Valide a assinatura

Para cada assinatura:

  1. decodifique a chave pública de Base64;
  2. carregue a chave no formato X.509;
  3. decodifique a assinatura de Base64;
  4. utilize SHA256withECDSA;
  5. valide a assinatura contra os bytes do payload original;
  6. aceite a notificação se pelo menos uma assinatura for válida.
Payload original
      +
Chave pública
      +
Assinatura recebida
      │
      ▼
SHA256withECDSA
      │
      ├── uma assinatura válida → processar
      └── nenhuma válida → rejeitar

Exemplos

const crypto = require("crypto");

function toPublicKeyPem(publicKeyBase64) {
  const lines = publicKeyBase64.match(/.{1,64}/g) ?? [];

  return [
    "-----BEGIN PUBLIC KEY-----",
    ...lines,
    "-----END PUBLIC KEY-----"
  ].join("\n");
}

function normalizeSignatures(headerValue) {
  if (!headerValue) return [];

  const values = Array.isArray(headerValue)
    ? headerValue
    : headerValue.split(",");

  return values.map((value) => value.trim()).filter(Boolean);
}

function validateWebhook({ rawBody, signatureHeader, publicKeyBase64 }) {
  if (!Buffer.isBuffer(rawBody)) {
    throw new TypeError("rawBody deve ser o Buffer original da requisição.");
  }

  const signatures = normalizeSignatures(signatureHeader);
  if (signatures.length === 0) return false;

  const publicKeyPem = toPublicKeyPem(publicKeyBase64);

  return signatures.some((signatureBase64) => {
    try {
      return crypto.verify(
        "sha256",
        rawBody,
        publicKeyPem,
        Buffer.from(signatureBase64, "base64")
      );
    } catch {
      return false;
    }
  });
}
const express = require("express");
const app = express();

app.post(
  "/webhooks/pagbank",
  express.raw({ type: "application/json" }),
  async (request, response) => {
    const isValid = validateWebhook({
      rawBody: request.body,
      signatureHeader: request.headers["x-payload-signature"],
      publicKeyBase64: process.env.PAGBANK_WEBHOOK_PUBLIC_KEY
    });

    if (!isValid) {
      return response.status(401).json({
        error: "INVALID_WEBHOOK_SIGNATURE"
      });
    }

    const event = JSON.parse(request.body.toString("utf8"));
    await processEvent(event);

    return response.sendStatus(204);
  }
);
import java.nio.charset.StandardCharsets
import java.security.KeyFactory
import java.security.Signature
import java.security.spec.X509EncodedKeySpec
import java.util.Base64

fun isWebhookAuthentic(
    rawPayload: String,
    headerSignatures: List<String>,
    publicKeyBase64: String
): Boolean {
    if (headerSignatures.isEmpty()) return false

    val publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64)
    val publicKeySpec = X509EncodedKeySpec(publicKeyBytes)
    val publicKey = KeyFactory
        .getInstance("EC")
        .generatePublic(publicKeySpec)

    return headerSignatures.any { signatureBase64 ->
        try {
            val verifier = Signature.getInstance("SHA256withECDSA")
            verifier.initVerify(publicKey)
            verifier.update(rawPayload.toByteArray(StandardCharsets.UTF_8))

            val signatureBytes = Base64
                .getDecoder()
                .decode(signatureBase64)

            verifier.verify(signatureBytes)
        } catch (exception: Exception) {
            false
        }
    }
}

4. Trate o resultado

Assinatura válida

Quando pelo menos uma assinatura for válida:

  1. converta o payload para o objeto esperado;
  2. verifique o tipo do evento;
  3. aplique controles de idempotência;
  4. processe a atualização;
  5. retorne uma resposta HTTP de sucesso.

Assinatura inválida

Quando nenhuma assinatura for válida:

  • não processe o evento;
  • não atualize pedidos, cobranças ou qualquer outro recurso crítico;
  • não execute operações financeiras;
  • registre a tentativa de forma segura;
  • retorne um erro HTTP de autenticação.

Quando x-payload-signature estiver ausente ou vazio, trate a notificação como não autenticada.

5. Renove o par de chaves

O par de chaves utilizado na autenticação pode ser renovado.

curl --location --request PUT \
  'https://sandbox.api.pagseguro.com/public-keys' \
  --header 'accept: application/json' \
  --header 'content-type: application/json' \
  --header 'Authorization: Bearer SEU_TOKEN' \
  --data '{
    "type": "webhook"
  }'

A renovação somente poderá ser realizada após sete dias da criação do par atual.

Durante a transição, o PagBank pode assinar o payload com a chave anterior e com a nova chave. Por isso, x-payload-signature pode conter mais de uma assinatura.

Após a renovação:

  1. consulte novamente a chave pública;
  2. atualize o cache da aplicação;
  3. valide todas as assinaturas recebidas;
  4. monitore falhas de autenticação durante a transição.

Diferença em relação ao modelo anterior

Modelo anteriorNovo modelo
Concatenação entre token e payloadValidação do payload com chave pública
Hash SHA-256 simplesAssinatura ECDSA com SHA-256
Header x-authenticity-tokenHeader x-payload-signature
Um hash esperadoUma ou mais assinaturas possíveis
Segredo compartilhado participa do cálculoA chave privada permanece no PagBank

Não utilize o token da conta para calcular a assinatura do payload. O token serve apenas para autenticar a consulta e a renovação da chave pública.

Tratamento de erros

CenárioTratamento
x-payload-signature ausenteRejeitar a notificação.
Header vazioRejeitar a notificação.
Chave pública inválidaNão processar e atualizar a chave pública.
Assinatura Base64 inválidaIgnorar esse valor e testar os demais.
Nenhuma assinatura válidaRejeitar a notificação.
Uma assinatura válida entre váriasConsiderar a notificação autêntica.
Payload alterado antes da validaçãoRepetir a validação com o corpo bruto original.
Falha ao consultar a chaveUtilizar uma chave válida em cache; não processar sem uma chave confiável.

Boas práticas

  • Valide a assinatura antes de processar o JSON;
  • Preserve o corpo bruto da requisição;
  • Valide todos os valores de x-payload-signature;
  • Armazene a chave pública em cache;
  • Proteja o token usado para consultar e renovar a chave;
  • Registre falhas sem expor dados sensíveis;
  • Aplique idempotência usando o identificador da notificação;
  • Utilize HTTPS;
  • Não confie apenas no IP de origem;
  • Não processe notificações sem assinatura válida.

© 1996- Todos os direitos reservados.

PAGSEGURO INTERNET INSTITUIÇÃO DE PAGAMENTO S/A - CNPJ/MF 08.561.701/0001-01

Av. Brigadeiro Faria Lima, 1.384, São Paulo - SP - CEP 01451-001