Como receber um webhook toda vez que você compra algo
Receba um POST quando cair movimentação nova na conta ou no cartão: endpoints, payloads, validação de assinatura e o atalho de avisar direto num canal do Discord ou Slack.
Run with AI
Skip the reading: send this guide as a ready-to-run prompt. It tells the assistant to stop and ask you whenever it needs a credential or a step only you can do.
Claude Code opens with the prompt filled in but not sent, so you can review it first.
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. |
More guides
Como identificar e cancelar assinaturas que você não usa
Uma sequência de prompts que varre o extrato e a fatura do cartão, acha as cobranças recorrentes esquecidas, calcula o custo anual e monta o plano de cancelamento.
GuidesComo construir um gestor financeiro pessoal no Lovable
Do zero ao painel: arquitetura, schema, edge function e telas — com os prompts prontos pra colar no Lovable e os dados vindo direto do Banco MCP.
Guides