Skip to main content
Build a service that notifies you when a transaction exceeds a threshold, matches a specific category, or hits a particular account — all powered by Yoshi webhooks.

Register a webhook endpoint

Create an endpoint to receive transaction.created events:
curl -X POST https://api.yoshi.ai/v1/webhooks/endpoints \
  -H "Authorization: Bearer yoshi_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhooks/yoshi",
    "filter_types": ["transaction.created"]
  }'
You can also manage endpoints through the self-service portal.

Build the webhook receiver

Set up an Express server that verifies the webhook signature and filters transactions:
import express from "express";
import { Webhook } from "svix";

const app = express();
app.use(express.raw({ type: "application/json" }));

const ALERT_THRESHOLD_CENTS = 10000; // $100.00

app.post("/webhooks/yoshi", async (req, res) => {
  // Verify the signature first
  const wh = new Webhook(process.env.WEBHOOK_SECRET);
  let event;
  try {
    event = wh.verify(req.body, {
      "webhook-id": req.headers["webhook-id"],
      "webhook-timestamp": req.headers["webhook-timestamp"],
      "webhook-signature": req.headers["webhook-signature"],
    });
  } catch (err) {
    return res.status(400).send("Invalid signature");
  }

  // Acknowledge immediately, process async
  res.sendStatus(200);

  if (event.type === "transaction.created") {
    await processTransactionEvent(event.data);
  }
});

app.listen(3000);

Filter and alert

Fetch the full transaction details using the IDs from the webhook payload, then apply your filtering logic:
import Yoshi from "@yoshi-ai/sdk";

const yoshi = new Yoshi();

async function processTransactionEvent(data) {
  const transactions = await yoshi.transactions.list({
    account_id: data.account_id,
    limit: data.count,
  });

  for (const tx of transactions.data) {
    // Filter by amount
    if (tx.amount_absolute >= ALERT_THRESHOLD_CENTS / 100) {
      await sendAlert({
        message: `Large transaction: $${tx.amount_absolute} at ${tx.counterparty_name}`,
        account: tx.account_name,
        category: tx.category_label,
      });
    }

    // Filter by category
    if (tx.category_tier1 === "Travel") {
      await sendAlert({
        message: `Travel expense: $${tx.amount_absolute} at ${tx.counterparty_name}`,
      });
    }
  }
}

Send to Slack

Post alerts to a Slack channel using an incoming webhook:
async function sendAlert({ message, account, category }) {
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      text: `🔔 *Transaction Alert*\n${message}`,
      blocks: [
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: [
              `*${message}*`,
              account ? `Account: ${account}` : null,
              category ? `Category: ${category}` : null,
            ]
              .filter(Boolean)
              .join("\n"),
          },
        },
      ],
    }),
  });
}

Handle duplicates

Webhook events can be delivered more than once. Use the event id for deduplication:
const processedEvents = new Set();

app.post("/webhooks/yoshi", async (req, res) => {
  const event = verifyWebhook(req);
  res.sendStatus(200);

  if (processedEvents.has(event.id)) {
    return; // Already processed
  }
  processedEvents.add(event.id);

  await processTransactionEvent(event.data);
});
For production use, store processed event IDs in a database or Redis instead of an in-memory Set.

Python example

from fastapi import FastAPI, Request, HTTPException
from svix.webhooks import Webhook
import os, httpx

app = FastAPI()
THRESHOLD = 100.00  # dollars

@app.post("/webhooks/yoshi")
async def handle_webhook(request: Request):
    body = await request.body()
    headers = dict(request.headers)

    wh = Webhook(os.environ["WEBHOOK_SECRET"])
    try:
        event = wh.verify(body, headers)
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid signature")

    if event["type"] == "transaction.created":
        await process_transactions(event["data"])

    return {"status": "ok"}

async def process_transactions(data):
    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://api.yoshi.ai/transactions",
            params={"account_id": data["account_id"], "limit": data["count"]},
            headers={"Authorization": f"Bearer {os.environ['YOSHI_API_KEY']}"},
        )
        for tx in response.json()["data"]:
            if tx["amount_absolute"] >= THRESHOLD:
                print(f"ALERT: ${tx['amount_absolute']} at {tx['counterparty_name']}")

What’s next

Event catalog

See all available event types.

Signature verification

Secure your webhook endpoint.
Last modified on April 17, 2026