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#
- Envie-nos uma URL HTTPS. Uma por ambiente se você quiser validar contra um consumidor separado primeiro. HTTP simples é rejeitado.
- 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. - 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:
| Campo | Significado |
|---|---|
eventId | UUID, único por evento e estável em tentativas. Sua chave de idempotência. Também enviado como o cabeçalho X-Pluggy-Event-Id. |
sequence | Monotônico, atribuído quando o evento é criado. O sequence crescente é a ordem de criação — não é a ordem de entrega, veja abaixo. |
event | Um dos cinco tipos de eventos abaixo. |
occurredAt | Quando observamos a mudança. Uma nova tentativa não altera isso. |
ticket | O ticket após a mudança: key, number (número do nosso suporte), title, state, status, updatedAt, portalUrl e priority onde sabemos. |
previous | Presente apenas onde há um valor anterior — state/status, priority ou title. |
message | Presente 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.
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:
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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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:
| Tentativa | Enviada |
|---|---|
| 1 | Imediatamente, 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
tmais velho que cinco minutos. - De-duplicate em
eventIdantes de fazer qualquer trabalho. - Responda
2xxrapidamente, 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.
