Toda vez que uma compra nova cai na sua conta ou no seu cartão, o Banco MCP pode avisar o seu sistema — sem você ficar consultando extrato de minuto em minuto.
Neste guia você configura um webhook do zero: descobre o id da sua instalação, assina o evento certo, recebe o callback, valida a assinatura e busca a transação que disparou tudo. No fim, um atalho: mandar o aviso direto pra um canal do Discord ou do Slack, sem escrever backend nenhum.
Como o evento chega até você
O caminho é este:
- O banco publica a movimentação no Open Finance.
- O provedor sincroniza a conexão e nos avisa.
- O MCP.AI descobre de qual instalação é aquela conexão e faz um
POSTno seu endpoint.
O ponto que economiza mais tempo de debug: o callback é um toque, não um extrato. Ele diz "essa conexão mexeu", com o id da conexão — não vem valor, nem estabelecimento, nem descrição. Nenhum dado bancário atravessa este webhook. Quem quer saber o que foi comprado faz uma segunda chamada, no passo 7.
Antes de começar
Você vai precisar de:
- O Banco MCP instalado e pelo menos um banco conectado.
- Uma Workspace API key (
sk_live_…), criada em Configurações → API keys noapp.mcp.ai. - Um endpoint HTTPS público que aceite
POST. Endereçohttp://,localhost,127.0.0.1e.localsão recusados — em desenvolvimento, use um túnel com hostname https (ngrok, Cloudflare Tunnel e afins).
A key precisa ser de workspace. Key com escopo de credencial (travada num MCP + conexão) é só pro espelho REST e leva
403nas rotas de webhook.
Passo 1 — Descobrir o id da instalação
O evento de movimentação bancária nasce numa instalação (mi_…), e é lá que
ele aparece no catálogo de eventos. Liste o que você tem instalado:
curl -s https://app.mcp.ai/api/mcps \
-H "Authorization: Bearer sk_live_..."Procure a entrada do Banco MCP e guarde o id — é um mi_….
Se você prefere delegar, o mesmo passo em linguagem natural:
Liste os MCPs instalados no meu workspace do MCP.AI chamando
GET https://app.mcp.ai/api/mcps com o header Authorization: Bearer <minha workspace API key>.
Me diga o id (mi_...) da instalação do Banco MCP.Passo 2 — Ver quais eventos existem
Nunca chute nome de evento. Cada MCP declara os seus, e a própria API devolve a lista:
curl -s https://api.mcp.ai/api/installs/mi_ABC123/webhook \
-H "Authorization: Bearer sk_live_..."A resposta traz a configuração atual (vazia, se você ainda não criou nada), o catálogo de eventos e o que dá pra escolher em formato e transporte:
{
"install_id": "mi_ABC123",
"mcp_slug": "openfinance",
"url": null,
"events": [],
"include_result": true,
"enabled": true,
"format": "json",
"method": "POST",
"headers": {},
"has_secret": false,
"available_events": [
{ "id": "tool.call" },
{ "id": "disconnect" },
{ "id": "connection.created" },
{ "id": "pluggy.inbound" },
{ "id": "pluggy.*", "wildcard": true }
],
"available_formats": ["json", "discord", "slack"],
"available_methods": ["POST", "PUT", "PATCH"]
}Repare: não existe um evento transaction.created. Movimentação nova chega
como pluggy.inbound — o tipo específico vem dentro do payload, no campo
pluggy_event. É esse o evento que você quer assinar.
Duas coisas que a leitura não devolve, de propósito: o secret (você recebe
só has_secret, dizendo se já existe um) e o valor dos headers customizados
(voltam com o nome em claro e o valor mascarado).
Passo 3 — Criar o webhook
Não existe POST aqui: criar e atualizar são o mesmo PUT.
curl -X PUT https://api.mcp.ai/api/installs/mi_ABC123/webhook \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://meuapp.com/hooks/banco",
"events": ["pluggy.inbound"],
"secret": "um-segredo-longo-e-aleatorio"
}'Os campos aceitos:
| Campo | Obrigatório | Default | O que faz |
|---|---|---|---|
url | sim | — | Endpoint HTTPS que recebe a entrega. Sem localhost/.local. |
events | não | todos | Eventos assinados. Lista vazia ou ausente = todos. |
enabled | não | true | false mantém a configuração e para a entrega. |
secret | não | — | Liga a assinatura HMAC. Ver a regra abaixo. |
format | não | json | json, discord ou slack — como o corpo é apresentado. |
method | não | POST | POST, PUT ou PATCH. |
headers | não | {} | Até 10 headers customizados (ex.: autenticar no seu endpoint). |
include_result | não | true | Só afeta tool.call. |
⚠️ O PUT reescreve o documento inteiro. Campo que você omitir volta ao
default: sem events você passa a receber tudo, sem headers os customizados
somem. Sempre faça o GET do passo 2 antes de alterar algo.
O secret é a única exceção — e ela existe justamente porque a leitura não
devolve o valor:
- ausente → mantém o que já estava guardado
- valor → troca
- string vazia → limpa (as entregas deixam de ser assinadas)
Em vez do curl, você pode pedir pro seu agente:
Configure o webhook da minha instalação do Banco MCP no MCP.AI.
1. Primeiro leia a config atual: GET https://api.mcp.ai/api/installs/<meu mi_...>/webhook
com Authorization: Bearer <minha workspace API key>.
2. Confira em available_events que "pluggy.inbound" existe.
3. Depois faça PUT no mesmo endereço com este body:
{ "url": "https://meuapp.com/hooks/banco", "events": ["pluggy.inbound"], "secret": "<meu segredo>" }
Atenção: o PUT reescreve o documento inteiro, então reenvie os campos que já
estavam configurados (menos o secret, que é mantido quando você omite). Não
invente nomes de evento fora de available_events.Para desligar temporariamente, mande "enabled": false. Para remover de vez,
DELETE no mesmo endereço.
A resposta do
PUTecoa o documento salvo — inclusive osecretem claro, se houver. É a única superfície que devolve ele; não jogue esse retorno em log.
Passo 4 — O que o seu endpoint recebe
Quando uma compra nova é sincronizada, chega isto:
{
"event": "pluggy.inbound",
"provider": "pluggy",
"pluggy_event": "transactions/created",
"external_id": "0f2c5a1e-1111-2222-3333-444455556666",
"itemId": "0f2c5a1e-1111-2222-3333-444455556666",
"clientUserId": "usr_...",
"install_id": "mi_ABC123",
"account_id": "acc_...",
"connection": {
"key": "0f2c5a1e-1111-2222-3333-444455556666",
"label": null,
"connector_name": "Nubank",
"status": "UPDATED"
},
"triggered_at": "2026-08-07T20:11:02.345Z"
}Filtre por pluggy_event para separar movimentação de outros sinais da conexão
(item/updated, erro de login etc.). E guarde o itemId — é a chave do passo 7.
Passo 5 — Validar a assinatura
Se você configurou um secret, toda entrega vem com o header:
X-MCP-Signature: t=1754596262345,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08A assinatura é um HMAC-SHA256 da string `${t}.${corpo}`, com o secret
como chave. Duas regras que costumam derrubar a validação:
- Use o corpo cru da requisição, byte a byte. Se você fizer
JSON.stringify(req.body)para reconstruir, a assinatura não bate. Vale também quando oformatédiscord/slack: o que é assinado são os bytes renderizados, os mesmos que foram enviados. - Compare em tempo constante.
Em Node/Express:
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post(
"/hooks/banco",
express.raw({ type: "application/json" }),
(req, res) => {
const header = req.get("X-MCP-Signature") || "";
const t = header.match(/t=(\d+)/)?.[1];
const v1 = header.match(/v1=([a-f0-9]+)/)?.[1];
if (!t || !v1) return res.sendStatus(401);
// Rejeita entrega velha (proteção contra replay).
if (Math.abs(Date.now() - Number(t)) > 5 * 60 * 1000) return res.sendStatus(401);
const expected = crypto
.createHmac("sha256", process.env.MCP_WEBHOOK_SECRET)
.update(`${t}.${req.body}`) // req.body é o Buffer cru
.digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
const payload = JSON.parse(req.body.toString("utf8"));
// ... enfileira o processamento e responde rápido
res.sendStatus(200);
},
);Sem secret configurado, o header simplesmente não vem.
Passo 6 — Sem backend: avisar no Discord ou no Slack
Se você só quer ver a movimentação chegando, pule o servidor. Crie um webhook de canal no Discord (ou um incoming webhook no Slack), cole a URL e escolha o formato:
curl -X PUT https://api.mcp.ai/api/installs/mi_ABC123/webhook \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://discord.com/api/webhooks/.../...",
"events": ["pluggy.inbound"],
"format": "discord"
}'O corpo sai como um embed do Discord (ou attachment do Slack) em vez de JSON
cru — canal de chat recusa JSON cru com 400.
Duas ressalvas: o card só mostra campos escalares, então o bloco
connection { … } não aparece (o connector_name, o nome do banco, fica de
fora); e a assinatura passa a cobrir os bytes renderizados. Para integrar de
verdade, use format: "json" com o seu endpoint.
Precisa autenticar no seu endpoint? Use headers:
{ "headers": { "X-Api-Key": "a-chave-do-meu-servico" } }São até 10, e a leitura devolve o valor mascarado (o nome fica em claro).
X-MCP-Signature e headers de transporte (Host, Content-Length…) são
recusados.
Como o PUT reescreve tudo, reenvie o mapa headers inteiro — mas você não
precisa redigitar os valores: mandar de volta o valor mascarado que veio do GET
é entendido como "não mexi neste", e o valor guardado é preservado.
Passo 7 — Buscar a compra de verdade
O callback te deu o itemId. Agora sim você pega as transações daquela conexão.
Pelo espelho REST:
curl -X POST https://api.mcp.ai/api/openfinance/transactions/by-item \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"item_id": "0f2c5a1e-1111-2222-3333-444455556666",
"from": "2026-08-01",
"to": "2026-08-07",
"granularity": "raw"
}'granularity: "raw" traz as linhas individuais; o default (monthly) devolve só
o resumo consolidado do período. Se você quiser as transações de uma conta
específica em vez da conexão inteira, use openfinance_list_transactions com o
account_id.
Pelo agente, o fluxo completo em um prompt:
Chegou um webhook do Banco MCP com itemId "0f2c5a1e-1111-2222-3333-444455556666".
Use a tool openfinance_list_transactions_by_item com esse item_id, from = ontem,
to = hoje e granularity = "raw". Me liste só as transações de saída, com data,
descrição e valor, ordenadas da maior para a menor.Limites que valem saber
- Entrega no máximo uma vez. Timeout de 15 segundos e sem retry
automático. Responda
2xxrápido e processe em background — se o seu endpoint demorar, a entrega é perdida. - Idempotência é sua. Guarde o
itemId+triggered_atque você já processou. Do nosso lado há deduplicação de replay do provedor, mas o seu handler deve tolerar repetição. - Três escopos entregam. Além da instalação (
mi_…), o webhook do toolkit e o global do workspace também recebem, desde que assinem o evento (lembre: lista de eventos vazia = assina tudo). Os destinos são deduplicados por URL, e cada um usa o própriosecret,formateheaders. O catálogoavailable_eventscompluggy.inboundsó aparece no escopo da instalação — nos outros dois você precisa escrever o nome do evento à mão. - Correlação é fechada por padrão. Se a conexão do evento não pertence a
nenhuma instalação sua, nada é entregue. É por isso que um evento de teste com
um
itemIdinventado não chega em lugar nenhum.
Não chegou nada?
| Sintoma | O que checar |
|---|---|
| Nenhuma entrega | O GET do passo 2 mostra a url que você espera? enabled está true? O evento pluggy.inbound está em events (ou a lista está vazia)? |
400 no PUT | A URL é https e não aponta pra localhost/.local? Algum header com nome/valor inválido ou na lista de recusados? |
403 no PUT | A key é de workspace (não escopada num MCP) e do workspace certo? |
| Assinatura parou de bater | Você mandou "secret": "" num PUT? String vazia limpa o segredo. |
| Assinatura nunca bateu | Você está assinando o corpo cru, e não o JSON re-serializado? |
| Chega no Discord mas falta o banco | Esperado: o card só renderiza campos escalares, e connection é um objeto. Use format: "json". |
| Perdi headers customizados ao salvar | O PUT reescreve tudo — reenvie o mapa headers inteiro. |