Procurando a documentação anterior?Acesse v1.docs.pluggy.ai
PluggyDocs

Webhooks de Chamados de Suporte

Receba um evento sempre que um dos seus tickets da API de Suporte ao Parceiro mudar — status, resolução, respostas e prioridade — assinado com HMAC-SHA256 e refeito em caso de falha.

Ver como Markdown

Visão Geral#

Se você abrir tickets através da Referência da API de Suporte ao Parceiro, não precisa fazer polling em GET /api/partners/tickets para saber que um caso foi movido. Dê-nos uma URL HTTPS e nós POST um evento JSON para ela sempre que um dos seus tickets mudar — uma mudança de status, uma resolução, uma resposta da nossa equipe de suporte, uma mudança de prioridade ou um novo título.

A API não desaparece. Ela continua sendo a autoridade: você continua usando-a para abrir tickets, postar acompanhamentos e reconciliar. O webhook é o caminho rápido, não o único.

POST https://your-endpoint.example.com/pluggy/tickets
Content-Type: application/json
X-Pluggy-Signature: t=1758202927,v1=6f9c…
X-Pluggy-Event-Id: 0f2b6c4e-7a1d-4a2e-9c3f-5b8d1e0a7c44

Somente respostas visíveis para o cliente são enviadas

Nosso suporte mantém notas internas sobre os mesmos tickets. Elas nunca são emitidas. ticket.message_added carrega uma resposta escrita para você e nada mais — o filtro é positivo (uma resposta pública da nossa equipe), então qualquer coisa nova e interna é excluída por padrão, em vez de incluída até que alguém se lembre de excluí-la. Suas próprias mensagens também não são ecoadas de volta para você.

Configurando#

  1. Envie-nos uma URL HTTPS. Uma por ambiente se você quiser validar contra um consumidor separado primeiro. HTTP simples é rejeitado.
  2. Retornamos um segredo de assinatura, do formato whsec_ seguido por 64 caracteres hexadecimais. Ele é mostrado uma vez, no momento da geração, e não podemos lê-lo de volta para você depois — armazene-o como um segredo do seu lado. Se for perdido ou vazado, peça-nos para girá-lo; o novo segredo substitui o antigo imediatamente, então faça a troca em um passo.
  3. Habilitamos a entrega. A partir desse momento, toda mudança em um ticket que pertence à sua conta produz um evento.

Seu endpoint deve responder a qualquer 2xx assim que tiver aceitado duravelmente o evento — coloque-o na fila e processe de forma assíncrona. Esperamos 8 segundos por uma resposta; qualquer coisa mais lenta é tratada como uma entrega falhada e é tentada novamente.

A carga útil#

Toda entrega tem o mesmo envelope:

CampoSignificado
eventIdUUID, único por evento e estável em tentativas. Sua chave de idempotência. Também enviado como o cabeçalho X-Pluggy-Event-Id.
sequenceMonotônico, atribuído quando o evento é criado. O sequence crescente é a ordem de criação — não é a ordem de entrega, veja abaixo.
eventUm dos cinco tipos de eventos abaixo.
occurredAtQuando observamos a mudança. Uma nova tentativa não altera isso.
ticketO ticket após a mudança: key, number (número do nosso suporte), title, state, status, updatedAt, portalUrl e priority onde sabemos.
previousPresente apenas onde há um valor anterior — state/status, priority ou title.
messagePresente apenas em ticket.message_added.

O bloco do ticket é deliberadamente pequeno. Ele diz o que mudou; GET /api/partners/tickets/{key} é a autoridade sobre tudo o mais.

state é o bucket de ciclo de vida grosso e é um dos new, on_you, on_customer, on_hold ou closed. status é o rótulo legível que o acompanha — New, On you, On customer, Aguardando Detentora, Aguardando Engenharia, Closed — e um status que ainda não mapeamos chega como ele mesmo, então combine com state e trate status como texto de exibição.

Cada timestamp é UTC, RFC 3339, com sufixo Z. ticket.updatedAt e message.sentAt se originam no sistema de tickets, que os reporta com um deslocamento; nós os normalizamos antes de enviar, então uma carga útil nunca carrega dois formatos.

Verificando a assinatura#

X-Pluggy-Signature é formatado como t=<segundos unix>,v1=<hmac hex>. A string assinada é "{t}.{corpo bruto}" — o corpo da requisição exatamente como chegou na rede, antes de qualquer análise JSON. Re-serializar o objeto analisado muda os bytes e a assinatura não corresponderá.

t é também o token anti-replay: rejeite uma requisição cujo t seja mais velho que cinco minutos.

js
import { createHmac, timingSafeEqual } from "node:crypto";
 
const TOLERANCE_SECONDS = 5 * 60;
 
export function verifyPluggySignature(rawBody, header, secret) {
  // header: "t=1758202927,v1=6f9c…"
  const parts = new Map(
    String(header ?? "")
      .split(",")
      .map((part) => {
        const i = part.indexOf("=");
        return [part.slice(0, i).trim(), part.slice(i + 1).trim()];
      }),
  );
 
  const timestamp = Number(parts.get("t"));
  const received = parts.get("v1");
  if (!Number.isFinite(timestamp) || !received) return false;
 
  // Anti-replay: uma assinatura antiga ainda é uma assinatura válida.
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > TOLERANCE_SECONDS) return false;
 
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
 
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(received, "utf8");
  return a.length === b.length && timingSafeEqual(a, b);
}

Integrado em um aplicativo Express, mantendo o corpo bruto:

js
import express from "express";
 
const app = express();
 
app.post(
  "/pluggy/tickets",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const raw = req.body.toString("utf8"); // os bytes que assinamos
    if (!verifyPluggySignature(raw, req.get("X-Pluggy-Signature"), process.env.PLUGGY_WEBHOOK_SECRET)) {
      return res.status(401).end();
    }
 
    const event = JSON.parse(raw);
    enqueue(event); // de-duplicar em event.eventId, depois processar
    res.status(200).end();
  },
);

Uma verificação que você pode executar contra uma entrega que você capturou: createHmac("sha256", secret).update(t + "." + rawBody).digest("hex") deve ser igual ao valor v1, caractere por caractere.

Eventos#

ticket.status_changed#

O ticket se moveu no fluxo de trabalho — state, o status do portal, ou ambos. previous carrega o que eles eram.

json
{
  "eventId": "0f2b6c4e-7a1d-4a2e-9c3f-5b8d1e0a7c44",
  "sequence": 1042,
  "event": "ticket.status_changed",
  "occurredAt": "2026-09-18T13:42:07.912Z",
  "ticket": {
    "key": "SUP2-1234",
    "number": 418,
    "title": "[YourOrg/Itaú] Transação não retornada (YOUR-1947)",
    "state": "on_you",
    "status": "On you",
    "updatedAt": "2026-09-18T13:41:58Z",
    "portalUrl": "https://pluggy.atlassian.net/servicedesk/customer/portal/1/SUP2-1234"
  },
  "previous": { "state": "new", "status": "New" }
}

ticket.resolved#

O ticket foi fechado. Ele é enviado em vez de ticket.status_changed, nunca em adição: uma mudança no mundo real produz um evento, então um consumidor que age em ambos não age duas vezes. Não há bloco previous.

json
{
  "eventId": "7c1a55d2-3e64-4b0a-8f2d-9a0c6e4b21f7",
  "sequence": 1043,
  "event": "ticket.resolved",
  "occurredAt": "2026-09-18T18:05:31.004Z",
  "ticket": {
    "key": "SUP2-1234",
    "number": 418,
    "title": "[YourOrg/Itaú] Transação não retornada (YOUR-1947)",
    "state": "closed",
    "status": "Closed",
    "updatedAt": "2026-09-18T18:05:12Z",
    "portalUrl": "https://pluggy.atlassian.net/servicedesk/customer/portal/1/SUP2-1234"
  }
}

ticket.message_added#

Uma resposta da nossa equipe de suporte foi publicada no ticket. Este é o único evento que carrega conteúdo: message.text é a resposta escrita, em texto simples.

ticket.updatedAt neste evento é o momento em que a mensagem foi enviada.

json
{
  "eventId": "b83f0c19-55ad-4c6e-9f41-2d7e8ab10c53",
  "sequence": 1044,
  "event": "ticket.message_added",
  "occurredAt": "2026-09-18T15:20:44.881Z",
  "ticket": {
    "key": "SUP2-1234",
    "number": 418,
    "title": "[YourOrg/Itaú] Transação não retornada (YOUR-1947)",
    "state": "on_you",
    "status": "On you",
    "updatedAt": "2026-09-18T15:20:31Z",
    "portalUrl": "https://pluggy.atlassian.net/servicedesk/customer/portal/1/SUP2-1234"
  },
  "message": {
    "author": "Pluggy Support",
    "text": "Identificamos a causa na coleta da fatura fechada e o ajuste entrou em produção hoje. Pode validar nos itens afetados e nos confirmar?",
    "sentAt": "2026-09-18T15:20:31Z"
  }
}

ticket.priority_changed#

A prioridade do ticket mudou. previous.priority é o valor que tinha antes.

O novo valor é ticket.priority, e previous.priority é o que era. Ambos são os próprios nomes do sistema de tickets (Highest, High, Medium, Low), que não são os rótulos P0 — Crítico … P3 — Baixo que o endpoint de criação aceita — o mesmo conceito em dois vocabulários, que preferimos te contar do que você descobrir.

A primeira vez que observamos uma prioridade em um ticket não é uma mudança e não produz evento.

json
{
  "eventId": "2ad9e5b7-1c08-49f3-b6a1-70c2d4e95f18",
  "sequence": 1045,
  "event": "ticket.priority_changed",
  "occurredAt": "2026-09-18T14:02:19.377Z",
  "ticket": {
    "key": "SUP2-1234",
    "number": 418,
    "title": "[YourOrg/Itaú] Transação não retornada (YOUR-1947)",
    "state": "on_you",
    "status": "On you",
    "updatedAt": "2026-09-18T14:02:05Z",
    "portalUrl": "https://pluggy.atlassian.net/servicedesk/customer/portal/1/SUP2-1234"
  },
  "previous": { "priority": "Medium" }
}

ticket.updated#

O título do ticket mudou. Não é enviado quando o ticket é fechado na mesma passagem — a resolução é o evento que importa ali.

json
{
  "eventId": "e40b7f36-9c52-4a8d-83b1-6f0a2c5d7e91",
  "sequence": 1046,
  "event": "ticket.updated",
  "occurredAt": "2026-09-18T16:11:02.560Z",
  "ticket": {
    "key": "SUP2-1234",
    "number": 418,
    "title": "[YourOrg/Itaú] Transações do cartão não retornadas (YOUR-1947)",
    "state": "on_you",
    "status": "On you",
    "updatedAt": "2026-09-18T16:10:47Z",
    "portalUrl": "https://pluggy.atlassian.net/servicedesk/customer/portal/1/SUP2-1234"
  },
  "previous": { "title": "[YourOrg/Itaú] Transação não retornada (YOUR-1947)" }
}

Idempotência#

De-duplicate em eventId. É um UUID, único por evento, e não muda quando tentamos novamente — um reenvio do mesmo evento carrega o mesmo id. O mesmo id chegando duas vezes significa a mesma mudança, não duas mudanças.

Você verá repetições na operação normal. Dois caminhos independentes notam que um ticket se moveu (uma notificação ao vivo e uma comparação completa periódica, que é o que torna o espelho inegável), e um reenvio manual pela nossa equipe reutiliza o eventId original de propósito.

Trate um evento cujo id você já processou como um no-op e responda 2xx — um 409 do seu lado parece uma entrega falhada para nós e será tentado novamente.

Ordenação#

A ordenação não é garantida. Um evento refeito chega após eventos criados mais tarde do que ele, e duas mudanças a segundos de distância podem chegar em qualquer ordem. O sequence informa qual evento foi criado primeiro, então você pode descartar um que já foi superado — mas não é a ordem de entrega, e você não deve reconstruir o estado a partir do fluxo. Quando importa, leia o ticket.

A regra que mantém um consumidor correto: leia o ticket quando receber um evento. O evento te diz que algo se moveu; GET /api/partners/tickets/{key} te diz o que é verdade agora.

Tentativas#

Uma entrega é bem-sucedida em qualquer 2xx. Qualquer outro status, um erro de conexão, ou nenhuma resposta dentro de 8 segundos conta como uma falha e é tentada novamente com backoff:

TentativaEnviada
1Imediatamente, quando o evento é criado
2~30 segundos depois
3~2 minutos depois
4~10 minutos depois
5~1 hora depois
6~6 horas depois

Seis tentativas no total. Após a sexta falha, a entrega é marcada como falhada e não é tentada novamente automaticamente — peça-nos e podemos reenviá-la manualmente, com o mesmo eventId.

Os atrasos são quando uma nova tentativa se torna devida; a passagem de nova tentativa roda em uma programação própria, então as duas primeiras tentativas podem chegar um pouco mais tarde do que o atraso nominal. Cada tentativa é registrada do nosso lado — o que enviamos, o que voltou e quantas vezes tentamos — então "você realmente enviou?" tem uma resposta que não é um palpite.

Lista de verificação antes de entrar em produção#

  • Verifique a assinatura sobre o corpo bruto, e rejeite em caso de discrepância.
  • Rejeite um t mais velho que cinco minutos.
  • De-duplicate em eventId antes de fazer qualquer trabalho.
  • Responda 2xx rapidamente, processe de forma assíncrona.
  • Releia o ticket em vez de confiar na ordem dos eventos.
  • Mantenha o segredo fora do controle de versão e nos avise se precisar ser girado.
Esta página foi útil?