# API key authentication and key management Source: https://docs.yoshi.ai/authentication Create and manage Yoshi API keys from your dashboard. Learn how to authenticate requests with Bearer tokens and rotate keys safely. ## Getting an API Key **1. Go to Settings** Open **Settings → API Keys** in your [Yoshi dashboard](https://beta.yoshi.ai). **2. Create a key** Click **Create API Key** and give it a name. **3. Copy the key** Copy the key immediately — it won't be shown again. Keys start with `yoshi_`. ## Using Your Key Include it as a Bearer token in the `Authorization` header: ```bash curl theme={null} curl https://api.yoshi.ai/accounts \ -H "Authorization: Bearer yoshi_3xK9mP..." ``` ```python Python theme={null} import httpx response = httpx.get( "https://api.yoshi.ai/accounts", headers={"Authorization": "Bearer yoshi_3xK9mP..."}, ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.yoshi.ai/accounts", { headers: { Authorization: "Bearer yoshi_3xK9mP..." }, }); const data = await response.json(); ``` ## Key Management * You can have **multiple active keys** for zero-downtime rotation * Revoke a key instantly from the dashboard — it takes effect globally within seconds * Keys are hashed by our provider and never stored in plaintext > [!WARNING] > > Never commit API keys to source control. The `yoshi_` prefix enables > secret scanning in GitHub and GitGuardian. ## Rate Limit Headers Every response includes rate limit information. See [Rate Limits](/rate-limits) for details. # Changelog Source: https://docs.yoshi.ai/changelog Chronological release notes for the Yoshi API, including new endpoints, breaking changes, deprecations, bug fixes, and SDK version updates. ## April 2026 ### v1.0 — Initial public API release The Yoshi consumer API is now publicly available. This release includes: **Core data endpoints:** * `GET /accounts` — List linked financial accounts with balances * `GET /accounts/{id}/balances` — Historical daily balance series * `GET /transactions` — List transactions with cursor-based pagination * `GET /scores` — Financial health scores (Yoshi, Recovery, Capacity, Baseline) * `GET /goals` — Financial goals and progress * `GET /recurring` — Recurring streams (subscriptions, bills, income) * `GET /investments` — Investment holdings grouped by asset class * `GET /income` — Income analysis and distribution * `GET /me` — User profile * `GET /me/summary` — Comprehensive financial overview **Card benefits:** * `GET /benefits/summary` — Active card benefit periods with usage * `GET /benefits/expiring` — Benefits expiring within a configurable window **Paper trading:** * `POST /paper-trading/accounts` — Create virtual trading accounts * `GET /paper-trading/accounts` — List paper trading accounts * `POST /paper-trading/accounts/{id}/trades` — Place simulated trades * `GET /paper-trading/accounts/{id}/holdings` — Current positions * `GET /paper-trading/accounts/{id}/trades` — Trade history **Actions & approvals:** * `POST /actions` — Create action proposals * `GET /actions` — List actions * `GET /actions/{id}` — Retrieve single action * `GET /approvals/{threadId}` — Check approval status **Webhooks:** * Real-time event delivery for transaction, balance, account, investment, score, income, and recurring changes * HMAC-SHA256 signature verification * Self-service portal for endpoint management **SDKs:** * TypeScript (`@yoshi-ai/sdk`) * Python (`yoshi-sdk`) * Go (`github.com/yoshi-ai-dev/yoshi-go`) **CLI:** * `yoshi` CLI via Homebrew or Go install **Infrastructure:** * Unkey-based API key authentication with `yoshi_` prefix * Cursor-based pagination * RFC 9457-inspired error envelope * 100 requests/minute rate limiting per key * `Idempotency-Key` support on mutating endpoints *** > [!NOTE] > > Future updates will be listed here in reverse chronological order. Breaking changes will include migration guidance. # Best practices for building with the Yoshi API Source: https://docs.yoshi.ai/concepts/best-practices Patterns for error handling, retry strategies, caching, webhook processing, security, and rate limit budgeting when building on the Yoshi API. Follow these patterns to build reliable, performant, and secure integrations with the Yoshi API. ## Error handling Always check the response envelope before accessing data: ```typescript TypeScript theme={null} const response = await fetch("https://api.yoshi.ai/accounts", { headers: { Authorization: `Bearer ${process.env.YOSHI_API_KEY}` }, }); if (!response.ok) { const { error, meta } = await response.json(); console.error(`[${meta.request_id}] ${error.code}: ${error.message}`); throw new Error(error.display_message || error.message); } const { data } = await response.json(); ``` ```python Python theme={null} response = httpx.get( "https://api.yoshi.ai/accounts", headers={"Authorization": f"Bearer {os.environ['YOSHI_API_KEY']}"}, ) if response.status_code != 200: body = response.json() error = body["error"] request_id = body["meta"]["request_id"] raise Exception(f"[{request_id}] {error['code']}: {error['message']}") data = response.json()["data"] ``` Include `request_id` in error logs and support tickets — it lets Yoshi trace exactly what happened. ## Retry strategy | Status | Retry? | Strategy | | - | - | - | | `429` | Yes | Wait `retry_after` seconds, then retry | | `500` | Yes | Exponential backoff: 1s, 2s, 4s (max 3 retries) | | `503` | Yes | Exponential backoff | | `400` | No | Fix the request — the input is invalid | | `401` | No | Check your API key | | `404` | No | The resource doesn't exist | | `409` | No | Duplicate idempotency key with different parameters | ```typescript theme={null} async function fetchWithRetry(url, options, maxRetries = 3) { for (let attempt = 0; attempt <= maxRetries; attempt++) { const response = await fetch(url, options); if (response.ok) return response; if (response.status === 429) { const retryAfter = Number(response.headers.get("Retry-After")) || 60; await sleep(retryAfter * 1000); continue; } if (response.status >= 500 && attempt < maxRetries) { await sleep(1000 * Math.pow(2, attempt)); continue; } return response; // Don't retry 4xx errors } } ``` > [!TIP] > > The SDKs handle retries automatically with configurable `maxRetries` (default: 2). ## Caching | Resource | Safe to cache | Duration | | - | - | - | | Account metadata (names, types) | Yes | 5–15 minutes | | Balances | No | Changes every sync | | Transactions list | Briefly | 1–5 minutes | | Scores | Yes | 1 hour (recalculated daily) | | Recurring streams | Yes | 1 hour | | Card products catalog | Yes | 24 hours | Never cache balance data — it changes with every sync and users expect current numbers. ## Webhook processing Respond fast, process later: ```javascript theme={null} app.post("/webhooks/yoshi", async (req, res) => { // Verify signature const event = verifyWebhook(req); // Acknowledge immediately res.sendStatus(200); // Process asynchronously await queue.add("process-event", event); }); ``` > [!WARNING] > > Return a `2xx` response within 15 seconds. If your handler takes longer, Yoshi marks the delivery as failed and retries. **Idempotent processing:** Events can be delivered more than once. Use `event.id` to deduplicate: ```javascript theme={null} async function processEvent(event) { const already = await db.get(`processed:${event.id}`); if (already) return; await handleEvent(event); await db.set(`processed:${event.id}`, true, { ex: 86400 }); } ``` ## Security **Never expose API keys client-side.** API keys grant full access to the user's financial data. Always make API calls from a backend server. ``` ❌ fetch("https://api.yoshi.ai/accounts", { headers: { Authorization: "Bearer yoshi_3xK9mP..." } }) // in browser JavaScript ✅ // Browser calls YOUR backend, which calls Yoshi fetch("/api/accounts") ``` **Use environment variables.** Don't hardcode keys in source code: ```bash theme={null} export YOSHI_API_KEY=yoshi_3xK9mP... ``` **Rotate keys periodically.** Create a new key, update your services, then revoke the old one. Multiple active keys are supported for zero-downtime rotation. **Verify webhook signatures.** Always [verify the signature](/webhooks/verification) before processing webhook events — otherwise anyone can send fake events to your endpoint. ## Rate limit budgeting Each API key has a limit of **100 requests per minute**. If you have multiple services sharing a key: * Track usage across services using the `X-RateLimit-Remaining` header * Prioritize user-facing requests over background jobs * Use webhooks to reduce polling — each webhook you process replaces multiple poll requests * Consider separate keys for different services so they have independent limits ```typescript theme={null} const remaining = Number(response.headers.get("X-RateLimit-Remaining")); if (remaining < 10) { console.warn("Approaching rate limit, throttling background jobs"); pauseBackgroundJobs(); } ``` ## What's next > **[Errors](/errors)** > > Full error codes reference. > **[Rate limits](/rate-limits)** > > Detailed rate limiting documentation. # Data freshness and sync model Source: https://docs.yoshi.ai/concepts/data-freshness Understand when and how Yoshi syncs financial data from connected institutions, what the as_of timestamp means, and how to handle stale data gracefully. Yoshi syncs data from your connected financial institutions automatically. This page explains the sync model, how to interpret freshness signals, and how to build UIs that handle stale data gracefully. ## How syncing works When you connect a financial account, Yoshi begins syncing data from that institution on a recurring schedule: | Data type | Typical sync frequency | Notes | | - | - | - | | Transactions | Every few hours | New and updated transactions are pulled in each sync | | Balances | With each sync | Updated alongside transactions | | Investment holdings | Daily | Some institutions update less frequently | | Recurring streams | After each transaction sync | Patterns are re-analyzed as new data arrives | | Scores | Daily | Recalculated once per day based on the latest data | > [!NOTE] > > Sync frequency varies by institution. Some provide near real-time data, while others update once per business day. ## The `as_of` timestamp Each account includes an `as_of` field that tells you when its data was last refreshed: ```json theme={null} { "id": "acc_abc123", "name": "Chase Checking (...4521)", "balance_current": 4523.17, "as_of": "2026-04-10T08:30:00.000Z" } ``` Use `as_of` to show users when their data was last updated: ```typescript theme={null} const account = accounts[0]; const lastSync = new Date(account.as_of); const minutesAgo = Math.round((Date.now() - lastSync.getTime()) / 60000); if (minutesAgo > 60) { console.log(`Balance may be stale (last updated ${minutesAgo} minutes ago)`); } ``` ## Use webhooks instead of polling Rather than polling endpoints on a timer, subscribe to webhook events for real-time updates: | Event | Fires when | | - | - | | `transaction.created` | New transactions are synced | | `transaction.updated` | Existing transactions change (e.g., pending → posted) | | `balance.updated` | Account balance changes | | `investment.updated` | Investment holdings are refreshed | | `score.updated` | Financial health scores are recalculated | ```bash theme={null} 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", "filter_types": ["transaction.created", "balance.updated"] }' ``` See the [Webhooks guide](/webhooks/overview) for full setup instructions. ## Handle stale data in your UI Financial data may be minutes to hours old depending on the institution. Here are patterns for communicating freshness to users: **Show the last sync time:** ``` Chase Checking: $4,523.17 · Updated 2 hours ago ``` **Flag potentially stale data:** ```typescript theme={null} const STALE_THRESHOLD_MS = 4 * 60 * 60 * 1000; // 4 hours const isStale = Date.now() - new Date(account.as_of).getTime() > STALE_THRESHOLD_MS; ``` **Don't promise real-time accuracy.** Balances and transactions reflect the most recent sync, not the current moment. A user may have made a purchase minutes ago that hasn't synced yet. ## Pending transactions Transactions with `pending: true` are authorized but not yet settled. They can: * Change amount (tips, holds) * Change date * Disappear entirely (voided authorizations) Once a transaction posts (`pending: false`), it's final. Always use the transaction `id` for tracking — don't match on amount or description, which can change. ## What's next > **[Webhooks](/webhooks/overview)** > > Get real-time notifications when data changes. > **[Best practices](/concepts/best-practices)** > > Patterns for building reliable integrations. # Understanding your financial data Source: https://docs.yoshi.ai/concepts/data-models Reference for Yoshi resources: accounts, transactions, scores, recurring streams, goals, income, and investments, plus key fields and relationships. Yoshi organizes your financial data into a set of core resources. This page explains what each one represents and how to interpret key fields. ## Accounts An account represents a single financial account — a checking account, savings account, credit card, investment account, or loan. Each account belongs to a financial institution and has a current balance. | Field | Description | | - | - | | `type` | Top-level category: `depository`, `credit`, `investment`, `loan` | | `subtype` | More specific: `checking`, `savings`, `credit card`, `401k`, `mortgage`, etc. | | `balance_current` | Most recent balance as reported by the institution | | `balance_available` | Available to spend or withdraw (may differ from current for credit/checking) | | `balance_limit` | Credit limit (credit accounts only) | | `as_of` | When the balance was last refreshed | | `hidden` | Whether the user has hidden this account from their dashboard | > [!NOTE] > > `balance_available` may be `null` for accounts that don't report available balances, such as some investment accounts. ## Transactions A transaction is a single financial event — a purchase, payment, deposit, transfer, or fee. Transactions belong to an account and include merchant information, categorization, and timing data. ### Amount and direction | Field | Description | | - | - | | `amount` | Signed amount. Negative for outflows, positive for inflows. | | `amount_absolute` | Unsigned amount (always positive). Use this for display. | | `cash_flow_direction` | `"outflow"` (you spent money) or `"inflow"` (you received money) | > [!WARNING] > > The sign of `amount` follows accounting convention — a credit card purchase appears as a positive value on a credit account but as an outflow in `cash_flow_direction`. Always use `cash_flow_direction` for logic and `amount_absolute` for display. ### Dates | Field | Description | | - | - | | `date_posted` | When the transaction settled (final). Use this for reporting. | | `date_authorized` | When the transaction was first authorized. May be `null`. | | `pending` | `true` if the transaction hasn't settled yet | Pending transactions may change amount, date, or disappear entirely before posting. Use the transaction `id` for deduplication — don't match on amount or description. ### Categories | Field | Description | | - | - | | `category_label` | Human-readable label, e.g. "Groceries" | | `category_tier1` | Top-level category, e.g. "Food & Drink" | | `category_tier2` | Sub-category, e.g. "Groceries". May be `null`. | ### Merchant | Field | Description | | - | - | | `counterparty_name` | Cleaned merchant name, e.g. "Whole Foods" | | `counterparty_logo_url` | Merchant logo URL. May be `null`. | | `original_description` | Raw transaction description from the institution | ## Recurring streams A recurring stream is a detected pattern of repeated transactions — subscriptions, monthly bills, or regular income. Yoshi identifies these automatically from transaction history. | Field | Description | | - | - | | `direction` | `"outflow"` (bill/subscription) or `"inflow"` (income) | | `frequency` | Detected pattern: `monthly`, `weekly`, `biweekly`, `annual`, etc. | | `status` | `"active"` or `"inactive"` (stream appears to have stopped) | | `average_amount` | Typical amount based on recent occurrences | | `predicted_next_date` | When the next occurrence is expected | ## Scores Yoshi calculates four financial health scores, each on a 0–100 scale: | Score | What it measures | | - | - | | `yoshi` | Composite overall financial health | | `recovery` | How quickly you recover from spending dips | | `capacity` | Available financial cushion relative to obligations | | `baseline` | Consistency and stability of your financial patterns | Scores are recalculated daily. The `observation_date` field indicates when the scores were last computed. ## Goals A goal represents a financial objective the user is working toward: | Field | Description | | - | - | | `goal_type` | What kind of goal: `savings`, `debt_payoff`, etc. | | `status` | `active`, `paused`, or `completed` | | `target_amount` | Dollar amount the user is trying to reach | | `current_value` | Progress toward the target | | `target_date` | When the user wants to reach the goal | ## Income The income resource provides an analysis of the user's income sources: | Field | Description | | - | - | | `total_income_annual` | Estimated annual income across all sources | | `total_income_monthly` | Estimated monthly income | | `income_sources` | Array of detected income streams | | `income_sources[].confidence` | `high`, `medium`, or `low` — how confident the detection is | ## Investments Investment data shows the user's holdings grouped by asset class: | Field | Description | | - | - | | `symbol` | Ticker symbol, e.g. "AAPL" | | `current_value` | Market value of the position | | `quantity` | Number of shares held | | `cost_basis_total` | Original purchase cost | | `total_gain_loss` | Unrealized gain or loss | | `day_change` | Change in value today | | `percent_of_account` | Position as a percentage of the total account (0–100) | ## Resource relationships ``` User ├── Accounts │ ├── Transactions │ └── Balance history ├── Recurring streams ├── Scores ├── Goals ├── Income sources └── Investments (holdings by account) ``` Every resource is scoped to the authenticated user. You can only access your own data — there is no cross-user access. # Idempotency for safe retries Source: https://docs.yoshi.ai/concepts/idempotency Use the Idempotency-Key header to safely retry POST requests without creating duplicate trades, actions, or accounts when networks fail or time out. Idempotency keys let you retry `POST` requests safely. If a request fails due to a network error or timeout, you can resend it with the same key and get the original response back — without creating a duplicate. ## How it works Include an `Idempotency-Key` header with a unique value on `POST` requests: ```bash theme={null} curl -X POST https://api.yoshi.ai/paper-trading/accounts/acc_123/trades \ -H "Authorization: Bearer yoshi_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: buy-aapl-2026-04-10-001" \ -d '{"symbol": "AAPL", "side": "buy", "quantity": 10}' ``` If you send the same request again with the same key: | Scenario | Result | | - | - | | Same key, same parameters | Returns the cached response (no duplicate created) | | Same key, different parameters | Returns `409 Conflict` | ## Generate keys Use UUIDv4 for unique keys, or derive them deterministically from your own identifiers: ```typescript TypeScript theme={null} import { randomUUID } from "crypto"; // Random key const key = randomUUID(); // Deterministic key (same input always produces the same key) const key = `trade-${accountId}-${symbol}-${side}-${Date.now()}`; ``` ```python Python theme={null} import uuid # Random key key = str(uuid.uuid4()) # Deterministic key key = f"trade-{account_id}-{symbol}-{side}-{int(time.time())}" ``` > [!TIP] > > Deterministic keys are useful when you want the same logical operation to always map to the same key — for example, "buy AAPL in portfolio X today" should only happen once regardless of how many times you retry. ## Which endpoints support idempotency | Endpoint | Supports `Idempotency-Key` | | - | - | | `POST /paper-trading/accounts` | Yes | | `POST /paper-trading/accounts/{id}/trades` | Yes | | `POST /actions` | Yes | | All `GET` endpoints | Not needed (reads are naturally idempotent) | ## Key lifetime Idempotency keys are cached for **24 hours**. After that, the same key creates a new resource. Design your keys so that reuse after 24 hours either doesn't happen or is intentional. ## Best practices * **Always use idempotency keys** on `POST` endpoints in production. Network failures happen — make your retries safe by default. * **Generate the key before the request**, not after. If the request succeeds but the response is lost, you need the same key to retrieve the result. * **Don't reuse keys across different operations.** Each logical operation should have its own key. * **Log your keys.** If something goes wrong, the idempotency key helps trace what happened. # Cursor-based pagination Source: https://docs.yoshi.ai/concepts/pagination Navigate large result sets with cursor-based pagination. Learn how to iterate through all pages, handle concurrent changes, and avoid common pitfalls. List endpoints return results in pages. Yoshi uses **cursor-based pagination** — each response includes an opaque cursor that points to the next page. ## How it works Send a request with an optional `limit` to control page size: ```bash theme={null} curl "https://api.yoshi.ai/transactions?limit=50" \ -H "Authorization: Bearer yoshi_YOUR_KEY" ``` The response includes pagination fields alongside `data`: ```json theme={null} { "data": [...], "has_more": true, "cursor": "eyJpZCI6InR4bl8xMjM0NTYifQ==", "count": 50, "meta": { ... } } ``` | Field | Description | | - | - | | `has_more` | `true` if more pages exist | | `cursor` | Opaque string to pass as `?cursor=` on the next request. `null` on the last page. | | `count` | Number of items in this page | To get the next page, pass the cursor from the previous response: ```bash theme={null} curl "https://api.yoshi.ai/transactions?limit=50&cursor=eyJpZCI6InR4bl8xMjM0NTYifQ==" \ -H "Authorization: Bearer yoshi_YOUR_KEY" ``` ## Iterate all pages ```typescript TypeScript theme={null} import Yoshi from "@yoshi-ai/sdk"; const yoshi = new Yoshi(); // Auto-pagination with the SDK const allTransactions = []; for await (const tx of yoshi.transactions.list({ limit: 100 })) { allTransactions.push(tx); } console.log(`Fetched ${allTransactions.length} transactions`); ``` ```python Python theme={null} from yoshi import Yoshi yoshi = Yoshi() all_transactions = [] for tx in yoshi.transactions.list(limit=100): all_transactions.append(tx) print(f"Fetched {len(all_transactions)} transactions") ``` ```typescript Manual pagination theme={null} let cursor: string | undefined; const allTransactions = []; do { const response = await fetch( `https://api.yoshi.ai/transactions?limit=100${cursor ? `&cursor=${cursor}` : ""}`, { headers: { Authorization: `Bearer ${process.env.YOSHI_API_KEY}` } }, ); const body = await response.json(); allTransactions.push(...body.data); cursor = body.cursor; } while (cursor); ``` > [!TIP] > > The SDKs handle pagination automatically with async iterators. Use manual pagination only if you're calling the API directly. ## Parameters | Parameter | Type | Default | Description | | - | - | - | - | | `limit` | integer | 50 | Items per page (1–100) | | `cursor` | string | — | Cursor from the previous page | ## Common pitfalls **Don't parse the cursor.** It's an opaque string — its format may change without notice. **Don't store cursors long-term.** Cursors are designed for sequential traversal within a single session. They may become invalid over time. **Don't assume stable ordering across pages.** If new items are added while you're paginating, you may see them on a later page or miss them. For transactions, new items typically appear at the beginning of the list. **Handle invalid cursors.** If a cursor is expired or malformed, the API returns a `400` error: ```json theme={null} { "error": { "type": "https://api.yoshi.ai/errors/validation-error", "code": "VALIDATION_ERROR", "message": "Invalid or expired cursor", "param": "cursor" } } ``` When this happens, restart pagination from the beginning (without a cursor). ## Which endpoints support pagination | Endpoint | Paginated | | - | - | | `GET /transactions` | Yes | | `GET /paper-trading/accounts/{id}/trades` | Yes | | `GET /actions` | Yes | | `GET /accounts` | No (returns all accounts) | | `GET /recurring` | No (returns all streams) | | `GET /investments` | No (returns all holdings) | # Local development Source: https://docs.yoshi.ai/development Set up the Mintlify CLI to preview Yoshi API docs locally. Includes install steps, custom ports, link validation, and troubleshooting. > [!NOTE] > > **Prerequisites**: > > * Node.js version 19 or higher > * This repository cloned locally **1. Install the Mintlify CLI** ```bash theme={null} npm i -g mint ``` **2. Preview locally** Navigate to this repo's root directory and run: ```bash theme={null} mint dev ``` A local preview will be available at `http://localhost:3000`. ## Custom ports ```bash theme={null} mint dev --port 3333 ``` ## Validating links ```bash theme={null} mint broken-links ``` ## Updating the CLI ```bash theme={null} npm i -g mint@latest ``` ## OpenAPI spec The `api-reference/openapi.json` file is auto-synced from the Yoshi API via CI after each deploy. To update it manually: ```bash theme={null} curl -sf https://api-staging.yoshi.ai/openapi.json -o api-reference/openapi.json ``` ## Troubleshooting > **Error: Could not load the "sharp" module** > > 1. Remove the CLI: `npm remove -g mint` > 2. Upgrade to Node v19 or higher > 3. Reinstall: `npm i -g mint` > **Unknown error on mint dev** > > Delete `~/.mintlify` and run `mint dev` again. # Error codes, response format, and handling Source: https://docs.yoshi.ai/errors Reference for Yoshi API error codes, the standard error response envelope, and code examples for handling 401, 404, 429, and 500 errors. ## Error Response Format All errors follow a consistent envelope: ```json theme={null} { "error": { "type": "https://api.yoshi.ai/errors/not-found", "code": "NOT_FOUND", "message": "The requested resource was not found.", "display_message": "We couldn't find what you're looking for.", "param": null, "retry_after": null }, "meta": { "request_id": "req_abc123", "timestamp": "2026-04-10T12:00:00Z" } } ``` ## Error Codes | Code | HTTP Status | Description | | - | - | - | | `UNAUTHORIZED` | 401 | Invalid, expired, or missing API key | | `NOT_FOUND` | 404 | Resource doesn't exist or you don't have access | | `VALIDATION_ERROR` | 422 | Invalid request parameters (check `param` field) | | `RATE_LIMITED` | 429 | Too many requests (check `retry_after` field) | | `INTERNAL_ERROR` | 500 | Something went wrong on our end | ## Error Fields | Field | Type | Description | | - | - | - | | `type` | `string` | Stable URI for this error type (inspired by [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html)) | | `code` | `string` | Machine-readable error code for programmatic handling | | `message` | `string` | Developer-facing explanation with details | | `display_message` | `string \| null` | Safe to show to end users in a UI | | `param` | `string \| null` | Which request parameter caused a validation error | | `retry_after` | `number \| null` | Seconds until you can retry (only on `RATE_LIMITED`) | ## Handling Errors ```python Python theme={null} import httpx response = httpx.get( "https://api.yoshi.ai/accounts", headers={"Authorization": "Bearer yoshi_YOUR_KEY"}, ) if response.status_code == 200: data = response.json()["data"] elif response.status_code == 429: error = response.json()["error"] retry_after = error["retry_after"] print(f"Rate limited. Retry after {retry_after} seconds.") elif response.status_code == 401: print("Invalid API key.") else: error = response.json()["error"] print(f"Error: {error['code']} - {error['message']}") ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.yoshi.ai/accounts", { headers: { Authorization: "Bearer yoshi_YOUR_KEY" }, }); if (response.ok) { const { data } = await response.json(); } else { const { error } = await response.json(); if (error.code === "RATE_LIMITED") { console.log(`Retry after ${error.retry_after} seconds`); } else { console.error(`${error.code}: ${error.message}`); } } ``` ## Request IDs Every response includes a `request_id` in the `meta` object and as an `X-Request-Id` response header. Include this when contacting support about a specific request. ```bash theme={null} curl -v https://api.yoshi.ai/accounts \ -H "Authorization: Bearer yoshi_YOUR_KEY" 2>&1 | grep X-Request-Id # < X-Request-Id: req_a1b2c3d4-e5f6-7890-abcd-ef1234567890 ``` # Actions and approvals for safe financial operations Source: https://docs.yoshi.ai/guides/actions-approvals Create, track, and approve actions that modify financial data. Understand the approval state machine and build approval workflows into your application. Actions are Yoshi's safety layer for operations that modify financial data. Instead of executing immediately, mutating operations create a **pending action** that must be approved before taking effect. ## Why actions exist Financial operations — placing trades, creating accounts, transferring funds — carry real consequences. The action model ensures: * Every mutating operation requires explicit approval * You can review what will happen before it executes * Actions are auditable with full history * Integrations can propose operations that a human approves ## The action lifecycle ``` create approve execute │ │ │ ▼ ▼ ▼ pending ──→ approved ──→ executed │ └──→ rejected │ └──→ expired (if expires_at passes) ``` | Status | Description | | - | - | | `pending` | Action created, waiting for approval | | `approved` | User approved the action | | `rejected` | User rejected the action | | `executed` | Action was approved and successfully executed | | `expired` | Action expired before a decision was made | ## Create an action Actions are created automatically when you call mutating endpoints like paper trading. You can also create custom actions: ```typescript TypeScript theme={null} import Yoshi from "@yoshi-ai/sdk"; const yoshi = new Yoshi(); const action = await yoshi.actions.create({ action_type: "paper_trade", title: "Buy 10 shares of AAPL", description: "Paper trade in Tech Portfolio account", proposal_data: { symbol: "AAPL", side: "buy", quantity: 10, }, source: "consumer_api", }); console.log("Action ID:", action.id); console.log("Approve at:", action.approval_url); ``` ```python Python theme={null} from yoshi import Yoshi yoshi = Yoshi() action = yoshi.actions.create( action_type="paper_trade", title="Buy 10 shares of AAPL", description="Paper trade in Tech Portfolio account", proposal_data={ "symbol": "AAPL", "side": "buy", "quantity": 10, }, source="consumer_api", ) print(f"Action ID: {action.id}") print(f"Approve at: {action.approval_url}") ``` ## Check action status Poll the action to check whether it's been approved: ```typescript TypeScript theme={null} const action = await yoshi.actions.retrieve("act_abc123"); console.log("Status:", action.status); if (action.status === "executed") { console.log("Result:", action.execution_result); } ``` ```python Python theme={null} action = yoshi.actions.retrieve("act_abc123") print(f"Status: {action.status}") if action.status == "executed": print(f"Result: {action.execution_result}") ``` ## List and filter actions View all actions with optional filters: ```typescript TypeScript theme={null} // List pending actions for await (const action of yoshi.actions.list({ status: "pending" })) { console.log(`${action.title} — ${action.status}`); } // List by type for await (const action of yoshi.actions.list({ action_type: "paper_trade", })) { console.log(`${action.title} — ${action.status}`); } ``` ```python Python theme={null} for action in yoshi.actions.list(status="pending"): print(f"{action.title} — {action.status}") ``` ## Approve or reject Submit an approval decision programmatically: ```typescript TypeScript theme={null} const result = await fetch( "https://api.yoshi.ai/actions/act_abc123/decide", { method: "POST", headers: { Authorization: `Bearer ${process.env.YOSHI_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ decision: "approved" }), }, ); const { data } = await result.json(); console.log("Decision:", data.decision, "Status:", data.status); ``` ```python Python theme={null} response = httpx.post( "https://api.yoshi.ai/actions/act_abc123/decide", headers={"Authorization": f"Bearer {os.environ['YOSHI_API_KEY']}"}, json={"decision": "approved"}, ) data = response.json()["data"] print(f"Decision: {data['decision']} Status: {data['status']}") ``` You can also approve from the Yoshi web app using the `approval_url` returned when the action was created. ## Use idempotency keys Prevent duplicate actions when retrying: ```bash theme={null} curl -X POST https://api.yoshi.ai/actions \ -H "Authorization: Bearer yoshi_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: buy-aapl-2026-04-10" \ -d '{"action_type": "paper_trade", "title": "Buy AAPL", ...}' ``` If a pending action with the same idempotency key already exists, the API returns the existing action instead of creating a duplicate. A `409 Conflict` is returned if the key matches but the parameters differ. ## What's next > **[Paper trading](/guides/paper-trading)** > > See actions in practice with paper trades. > **[Idempotency](/concepts/idempotency)** > > Prevent duplicate operations safely. # Track card rewards and expiring benefits Source: https://docs.yoshi.ai/guides/card-rewards Use the benefits endpoints to track credit card perks, monitor usage against period limits, and surface expiring benefits before value goes to waste. Yoshi automatically detects your credit cards and tracks the benefits they offer — statement credits, lounge access, travel perks, and more. Use the benefits endpoints to build a rewards dashboard that shows what you've used, what's remaining, and what's about to expire. ## Get your benefits summary Fetch all active benefit periods with usage data: ```typescript TypeScript theme={null} import Yoshi from "@yoshi-ai/sdk"; const yoshi = new Yoshi(); const response = await fetch("https://api.yoshi.ai/benefits/summary", { headers: { Authorization: `Bearer ${process.env.YOSHI_API_KEY}` }, }); const { data } = await response.json(); console.log(`Total annual value: $${(data.total_value_cents / 100).toFixed(2)}`); console.log(`Used: $${(data.total_used_cents / 100).toFixed(2)}`); console.log(`Remaining: $${(data.total_remaining_cents / 100).toFixed(2)}`); ``` ```python Python theme={null} import httpx, os response = httpx.get( "https://api.yoshi.ai/benefits/summary", headers={"Authorization": f"Bearer {os.environ['YOSHI_API_KEY']}"}, ) data = response.json()["data"] print(f"Total annual value: ${data['total_value_cents'] / 100:.2f}") print(f"Used: ${data['total_used_cents'] / 100:.2f}") print(f"Remaining: ${data['total_remaining_cents'] / 100:.2f}") ``` ## Understand the data model Each benefit has a **period** — a time window during which it can be used: | Field | Description | | - | - | | `benefit_name` | The perk name, e.g. "Uber Credit" or "Airline Fee Credit" | | `benefit_type` | Category: `statement_credit`, `lounge_access`, `free_checked_bag`, etc. | | `reset_cadence` | How often the benefit resets: `calendar_month`, `calendar_year`, `card_anniversary`, etc. | | `value_limit_cents` | Maximum value for this period | | `value_used_cents` | How much has been used | | `value_remaining_cents` | How much is left | | `usage_pct` | Usage as a percentage (0–100) | | `days_remaining` | Days until this period expires | | `status` | `available`, `partially_used`, `fully_used`, or `expired` | ## Display benefits by card Group periods by card to show a per-card breakdown: ```typescript theme={null} const { data } = await response.json(); const byCard = {}; for (const period of data.periods) { const card = period.card_product_name; if (!byCard[card]) byCard[card] = []; byCard[card].push(period); } for (const [card, periods] of Object.entries(byCard)) { console.log(`\n${card}:`); for (const p of periods) { const used = (p.value_used_cents / 100).toFixed(2); const limit = (p.value_limit_cents / 100).toFixed(2); console.log(` ${p.benefit_name}: $${used} / $${limit} (${p.usage_pct}%)`); } } ``` ## Surface expiring benefits Check for benefits that are about to expire with unused value: ```typescript TypeScript theme={null} const response = await fetch( "https://api.yoshi.ai/benefits/expiring?days=14", { headers: { Authorization: `Bearer ${process.env.YOSHI_API_KEY}` } }, ); const { data } = await response.json(); for (const benefit of data.expiring) { const remaining = (benefit.value_remaining_cents / 100).toFixed(2); console.log( `⚠️ ${benefit.benefit_name} (${benefit.card_product_name}): ` + `$${remaining} expires in ${benefit.days_remaining} days` ); } ``` ```python Python theme={null} response = httpx.get( "https://api.yoshi.ai/benefits/expiring", params={"days": 14}, headers={"Authorization": f"Bearer {os.environ['YOSHI_API_KEY']}"}, ) data = response.json()["data"] for benefit in data["expiring"]: remaining = benefit["value_remaining_cents"] / 100 print( f"⚠️ {benefit['benefit_name']} ({benefit['card_product_name']}): " f"${remaining:.2f} expires in {benefit['days_remaining']} days" ) ``` > [!TIP] > > Set `days=30` to get a monthly view, or `days=7` for urgent alerts only. ## Separate trackable vs always-on benefits Some benefits like lounge access or travel insurance don't have a dollar value to track. Filter them by type: ```typescript theme={null} const trackable = data.periods.filter((p) => ["statement_credit", "membership_credit", "subscription_credit", "travel_bank_credit", "flight_discount"].includes(p.benefit_type) ); const alwaysOn = data.periods.filter((p) => ["lounge_access", "insurance_benefit", "status_benefit", "free_checked_bag", "companion_pass"].includes(p.benefit_type) ); ``` Display trackable benefits with progress bars and dollar amounts. Show always-on benefits as a simple checklist of perks your cards provide. ## Calculate annual fee ROI Combine benefit usage with your card's annual fee to show return on investment: ```typescript theme={null} const annualFee = 695_00; // $695.00 in cents const totalUsed = data.total_used_cents; const roi = ((totalUsed / annualFee) * 100).toFixed(0); console.log(`Annual fee: $${(annualFee / 100).toFixed(2)}`); console.log(`Benefits used: $${(totalUsed / 100).toFixed(2)}`); console.log(`ROI: ${roi}%`); ``` ## What's next > **[Data models](/concepts/data-models)** > > Understand how financial data is structured. > **[Webhooks](/webhooks/overview)** > > Get notified when new transactions match benefits. # CLI: manage your data from the terminal Source: https://docs.yoshi.ai/guides/cli Install the Yoshi CLI and use it to list accounts, fetch transactions, check scores, and manage your financial data from the command line. The Yoshi CLI lets you interact with the API directly from your terminal. It's built with Go and generated from the same OpenAPI spec that powers the SDKs. ## Installation ### Homebrew (macOS/Linux) ```bash theme={null} brew install yoshi-ai-dev/yoshi-cli/yoshi ``` ### Go Requires Go 1.22 or later: ```bash theme={null} go install github.com/yoshi-ai-dev/yoshi-cli/cmd/yoshi@latest ``` Make sure `$GOPATH/bin` is in your `PATH`: ```bash theme={null} export PATH="$PATH:$(go env GOPATH)/bin" ``` ## Authentication Set your API key as an environment variable: ```bash theme={null} export YOSHI_API_KEY=yoshi_3xK9mP... ``` Or pass it directly with `--api-key`: ```bash theme={null} yoshi accounts list --api-key yoshi_3xK9mP... ``` ## Usage The CLI follows a `yoshi [resource] ` structure: ### List accounts ```bash theme={null} yoshi accounts list ``` ### Fetch transactions ```bash theme={null} yoshi transactions list --limit 10 ``` ### Check financial scores ```bash theme={null} yoshi scores list ``` ### View recurring streams ```bash theme={null} yoshi recurring list ``` ### Get your profile ```bash theme={null} yoshi me retrieve ``` ### Financial summary ```bash theme={null} yoshi me summary ``` ### Paper trading ```bash theme={null} # List paper trading accounts yoshi paper-trading accounts list # View holdings yoshi paper-trading accounts holdings list acc_paper123 ``` ## Output formats Control how results are displayed with `--format`: | Format | Description | | - | - | | `auto` | Pretty output for terminals, JSON for pipes (default) | | `json` | Standard JSON | | `jsonl` | JSON Lines (one object per line) | | `pretty` | Indented, colored JSON | | `yaml` | YAML output | | `raw` | Raw API response body | | `explore` | Interactive explorer | ```bash theme={null} # Get JSON for piping yoshi accounts list --format json | jq '.data.accounts[].name' # YAML for readability yoshi scores list --format yaml ``` ## Transform output Use `--transform` with [GJSON syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) to extract specific fields: ```bash theme={null} # Get just account names yoshi accounts list --transform "data.accounts.#.name" # Get the Yoshi score yoshi scores list --transform "data.yoshi" ``` ## Global flags | Flag | Description | | - | - | | `--api-key` | API key (or set `YOSHI_API_KEY`) | | `--base-url` | Custom API base URL | | `--format` | Output format | | `--transform` | GJSON transform expression | | `--debug` | Show HTTP request/response details | | `--version`, `-v` | Show CLI version | | `--help` | Show help for any command | ## Explore commands Run `yoshi --help` to see all available resources, or `yoshi [resource] --help` for command-specific options: ```bash theme={null} yoshi --help yoshi transactions --help yoshi transactions list --help ``` ## What's next > **[SDKs](/sdks)** > > Use the TypeScript or Python SDK for programmatic access. > **[Authentication](/authentication)** > > Create and manage your API key. # Build a personal finance dashboard with the Yoshi API Source: https://docs.yoshi.ai/guides/financial-dashboard Fetch accounts, transactions, scores, and recurring data to build a personal finance dashboard. Includes caching strategies and webhook-driven updates. Build a dashboard that shows a user's linked accounts, recent transactions, financial health scores, and recurring bills — all from a single API key. ## Fetch the overview Start with `GET /me/summary` to get accounts, scores, and goals in one call: ```typescript TypeScript theme={null} import Yoshi from "@yoshi-ai/sdk"; const yoshi = new Yoshi(); const summary = await yoshi.me.summary(); console.log("Accounts:", summary.accounts.total); console.log("Yoshi Score:", summary.scores?.yoshi); console.log("Active Goals:", summary.goals.length); ``` ```python Python theme={null} from yoshi import Yoshi yoshi = Yoshi() summary = yoshi.me.summary() print(f"Accounts: {summary.accounts.total}") print(f"Yoshi Score: {summary.scores.yoshi if summary.scores else 'N/A'}") print(f"Active Goals: {len(summary.goals)}") ``` This single call replaces three separate requests to `/accounts`, `/scores`, and `/goals`. ## List recent transactions Fetch the latest transactions across all accounts: ```typescript TypeScript theme={null} const page = await yoshi.transactions.list({ limit: 20 }); for (const tx of page.data) { const direction = tx.cash_flow_direction === "outflow" ? "-" : "+"; console.log(`${direction}$${tx.amount_absolute} ${tx.counterparty_name}`); } ``` ```python Python theme={null} page = yoshi.transactions.list(limit=20) for tx in page.data: direction = "-" if tx.cash_flow_direction == "outflow" else "+" print(f"{direction}${tx.amount_absolute} {tx.counterparty_name}") ``` Filter by account to show transactions for a specific card or bank account: ```bash theme={null} curl "https://api.yoshi.ai/transactions?account_id=acc_abc123&limit=10" \ -H "Authorization: Bearer yoshi_YOUR_KEY" ``` ## Show balance history Plot account balances over time with the balance series endpoint: ```typescript TypeScript theme={null} const balances = await yoshi.accounts.balanceSeries.list("acc_abc123", { days: 30, }); for (const point of balances.points) { console.log(`${point.date}: $${point.balance}`); } ``` ```python Python theme={null} balances = yoshi.accounts.balance_series.list("acc_abc123", days=30) for point in balances.points: print(f"{point.date}: ${point.balance}") ``` ## Add recurring streams Show subscriptions, bills, and income streams: ```typescript TypeScript theme={null} const recurring = await yoshi.recurring.list(); const bills = recurring.streams.filter( (s) => s.direction === "outflow" && s.is_active ); const income = recurring.streams.filter( (s) => s.direction === "inflow" && s.is_active ); console.log(`${bills.length} active bills, ${income.length} income streams`); ``` ```python Python theme={null} recurring = yoshi.recurring.list() bills = [s for s in recurring.streams if s.direction == "outflow" and s.is_active] income = [s for s in recurring.streams if s.direction == "inflow" and s.is_active] print(f"{len(bills)} active bills, {len(income)} income streams") ``` ## Caching strategy Not all data changes at the same rate: | Resource | Cache duration | Why | | - | - | - | | Accounts metadata | 5–15 minutes | Names and types rarely change | | Balances | Do not cache | Changes with every sync | | Transactions | 1–5 minutes | New transactions arrive periodically | | Scores | 1 hour | Recalculated daily | | Recurring streams | 1 hour | Updated infrequently | > [!TIP] > > Use [webhooks](/webhooks/overview) instead of polling. Subscribe to `transaction.created` and `balance.updated` events to refresh your dashboard in real time. ## Polling vs webhooks For a simple dashboard, poll on a timer: ```typescript theme={null} setInterval(async () => { const summary = await yoshi.me.summary(); updateUI(summary); }, 60_000); // every 60 seconds ``` For real-time updates, register a [webhook endpoint](/webhooks/management) and listen for events: ```typescript theme={null} app.post("/webhooks/yoshi", async (req, res) => { const event = req.body; if (event.type === "transaction.created") { await refreshTransactions(event.data.account_id); } else if (event.type === "balance.updated") { await refreshBalances(event.data.account_id); } res.sendStatus(200); }); ``` ## Handle pending transactions Transactions can arrive in a `pending` state before they post: ```typescript theme={null} const page = await yoshi.transactions.list({ limit: 50 }); const posted = page.data.filter((tx) => !tx.pending); const pending = page.data.filter((tx) => tx.pending); ``` > [!WARNING] > > Pending transactions may change amount, date, or even disappear before posting. Always use the transaction `id` for deduplication — don't rely on amount or description matching. ## What's next > **[Webhooks](/webhooks/overview)** > > Set up real-time updates instead of polling. > **[Data freshness](/concepts/data-freshness)** > > Understand when and how data syncs. # Connect Yoshi to ChatGPT, Claude, and other AI assistants Source: https://docs.yoshi.ai/guides/mcp Use Yoshi from ChatGPT with the Yoshi plugin, or from Claude, Cursor, Perplexity, and other assistants through Yoshi's MCP server. Sign in with your Yoshi account and approve every money move in Yoshi. You can use Yoshi from another AI assistant. Ask it about your Yoshi accounts, spending, and investments, or have it get a trade or transfer ready for you to approve. There are two ways to connect: * **ChatGPT**: install the Yoshi plugin. * **Any other assistant that supports custom MCP connectors**, such as Claude, Claude Code, Cursor, or Perplexity: add Yoshi's MCP server: ```text theme={null} https://agents.yoshi.ai/mcp ``` Both ways end on the same screen: you sign in to your Yoshi account and approve access with your Yoshi passkey. > [!NOTE] > > You need an existing Yoshi account with a passkey. You can't create a Yoshi account from another assistant. ## Connect ChatGPT **1. Open the Yoshi plugin** Go to [chatgpt.com/plugins/yoshi](https://chatgpt.com/plugins/yoshi). In Yoshi, you can also open **Menu** › **Connections** › **ChatGPT** and click **Connect**. **2. Install and connect** Click **Install plugin**, then **Connect**. **3. Sign in to Yoshi** Sign in to your Yoshi account, review the access ChatGPT is asking for, and click **Allow access**. Yoshi asks for your passkey. **4. Ask for Yoshi** In a ChatGPT conversation, type **@Yoshi** or pick Yoshi from the plugins menu, then ask your question. Plugin availability depends on your ChatGPT plan and workspace settings. If you see "Disabled by admin," ask your ChatGPT workspace admin to enable plugins. ## Connect another assistant Yoshi doesn't have a native app for other assistants yet. Any assistant that supports custom remote MCP connectors can still use Yoshi through this URL: ```text theme={null} https://agents.yoshi.ai/mcp ``` In Yoshi, open **Menu** › **Connections** › **Yoshi MCP** and click **Copy URL** to copy it. **Claude** 1. In Claude, open **Customize** › **Connectors**. 2. Click **Add**, then **Add custom connector**. 3. Name it **Yoshi** and paste `https://agents.yoshi.ai/mcp`. 4. Sign in to Yoshi and approve with your passkey. 5. In a conversation, open the **+** menu › **Connectors** and turn on Yoshi. On Claude Team and Enterprise plans, an owner adds the connector for the organization first. Each member then clicks **Connect** and signs in with their own Yoshi account. A connector added on Claude web or desktop is also available in the Claude mobile apps. **Claude Code** ```bash theme={null} claude mcp add --transport http yoshi https://agents.yoshi.ai/mcp ``` Then run `/mcp` in Claude Code, select Yoshi, and sign in to Yoshi in the browser window that opens. **Cursor** Add Yoshi to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project): ```json theme={null} { "mcpServers": { "yoshi": { "type": "http", "url": "https://agents.yoshi.ai/mcp" } } } ``` Open Cursor's MCP settings and sign in to Yoshi when prompted. **Perplexity** In Perplexity's connector settings, add a custom **remote** MCP connector with the URL `https://agents.yoshi.ai/mcp` and choose OAuth sign-in. Then sign in to Yoshi and approve with your passkey. Custom connectors need a paid Perplexity plan. **Other assistants** VS Code with GitHub Copilot, OpenAI Codex, and many other assistants support custom remote MCP servers. Add `https://agents.yoshi.ai/mcp` as a remote (HTTP) MCP server, then sign in to Yoshi when the assistant asks. * **VS Code**: run **MCP: Add Server** from the Command Palette, choose **HTTP**, and paste the URL. * **Codex**: run `codex mcp add yoshi --url https://agents.yoshi.ai/mcp`, then `codex mcp login yoshi`. If your assistant doesn't support custom remote MCP connectors, it can't connect to Yoshi yet. ## What your assistant can do Once connected, your assistant uses Yoshi's tools on your behalf: | You can ask about | Examples | | - | - | | Accounts and cash | Balances, net worth, balance history, credit and debt | | Spending | Transactions, pending activity, spending by category, recurring charges | | Investments and markets | Holdings, performance, gains, quotes, news, research, and comparisons | | Advice | Yoshi's own personalized recommendation on a security, after Yoshi checks it can give one | | Briefs and activity | Your Briefs, notifications, recent activity, and transfers in progress | Your assistant can also **get these ready for you**: trades, transfers, checks, automations, and cancellations. > [!WARNING] > > Nothing moves until you approve it. When your assistant prepares a trade or transfer, it gives you a link to review it in Yoshi, where you approve with your passkey. Some things stay in the Yoshi app: linking a bank account, paying other people, bill pay, and depositing a check. Your assistant can give you a link to the app for these. ## What connecting does and doesn't do * Your assistant gets access to Yoshi's tools for your account only. It doesn't get your Yoshi chat history. * Each person connects their own Yoshi account. One connection doesn't cover anyone else. * Yoshi doesn't connect to other apps' plugins, MCP servers, or agents. Your assistant connects to Yoshi, not the other way around. ## See and remove connections In Yoshi, open **Menu** › **Connections**: * **ChatGPT** shows **Connect**, **Open ChatGPT** once you're connected, or **Reconnect** if the connection expired. * Other assistants you've connected are listed with the date you connected them. To disconnect: * **In Yoshi on the web**: open **Menu** › **Connections**, click **…** next to the assistant, then **Revoke** › **Revoke access**. The Yoshi iOS app can't disconnect assistants yet. * **In ChatGPT**: open **Settings** › **Plugins** › **Yoshi**, then **Disconnect**. * **In Claude**: open **Customize** › **Connectors**, click **⋮** next to Yoshi, then **Remove**. ## Troubleshooting > **I don't see ChatGPT or Yoshi MCP under Connections** > > These rows appear once your Yoshi account is fully set up. If you're on the iOS app, update to the latest version. You can still open [chatgpt.com/plugins/yoshi](https://chatgpt.com/plugins/yoshi) or use `https://agents.yoshi.ai/mcp` directly. > **ChatGPT says my Yoshi connection expired** > > In Yoshi, open **Menu** › **Connections** › **ChatGPT** and click **Reconnect**, or connect again from the plugin in ChatGPT. > **Sign-in says a passkey is required** > > Approving access needs your Yoshi passkey. Set one up in the Yoshi app, then try connecting again. > **My assistant connected, but Yoshi doesn't show up in a conversation** > > Most assistants let you turn connectors on per conversation. Check the assistant's connector or tools menu and turn Yoshi on. # Paper trading: simulate trades with virtual portfolios Source: https://docs.yoshi.ai/guides/paper-trading Create paper trading accounts, place simulated buy and sell orders, track holdings, and understand the action approval flow for safe financial operations. Paper trading lets you simulate stock trades with virtual money. Create an account with a starting balance, buy and sell securities, and track your portfolio — all without risking real capital. ## How it works Paper trading uses Yoshi's **action approval** model. When you create an account or place a trade, the API returns a pending action that must be approved before execution. This safety layer prevents accidental trades. ``` Create action → Pending → Approved → Executed ↘ Rejected ``` ## Create a paper trading account ```typescript TypeScript theme={null} import Yoshi from "@yoshi-ai/sdk"; const yoshi = new Yoshi(); const result = await yoshi.paperTrading.accounts.create({ name: "Tech Portfolio", starting_cash_balance: "50000", // $50,000 }); console.log("Action ID:", result.action_id); console.log("Status:", result.status); console.log("Approve at:", result.approval_url); ``` ```python Python theme={null} from yoshi import Yoshi yoshi = Yoshi() result = yoshi.paper_trading.accounts.create( name="Tech Portfolio", starting_cash_balance="50000", ) print(f"Action ID: {result.action_id}") print(f"Status: {result.status}") print(f"Approve at: {result.approval_url}") ``` ```bash curl theme={null} curl -X POST https://api.yoshi.ai/paper-trading/accounts \ -H "Authorization: Bearer yoshi_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Tech Portfolio", "starting_cash_balance": "50000"}' ``` The response includes an `approval_url` — open it in your Yoshi dashboard to approve the account creation. ## Place a trade Buy stock by share quantity or dollar amount: ```typescript TypeScript theme={null} // Buy 10 shares of AAPL const trade = await yoshi.paperTrading.accounts.trades.create("acc_paper123", { symbol: "AAPL", side: "buy", quantity: 10, }); // Or buy $5,000 worth of AAPL const notionalTrade = await yoshi.paperTrading.accounts.trades.create( "acc_paper123", { symbol: "AAPL", side: "buy", notional: 5000, }, ); console.log("Approve trade:", trade.approval_url); ``` ```python Python theme={null} # Buy 10 shares of AAPL trade = yoshi.paper_trading.accounts.trades.create( "acc_paper123", symbol="AAPL", side="buy", quantity=10, ) # Or buy $5,000 worth of AAPL notional_trade = yoshi.paper_trading.accounts.trades.create( "acc_paper123", symbol="AAPL", side="buy", notional=5000, ) print(f"Approve trade: {trade.approval_url}") ``` > [!WARNING] > > Provide either `quantity` or `notional`, not both. The API returns a validation error if you include both fields. ## Use idempotency keys Prevent duplicate trades when retrying failed requests: ```bash theme={null} curl -X POST https://api.yoshi.ai/paper-trading/accounts/acc_paper123/trades \ -H "Authorization: Bearer yoshi_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: trade-aapl-2026-04-10-001" \ -d '{"symbol": "AAPL", "side": "buy", "quantity": 10}' ``` If you send the same `Idempotency-Key` again, the API returns the cached response instead of creating a duplicate trade. See [Idempotency](/concepts/idempotency) for details. ## Check the approval status Poll the approval endpoint to track whether the action was approved: ```typescript TypeScript theme={null} const status = await yoshi.approvals.retrieve("thread_abc123"); console.log("Status:", status.status); // "pending", "approved", "rejected", "executed" ``` ```python Python theme={null} status = yoshi.approvals.retrieve("thread_abc123") print(f"Status: {status.status}") ``` ## View your portfolio List current holdings and trade history: ```typescript TypeScript theme={null} // Current holdings const holdings = await yoshi.paperTrading.accounts.holdings.list("acc_paper123"); for (const h of holdings.holdings) { console.log(`${h.symbol}: ${h.quantity} shares @ $${h.institution_price}`); } // Trade history for await (const trade of yoshi.paperTrading.accounts.trades.list("acc_paper123")) { console.log(`${trade.type} ${trade.symbol}: ${trade.quantity} @ $${trade.price}`); } ``` ```python Python theme={null} # Current holdings holdings = yoshi.paper_trading.accounts.holdings.list("acc_paper123") for h in holdings.holdings: print(f"{h.symbol}: {h.quantity} shares @ ${h.institution_price}") # Trade history for trade in yoshi.paper_trading.accounts.trades.list("acc_paper123"): print(f"{trade.type} {trade.symbol}: {trade.quantity} @ ${trade.price}") ``` ## What's next > **[Actions & Approvals](/guides/actions-approvals)** > > Understand the full approval lifecycle. > **[Idempotency](/concepts/idempotency)** > > Prevent duplicate operations. # Set up transaction alerts with webhooks Source: https://docs.yoshi.ai/guides/transaction-alerts Register a webhook endpoint and filter transaction events by amount, category, or account to send Slack or email notifications for large or unusual purchases. 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: ```bash theme={null} 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](/webhooks/overview#consumer-portal). ## Build the webhook receiver Set up an Express server that verifies the webhook signature and filters transactions: ```javascript theme={null} 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: ```javascript theme={null} 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: ```javascript theme={null} 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: ```javascript theme={null} 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); }); ``` > [!NOTE] > > For production use, store processed event IDs in a database or Redis instead of an in-memory Set. ## Python example ```python FastAPI theme={null} 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](/webhooks/events)** > > See all available event types. > **[Signature verification](/webhooks/verification)** > > Secure your webhook endpoint. # Troubleshooting and frequently asked questions Source: https://docs.yoshi.ai/guides/troubleshooting Solutions and FAQs for common Yoshi API issues, including authentication errors, rate limiting, webhook delivery failures, and transaction data quirks. ## Authentication ### 401 Unauthorized **"Invalid, expired, or missing API key"** * Confirm your key starts with `yoshi_` * Check that the `Authorization` header uses the `Bearer` scheme: `Authorization: Bearer yoshi_YOUR_KEY` * Verify the key hasn't been revoked in your [dashboard](https://beta.yoshi.ai/settings) * Make sure there are no extra spaces or newlines in the key ```bash theme={null} # Correct curl https://api.yoshi.ai/accounts \ -H "Authorization: Bearer yoshi_3xK9mP..." # Wrong — missing "Bearer" curl https://api.yoshi.ai/accounts \ -H "Authorization: yoshi_3xK9mP..." ``` ## Rate limiting ### 429 Too Many Requests **"Rate limit exceeded"** Check the response headers to see your current limit and when it resets: ``` X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1712764800 Retry-After: 43 ``` Wait `Retry-After` seconds before retrying. Implement exponential backoff for automated systems: ```typescript theme={null} if (response.status === 429) { const retryAfter = Number(response.headers.get("Retry-After")) || 60; await new Promise((r) => setTimeout(r, retryAfter * 1000)); } ``` > [!TIP] > > Use [webhooks](/webhooks/overview) to replace polling. Every webhook you process is one fewer API call. ## Webhooks ### Not receiving events * **URL must be HTTPS.** HTTP endpoints are rejected. * **Return `2xx` within 15 seconds.** If your handler times out, the event is marked as failed and retried. * **Check the portal.** Open the [self-service portal](/webhooks/overview#consumer-portal) to see delivery attempts, response codes, and retry status. * **Check event filters.** If you specified `filter_types` when creating the endpoint, only those events are delivered. ### Signature verification failing * Use the **raw request body** for verification, not a parsed JSON object. Parsing and re-serializing changes whitespace and key order, which breaks the signature. * Verify the timestamp is within your tolerance window (default: 5 minutes). * Make sure you're using the correct signing secret for this endpoint. See [Signature Verification](/webhooks/verification) for framework-specific examples. ## Transactions ### Duplicate transactions Transactions can appear twice during the pending-to-posted transition. A pending transaction and its posted version may coexist briefly until the pending record clears. Always use the transaction `id` for deduplication — don't match on amount, date, or description. ### Amount has unexpected sign The `amount` field follows accounting convention: * On a **credit card** account, a purchase (outflow) has a **positive** amount * On a **checking** account, a purchase (outflow) has a **negative** amount Use `cash_flow_direction` for logic and `amount_absolute` for display: ```typescript theme={null} // Display const label = `${tx.cash_flow_direction === "outflow" ? "-" : "+"}$${tx.amount_absolute}`; // Logic if (tx.cash_flow_direction === "outflow" && tx.amount_absolute > 100) { // Large purchase } ``` ### `date_posted` vs `date_authorized` | Field | Use when | | - | - | | `date_posted` | Reporting, accounting, reconciliation. This is the final settlement date. | | `date_authorized` | Showing when the user actually made the purchase. May be `null` for some transaction types. | ## Data freshness ### Stale data / `as_of` timestamp Yoshi syncs data from your connected institutions on a recurring schedule. The `as_of` field on each account tells you when data was last refreshed. If `as_of` is more than a few hours old: * The institution may sync less frequently (some update once per business day) * There may be a temporary connection issue Show users the `as_of` timestamp so they know how current the data is. Do not promise real-time accuracy. ### Missing recent transactions Transactions appear after the next sync cycle, not instantly. If a user just made a purchase: 1. It won't appear until the next sync 2. It may appear as `pending` first 3. It settles as `posted` within 1–3 business days ## Pagination ### Cursor returns the same data * Make sure you're passing the `cursor` from the **most recent** response, not a cached cursor. * Don't store cursors between sessions — they're designed for sequential traversal. * If a cursor becomes invalid, the API returns `400`. Restart pagination without a cursor. See [Pagination](/concepts/pagination) for the full guide. ## Still stuck? Include the `request_id` from the error response when reaching out for support. Every API response includes it in the `meta` object and as an `X-Request-Id` header: ```json theme={null} { "meta": { "request_id": "req_a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } ``` # SDK helpers for currency, idempotency, and conversions Source: https://docs.yoshi.ai/helpers Use Yoshi SDK helper functions to format currency amounts, generate idempotency keys for POST requests, and convert between cents and dollars. Both SDKs include convenience helpers for common tasks. These are custom modules that ship alongside the generated SDK code. ## Format amounts Format a numeric amount as a localized currency string: ```typescript TypeScript theme={null} import { formatAmount } from "@yoshi-ai/sdk/lib/helpers"; formatAmount(1234.5, "USD"); // "$1,234.50" formatAmount(1234.5, "EUR", "de-DE"); // "1.234,50 €" formatAmount(999, "JPY"); // "¥999" ``` ```python Python theme={null} from yoshi.lib.helpers import format_amount format_amount(1234.5, "USD") # "$1,234.50" format_amount(1234.5, "EUR") # "1,234.50 €" format_amount(999, "JPY") # "¥999" ``` The TypeScript helper uses `Intl.NumberFormat` for locale-aware formatting. The Python helper uses a built-in currency symbol map and is thread-safe. ### Supported currencies The Python helper includes built-in formatting for: USD, EUR, GBP, JPY, CAD, AUD, MXN, BRL, CNY, INR, KRW, and CHF. The TypeScript helper supports any currency code recognized by `Intl.NumberFormat`. ## Generate idempotency keys Generate a unique key for POST requests to prevent duplicate operations: ```typescript TypeScript theme={null} import { generateIdempotencyKey } from "@yoshi-ai/sdk/lib/helpers"; const key = generateIdempotencyKey(); // "idk_a1b2c3d4e5f67890a1b2c3d4e5f67890" ``` ```python Python theme={null} from yoshi.lib.helpers import generate_idempotency_key key = generate_idempotency_key() # "idk_a1b2c3d4e5f67890a1b2c3d4e5f67890" ``` Keys are prefixed with `idk_` followed by 32 random hex characters. Pass the key in the `Idempotency-Key` header or the `idempotency_key` field on supported endpoints like `POST /actions`: ```typescript TypeScript theme={null} import Yoshi from "@yoshi-ai/sdk"; import { generateIdempotencyKey } from "@yoshi-ai/sdk/lib/helpers"; const yoshi = new Yoshi(); // The SDK sends the key in the Idempotency-Key header automatically const action = await yoshi.actions.create({ action_type: "paper_trade", idempotency_key: generateIdempotencyKey(), proposal_data: { /* ... */ }, }); ``` ```python Python theme={null} from yoshi import Yoshi from yoshi.lib.helpers import generate_idempotency_key yoshi = Yoshi() action = yoshi.actions.create( action_type="paper_trade", idempotency_key=generate_idempotency_key(), proposal_data={ ... }, ) ``` If a duplicate key is detected for the same user, the API returns a `409 Conflict` response. ## Convert between cents and dollars ```typescript TypeScript theme={null} import { centsToDollars, dollarsToCents } from "@yoshi-ai/sdk/lib/helpers"; centsToDollars(1050); // 10.5 dollarsToCents(10.5); // 1050 dollarsToCents(19.99); // 1999 ``` ```python Python theme={null} from yoshi.lib.helpers import cents_to_dollars, dollars_to_cents cents_to_dollars(1050) # 10.5 dollars_to_cents(10.5) # 1050 dollars_to_cents(19.99) # 1999 ``` `dollarsToCents` / `dollars_to_cents` rounds the result to avoid floating-point precision issues. # Yoshi API introduction and key concepts Source: https://docs.yoshi.ai/index Get started with the Yoshi API to access financial accounts, transactions, investments, and scores. Includes base URL, auth, and pagination. The Yoshi API gives you read access to your linked financial accounts, transactions, investment holdings, financial scores, and more. > **[Authentication](/authentication)** > > Get your API key and make your first request. > **[Quickstart](/quickstart)** > > Fetch your accounts and transactions in 3 steps. > **[Rate Limits](/rate-limits)** > > Understand rate limiting and best practices. > **[Error Handling](/errors)** > > Error codes, response format, and troubleshooting. ## Base URL All API requests use: ``` https://api.yoshi.ai/v1 ``` ## Key Concepts * **API Keys** — Authenticate with `Authorization: Bearer yoshi_...` * **Rate Limits** — 100 requests per minute per key * **Pagination** — Cursor-based for list endpoints * **Response Envelope** — All responses wrapped in `{ data, meta }` or `{ error, meta }`. Paginated endpoints include top-level `has_more`, `cursor`, and `count` fields * **Request IDs** — Every response includes a unique `request_id` for debugging # Quickstart: fetch accounts and transactions Source: https://docs.yoshi.ai/quickstart Make your first Yoshi API call in under a minute. Fetch linked accounts and transactions in three steps with cursor-based pagination. **1. Get your API key** Follow the [Authentication](/authentication) guide to create a key. **2. Fetch your accounts** List all your linked financial accounts: ```bash theme={null} curl https://api.yoshi.ai/accounts \ -H "Authorization: Bearer yoshi_YOUR_KEY" ``` You'll get a response like: ```json theme={null} { "data": { "accounts": [ { "id": "acc_abc123", "name": "Chase Checking (...4521)", "type": "depository", "subtype": "checking", "balance_current": 4523.17, "institution_name": "Chase" } ] }, "meta": { "request_id": "req_a1b2c3d4", "timestamp": "2026-04-10T12:00:00.000Z" } } ``` **3. Fetch your transactions** Get your recent transactions with pagination: ```bash theme={null} curl "https://api.yoshi.ai/transactions?limit=5" \ -H "Authorization: Bearer yoshi_YOUR_KEY" ``` ## Pagination List endpoints support cursor-based pagination: ```bash theme={null} # First page curl "https://api.yoshi.ai/transactions?limit=50" \ -H "Authorization: Bearer yoshi_YOUR_KEY" # Next page (use the cursor from the previous response) curl "https://api.yoshi.ai/transactions?limit=50&cursor=eyJpZCI6Ii..." \ -H "Authorization: Bearer yoshi_YOUR_KEY" ``` The response includes pagination fields at the top level alongside `data`: ```json theme={null} { "data": [...], "has_more": true, "cursor": "eyJpZCI6Ii...", "count": 50, "meta": { ... } } ``` When `has_more` is `false`, you've reached the last page. ## What's Next? > **[API Reference](/sdks#available-resources)** > > Browse all available endpoints. > **[Rate Limits](/rate-limits)** > > Understand rate limiting before building integrations. # Rate limits, headers, and retry strategies Source: https://docs.yoshi.ai/rate-limits Learn how Yoshi API rate limiting works, including the 100 requests-per-minute cap, response headers, 429 handling, and retry strategies. ## Limits Each API key is limited to **100 requests per minute** (sliding window). ## Response Headers Every response includes rate limit headers: | Header | Description | | - | - | | `X-RateLimit-Limit` | Maximum requests per window (100) | | `X-RateLimit-Remaining` | Requests remaining in current window | | `X-RateLimit-Reset` | Unix timestamp (ms) when the window resets | | `Retry-After` | Seconds until reset (only on 429 responses) | ## When You Hit the Limit You'll receive a `429` response: ```json theme={null} { "error": { "type": "https://api.yoshi.ai/errors/rate-limited", "code": "RATE_LIMITED", "message": "Rate limit exceeded. Retry after 45 seconds.", "display_message": "Please wait a moment and try again.", "param": null, "retry_after": 45 }, "meta": { "request_id": "req_abc123", "timestamp": "2026-04-10T12:00:00Z" } } ``` ## Best Practices > **Respect the Retry-After header** > > When you receive a `429`, wait the number of seconds specified in `retry_after` before retrying. Don't retry immediately. > **Use exponential backoff** > > For retries on server errors (`5xx`), use exponential backoff: wait 1s, then 2s, then 4s, etc. > **Cache responses where appropriate** > > Account data and scores don't change every second. Cache responses for a reasonable duration to reduce API calls. > **Use pagination efficiently** > > Fetch the data you need with appropriate `limit` values. The default is 50, max is 100. > [!NOTE] > > If you need higher rate limits for your use case, contact support. # TypeScript and Python SDK setup and usage Source: https://docs.yoshi.ai/sdks Install and configure the official Yoshi SDKs for TypeScript and Python. Includes usage examples, configuration options, error handling, and auto-pagination. Official SDKs are available for TypeScript and Python. Both provide typed methods for every endpoint, automatic retries, and built-in pagination. ## Installation ```bash TypeScript theme={null} npm install @yoshi-ai/sdk # or pnpm add @yoshi-ai/sdk ``` ```bash Python theme={null} pip install yoshi-sdk # or uv add yoshi-sdk ``` ## Quick example ```typescript TypeScript theme={null} import Yoshi from "@yoshi-ai/sdk"; const yoshi = new Yoshi(); // reads YOSHI_API_KEY from env const accounts = await yoshi.accounts.list(); const scores = await yoshi.scores.list(); const me = await yoshi.me.retrieve(); const summary = await yoshi.me.summary(); ``` ```python Python theme={null} from yoshi import Yoshi yoshi = Yoshi() # reads YOSHI_API_KEY from env accounts = yoshi.accounts.list() scores = yoshi.scores.list() me = yoshi.me.retrieve() summary = yoshi.me.summary() ``` ## Configuration Both SDKs accept the same core options: | Option | Default | Description | | - | - | - | | `apiKey` / `api_key` | `YOSHI_API_KEY` env var | Your API key (starts with `yoshi_`) | | `environment` | `"production"` | `"production"` or `"staging"` | | `maxRetries` / `max_retries` | `2` | Maximum number of automatic retries on failure | | `timeout` | `30000` ms / `30.0` s | Request timeout | ```typescript TypeScript theme={null} import Yoshi from "@yoshi-ai/sdk"; const yoshi = new Yoshi({ apiKey: "yoshi_3xK9mP...", environment: "staging", maxRetries: 3, timeout: 60_000, }); ``` ```python Python theme={null} from yoshi import Yoshi yoshi = Yoshi( api_key="yoshi_3xK9mP...", environment="staging", max_retries=3, timeout=60.0, ) ``` ## Auto-pagination List endpoints return paginated results. The SDKs handle cursor-based pagination automatically with async iterators: ```typescript TypeScript theme={null} // Iterate over all transactions automatically for await (const tx of yoshi.transactions.list({ limit: 100 })) { console.log(tx.merchant_name, tx.amount); } ``` ```python Python (sync) theme={null} for tx in yoshi.transactions.list(limit=100): print(tx.merchant_name, tx.amount) ``` ```python Python (async) theme={null} from yoshi import AsyncYoshi yoshi = AsyncYoshi() async for tx in yoshi.transactions.list(limit=100): print(tx.merchant_name, tx.amount) ``` You can also fetch a single page: ```typescript TypeScript theme={null} const page = await yoshi.transactions.list({ limit: 50 }); console.log(page.data); console.log(page.has_more); ``` ```python Python theme={null} page = yoshi.transactions.list(limit=50) print(page.data) print(page.has_more) ``` ## Error handling The SDKs raise typed exceptions for common HTTP error codes: ```typescript TypeScript theme={null} import Yoshi from "@yoshi-ai/sdk"; try { await yoshi.accounts.list(); } catch (err) { if (err instanceof Yoshi.AuthenticationError) { // 401 — invalid or revoked API key } else if (err instanceof Yoshi.RateLimitError) { // 429 — rate limited console.log("Retry after:", err.headers["retry-after"]); } else if (err instanceof Yoshi.NotFoundError) { // 404 — resource not found } } ``` ```python Python theme={null} from yoshi import Yoshi, AuthenticationError, RateLimitError, NotFoundError try: yoshi.accounts.list() except AuthenticationError: # 401 — invalid or revoked API key pass except RateLimitError as e: # 429 — rate limited print(f"Retry after: {e.response.headers['retry-after']}") except NotFoundError: # 404 — resource not found pass ``` ## Available resources The SDKs expose methods that map directly to the API reference: | SDK method | Endpoint | | - | - | | `accounts.list()` | `GET /accounts` | | `accounts.balanceSeries.list(id)` | `GET /accounts/{id}/balances` | | `transactions.list()` | `GET /transactions` | | `scores.list()` | `GET /scores` | | `goals.list()` | `GET /goals` | | `recurring.list()` | `GET /recurring` | | `investments.list()` | `GET /investments` | | `income.retrieve()` | `GET /income` | | `me.retrieve()` | `GET /me` | | `me.summary()` | `GET /me/summary` | | `paperTrading.accounts.create()` | `POST /paper-trading/accounts` | | `paperTrading.accounts.list()` | `GET /paper-trading/accounts` | | `paperTrading.accounts.trades.create(id)` | `POST /paper-trading/accounts/{id}/trades` | | `paperTrading.accounts.trades.list(id)` | `GET /paper-trading/accounts/{id}/trades` | | `paperTrading.accounts.holdings.list(id)` | `GET /paper-trading/accounts/{id}/holdings` | | `actions.create()` | `POST /actions` | | `actions.list()` | `GET /actions` | | `actions.retrieve(id)` | `GET /actions/{id}` | | `approvals.retrieve(threadId)` | `GET /approvals/{threadId}` | > [!NOTE] > > Python method names use `snake_case` (e.g., `paper_trading.accounts.create()`). ## Requirements | SDK | Minimum version | | - | - | | TypeScript | Node.js 18+ | | Python | Python 3.8+ | # Webhook delivery guarantees, retries, and idempotency Source: https://docs.yoshi.ai/webhooks/delivery How Yoshi delivers webhooks with at-least-once semantics. Covers the retry schedule, event deduplication, ordering, and failure handling. Yoshi delivers webhooks with at-least-once semantics. Your endpoint may receive the same event more than once, so design your handler to be idempotent. ## Delivery guarantee | Aspect | Behavior | | - | - | | **Guarantee** | At-least-once. Your endpoint may receive duplicate events. | | **Success criteria** | Your server must return a `2xx` status code within **15 seconds**. | | **Ordering** | Events are **not** guaranteed to arrive in order. Use `created_at` for ordering. | | **Batching** | One HTTP request per event per endpoint. Events are not batched. | > [!NOTE] > > Return a `200` response immediately, then process the event asynchronously. If your handler takes longer than 15 seconds, the delivery will be marked as failed and retried. ## Retry schedule When your endpoint returns a non-`2xx` response or times out, Yoshi retries with exponential backoff: | Attempt | Delay | Total elapsed | | - | - | - | | 1st retry | 5 seconds | 5 seconds | | 2nd retry | 5 minutes | \~5 minutes | | 3rd retry | 30 minutes | \~35 minutes | | 4th retry | 2 hours | \~2.5 hours | | 5th retry | 5 hours | \~7.5 hours | | 6th retry | 10 hours | \~17.5 hours | | 7th retry | 10 hours | \~27.5 hours | After all retries are exhausted (approximately 28 hours), the event is moved to a dead letter queue. ## Endpoint failure handling If your endpoint consistently fails to respond: **1. Retries exhaust** After all retry attempts fail, the event is marked as failed. **2. Endpoint is disabled** If an endpoint fails repeatedly over multiple events, it is automatically disabled to prevent unnecessary traffic. **3. You investigate and fix** Check the delivery logs via `GET /v1/webhooks/deliveries` or the [consumer portal](/webhooks/overview#consumer-portal) to see response codes and error details. **4. Re-enable and replay** After fixing the issue, re-enable the endpoint via the API or portal. Use the portal's replay button to resend failed events. ## Idempotency Every event has a unique `id` field that is stable across retries. Use this ID to deduplicate events in your handler: ```typescript TypeScript theme={null} const processedEvents = new Set(); // In production, use Redis or a database app.post("/webhooks/yoshi", (req, res) => { const event = JSON.parse(req.body); if (processedEvents.has(event.id)) { // Already processed — return 200 to stop retries return res.status(200).send("OK"); } processedEvents.add(event.id); // Process the event... handleEvent(event); res.status(200).send("OK"); }); ``` ```python Python theme={null} processed_events: set[str] = set() # In production, use Redis or a database @app.post("/webhooks/yoshi") def handle_webhook(request): event = request.json() if event["id"] in processed_events: # Already processed — return 200 to stop retries return Response(status_code=200) processed_events.add(event["id"]) # Process the event... handle_event(event) return Response(status_code=200) ``` > [!NOTE] > > For production use, store processed event IDs in a database or Redis with a TTL of at least 7 days. An in-memory set won't survive server restarts. ## Ordering Events are not guaranteed to arrive in the order they occurred. For example, you might receive `transaction.updated` before `transaction.created` for the same transaction. To handle this: * Use the `created_at` timestamp to determine the chronological order of events. * Design your handlers to be order-independent when possible. * If you need strict ordering, buffer events and sort by `created_at` before processing. ## Best practices > **Return 200 immediately, process asynchronously** > > Your webhook handler should acknowledge receipt as fast as possible. Enqueue the event for background processing rather than doing work inline. This prevents timeouts and retries. > **Use event IDs for deduplication** > > The `id` field is stable across retries. Store processed event IDs to prevent duplicate processing, especially for events that trigger side effects like sending emails or creating records. > **Verify signatures before processing** > > Always [verify the webhook signature](/webhooks/verification) before trusting the payload. An unverified webhook could be forged by a third party. > **Fetch full details from the API** > > Webhook payloads contain resource IDs and metadata, not full objects. Use the IDs to fetch current data from the API. This ensures you always have the latest state, even if events arrive out of order. > **Monitor your delivery logs** > > Check `GET /v1/webhooks/deliveries` or the consumer portal regularly to catch failed deliveries early. Set up alerts if your failure rate spikes. > **Handle unknown event types gracefully** > > Yoshi may add new event types in the future. If your handler encounters an event type it doesn't recognize, return `200` and ignore it. Never return an error for unknown types — that would trigger unnecessary retries. # Webhook event types and payload reference Source: https://docs.yoshi.ai/webhooks/events Complete list of Yoshi webhook event types with example payloads for accounts, transactions, balances, investments, scores, and income. Events use a `resource.action` naming convention. Each event is delivered as a JSON object with the standard [event envelope](/webhooks/overview#event-envelope). You can subscribe to specific event types when [creating an endpoint](/webhooks/management#create-an-endpoint), or receive all events by leaving the filter empty. ## Account events Account events fire when a user's linked financial accounts change. > **account.connected** > > A new financial account was linked. > > Fired when a user successfully connects a financial institution through the Yoshi link flow. > > ```json theme={null} > { > "id": "evt_9YnQRSnLE4FrI3XmYdSTzUHfCU", > "type": "account.connected", > "created_at": "2026-04-10T12:00:00.000Z", > "api_version": "2026-04-10", > "data": { > "id": "acc_xyz789", > "name": "Chase Checking (...4521)", > "type": "depository", > "subtype": "checking", > "institution_name": "Chase" > } > } > ``` > > | Field | Type | Description | > | - | - | - | > | `data.id` | `string` | Account ID. Use with `GET /accounts` to fetch full details. | > | `data.name` | `string` | Display name with masked account number. | > | `data.type` | `string` | Account type: `depository`, `credit`, `investment`, `loan`, or `other`. | > | `data.subtype` | `string \| null` | Account subtype, e.g. `checking`, `savings`, `credit card`. | > | `data.institution_name` | `string \| null` | Name of the financial institution. | > **account.disconnected** > > Account access was revoked. > > Fired when a financial account is disconnected — either by the user unlinking it or due to an institution-side error that revoked access. > > ```json theme={null} > { > "id": "evt_7XmPQRnKE3EqH2WlXcRTzTGeBU", > "type": "account.disconnected", > "created_at": "2026-04-10T16:00:00.000Z", > "api_version": "2026-04-10", > "data": { > "id": "acc_xyz789", > "name": "Chase Checking (...4521)", > "reason": "ITEM_LOGIN_REQUIRED" > } > } > ``` > > | Field | Type | Description | > | - | - | - | > | `data.id` | `string` | Account ID. | > | `data.name` | `string` | Display name of the disconnected account. | > | `data.reason` | `string \| null` | Reason for disconnection, if available. | > **account.updated** > > Account metadata changed. > > Fired when an account's name, type, or other metadata changes during a sync. > > ```json theme={null} > { > "id": "evt_5WkNOPmJD2DpG1VkWbQSySFdBT", > "type": "account.updated", > "created_at": "2026-04-10T13:00:00.000Z", > "api_version": "2026-04-10", > "data": { > "id": "acc_xyz789", > "name": "Chase Savings (...4521)", > "type": "depository", > "subtype": "savings" > } > } > ``` > > | Field | Type | Description | > | - | - | - | > | `data.id` | `string` | Account ID. | > | `data.name` | `string` | Updated display name. | > | `data.type` | `string` | Account type. | > | `data.subtype` | `string \| null` | Account subtype. | ## Transaction events Transaction events fire when transactions are synced from financial institutions. > **transaction.created** > > New transactions synced from an institution. > > Fired after new transactions are persisted during a transaction sync. The payload contains transaction IDs — fetch full details via `GET /transactions`. > > ```json theme={null} > { > "id": "evt_2KWPBQMnRE2EqH9WlXbQTzSGeAT", > "type": "transaction.created", > "created_at": "2026-04-10T14:30:00.000Z", > "api_version": "2026-04-10", > "data": { > "account_id": "acc_xyz789", > "transaction_ids": ["txn_abc123", "txn_def456", "txn_ghi789"], > "count": 3 > } > } > ``` > > | Field | Type | Description | > | - | - | - | > | `data.account_id` | `string` | The account that the transactions belong to. | > | `data.transaction_ids` | `string[]` | IDs of the new transactions. | > | `data.count` | `number` | Number of new transactions in this event. | > **transaction.updated** > > Transaction details were modified. > > Fired when existing transactions are re-categorized, have merchant names corrected, or are otherwise updated by the institution. > > ```json theme={null} > { > "id": "evt_4VjMNOlIC1CnF0UjVaPS1REdAS", > "type": "transaction.updated", > "created_at": "2026-04-10T15:00:00.000Z", > "api_version": "2026-04-10", > "data": { > "account_id": "acc_xyz789", > "transaction_ids": ["txn_abc123"], > "count": 1 > } > } > ``` > > | Field | Type | Description | > | - | - | - | > | `data.account_id` | `string` | The account that the transactions belong to. | > | `data.transaction_ids` | `string[]` | IDs of the updated transactions. | > | `data.count` | `number` | Number of updated transactions. | > **transaction.removed** > > Transactions were removed by the institution. > > Fired when an institution retroactively removes transactions — for example, when a pending transaction is voided. > > ```json theme={null} > { > "id": "evt_6XoPRSnME4GsJ2YmZeUTzVIfDW", > "type": "transaction.removed", > "created_at": "2026-04-10T16:30:00.000Z", > "api_version": "2026-04-10", > "data": { > "account_id": "acc_xyz789", > "transaction_ids": ["txn_abc123"], > "count": 1 > } > } > ``` > > | Field | Type | Description | > | - | - | - | > | `data.account_id` | `string` | The account that the transactions belonged to. | > | `data.transaction_ids` | `string[]` | IDs of the removed transactions. | > | `data.count` | `number` | Number of removed transactions. | ## Balance events > **balance.updated** > > Account balance was refreshed. > > Fired when an account's current or available balance changes during a sync. > > ```json theme={null} > { > "id": "evt_8XmPQRnKE3EqH2WlXcRTzTGeBS", > "type": "balance.updated", > "created_at": "2026-04-10T15:00:00.000Z", > "api_version": "2026-04-10", > "data": { > "account_id": "acc_xyz789", > "current": 4523.17, > "available": 4423.17, > "currency": "USD" > } > } > ``` > > | Field | Type | Description | > | - | - | - | > | `data.account_id` | `string` | Account ID. | > | `data.current` | `number \| null` | Current balance. | > | `data.available` | `number \| null` | Available balance (may differ from current due to holds). | > | `data.currency` | `string` | ISO 4217 currency code. | ## Investment events > **investment.holdings\_updated** > > Investment holdings data was refreshed. > > Fired after an investment account sync updates holdings positions. > > ```json theme={null} > { > "id": "evt_3UiLMNkHB0BmE9TiUZOR0QDcZR", > "type": "investment.holdings_updated", > "created_at": "2026-04-10T09:00:00.000Z", > "api_version": "2026-04-10", > "data": { > "account_id": "acc_inv456", > "holding_count": 12 > } > } > ``` > > | Field | Type | Description | > | - | - | - | > | `data.account_id` | `string` | Investment account ID. | > | `data.holding_count` | `number` | Number of holdings in the account after the sync. | > **investment.transaction\_created** > > New investment transactions detected. > > Fired when new investment transactions (buys, sells, dividends) are synced. > > ```json theme={null} > { > "id": "evt_1ThKLMjGA9AlD8ShTYNQ9PCbYQ", > "type": "investment.transaction_created", > "created_at": "2026-04-10T09:30:00.000Z", > "api_version": "2026-04-10", > "data": { > "account_id": "acc_inv456", > "transaction_ids": ["itxn_abc123"], > "count": 1 > } > } > ``` > > | Field | Type | Description | > | - | - | - | > | `data.account_id` | `string` | Investment account ID. | > | `data.transaction_ids` | `string[]` | IDs of the new investment transactions. | > | `data.count` | `number` | Number of new investment transactions. | ## Score events > **score.updated** > > Financial health scores were recalculated. > > Fired after a user's financial health scores are recomputed based on new data. > > ```json theme={null} > { > "id": "evt_0SgJKLiF98ZkC7RgSXMP8OBaXP", > "type": "score.updated", > "created_at": "2026-04-10T10:00:00.000Z", > "api_version": "2026-04-10", > "data": { > "user_id": "usr_abc123", > "score_types": ["recovery", "capacity", "baseline", "composite"] > } > } > ``` > > | Field | Type | Description | > | - | - | - | > | `data.user_id` | `string` | User whose scores were updated. | > | `data.score_types` | `string[]` | Which score types were recalculated. | ## Income events > **income.detected** > > Income stream identified or updated. > > Fired when income analysis detects or updates recurring income streams for a user. > > ```json theme={null} > { > "id": "evt_BRfHIKhE87YjB6QfRWLO7NAzWO", > "type": "income.detected", > "created_at": "2026-04-10T11:00:00.000Z", > "api_version": "2026-04-10", > "data": { > "user_id": "usr_abc123", > "stream_count": 2 > } > } > ``` > > | Field | Type | Description | > | - | - | - | > | `data.user_id` | `string` | User whose income was analyzed. | > | `data.stream_count` | `number` | Number of income streams detected. | ## Recurring events > **recurring.updated** > > Recurring transaction streams were re-analyzed. > > Fired when Yoshi re-evaluates a user's recurring transactions (subscriptions, bills, regular transfers). > > ```json theme={null} > { > "id": "evt_CQeGHJgD76XiA5PeQVKN6MZyVN", > "type": "recurring.updated", > "created_at": "2026-04-10T11:30:00.000Z", > "api_version": "2026-04-10", > "data": { > "user_id": "usr_abc123", > "stream_count": 8 > } > } > ``` > > | Field | Type | Description | > | - | - | - | > | `data.user_id` | `string` | User whose recurring streams were analyzed. | > | `data.stream_count` | `number` | Number of recurring streams detected. | # Manage webhook endpoints Source: https://docs.yoshi.ai/webhooks/management Create, update, list, and delete webhook endpoints via the API. Configure event filters, rotate signing secrets, and manage endpoint lifecycle. Manage your webhook endpoints programmatically through the `/v1/webhooks` API. All endpoints require authentication with your API key. ## Create an endpoint Register a new webhook endpoint. You'll receive a signing secret in the response — **store it securely**, as it's only shown once. ```bash theme={null} curl -X POST https://api.yoshi.ai/v1/webhooks/endpoints \ -H "Authorization: Bearer yoshi_3xK9mP..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/yoshi", "description": "Production webhook handler", "filter_types": ["transaction.created", "account.connected"] }' ``` ```json theme={null} { "data": { "id": "ep_a1b2c3d4-e5f6-7890-abcd-ef1234567890", "url": "https://example.com/webhooks/yoshi", "description": "Production webhook handler", "filter_types": ["transaction.created", "account.connected"], "status": "active", "secret": "whsec_REPLACE_WITH_YOUR_SIGNING_SECRET", "created_at": "2026-04-10T12:00:00.000Z" }, "meta": { "request_id": "req_a1b2c3d4", "timestamp": "2026-04-10T12:00:00.000Z" } } ``` | Parameter | Type | Required | Description | | - | - | - | - | | `url` | `string` | Yes | HTTPS URL where events will be delivered. | | `description` | `string` | No | Human-readable label for the endpoint. Max 256 characters. | | `filter_types` | `string[]` | No | Event types to subscribe to. Empty array or omitted means all events. See the [event catalog](/webhooks/events). | > [!WARNING] > > The `secret` field is only included in the create response. Store it immediately in a secure location like an environment variable or secrets manager. If you lose it, you can [rotate](#rotate-signing-secret) to get a new one. **Limits:** You can register up to **5 active endpoints** per API key. URLs must use HTTPS. ## List endpoints ```bash theme={null} curl https://api.yoshi.ai/v1/webhooks/endpoints \ -H "Authorization: Bearer yoshi_3xK9mP..." ``` ```json theme={null} { "data": { "endpoints": [ { "id": "ep_a1b2c3d4-e5f6-7890-abcd-ef1234567890", "url": "https://example.com/webhooks/yoshi", "description": "Production webhook handler", "filter_types": ["transaction.created", "account.connected"], "status": "active", "created_at": "2026-04-10T12:00:00.000Z", "updated_at": "2026-04-10T12:00:00.000Z" } ] }, "meta": { "request_id": "req_a1b2c3d4", "timestamp": "2026-04-10T12:00:00.000Z" } } ``` ## Get an endpoint ```bash theme={null} curl https://api.yoshi.ai/v1/webhooks/endpoints/{endpoint_id} \ -H "Authorization: Bearer yoshi_3xK9mP..." ``` ## Update an endpoint Update the URL, description, event filters, or status of an existing endpoint. Only include the fields you want to change. ```bash theme={null} curl -X PUT https://api.yoshi.ai/v1/webhooks/endpoints/{endpoint_id} \ -H "Authorization: Bearer yoshi_3xK9mP..." \ -H "Content-Type: application/json" \ -d '{ "description": "Updated production handler", "filter_types": ["transaction.created", "balance.updated"], "status": "active" }' ``` | Parameter | Type | Description | | - | - | - | | `url` | `string` | New HTTPS URL. | | `description` | `string \| null` | Updated description, or `null` to clear. | | `filter_types` | `string[]` | Updated event type filter. Empty array means all events. | | `status` | `string` | `"active"` or `"disabled"`. | ## Delete an endpoint Permanently remove a webhook endpoint. This immediately stops all future deliveries to this URL. ```bash theme={null} curl -X DELETE https://api.yoshi.ai/v1/webhooks/endpoints/{endpoint_id} \ -H "Authorization: Bearer yoshi_3xK9mP..." ``` ```json theme={null} { "data": { "deleted": true }, "meta": { "request_id": "req_a1b2c3d4", "timestamp": "2026-04-10T12:00:00.000Z" } } ``` ## Rotate signing secret Generate a new signing secret for an endpoint. During the rotation window, both the old and new secrets are valid — see the [verification guide](/webhooks/verification#secret-rotation) for handling this. ```bash theme={null} curl -X POST https://api.yoshi.ai/v1/webhooks/endpoints/{endpoint_id}/rotate \ -H "Authorization: Bearer yoshi_3xK9mP..." ``` ```json theme={null} { "data": { "secret": "whsec_REPLACE_WITH_YOUR_ROTATED_SECRET" }, "meta": { "request_id": "req_a1b2c3d4", "timestamp": "2026-04-10T12:00:00.000Z" } } ``` > [!WARNING] > > Store the new secret immediately. Like the initial secret, it is only returned once. ## List event types Get all available event types that you can subscribe to: ```bash theme={null} curl https://api.yoshi.ai/v1/webhooks/events \ -H "Authorization: Bearer yoshi_3xK9mP..." ``` ```json theme={null} { "data": { "event_types": [ { "type": "account.connected", "description": "A new financial account was linked." }, { "type": "account.disconnected", "description": "Account access was revoked." }, { "type": "transaction.created", "description": "New transaction(s) synced from institution." }, { "type": "balance.updated", "description": "Account balance refreshed." }, { "type": "score.updated", "description": "Financial health scores recalculated." } ] }, "meta": { "request_id": "req_a1b2c3d4", "timestamp": "2026-04-10T12:00:00.000Z" } } ``` For full payload examples, see the [event catalog](/webhooks/events). ## API reference | Method | Endpoint | Description | | - | - | - | | `POST` | `/v1/webhooks/endpoints` | Create a webhook endpoint | | `GET` | `/v1/webhooks/endpoints` | List all endpoints | | `GET` | `/v1/webhooks/endpoints/{id}` | Get endpoint details | | `PUT` | `/v1/webhooks/endpoints/{id}` | Update an endpoint | | `DELETE` | `/v1/webhooks/endpoints/{id}` | Delete an endpoint | | `POST` | `/v1/webhooks/endpoints/{id}/test` | Send a test event | | `POST` | `/v1/webhooks/endpoints/{id}/rotate` | Rotate signing secret | | `GET` | `/v1/webhooks/events` | List available event types | | `GET` | `/v1/webhooks/deliveries` | List delivery attempts | | `GET` | `/v1/webhooks/portal` | Get self-service portal URL | # Webhooks for real-time financial data updates Source: https://docs.yoshi.ai/webhooks/overview Receive real-time HTTP callbacks from Yoshi when financial data changes. Covers event structure, security headers, and the self-service portal. Webhooks let you react to changes in your users' financial data as they happen. Instead of polling the API, Yoshi sends an HTTP POST request to your server whenever an event occurs — a new transaction is synced, a balance updates, an account is connected, and more. ## How it works **1. Register an endpoint** Tell Yoshi where to send events by creating a webhook endpoint via the API or the [self-service portal](#consumer-portal). You provide an HTTPS URL and optionally choose which event types to receive. **2. Yoshi detects a change** When a user's financial data changes — new transactions arrive from their bank, a balance refreshes, or an account is linked — Yoshi generates a webhook event. **3. Your server receives the event** Yoshi sends an HTTP POST to your endpoint with the event payload. Each request is signed so you can [verify](/webhooks/verification) it came from Yoshi. **4. You respond with 200** Return a `2xx` status code within 15 seconds to acknowledge receipt. Process the event asynchronously — don't do heavy work in the handler. ## Event envelope Every webhook event follows the same structure: ```json theme={null} { "id": "evt_2KWPBQMnRE2EqH9WlXbQTzSGeAT", "type": "transaction.created", "created_at": "2026-04-10T14:30:00.000Z", "api_version": "2026-04-10", "data": { "account_id": "acc_xyz789", "transaction_ids": ["txn_abc123", "txn_def456"], "count": 2 } } ``` | Field | Description | | - | - | | `id` | Unique event identifier. Stable across retries — use it for [idempotency](/webhooks/delivery#idempotency). | | `type` | Dot-notation event type, e.g. `transaction.created`. See the full [event catalog](/webhooks/events). | | `created_at` | ISO 8601 timestamp of when the event occurred. | | `api_version` | The API version that generated this event. Currently `2026-04-10`. | | `data` | Event-specific payload. Contains resource IDs and non-sensitive metadata. | > [!NOTE] > > Webhook payloads contain resource IDs and metadata, not full resource objects. To fetch complete details, call the corresponding API endpoint with the ID from the event. This keeps payloads small and minimizes sensitive data in transit. ## Security Every webhook request includes three headers for [signature verification](/webhooks/verification): | Header | Description | | - | - | | `webhook-id` | Unique message identifier | | `webhook-timestamp` | Unix timestamp (seconds) when the webhook was sent | | `webhook-signature` | HMAC-SHA256 signature for verifying authenticity | Always verify signatures before processing events. The SDKs include a built-in helper that handles verification and replay protection automatically. ## Consumer portal Every API key holder has access to a self-service webhook portal for managing endpoints, viewing delivery logs, and replaying failed events. Generate a portal link via the API: ```bash theme={null} curl https://api.yoshi.ai/v1/webhooks/portal \ -H "Authorization: Bearer yoshi_3xK9mP..." ``` The response contains a one-time URL that opens the portal: ```json theme={null} { "data": { "url": "https://app.svix.com/login#key=..." }, "meta": { "request_id": "req_a1b2c3d4", "timestamp": "2026-04-10T12:00:00.000Z" } } ``` The portal lets you: * Create, edit, and delete webhook endpoints * View delivery attempts with response codes * Replay failed events with one click * Browse the event log with full payloads ## What's next > **[Event catalog](/webhooks/events)** > > Browse all available event types and their payloads. > **[Verify signatures](/webhooks/verification)** > > Ensure webhook requests are authentic. > **[Delivery and retries](/webhooks/delivery)** > > Understand retry behavior and idempotency. > **[Manage endpoints](/webhooks/management)** > > Register and configure webhook endpoints via the API. # Test and debug webhook integrations locally Source: https://docs.yoshi.ai/webhooks/testing Test Yoshi webhook integrations locally with ngrok, send test events, inspect delivery logs, replay failed events, and debug common issues. Yoshi provides several ways to test your webhook integration before going to production. ## Send a test event Every endpoint has a test action that sends a synthetic `test.ping` event. Use it to verify your handler is reachable and responding correctly: ```bash theme={null} curl -X POST https://api.yoshi.ai/v1/webhooks/endpoints/{endpoint_id}/test \ -H "Authorization: Bearer yoshi_3xK9mP..." ``` ```json theme={null} { "data": { "sent": true, "event_id": "evt_test_a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "meta": { "request_id": "req_a1b2c3d4", "timestamp": "2026-04-10T12:00:00.000Z" } } ``` Your endpoint will receive a webhook with this payload: ```json theme={null} { "id": "evt_test_a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "test.ping", "created_at": "2026-04-10T12:00:00.000Z", "api_version": "2026-04-10", "data": { "test": true, "message": "This is a test webhook from Yoshi." } } ``` > [!NOTE] > > The `test.ping` event type is synthetic — it's only sent when you explicitly trigger a test. It won't appear in your event subscriptions or filter settings. ## Test locally To receive webhooks on your local machine during development, expose your local server to the internet using a tunnel. ### Using ngrok **1. Start your local server** Run your webhook handler on a local port (e.g., `http://localhost:3000`). **2. Start ngrok** ```bash theme={null} ngrok http 3000 ``` ngrok outputs a public URL like `https://a1b2c3d4.ngrok-free.app`. **3. Register the tunnel URL as an endpoint** ```bash theme={null} curl -X POST https://api.yoshi.ai/v1/webhooks/endpoints \ -H "Authorization: Bearer yoshi_3xK9mP..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://a1b2c3d4.ngrok-free.app/webhooks/yoshi", "description": "Local development" }' ``` Save the `secret` from the response — you'll need it for [signature verification](/webhooks/verification). **4. Send a test event** ```bash theme={null} curl -X POST https://api.yoshi.ai/v1/webhooks/endpoints/{endpoint_id}/test \ -H "Authorization: Bearer yoshi_3xK9mP..." ``` You should see the `test.ping` event arrive in your local server logs. ### Using webhook.site If you just want to inspect webhook payloads without building a handler: 1. Go to [webhook.site](https://webhook.site) to get a temporary URL. 2. Register it as a webhook endpoint via the API. 3. Send a test event or wait for real events to arrive. 4. Inspect the full request headers and body on the webhook.site dashboard. This is useful for verifying that events are being sent and payloads look correct before writing handler code. ## Inspect delivery logs View recent delivery attempts via the API: ```bash theme={null} curl https://api.yoshi.ai/v1/webhooks/deliveries \ -H "Authorization: Bearer yoshi_3xK9mP..." ``` ```json theme={null} { "data": { "deliveries": [ { "id": "d1b2c3d4-e5f6-7890-abcd-ef1234567890", "event_type": "transaction.created", "event_id": "msg_a1b2c3d4", "status": "delivered", "created_at": "2026-04-10T14:30:00.000Z" } ] }, "has_more": false, "cursor": null, "count": 1, "meta": { "request_id": "req_a1b2c3d4", "timestamp": "2026-04-10T15:00:00.000Z" } } ``` The `status` field is one of: | Status | Description | | - | - | | `delivered` | Your endpoint returned `2xx`. | | `pending` | Delivery is in progress or queued for retry. | | `failed` | All delivery attempts were exhausted. | For more detailed logs, use the [consumer portal](/webhooks/overview#consumer-portal) which shows full request/response details for each delivery attempt. ## Replay failed events If deliveries fail due to a temporary issue on your end (e.g., a deploy that caused downtime), you can replay them from the consumer portal: 1. Open the portal via `GET /v1/webhooks/portal`. 2. Navigate to the failed deliveries. 3. Click **Replay** to resend individual events or replay all failed events for a time range. ## Debugging checklist If webhooks aren't arriving at your endpoint: > **Is the endpoint URL reachable?** > > Your endpoint must be publicly accessible over HTTPS. Local URLs like `http://localhost:3000` won't work — use a tunnel like ngrok for local development. > **Is the endpoint returning 200?** > > Check `GET /v1/webhooks/deliveries` for the HTTP status code your endpoint returned. Non-`2xx` responses trigger retries. > **Is your handler responding within 15 seconds?** > > If your handler takes too long, the delivery times out and is retried. Return `200` immediately and process the event in the background. > **Is the endpoint active?** > > Check `GET /v1/webhooks/endpoints` to verify the endpoint's `status` is `active`. Endpoints are automatically disabled after repeated failures. > **Are you subscribed to the right event types?** > > If you set `filter_types` when creating the endpoint, only those event types will be delivered. An empty `filter_types` array receives all events. > **Is signature verification failing?** > > If your handler rejects the signature, double-check that you're using the **raw request body** (not a parsed-then-serialized version) and the correct signing secret. # Webhook signature verification with HMAC-SHA256 Source: https://docs.yoshi.ai/webhooks/verification Verify Yoshi webhook authenticity with HMAC-SHA256 signatures. Includes SDK helpers, manual verification, and replay protection examples. Every webhook request from Yoshi is signed with HMAC-SHA256 so you can verify it came from Yoshi and hasn't been tampered with. Always verify signatures before processing events. ## Quick start The fastest way to verify webhooks is with the SDK: ```typescript TypeScript theme={null} import { verifySignature } from "@yoshi-ai/sdk/lib/webhooks"; app.post("/webhooks/yoshi", (req, res) => { const isValid = verifySignature( req.body, req.headers["x-yoshi-signature"], req.headers["x-yoshi-timestamp"], process.env.WEBHOOK_SECRET, ); if (!isValid) return res.status(400).send("Invalid signature"); const event = JSON.parse(req.body); // Process event asynchronously... res.status(200).send("OK"); }); ``` ```python Python theme={null} from yoshi.lib.webhooks import verify_signature @app.post("/webhooks/yoshi") def handle_webhook(request): is_valid = verify_signature( payload=request.body, signature=request.headers["x-yoshi-signature"], timestamp=request.headers["x-yoshi-timestamp"], secret=os.environ["WEBHOOK_SECRET"], ) if not is_valid: return Response(status_code=400) event = request.json() # Process event asynchronously... return Response(status_code=200) ``` ## How signing works Every webhook request includes two headers: | Header | Example | Description | | - | - | - | | `x-yoshi-signature` | `a1b2c3d4e5f6...` | HMAC-SHA256 hex digest of the signed content | | `x-yoshi-timestamp` | `1714444800` | Unix timestamp (seconds) when the event was sent | The signature is computed as: ``` signed_content = "{timestamp}.{raw_body}" signature = hex(HMAC-SHA256(secret, signed_content)) ``` Where `secret` is your webhook signing secret (starts with `whsec_`), `timestamp` is the value of the `x-yoshi-timestamp` header, and `raw_body` is the raw request body string. > [!WARNING] > > You must use the **raw request body** for verification, not a parsed-then-serialized version. Most frameworks parse JSON automatically — make sure you capture the raw body before parsing. ## Framework examples ```typescript Express theme={null} import express from "express"; import { verifySignature } from "@yoshi-ai/sdk/lib/webhooks"; const app = express(); // Capture raw body for signature verification app.post("/webhooks/yoshi", express.raw({ type: "application/json" }), (req, res) => { const isValid = verifySignature( req.body, req.headers["x-yoshi-signature"] as string, req.headers["x-yoshi-timestamp"] as string, process.env.WEBHOOK_SECRET!, ); if (!isValid) return res.status(400).send("Invalid signature"); const event = JSON.parse(req.body.toString()); console.log(`Received ${event.type}:`, event.id); res.status(200).send("OK"); }); ``` ```typescript Next.js (App Router) theme={null} // app/api/webhooks/yoshi/route.ts import { verifySignature } from "@yoshi-ai/sdk/lib/webhooks"; export async function POST(request: Request) { const body = await request.text(); const isValid = verifySignature( body, request.headers.get("x-yoshi-signature")!, request.headers.get("x-yoshi-timestamp")!, process.env.WEBHOOK_SECRET!, ); if (!isValid) { return new Response("Invalid signature", { status: 400 }); } const event = JSON.parse(body); console.log(`Received ${event.type}:`, event.id); return new Response("OK", { status: 200 }); } ``` ```python FastAPI theme={null} from fastapi import FastAPI, Request, Response from yoshi.lib.webhooks import verify_signature import os app = FastAPI() @app.post("/webhooks/yoshi") async def handle_webhook(request: Request): body = await request.body() is_valid = verify_signature( payload=body, signature=request.headers["x-yoshi-signature"], timestamp=request.headers["x-yoshi-timestamp"], secret=os.environ["WEBHOOK_SECRET"], ) if not is_valid: return Response(status_code=400) event = await request.json() print(f"Received {event['type']}: {event['id']}") return Response(status_code=200) ``` ```python Django theme={null} # views.py import json from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from yoshi.lib.webhooks import verify_signature import os @csrf_exempt def webhook_handler(request): is_valid = verify_signature( payload=request.body, signature=request.headers["X-Yoshi-Signature"], timestamp=request.headers["X-Yoshi-Timestamp"], secret=os.environ["WEBHOOK_SECRET"], ) if not is_valid: return HttpResponse(status=400) event = json.loads(request.body) print(f"Received {event['type']}: {event['id']}") return HttpResponse(status=200) ``` ## Replay protection The SDK rejects webhooks with timestamps older than 5 minutes by default. This prevents replay attacks where an attacker resends a captured webhook request. You can customize this tolerance: ```typescript TypeScript theme={null} verifySignature(payload, signature, timestamp, secret, { tolerance: 600, // 10 minutes }); ``` ```python Python theme={null} verify_signature( payload=payload, signature=signature, timestamp=timestamp, secret=secret, tolerance=600, # 10 minutes ) ``` ## Manual verification If you prefer to verify signatures without the SDK: ```typescript TypeScript / Node.js theme={null} import { createHmac, timingSafeEqual } from "node:crypto"; function verifyWebhook(body: string, signature: string, timestamp: string, secret: string): boolean { // Check timestamp is within tolerance (5 minutes) const ts = parseInt(timestamp, 10); const now = Math.floor(Date.now() / 1000); if (Math.abs(now - ts) > 300) return false; // Compute expected signature const signedPayload = `${timestamp}.${body}`; const expected = createHmac("sha256", secret).update(signedPayload).digest("hex"); // Constant-time comparison const sigBuf = Buffer.from(signature, "utf-8"); const expectedBuf = Buffer.from(expected, "utf-8"); if (sigBuf.length !== expectedBuf.length) return false; return timingSafeEqual(sigBuf, expectedBuf); } ``` ```python Python theme={null} import hashlib import hmac import time def verify_webhook(body: str, signature: str, timestamp: str, secret: str) -> bool: # Check timestamp is within tolerance (5 minutes) ts = int(timestamp) now = int(time.time()) if abs(now - ts) > 300: return False # Compute expected signature signed_payload = f"{timestamp}.{body}".encode("utf-8") expected = hmac.new( secret.encode("utf-8"), signed_payload, hashlib.sha256, ).hexdigest() # Constant-time comparison return hmac.compare_digest(signature, expected) ``` ```go Go theme={null} package webhooks import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "math" "strconv" "time" ) func VerifyWebhook(body, signature, timestamp, secret string) bool { // Check timestamp is within tolerance (5 minutes) ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return false } now := time.Now().Unix() if math.Abs(float64(now-ts)) > 300 { return false } // Compute expected signature mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp + "." + body)) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expected)) } ``` > [!WARNING] > > Always use constant-time comparison (`timingSafeEqual` in Node.js, `hmac.compare_digest` in Python, `hmac.Equal` in Go) to prevent timing attacks. Never use `===` or `==` for signature comparison. ## Secret rotation When you [rotate your signing secret](/webhooks/management#rotate-signing-secret), there is a brief window where both the old and new secrets are valid. During this window, Yoshi signs webhooks with both secrets. Your verification logic should try the new secret first, then fall back to the old secret. The SDK handles this automatically if you pass an array of secrets: ```typescript TypeScript theme={null} const secrets = [process.env.WEBHOOK_SECRET_NEW, process.env.WEBHOOK_SECRET_OLD]; const isValid = secrets.some((secret) => verifySignature(req.body, req.headers["x-yoshi-signature"], req.headers["x-yoshi-timestamp"], secret) ); ``` ```python Python theme={null} secrets = [os.environ["WEBHOOK_SECRET_NEW"], os.environ["WEBHOOK_SECRET_OLD"]] is_valid = any( verify_signature( payload=request.body, signature=request.headers["x-yoshi-signature"], timestamp=request.headers["x-yoshi-timestamp"], secret=secret, ) for secret in secrets ) ```