Pular para o conteúdo

Webhooks

Webhooks permitem que sua equipe receba notificações automáticas em tempo real no seu próprio servidor quando eventos acontecem no Pliic, como uma nova sugestão enviada ou uma mudança de status em um ticket.

Webhooks estão disponíveis nos planos Starter (até 3 endpoints) e Pro (ilimitados). O plano Gratuito não inclui este recurso. Apenas membros com papel Administrador ou Dono podem gerenciar webhooks.

  1. Acesse Configurações → Webhooks
  2. Clique em Adicionar endpoint
  3. Preencha os campos:
    • Nome: identificação interna do endpoint
    • URL: endereço HTTPS público que receberá as notificações
    • Eventos: selecione quais eventos devem disparar este endpoint
  4. Clique em Criar

Ao criar, um segredo de assinatura (whsec_…) é exibido uma única vez. Copie e armazene com segurança, ele não pode ser recuperado depois.

Se o segredo for perdido ou comprometido, abra o menu do endpoint e escolha Rotacionar segredo. O Pliic mostra o novo segredo uma vez e o anterior deixa de funcionar imediatamente — atualize o receptor antes ou logo depois de rotacionar.

EventoDescrição
suggestion.createdUma nova sugestão foi enviada.
suggestion.status_changedO status de uma sugestão foi alterado.
suggestion.commentedUm comentário foi adicionado a uma sugestão.
ticket.createdUm novo ticket foi aberto.
ticket.status_changedO status de um ticket foi alterado.
ticket.repliedUma resposta pública foi adicionada a um ticket. Notas internas nunca disparam este evento — elas não aparecem em nenhuma superfície fora do painel da sua equipe.
survey.response.createdUm usuário respondeu a uma pesquisa (survey).

Cada entrega é uma requisição POST com Content-Type: application/json e o seguinte formato:

{
"id": "018e1234-5678-7abc-def0-123456789abc",
"event": "suggestion.created",
"created_at": "2024-01-15T14:30:00Z",
"app": {
"id": 42,
"public_key": "pk_live_..."
},
"data": { ... }
}
CampoTipoDescrição
idstring (UUID)Identificador único da entrega.
eventstringNome do evento disparado.
created_atstring (ISO 8601)Data e hora do evento em UTC.
appobjectO app do seu time que originou o evento: id (identificador interno) e public_key (a mesma pk_live_... usada no widget desse app), para correlacionar o evento com a configuração certa quando o seu time tem mais de um app.
dataobjectDados específicos do evento.

Todo data de evento traz dois objetos de identidade, além dos campos específicos do evento:

CampoDescrição
authorO dono do registro: quem abriu o ticket, criou a sugestão ou respondeu a pesquisa. Formato { external_id, name }. external_id é o id que o seu próprio sistema atribuiu a esse usuário ao criá-lo.
senderQuem causou este evento específico. Formato { type, external_id, name }. type é "member" quando alguém da sua equipe agiu pelo painel do Pliic (external_id vem null, membros não têm id externo) ou "app_user" quando foi o próprio usuário final (external_id presente).

Em eventos disparados pelo próprio autor do registro (por exemplo, ele mesmo criando a sugestão), author e sender apontam para a mesma pessoa. Em eventos disparados por outra pessoa (a sua equipe respondendo um ticket, ou mudando o status de uma sugestão pelo board), eles divergem.

Exemplo de ticket.replied respondido pela sua equipe:

{
"ticket_id": 123,
"ticket_number": "TKT-0042",
"message_id": 987,
"sender_type": "agent",
"author": { "external_id": "usr_42", "name": "Ana" },
"sender": { "type": "member", "external_id": null, "name": "Suporte Pliic" }
}

Caso de uso comum: notificar o autor quando algo muda no registro dele, mas sem notificá-lo quando a mudança foi causada por ele mesmo (por exemplo, o próprio usuário respondendo o próprio ticket). Compare os dois external_id:

if (data.author.external_id !== data.sender.external_id) {
notificar(data.author);
}

Em survey.response.created, quem responde é sempre o sender (não existe fluxo de responder “em nome de” outro usuário), então author e sender sempre coincidem. O evento também mantém o campo legado app_user_id, ao lado do novo author.

Cada entrega inclui os seguintes cabeçalhos HTTP:

CabeçalhoDescrição
X-Pliic-SignatureAssinatura no formato t=<timestamp unix>,v1=<HMAC-SHA256>.
X-Pliic-EventNome do evento (ex: suggestion.created).
X-Pliic-DeliveryUUID único desta entrega.

Verificar a assinatura garante que a requisição foi enviada pelo Pliic e que o conteúdo não foi alterado no caminho. O cabeçalho tem o formato:

X-Pliic-Signature: t=1750000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

O t é o timestamp unix do envio e o v1 é HMAC-SHA256(segredo, "{t}.{corpo_bruto}"). Verificar o timestamp junto protege contra reenvio de payloads antigos capturados (replay).

PHP (com o SDK oficial):

use Pliic\Webhook;
use Pliic\Exceptions\SignatureVerificationException;
try {
$event = Webhook::constructEvent(
$request->getContent(),
$request->header('X-Pliic-Signature'),
$segredoDoEndpoint, // whsec_...
);
} catch (SignatureVerificationException $e) {
abort(400);
}
// $event->type, $event->data, $event->id

O SDK (composer require pliic/pliic-php) já compara em tempo constante e rejeita assinaturas com mais de 5 minutos.

Usando Laravel? A integração Laravel do SDK já registra essa rota pronta pra você, com a verificação de assinatura incluída.

PHP (manual):

function verificarAssinatura(string $segredo, string $corpoRaw, string $cabecalho): bool
{
if (preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $cabecalho, $m) !== 1) {
return false;
}
[, $timestamp, $assinatura] = $m;
if (abs(time() - (int) $timestamp) > 300) {
return false;
}
$esperado = hash_hmac('sha256', "{$timestamp}.{$corpoRaw}", $segredo);
return hash_equals($esperado, $assinatura);
}

Node.js:

const crypto = require('crypto');
function verificarAssinatura(segredo, corpoRaw, cabecalho) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(cabecalho);
if (!m) return false;
const [, timestamp, assinatura] = m;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const esperado = crypto
.createHmac('sha256', segredo)
.update(`${timestamp}.${corpoRaw}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(esperado),
Buffer.from(assinatura),
);
}

Se o seu servidor retornar um status diferente de 2xx ou a conexão expirar, o Pliic tentará reenviar automaticamente com backoff exponencial:

TentativaAtraso aproximado
1ª (reenvio)5 minutos
15 minutos
45 minutos
135 minutos
405 minutos (~6,75 horas)

Após 5 tentativas sem sucesso, a entrega é abandonada.

Reenvios do mesmo evento carregam o mesmo id no corpo. Se o seu processamento não for idempotente por natureza, guarde os id já processados e ignore repetidos.

O histórico completo de cada entrega (status, código de resposta e número de tentativas) está disponível na página de detalhes do endpoint.

Depois das 5 tentativas, se a entrega continuar falhando, o evento é abandonado e não há uma 6ª tentativa. Para não depender só do webhook chegar, use a API como rede de segurança:

GET /api/v1/tickets?updated_since=2024-01-15T14:30:00Z
GET /api/v1/suggestions?updated_since=2024-01-15T14:30:00Z

O parâmetro updated_since (timestamp ISO 8601) filtra os itens atualizados a partir daquele instante e devolve o resultado ordenado por updated_at crescente — o oposto do “mais recente primeiro” padrão desses endpoints, pensado justamente para varrer o que mudou em ordem cronológica.

Padrão recomendado: guarde o horário do último webhook processado com sucesso. Ao perceber um silêncio incomum ou uma entrega marcada como abandonada no histórico do endpoint, chame os dois endpoints com updated_since=<esse horário> e reprocesse o que vier, usando o mesmo tratamento idempotente que você já aplica a reenvios. A resposta de cada sugestão nesses endpoints também traz o objeto author, no mesmo formato usado nos eventos de webhook.

Na página de detalhes do endpoint, clique em Enviar teste. O Pliic enviará um evento webhook.ping para que você possa verificar se o servidor está recebendo requisições corretamente antes de ativar eventos reais.

  • Apenas URLs HTTPS públicas são aceitas. Endereços internos, de rede privada ou localhost são rejeitados.
  • O certificado TLS do servidor de destino é sempre verificado.
  • Nunca compartilhe o segredo de assinatura nem o inclua em código público ou logs.