# Technical Documentation: midday-ai/midday

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/midday-ai/midday)

### Overview (README.md)

<p align="center">
	<h1 align="center"><b>Midday</b></h1>
<p align="center">
    Your AI-Powered Business Assistant
    <br />
    <br />
    <a href="https://midday.ai">Website</a>
    ·
    <a href="https://github.com/midday-ai/midday/issues">Issues</a>
  </p>
</p>

<p align="center">
  <a href="https://go.midday.ai/K7GwMoQ">
    <img src="https://img.shields.io/badge/Supabase-3ECF8E?style=for-the-badge&logo=supabase&logoColor=white" alt="Supabase" />
  </a>
</p>

## About Midday

Midday is an all-in-one tool designed to help freelancers, contractors, consultants, and solo entrepreneurs manage their business operations more efficiently. It integrates various functions typically scattered across multiple platforms into a single, cohesive system.


## Features

**Time Tracking**: Allows for live time tracking of projects to boost productivity and collaboration, providing insightful project overviews.<br/>
**Invoicing**: An upcoming feature that will enable users to create web-based invoices, collaborate in real-time, and synchronize projects seamlessly.<br/>
**Magic Inbox**: Automatically matches incoming invoices or receipts to the correct transactions, simplifying financial tracking and organization.<br/>
**Vault**: Secure storage for important files like contracts and agreements, keeping everything in one place for easy access​.<br/>
**Seamless Export**: Facilitates easy export of financial data, packaged neatly in CSV files for accountants.<br/>
**Assistant**: Provides tailored insights into financial situations, helping users understand spending patterns, cut costs, and find documents.<br/>




## Get started

We are working on the documentation to get started with Midday for local development: https://docs.midday.ai

## App Architecture

- Monorepo
- Bun
- React
- TypeScript
- Nextjs
- Supabase
- Shadcn
- Tauri
- Expo
- TailwindCSS

### Hosting

- Supabase (database, storage, realtime, auth)
- Railway (API, Worker, Dashboard)
- Vercel (Website)
- Cloudflare (Engine, CDN/Proxy)

### Services

- Trigger.dev (background jobs)
- Resend (Transactional & Marketing)
- Github Actions (CI/CD)
- GoCardLess (Bank connection EU)
- Plaid (Bank connection in Canada and US)
- Teller (Bank connection in the US)
- OpenPanel (Events and Analytics)
- Polar (Payment processing)
- Typesense (Search)
- Gemini
- OpenAI

## Repo Activity



## License

This project is licensed under the **[AGPL-3.0](https://opensource.org/licenses/AGPL-3.0)** for non-commercial use. 

### Commercial Use

For commercial use or deployments requiring a setup fee, please contact us
for a commercial license at [engineer@midday.ai](mailto:engineer@midday.ai).

By using this software, you agree to the terms of the license.

---

### README (`docs/README.md`)

# Documentation

This directory contains technical documentation for the Midday.

## Contents

- **[weekly-insights.md](./weekly-insights.md)** - Technical documentation of the AI-powered weekly insights system including content generation, metric selection, data consistency, and advanced projections (runway dates, quarter pace, payment anomalies).
- **[inbox-matching.md](./inbox-matching.md)** - Detailed documentation of the V2 deterministic inbox matching algorithm with team calibration, hard-negative memory, and read-only verification tooling.
- **[invoice-recurring.md](./invoice-recurring.md)** - Technical documentation of the recurring invoice system including architecture, state machine, generation flow, and key design decisions.
- **[document-processing.md](./document-processing.md)** - Technical documentation of the document processing pipeline including AI classification, graceful degradation, retry functionality, and error handling.
- **[database-connection-pooling.md](./database-connection-pooling.md)** - Database connection pooling setup with Supabase Supavisor (transaction mode), multi-region read replica mapping across Railway, pool configuration, and prepared statement constraints.

## About

This documentation provides in-depth technical details about core Midday features and algorithms. It's intended for developers working on the codebase who need to understand the implementation details, data flows, and architectural decisions.

The documentation here is kept in sync with the actual implementation and provides more technical depth than the user-facing documentation in the apps/docs directory.

---

### Apps Website Src App Docs Content Apps Overview (`apps/website/src/app/docs/content/apps-overview.mdx`)

---
title: Apps and Integrations
description: Connect Midday to the tools you already use.
section: integrations
order: 0
---

Midday integrates with popular business tools to create a connected workflow. Export data to your accounting software, capture receipts from email, accept payments online, and more.

## Accounting software

Export your categorized transactions to your accounting software. Your accountant sees clean, organized data ready for import.

### Xero

Export transactions, categories, and receipts to Xero.

- Export categorized transactions when ready
- Categories map to your Xero chart of accounts
- Include attached receipts with exports

[Connect Xero →](/docs/connect-xero)

### QuickBooks

Connect to QuickBooks Online to export your transactions.

- Export categorized transactions to QuickBooks
- Map Midday categories to QuickBooks accounts
- You control when exports happen

[Connect QuickBooks →](/docs/connect-quickbooks)

### Fortnox

For businesses using Fortnox (popular in Sweden), export your financial data directly.

- Export with Swedish BAS accounting standards
- Category mapping for Fortnox accounts
- Streamlined year-end preparation

[Connect Fortnox →](/docs/connect-fortnox)

## Email integrations

Capture receipts automatically from your email without manual forwarding.

### Gmail

Connect your Gmail account to automatically capture receipts and invoices.

- AI identifies receipt emails in your inbox
- Attachments are extracted and processed
- Matching to transactions happens automatically
- Read-only access—Midday can't send emails

[Connect Gmail →](/docs/connect-gmail)

### Outlook

Connect Microsoft Outlook for automatic receipt capture.

- Works with personal and Microsoft 365 accounts
- Scans for receipts and invoices
- Processes attachments automatically
- Read-only access to your inbox

[Connect Outlook →](/docs/connect-outlook)

## Communication

### Slack

Share receipts and documents directly from Slack to your Midday inbox.

- Connect your Slack workspace
- Share files to a designated channel
- Documents appear in your Midday inbox
- Great for team receipt sharing

[Connect Slack →](/docs/connect-slack)

## Payments

### Stripe

Accept online payments on your invoices through Stripe.

- Customers pay with credit card
- Payments process securely through Stripe
- Invoices automatically mark as paid
- Funds deposit to your bank account

Standard Stripe processing fees apply. Midday doesn't charge additional payment fees.

[Set up online payments →](/docs/accept-online-payments)

## Developer tools

### API

Access your Midday data programmatically through our REST API.

- Read transactions, invoices, customers
- Build custom integrations
- Automate workflows
- Secure API key authentication

[API documentation →](/docs/api-reference)

### Build OAuth Apps

Create applications that integrate with Midday for your users.

- Build browser extensions, mobile apps, or web services
- Let users connect their Midday accounts via OAuth 2.0
- Access financial data with granular permissions
- Get verified and listed in the app directory

[Build an OAuth app →](/docs/build-oauth-app)

### MCP (Model Context Protocol)

Run your business from AI tools like Claude Desktop, Cursor, and ChatGPT.

- 80+ tools for transactions, invoices, time tracking, exports, reports, and more
- Create and send invoices, export to your accountant, track time
- Push transactions to Xero, QuickBooks, or Fortnox
- Works with any MCP-compatible client

[MCP documentation →](/docs/assistant-mcp)

## Managing integrations

### View connected apps

1. Go to [Apps](https://app.midday.ai/apps)
2. See all connected integrations
3. Check connection status

### Disconnect an app

1. Go to [Apps](https://app.midday.ai/apps)
2. Find the integration
3. Click **Disconnect**

Disconnecting removes the connection but doesn't delete historical data.

### Reconnect an app

Some integrations need periodic re-authentication:

1. You'll see a notification when reconnection is needed
2. Go to [Apps](https://app.midday.ai/apps)
3. Click **Reconnect** and re-authorize

## Data and privacy

- **Read-only where possible**: Email integrations only read—never send
- **Minimal permissions**: We request only what's needed
- **Secure connections**: All integrations use OAuth or encrypted credentials
- **You control access**: Disconnect anytime

## Coming soon

We're always adding new integrations. Have a request? Let us know at [support@midday.ai](mailto:support@midday.ai).

---

### Bank Account Reconnect (`docs/bank-account-reconnect.md`)

# Bank Account Reconnect Logic

## Overview

When a user reconnects a bank connection, the provider may assign new account IDs. This document describes how we safely match existing database accounts to the new API accounts and update the `account_id` field without data corruption.

## Reconnect Flow Diagram

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Provider-Specific Flow Variations

### GoCardless / EnableBanking (EU/UK)
```
User clicks Reconnect
    → Opens provider's auth page in new window
    → User authenticates at bank
    → Provider redirects to /api/gocardless/reconnect or /api/enablebanking/session
    → API route updates DB + redirects to frontend with ?step=reconnect&id=xxx
    → useReconnect hook detects URL params → triggers reconnect-connection job
    → URL params cleared to prevent re-triggering
```

### Teller (US)
```
User clicks Reconnect
    → Opens Teller Connect modal
    → User authenticates at bank
    → Teller Connect calls onComplete("reconnect")
    → BankConnection routes to triggerReconnect()
    → useReconnect hook triggers reconnect-connection job
```

### Plaid (US)
```
User clicks Reconnect  
    → Opens Plaid Link in "update mode"
    → User authenticates at bank
    → Plaid preserves account IDs (no remapping needed)
    → Plaid Link calls onComplete("sync")
    → BankConnection routes to triggerManualSync()
    → useReconnect hook triggers manual sync job
```

## The Problem

Bank providers (GoCardless, Teller, EnableBanking) may change account IDs after a reconnect. We need to match existing database accounts to the new API accounts to maintain transaction history continuity.

**The original bug**: If multiple accounts shared the same identifier (e.g., two accounts ending in "1234"), the update query would affect multiple rows, causing both accounts to point to the same `account_id`. One account would effectively stop syncing.

## Solution: Unified Matching Algorithm

All providers use a shared `matchAndUpdateAccountIds()` function that safely matches and updates accounts.

### Matching Priority

1. **resource_id (account_reference)** - Must match exactly
2. **type** - If DB has type, must match API type
3. **currency** - If DB has currency, must match API currency
4. **name** - Tiebreaker if multiple candidates remain

### Safety Mechanisms

- Each DB account can only be matched once (tracked via `matchedDbIds` Set)
- Updates use unique DB `id`, not potentially duplicated `account_reference`
- Unmatched accounts are logged for debugging

## Provider-Specific Behavior

### GoCardless (EU/UK)

- **resource_id**: `account.resourceId` (stable identifier)
- **Additional data**: `iban`, `bic`, `available_balance` (from `interimAvailable` balance type)
- **Note**: `credit_limit` is NOT available - only `creditLimitIncluded` boolean exists
- **Reconnect flow**: User re-authenticates → job triggered → accounts remapped

### Teller (US)

- **resource_id**: `last_four` (last 4 digits of account number)
- **Additional data**: `subtype`, `routing_number`, `wire_routing_number`, `account_number`, `sort_code`, `available_balance`
- **Account details**: Available instantly for most institutions (`verify.instant`), some require microdeposit verification
- **Balances**: `ledger` (total) and `available` (net pending) - no `credit_limit`
- **Note**: `last_four` can be shared between accounts (e.g., checking and savings with same last 4 digits)
- **Reconnect flow**: User re-authenticates → job triggered → accounts remapped

### EnableBanking (EU)

- **resource_id**: `identification_hash` (cryptographic hash, very stable)
- **Additional data**: `iban`, `bic`, `subtype`, `available_balance`
- **Note**: `credit_limit` is NOT in EnableBanking's documented API (may return from some banks but not guaranteed)
- **Reconnect flow**: API route updates connection → triggers reconnect job

### Plaid (US)

- **resource_id**: `persistent_account_id || mask`
- **Additional data**: `subtype`, `available_balance`, `credit_limit`
- **Note**: Plaid uses "update mode" for reconnects which preserves account IDs. No remapping needed.

## Data Fields

### Account Identification Fields

| Field | Description | Providers |
|-------|-------------|-----------|
| `account_reference` | Stable identifier for matching | All |
| `iban` | International Bank Account Number (encrypted) | GoCardless, EnableBanking |
| `bic` | Bank Identifier Code (SWIFT) | GoCardless, EnableBanking |
| `subtype` | Granular account type | Teller, Plaid, EnableBanking |

### US Bank Details (Teller, Plaid)

| Field | Description | Notes |
|-------|-------------|-------|
| `routing_number` | ACH routing number | Public info |
| `wire_routing_number` | Wire routing number | May differ from ACH |
| `account_number` | Full account number (encrypted) | Sensitive |
| `sort_code` | UK BACS sort code | UK accounts via Teller |

### Balance Fields

| Field | Description | Providers | Sync Frequency |
|-------|-------------|-----------|----------------|
| `balance` | Current/ledger balance (depository) or amount owed (credit) | All | Every sync |
| `available_balance` | Spendable funds (depository) or available credit (cards) | All | Every sync |
| `credit_limit` | Maximum credit line (cards only) | Plaid only* | Every sync |

*EnableBanking does NOT officially support `credit_limit` - our code handles it if returned but it's not guaranteed

**Balance vs Available Balance:**
- **Depository accounts**: `balance` is ledger balance, `available_balance` is spendable (minus holds/pending)
- **Credit accounts**: `balance` is amount owed, `available_balance` is remaining credit

### Encryption

Sensitive fields are encrypted at rest using `@midday/encryption`:
- `iban` - Encrypted
- `account_number` - Encrypted

Non-sensitive fields stored in plain text:
- `routing_number`, `wire_routing_number`, `bic`, `sort_code`

To retrieve decrypted values, use the `bankAccounts.getDetails` tRPC procedure.

### Subtype Values

- **Teller**: `checking`, `savings`, `money_market`, `certificate_of_deposit`, `treasury`, `sweep`, `credit_card`
- **Plaid**: `checking`, `savings`, `credit_card`, `mortgage`, `student`, `auto`, etc.
- **EnableBanking**: `cacc` (current), `card`, `svgs` (savings), `loan`, `cash`

## Sync Flow

### Regular Balance Sync (`syncAccount`)

The `syncAccount` job runs periodically and updates:
- `balance` - Primary balance from `/accounts/balance` endpoint
- `available_balance` - From same endpoint (added to response)
- `credit_limit` - From same endpoint (added to response)

### Static Field Backfill (`backfillAccountStaticFields`)

For existing accounts that were created before these fields existed, the sync job includes a temporary backfill that populates:
- `iban`, `subtype`, `bic` (EU/UK accounts)
- `routing_number`, `wire_routing_number`, `account_number`, `sort_code` (US/UK accounts)

This backfill only runs for accounts missing ALL static fields and can be removed after most accounts have been updated.

## Troubleshooting

### "No matching DB account found" Warning

This means an API account couldn't be matched to any database account. Possible causes:
1. User added a new account at the bank
2. Account was deleted from the database
3. `account_reference` changed (shouldn't happen)

### Accounts Not Syncing After Reconnect

1. Check `bank_accounts.error_retries` - may have exceeded limit
2. Verify `bank_accounts.account_id` matches the new API account ID
3. Check reconnect job logs for matching results

## Key Files

| File | Purpose |
|------|---------|
| `apps/dashboard/src/hooks/use-reconnect.ts` | Hook encapsulating all reconnect/sync logic |
| `apps/dashboard/src/components/bank-connections.tsx` | Frontend component using useReconnect hook |
| `apps/dashboard/src/components/reconnect-provider.tsx` | Provider-specific reconnect UI (Teller Connect, Plaid Link, etc.) |
| `apps/dashboard/src/app/api/enablebanking/session/route.ts` | EnableBanking OAuth callback - updates DB, redirects to frontend |
| `apps/dashboard/src/app/api/gocardless/reconnect/route.ts` | GoCardless OAuth callback - updates DB, redirects to frontend |
| `apps/dashboard/src/actions/transactions/reconnect-connection-action.ts` | Server action that triggers the reconnect job |
| `packages/jobs/src/tasks/reconnect/connection.ts` | Main reconnect job - fetches accounts, matches, updates IDs |
| `packages/jobs/src/utils/account-matching.ts` | Shared `matchAndUpdateAccountIds()` function |

## useReconnect Hook

All reconnect and sync logic is encapsulated in the `useReconnect` hook, providing a clean interface for the `BankConnection` component.

### Usage

```typescript
const { isSyncing, triggerReconnect, triggerManualSync } = useReconnect({
  connectionId: connection.id,
  provider: connection.provider,
});
```

### What the Hook Handles

1. **URL param detection** - Detects `step=reconnect&id=xxx` for OAuth providers
2. **URL param cleanup** - Clears params after triggering to prevent re-triggering
3. **Job triggering** - Via `reconnectConnectionAction` or `manualSyncTransactionsAction`
4. **Status tracking** - Uses `useSyncStatus` hook to track job progress
5. **Toast notifications** - Shows syncing, success, and error toasts
6. **Query invalidation** - Refreshes bank connections, accounts, and transactions on completion

### Architecture

```mermaid
flowchart TD
    subgraph hooks [Hooks Layer]
        useReconnect[useReconnect]
        useSyncStatus[useSyncStatus]
    end
    
    subgraph components [Components]
        BankConnection[BankConnection]
        ReconnectProvider[ReconnectProvider]
    end
    
    subgraph external [External]
        URLParams[URL Params via nuqs]
        TriggerJob[Trigger.dev Job]
        Toast[Toast Notifications]
        QueryClient[React Query]
    end
    
    BankConnection --> useReconnect
    ReconnectProvider --> BankConnection
    
    useReconnect --> useSyncStatus
    useReconnect --> URLParams
    useReconnect --> TriggerJob
    useReconnect --> Toast
    useReconnect --> QueryClient
```

## ReconnectProvider Callback

The `ReconnectProvider` component uses a unified `onComplete` callback:

```typescript
type OnCompleteType = "reconnect" | "sync";

// In ReconnectProvider
onComplete: (type: OnCompleteType) => void;

// In BankConnection
const handleComplete = (type: "reconnect" | "sync") => {
  if (type === "reconnect") {
    triggerReconnect();  // Account IDs may have changed
  } else {
    triggerManualSync(); // Account IDs preserved (Plaid)
  }
};
```

- **"reconnect"**: Provider may have changed account IDs (Teller) - triggers reconnect job
- **"sync"**: Provider preserves account IDs (Plaid update mode) - triggers manual sync

## Important Implementation Notes

### Single Job Trigger
The reconnect job should only be triggered **once** per reconnect flow. The `useReconnect` hook handles this:

```typescript
// In useReconnect hook - only triggers when params.id matches this connection
useEffect(() => {
  if (params.step === "reconnect" && params.id === connectionId) {
    reconnectConnection.execute({
      connectionId,
      provider: provider as Provider,
    });

    // Clear URL params to prevent re-triggering on page refresh
    setParams({ step: null, id: null });
  }
}, [params.step, params.id, connectionId, provider, setParams]);
```

API routes (EnableBanking, GoCardless) do **not** trigger the job - they only update the database and redirect. This ensures:
1. No duplicate job triggers (race conditions)
2. Frontend can track job progress via `runId`/`accessToken`
3. UI shows proper sync status feedback
4. URL params are cleared immediately to prevent re-triggering on page refresh

---

### Bot Messaging Integration (`docs/bot-messaging-integration.md`)

# Bot & Messaging Integration (Telegram, WhatsApp, Slack)

## Overview

The bot integration extends Midday into external messaging platforms — Telegram, WhatsApp, and Slack — so users can chat with the Midday AI assistant, submit receipts/invoices, and receive proactive notifications without opening the dashboard.

The system is built around three pillars:

1. **Conversational AI** — full assistant access from any connected platform.
2. **Inbox uploads** — send photos of receipts or PDFs directly in chat; they are extracted, stored, and matched automatically.
3. **Activity notifications** — batched and immediate notifications for transactions, invoices, and receipt matches, delivered to the platform the user is active on.

## Architecture

```mermaid
graph TB
    subgraph "External Platforms"
        TG[Telegram Bot API]
        WA[WhatsApp Cloud API]
        SL[Slack Events API]
    end

    subgraph "apps/api"
        WH_TG[POST /webhook/telegram]
        WH_WA[GET+POST /webhook/whatsapp]
        WH_SL[POST /apps/slack/webhook]
        OAUTH[GET /apps/slack/oauth-callback]
        RT[Bot Runtime]
        CI[Conversation Identity]
        LK[Linking]
        TS[Thread State]
    end

    subgraph "packages/bot"
        CHAT[Chat Engine]
        AN[Activity Notifications]
        WN[WhatsApp Notifications]
        TN[Telegram Notifications]
        IU[Inbox Upload]
    end

    subgraph "packages/db"
        PI[platform_identities]
        PLT[platform_link_tokens]
        PNB[provider_notification_batches]
    end

    subgraph "Infrastructure"
        REDIS[Redis - Thread State]
        VAULT[Supabase Vault]
    end

    TG --> WH_TG
    WA --> WH_WA
    SL --> WH_SL

    WH_TG --> RT
    WH_WA --> RT
    WH_SL --> RT
    OAUTH --> PI

    RT --> CI
    RT --> LK
    RT --> TS
    RT --> CHAT
    RT --> IU

    CHAT --> REDIS
    AN --> WN
    AN --> TN
    AN --> SL
    IU --> VAULT

    RT --> PI
    LK --> PLT
    AN --> PNB
    AN --> PI
```

## Package Structure

### `packages/bot/` — Shared bot logic

| File | Purpose |
|------|---------|
| `src/instance.ts` | Creates the `Chat` singleton with WhatsApp, Telegram, and Slack adapters, backed by Redis state. Debounce concurrency at 1500 ms. |
| `src/platform-rules.ts` | Exports `BotPlatform` type and `getPlatformInstructions()` which appends platform-specific formatting hints to the system prompt. |
| `src/activity-notifications.ts` | Notification orchestration: batching, flushing, and delivery across all platforms. Contains the `sendToProviders` entry point. |
| `src/whatsapp-notifications.ts` | WhatsApp Cloud API v21.0 calls: text messages, interactive buttons (match confirm/decline), and Meta Message Templates. |
| `src/telegram-notifications.ts` | Telegram Bot API calls for sending text notifications. |
| `src/inbox-upload.ts` | Processes file attachments: uploads to Supabase Vault, creates inbox entries, triggers document extraction and matching jobs. |
| `src/index.ts` | Barrel re-export of all modules. |

### `apps/api/src/bot/` — API-side runtime

| File | Purpose |
|------|---------|
| `runtime.ts` | `registerMiddayBotRuntime()` — wires chat events to message handlers. Resolves conversations, hydrates identities, streams the AI assistant, handles attachments. |
| `conversation-identity.ts` | Types and helpers for resolving a connected conversation's identity and extracting notification context from metadata. |
| `linking.ts` | `extractConnectionToken()` — parses `mb_*` link codes from Telegram `/start` payloads, WhatsApp/Slack connection messages. |
| `thread-state.ts` | Redis-backed `BotThreadState` and `canReuseCachedThreadState()` to avoid re-resolving identity on every message from the same user. |

---

## Database Schema

### `platform_identities`

Links an external messaging account to a Midday user and team.

| Column | Type | Description |
|--------|------|-------------|
| `id` | uuid | Primary key |
| `provider` | `platform_provider` enum | `slack`, `telegram`, or `whatsapp` |
| `team_id` | uuid FK → teams | The Midday team |
| `user_id` | uuid FK → users | The Midday user |
| `external_user_id` | text | Platform-specific user ID (phone number, Telegram user ID, Slack user ID) |
| `external_team_id` | text | Platform-specific workspace ID (Slack team ID; empty string for Telegram/WhatsApp) |
| `external_channel_id` | text | Outbound channel (e.g. Telegram chat ID for sending notifications) |
| `metadata` | jsonb | Mutable metadata: `lastSeenAt`, `lastNotificationContext`, `lastNotificationSentAt`, `displayName` |

**Unique constraint:** `(provider, external_team_id, external_user_id)` — one identity per external account.

### `platform_link_tokens`

Short-lived, one-time codes for securely connecting a messaging account to a Midday user.

| Column | Type | Description |
|--------|------|-------------|
| `id` | uuid | Primary key |
| `code` | text | `mb_` + nanoid(18), unique |
| `provider` | `platform_provider` enum | Which platform this token is for |
| `team_id` | uuid FK → teams | The team being linked |
| `user_id` | uuid FK → users | The user being linked |
| `expires_at` | timestamptz | Default 24h from creation |
| `used_at` | timestamptz | Set when consumed; null until used |

### `provider_notification_batches`

Coalesces multiple events of the same type into a single notification per identity.

| Column | Type | Description |
|--------|------|-------------|
| `id` | uuid | Primary key |
| `batch_key` | text | `{identity_id}:{event_family}`, unique |
| `platform_identity_id` | uuid FK → platform_identities | Recipient |
| `team_id` | uuid FK → teams | |
| `user_id` | uuid FK → users | |
| `provider` | `platform_provider` enum | |
| `event_family` | text | `transaction`, `invoice_paid`, `invoice_overdue`, `recurring_invoice_upcoming` |
| `payload` | jsonb | `{ entries: [...] }` — events accumulated during the batch window |
| `notification_context` | jsonb | Context metadata attached when sent |
| `window_ends_at` | timestamptz | When the batch should be flushed |
| `sent_at` | timestamptz | Set when delivered; null while pending |

### Row-Level Security

All three tables have RLS enabled. Policies restrict all operations (SELECT, INSERT, UPDATE, DELETE) to authenticated users whose `team_id` is in `private.get_teams_for_authenticated_user()`. Server-side workers use a service-role DB client that bypasses RLS.

---

## Flows

### 1. Account Linking

Users connect their messaging account from the Midday dashboard. The flow is the same across platforms, with platform-specific deep links.

```mermaid
sequenceDiagram
    participant User
    participant Dashboard
    participant API (tRPC)
    participant DB
    participant Platform

    User->>Dashboard: Click "Connect Telegram/WhatsApp"
    Dashboard->>API (tRPC): apps.createPlatformLinkToken({ provider })
    API (tRPC)->>DB: INSERT platform_link_tokens (code=mb_*, expires 24h)
    DB-->>API (tRPC): { code }
    API (tRPC)-->>Dashboard: { code }

    alt Telegram
        Dashboard->>User: Opens t.me/{bot}?start={code}
        User->>Platform: /start mb_abc123...
    else WhatsApp
        Dashboard->>User: Opens wa.me/{number}?text=Connect to Midday: {code}
        User->>Platform: Sends prefilled message
    else Slack
        Dashboard->>User: Shows code to DM the bot
        User->>Platform: DMs "Connect to Midday: {code}"
    end

    Platform->>API (webhook): Delivers message
    API (webhook)->>Bot Runtime: handleIncomingMessage()
    Bot Runtime->>Bot Runtime: extractConnectionToken() → mb_abc123...
    Bot Runtime->>DB: consumePlatformLinkToken({ code })
    DB-->>Bot Runtime: { teamId, userId }
    Bot Runtime->>DB: createOrUpdatePlatformIdentity()
    Bot Runtime->>DB: addTelegramConnection() / addWhatsAppConnection()
    Bot Runtime->>Platform: "Connected to {team}. You can now..."
```

**Slack workspace install** follows a separate OAuth flow:

1. Dashboard redirects to Slack OAuth (`/apps/slack/install-url`).
2. User authorizes in Slack.
3. Callback (`/apps/slack/oauth-callback`) exchanges the code, persists the app + Slack adapter installation, creates a `platform_identity`, and sends a welcome message.

### 2. Message Handling

Every incoming message from a connected platform follows the same pipeline.

```mermaid
sequenceDiagram
    participant Platform
    participant Webhook
    participant Runtime
    participant Redis
    participant DB
    participant AI Assistant

    Platform->>Webhook: Incoming message
    Webhook->>Runtime: handleIncomingMessage(thread, message)

    Runtime->>Runtime: Ignore if author is bot/self

    alt Cached thread state in Redis
        Runtime->>Redis: Read thread.state
        Redis-->>Runtime: { teamId, actingUserId, platform, externalUserId }
        Runtime->>Runtime: canReuseCachedThreadState() → true
    else No cached state
        Runtime->>DB: getPlatformIdentity({ provider, externalUserId })
        DB-->>Runtime: identity { teamId, userId }
        Runtime->>Redis: Save thread state
    end

    Runtime->>Runtime: hydrateResolvedConversationIdentity()
    Runtime->>DB: getPlatformIdentity() + hasTeamAccess()
    Note over Runtime: Slack channels also verify app↔team mapping

    alt Has attachments (images, PDFs)
        Runtime->>Runtime: processInboxUpload() → Vault + inbox + jobs
    end

    Runtime->>DB: getUserById(actingUserId)
    Runtime->>Runtime: Build system prompt + platform instructions
    Runtime->>AI Assistant: streamMiddayAssistant({ systemPrompt, history })
    AI Assistant-->>Runtime: { fullStream }
    Runtime->>Platform: Post response

    Runtime->>DB: Clear lastNotificationContext (if present)
```

### 3. Activity Notifications

Notifications are categorized into **immediate** (match) and **batched** (everything else).

```mermaid
graph TD
    subgraph "Event Sources"
        TX[Transaction sync]
        INV[Invoice status change]
        MATCH[Inbox ↔ Transaction match]
        REC[Recurring invoice upcoming]
    end

    subgraph "sendToProviders()"
        ROUTE{type == match?}
        IMMEDIATE[sendImmediateMatchNotifications]
        BATCH[queueTeamWideNotification]
    end

    subgraph "Batch Queue"
        Q[provider_notification_batches]
        FLUSH[flushDueActivityNotificationBatches]
        SEND[sendSummaryToIdentity]
    end

    TX --> ROUTE
    INV --> ROUTE
    MATCH --> ROUTE
    REC --> ROUTE

    ROUTE -->|Yes| IMMEDIATE
    ROUTE -->|No| BATCH

    BATCH --> Q
    Q -->|window_ends_at reached| FLUSH
    FLUSH --> SEND
```

#### Batch Windows

| Event Type | Window |
|------------|--------|
| `transaction` | 10 minutes |
| `invoice_paid` | 10 minutes |
| `invoice_overdue` | 30 minutes |
| `recurring_invoice_upcoming` | 60 minutes |

Events of the same type for the same identity are merged into a single batch (`payload.entries[]`). When the window expires, the worker flushes the batch, builds a human-readable summary, and delivers it.

#### Notification Gating

Before sending, the system checks:

1. **App installed** — `getAppByAppId()` for the provider and team.
2. **App setting enabled** — per-app toggle (`transactions`, `invoices`, `matches`).
3. **User notification preference** — `shouldSendNotification()` checks the user's `in_app` notification settings.

#### Platform Delivery

| Platform | Batched notifications | Match notifications |
|----------|----------------------|---------------------|
| **Slack** | DM via `conversations.open` + `chat.postMessage` | Thread reply in source channel with Block Kit |
| **Telegram** | Text message to `external_channel_id` | Text message to source chat |
| **WhatsApp (in-session)** | Free-form text message | Interactive buttons (confirm/decline) |
| **WhatsApp (out-of-session)** | Meta Message Template | Meta Message Template |

### 4. WhatsApp 24-Hour Session Window

Meta restricts free-form WhatsApp messages to users who have interacted within the last 24 hours. The system handles this transparently:

```mermaid
flowchart TD
    A[Notification ready to send] --> B{platform == whatsapp?}
    B -->|No| C[Send normally]
    B -->|Yes| D{isWithinWhatsAppSessionWindow?}
    D -->|Yes: lastSeenAt < 24h| E[Send free-form text / interactive buttons]
    D -->|No: outside window| F{Template available?}
    F -->|Yes| G[sendWhatsAppTemplateNotification]
    F -->|No| H[Skip — log and mark sent]
```

`lastSeenAt` is updated on every incoming message from a WhatsApp user. Templates are pre-approved in Meta Business Manager and mapped via `WHATSAPP_TEMPLATE_NAMES`:

| Internal Event | Template Name |
|---------------|---------------|
| `transaction` | `midday_new_transactions` |
| `invoice_paid` | `midday_invoice_paid` |
| `invoice_overdue` | `midday_invoice_overdue` |
| `recurring_invoice_upcoming` | `midday_recurring_upcoming` |
| `match` | `midday_receipt_matched` |

Templates are always sent with `language.code = "en"`.

### 5. Inbox Upload from Chat

Users can send images or PDFs directly in any chat platform. The bot processes them into the Midday inbox.

```mermaid
sequenceDiagram
    participant User
    participant Platform
    participant Runtime
    participant Vault
    participant DB
    participant Jobs

    User->>Platform: Send photo/PDF
    Platform->>Runtime: Message with attachment
    Runtime->>Runtime: isSupportedInboxUploadMediaType()
    Runtime->>Vault: Upload to Supabase Vault (vault/{teamId}/inbox/{file})
    Runtime->>DB: createInbox({ source: platform, platformMeta })
    Runtime->>DB: Document extraction (DocumentClient)
    Runtime->>Jobs: Trigger process-document
    Runtime->>Jobs: Trigger batch-process-matching
    Runtime-->>User: "receipt.pdf • 42.50 USD was added to your inbox."
```

Supported types: `image/*`, `application/pdf`, `application/octet-stream`.

### 6. Notification Context & Conversational Follow-Up

When a notification is sent, the system stores a `NotificationContext` on the platform identity:

```typescript
{
  eventType: "transaction",
  teamId: "...",
  userId: "...",
  entityType: "transaction",
  entityIds: ["tx_1", "tx_2"],
  summary: "3 new transactions. Reply \"show me them\"...",
  sourcePlatform: "whatsapp",
  suggestedPrompts: ["Show me them", "Which ones need receipts?"],
  sentAt: "2026-04-02T10:00:00Z"
}
```

When the user replies, this context is injected into the AI assistant's system prompt via `formatNotificationContextForPrompt()`, allowing the assistant to understand what the user is responding to. The context is cleared after the first reply.

---

## Security Model

### Identity Resolution

Every message goes through a multi-step authorization chain:

1. **Thread state cache** — Redis stores `{ teamId, actingUserId, platform, externalUserId }`. Reused only if the same external user on the same platform continues the conversation (`canReuseCachedThreadState`).
2. **Platform identity lookup** — `getPlatformIdentity()` resolves the external user to a Midday user/team.
3. **Identity validation** — `requireResolvedConversationIdentity()` confirms `identity.teamId == resolved.teamId` and `identity.userId == resolved.actingUserId`.
4. **Team access check** — `hasTeamAccess()` verifies the user still has access to the team (handles removed members).
5. **Slack channel validation** — for non-DM Slack messages, `getAppBySlackTeamId()` confirms the channel's Slack workspace matches the resolved team's installed app.

If any check fails, the thread state is cleared and the user is told to reconnect.

### Link Token Security

- Codes are `mb_` + `nanoid(18)` — high entropy, unguessable.
- Tokens expire after 24 hours.
- Tokens are single-use (`used_at` set on consumption).
- The `consumePlatformLinkToken` query atomically marks the token as used.

### Database-Level Isolation

- All three tables have RLS policies scoped to `private.get_teams_for_authenticated_user()`.
- Unique constraints prevent duplicate identities: `(provider, external_team_id, external_user_id)`.
- Custom error types (`PlatformIdentityAlreadyLinkedToAnotherTeamError`, etc.) handle conflict cases at the application layer.

### Bot User Scopes

Connected bot users operate with `apis.all` scope, granting full Midday assistant capabilities. This is intentional — the bot should be able to answer any question the user could ask in the dashboard.

---

## Webhook Endpoints

| Method | Path | Platform | Purpose |
|--------|------|----------|---------|
| POST | `/webhook/telegram` | Telegram | Bot API webhook updates |
| GET | `/webhook/whatsapp` | WhatsApp | Meta webhook verification (subscribe) |
| POST | `/webhook/whatsapp` | WhatsApp | Meta webhook events |
| POST | `/apps/slack/webhook` | Slack | Slack Events API |
| GET | `/apps/slack/oauth-callback` | Slack | OAuth code exchange |
| GET | `/apps/slack/install-url` | Slack | Generate OAuth install URL |

---

## Environment Variables

### API (`apps/api`)

| Variable | Required | Description |
|----------|----------|-------------|
| `SLACK_CLIENT_ID` | Yes | Slack app client ID |
| `SLACK_CLIENT_SECRET` | Yes | Slack app client secret |
| `SLACK_OAUTH_REDIRECT_URL` | Yes | OAuth callback URL |
| `SLACK_STATE_SECRET` | Yes | State parameter signing secret |
| `SLACK_SIGNING_SECRET` | Yes | Webhook signature verification |
| `SLACK_ENCRYPTION_KEY` | Recommended | Encrypt stored Slack installations |
| `TELEGRAM_BOT_TOKEN` | Yes | BotFather token |
| `TELEGRAM_WEBHOOK_SECRET_TOKEN` | Yes | Webhook verification secret |
| `TELEGRAM_BOT_USERNAME` | Yes | Bot username (without @) |
| `TELEGRAM_API_BASE_URL` | No | Custom Telegram API gateway |
| `WHATSAPP_PHONE_NUMBER_ID` | Yes | Meta phone number ID |
| `WHATSAPP_BUSINESS_ACCOUNT_ID` | Yes | Meta Business Account ID |
| `WHATSAPP_ACCESS_TOKEN` | Yes | Meta permanent access token |
| `WHATSAPP_VERIFY_TOKEN` | Yes | Webhook verification token |
| `WHATSAPP_APP_SECRET` | Yes | Webhook signature verification |
| `REDIS_URL` | Yes | Redis for chat thread state |

### Dashboard (`apps/dashboard`)

| Variable | Required | Description |
|----------|----------|-------------|
| `NEXT_PUBLIC_TELEGRAM_BOT_USERNAME` | Yes | Used in deep links (`t.me/{bot}?start=...`) |
| `NEXT_PUBLIC_WHATSAPP_NUMBER` | Yes | Used in deep links (`wa.me/{number}?text=...`) |

---

## Test Coverage

Tests live alongside the code they verify:

| Test File | Covers |
|-----------|--------|
| `apps/api/src/__tests__/routers/bot-runtime.test.ts` | Link-code consumption for all three platforms, verifying assistant is not called during linking |
| `apps/api/src/__tests__/routers/bot-linking.test.ts` | `extractConnectionToken` for Telegram `/start`, WhatsApp, and Slack message formats |
| `apps/api/src/__tests__/routers/bot-conversation-identity.test.ts` | Identity hydration, notification context extraction, mismatch rejection |
| `apps/api/src/__tests__/routers/bot-thread-state.test.ts` | Thread state reuse logic for same/different senders |
| `apps/api/src/__tests__/routers/slack-oauth-callback.test.ts` | Slack OAuth flow ordering (DB before adapter), failure handling |
| `packages/bot/src/activity-notifications.test.ts` | WhatsApp session window, batch template builders, match template builder |

---

## Key Types

```typescript
type BotPlatform = "dashboard" | "whatsapp" | "telegram" | "slack";

type BotThreadState = {
  teamId?: string;
  actingUserId?: string;
  platform?: BotPlatform;
  externalUserId?: string;
};

type ConnectedResolvedConversation = {
  connected: true;
  teamId: string;
  actingUserId: string;
  identityId?: string;
  notificationContext?: Record<string, unknown> | null;
};

type ProviderNotificationType =
  | "transaction"
  | "match"
  | "invoice_paid"
  | "invoice_overdue"
  | "recurring_invoice_upcoming";

type NotificationContext = {
  eventType: ProviderNotificationType;
  teamId: string;
  userId: string;
  entityType: string;
  entityIds: string[];
  summary: string;
  sourcePlatform: "slack" | "telegram" | "whatsapp";
  sourceMessageId?: string;
  suggestedPrompts: string[];
  sentAt: string;
};
```

---

### Credit Card Transaction Handling (`docs/credit-card-transaction-handling.md`)

# Credit Card Transaction Handling

This document explains how Midday handles credit card transactions from different banking providers to ensure correct categorization and amount signs.

## The Problem

Credit card transactions have unique semantics compared to regular bank accounts:

- **Purchase**: Money goes OUT (you're spending)
- **Payment to card**: Money goes IN (you're paying off balance)
- **Refund**: Money goes IN (merchant refunded you)
- **Cashback/Rewards**: Money goes IN (card benefit)

The challenge is that different providers report these transactions with different sign conventions, and **money coming IN to a credit card is NOT income** - it could be a payment, refund, or reward.

## Provider Sign Conventions

### Teller (US)

| Account Type | Transaction | Teller Reports | After Transform |
|--------------|-------------|----------------|-----------------|
| Credit | Purchase $100 | `+100` | `-100` (expense) |
| Credit | Payment $200 | `-200` | `+200` (payment) |
| Depository | Deposit $500 | `+500` | `+500` (income) |
| Depository | Withdrawal $50 | `-50` | `-50` (expense) |

**Transform**: Credit accounts have sign inverted (`amount * -1`), depository accounts use raw value.

### Plaid (US)

| Transaction | Plaid Reports | After Transform |
|-------------|---------------|-----------------|
| Money OUT | `+amount` | `-amount` |
| Money IN | `-amount` | `+amount` |

**Transform**: ALL amounts are inverted (`amount * -1`). Plaid convention is "positive = money out".

### Enable Banking (EU - PSD2)

| Transaction | API Reports | Result |
|-------------|-------------|--------|
| Money IN | `credit_debit_indicator: "CRDT"` + positive amount | Positive |
| Money OUT | `credit_debit_indicator: "DBIT"` + positive amount | Negative |

**Transform**: Uses `credit_debit_indicator` field. No account-type-specific handling needed.

### GoCardless (EU - PSD2)

| Transaction | API Reports | Result |
|-------------|-------------|--------|
| Money IN | Positive amount | Positive |
| Money OUT | Negative amount | Negative |

**Transform**: Uses signed amounts directly. No transformation needed.

## Category Logic

After amount transformation, all providers use consistent category logic:

```
if (amount > 0) {
  if (accountType === "credit") {
    // Money IN to credit card - NOT automatically income
    if (looksLikePayment(transaction)) {
      return "credit-card-payment";
    }
    // Might be refund, cashback, etc. - let user categorize
    return null;
  }
  return "income";  // Depository account
}
return null;  // Negative amounts (expenses) - let user categorize
```

### How "looksLikePayment" Works Per Provider

| Provider | Payment Indicators |
|----------|-------------------|
| **Teller** | `transaction.type` is `payment`, `bill_payment`, `digital_payment`, `ach`, or `transfer` |
| **Plaid** | `personal_finance_category.primary` is `LOAN_PAYMENTS` or `TRANSFER_IN`, or `transaction_code` is `"bill payment"` |
| **Enable Banking** | `bank_transaction_code.description` is `"Transfer"` or `"Payment"` |
| **GoCardless** | `proprietaryBankTransactionCode` is `"Transfer"` or `"Payment"` |

### Special Cases

1. **Cashback/Rewards (Teller)**: If `transaction.details.category === "income"`, categorized as `"income"`
2. **Plaid INCOME category**: If `personal_finance_category.primary === "INCOME"`, categorized as `"income"`
3. **Refunds**: Return `null` so user can manually categorize (could be miscategorized as income otherwise)

## Account Type Mapping

### Enable Banking & GoCardless (ISO 20022 Cash Account Types)

| `cashAccountType` / `cash_account_type` | Midday Type |
|----------------------------------------|-------------|
| `CARD` | `credit` |
| `LOAN` | `other_asset` |
| `CACC` (Current Account) | `depository` |
| `SVGS` (Savings) | `depository` |
| `TRAN` (Transaction) | `depository` |
| `CASH` | `depository` |
| Others | `depository` |

### Teller & Plaid

Account type is explicitly provided by the API (e.g., `credit`, `depository`).

## Test Coverage

Each provider has tests covering:

1. ✅ Depository income transaction → `"income"`
2. ✅ Depository expense transaction → `null`
3. ✅ Credit card purchase → `null` (negative amount)
4. ✅ Credit card payment → `"credit-card-payment"`
5. ✅ Credit card refund → `null` (user categorizes)
6. ✅ Account type mapping (for EU providers)

## Why This Matters

**Incorrect handling could cause:**

- Credit card payments appearing as income in reports
- Inflated income figures (every card payment counted as income)
- Incorrect profit/loss calculations
- Tax reporting issues

**The fix ensures:**

- Credit card payments are correctly categorized as `"credit-card-payment"` (a transfer, not income)
- Refunds are not auto-categorized (user decides)
- Only actual income on depository accounts is marked as `"income"`

## Files Modified

- `apps/engine/src/providers/teller/transform.ts`
- `apps/engine/src/providers/plaid/transform.ts`
- `apps/engine/src/providers/enablebanking/transform.ts`
- `apps/engine/src/providers/gocardless/transform.ts`
- Corresponding test files and snapshots

## Verification Sources

### Plaid Amount Convention (VERIFIED)

From Plaid's official API documentation and confirmed in our codebase comment:

> "Positive values when money moves out of the account; negative values when money moves in.
> For example, debit card purchases are positive; credit card payments, direct deposits, and refunds are negative."

**Source**: [Plaid Transactions API - amount field](https://plaid.com/docs/api/products/transactions/#transactions-get-response-transactions-amount)

Our implementation inverts ALL Plaid amounts, which converts their convention to: positive = money IN, negative = money OUT.

### Teller Amount Convention (VERIFIED)

From Teller's API documentation:

> "The signed amount of the transaction as a string."

For credit accounts specifically, Teller uses opposite signs:
- Positive = money OUT (purchase)
- Negative = money IN (payment)

**Source**: [Teller Transactions API](https://teller.io/docs/api/account/transactions)

Our implementation inverts ONLY credit account amounts.

### Enable Banking / GoCardless (PSD2 Standard - VERIFIED)

Both providers follow the **Berlin Group NextGenPSD2** standard which uses:

- `credit_debit_indicator`: `"CRDT"` (Credit = money IN) or `"DBIT"` (Debit = money OUT)
- `cashAccountType`: ISO 20022 account type codes (`CACC`, `CARD`, `SVGS`, etc.)

**Sources**:
- [Berlin Group NextGenPSD2 Framework](https://www.berlin-group.org/)
- [ISO 20022 External Code Sets](https://www.iso20022.org/catalogue-messages/additional-content-messages/external-code-sets)

### ISO 20022 Cash Account Types (VERIFIED)

| Code | Name | Description |
|------|------|-------------|
| `CACC` | Current | Account for transactional operations |
| `CARD` | CardAccount | Account for card payments |
| `CASH` | CashPayment | Cash payment account |
| `SVGS` | Savings | Savings account |
| `TRAN` | Transaction | Transaction account |
| `LOAN` | Loan | Loan account |

**Source**: [ISO 20022 External Cash Account Type Code](https://www.iso20022.org/)

## Implementation Confidence Level

| Provider | Sign Convention | Category Logic | Confidence |
|----------|----------------|----------------|------------|
| **Plaid** | ✅ Documented in code, matches Plaid docs | ✅ Uses Plaid's own categories | **HIGH** |
| **Teller** | ✅ Documented in code, verified with API | ✅ Uses transaction type field | **HIGH** |
| **Enable Banking** | ✅ Uses standard CRDT/DBIT indicator | ✅ Uses bank_transaction_code | **HIGH** |
| **GoCardless** | ✅ Uses standard signed amounts | ✅ Uses proprietaryBankTransactionCode | **HIGH** |

## References

- [Teller API Documentation](https://teller.io/docs/api/account/transactions)
- [Plaid API Documentation](https://plaid.com/docs/api/products/transactions/)
- [GoCardless Bank Account Data](https://developer.gocardless.com/bank-account-data/overview)
- [Enable Banking API](https://enablebanking.com/docs/api/reference/)
- [ISO 20022 External Code Sets](https://www.iso20022.org/catalogue-messages/additional-content-messages/external-code-sets)
- [Berlin Group NextGenPSD2](https://www.berlin-group.org/)

---

### Database Connection Pooling (`docs/database-connection-pooling.md`)

# Database Connection Pooling

Technical documentation for the database connection setup across Supabase and Railway.

## Overview

The application connects to Supabase Postgres through **Supavisor** (Supabase's shared connection pooler) in **transaction mode**. Each Railway region's API instance reads from the closest Supabase read replica and writes to the primary database in EU.

## Connection Modes

Supabase offers two pooling modes via `pooler.supabase.com`:

| Mode | Port | Behavior |
|------|------|----------|
| **Session mode** | `5432` | 1:1 client-to-backend mapping. No real pooling — each app connection holds a dedicated Postgres connection for its entire lifetime. |
| **Transaction mode** | `6543` | Real connection pooling. Backend connections are shared between clients and only held during a transaction, then returned to the pool. |

**We use transaction mode (port 6543).** This is critical — session mode on port 5432 provides zero pooling benefit despite routing through `pooler.supabase.com`.

### Connection String Format

```
postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
```

## Dedicated Pooler (PgBouncer)

Supabase also offers a dedicated PgBouncer co-located on the database machine at `db.<ref>.supabase.co:6543`. This has lower latency (no network hop to a separate server) but requires IPv6 connectivity or the Supabase IPv4 add-on ($4/mo per database).

Railway does **not** support IPv6 to Supabase's direct endpoints, so the shared Supavisor pooler is the correct choice for our infrastructure.

## Multi-Region Replica Mapping

The API runs in 3 Railway regions. Each instance reads from the closest Supabase read replica via the `RAILWAY_REPLICA_REGION` environment variable:

| Railway Region | Env Var | Supabase Region | Role |
|----------------|---------|-----------------|------|
| `europe-west4-drams3a` | `DATABASE_FRA_URL` | `eu-central-1` | Primary (reads + writes) |
| `us-east4-eqdc4a` | `DATABASE_IAD_URL` | `us-east-1` | Read replica |
| `us-west2` | `DATABASE_SJC_URL` | `us-west-1` | Read replica |

- **Reads** are routed to the regional replica via `executeOnReplica` and the `withReplicas` wrapper in `packages/db/src/replicas.ts`.
- **Writes** always go to `DATABASE_PRIMARY_URL` (the primary in `eu-central-1`).

## Pool Configuration

Defined in `packages/db/src/client.ts`:

| Setting | Development | Production |
|---------|-------------|------------|
| `max` | 8 | 40 |
| `idleTimeoutMillis` | 5,000ms | 60,000ms |
| `connectionTimeoutMillis` | 5,000ms | 5,000ms |
| `maxUses` | 100 | 0 (unlimited) |
| `ssl` | disabled | enabled (rejectUnauthorized: false) |

Each API instance creates up to 2 pools (primary + 1 regional replica), so the maximum client connections per instance is `40 × 2 = 80`. With Supavisor transaction mode, these are multiplexed into a much smaller number of actual Postgres backend connections. Pool sizes are tuned for PgBouncer's 600 client limit across API + worker instances.

## Prepared Statements

We use **pg** (node-postgres) for all database clients. Transaction mode (port 6543) does **not** support prepared statements — use **session pooler** (port 5432) or **direct connection** for pg compatibility.

See [Supabase: Disabling prepared statements](https://github.com/orgs/supabase/discussions/28239).

## Environment Variables

### API Service
- `DATABASE_PRIMARY_URL` — Primary database (EU, writes + reads)
- `DATABASE_FRA_URL` — EU read replica (same as primary)
- `DATABASE_IAD_URL` — US East read replica
- `DATABASE_SJC_URL` — US West read replica

### Worker Service
- `DATABASE_PRIMARY_POOLER_URL` — Primary database (EU)

### Dashboard Service
- No database variables — connects to the API via tRPC, not directly to Postgres.

## Railway Deploy Configuration

- **API**: 3 regions × 1 replica each (production), 3 regions × 1 replica each (staging)
- **Dashboard**: 3 regions × 2 replicas each (production), 3 regions × 1 replica each (staging, with serverless/sleep enabled)
- **Worker**: 1 region (EU) × 3 replicas (production)

---

### Document Processing (`docs/document-processing.md`)

# Document Processing Pipeline

## Overview

The Document Processing Pipeline automatically processes files uploaded to the Vault, extracting content, classifying documents using AI, and generating searchable metadata. The system is designed with **graceful degradation** - documents always reach a usable state even if AI classification fails, and users can retry processing at any time.

### Key Features

- **🤖 AI-Powered Classification**: Uses vision and text models to extract titles, summaries, dates, and tags
- **🔄 Graceful Degradation**: Documents complete even if AI fails - users can always access files and retry
- **⏱️ Stale Detection**: Identifies documents stuck in processing (>10 minutes) and allows recovery
- **🔁 Retry Functionality**: Users can reprocess failed or unclassified documents with one click
- **🖼️ HEIC Conversion**: Automatically converts HEIC/HEIF images to JPEG for compatibility
- **🏷️ Tag Embeddings**: Generates semantic embeddings for document tags for better search
- **🔐 Job Deduplication**: Prevents duplicate processing using deterministic job IDs
- **📊 Status Tracking**: Real-time visual feedback for processing, failed, and completed states

## Architecture

```mermaid
graph TB
    subgraph dashboard [Dashboard]
        Upload[File Upload]
        VaultItem[VaultItem Component]
        DataTable[Vault DataTable]
    end
    
    subgraph storage [Supabase Storage]
        Bucket[(vault bucket)]
        Trigger[Storage Trigger]
    end
    
    subgraph api [API Layer]
        ProcessAPI[processDocument]
        Reprocess[reprocessDocument]
    end
    
    subgraph db [Database]
        Documents[(documents table)]
        Tags[(document_tags)]
        Embeddings[(document_tag_embeddings)]
    end
    
    subgraph worker [Worker - BullMQ]
        ProcessDoc[process-document]
        ClassifyDoc[classify-document]
        ClassifyImg[classify-image]
        EmbedTags[embed-document-tags]
    end
    
    Upload --> Bucket
    Bucket --> Trigger
    Trigger --> Documents
    Upload -->|after upload| ProcessAPI
    ProcessAPI --> ProcessDoc
    
    ProcessDoc -->|PDF/text| ClassifyDoc
    ProcessDoc -->|image| ClassifyImg
    ClassifyDoc --> EmbedTags
    ClassifyImg --> EmbedTags
    
    ClassifyDoc --> Documents
    ClassifyImg --> Documents
    EmbedTags --> Tags
    EmbedTags --> Embeddings
    
    VaultItem -->|retry| Reprocess
    DataTable -->|retry| Reprocess
    Reprocess --> ProcessDoc
```

## Data Model

### Document Processing Status

The `documents` table tracks processing state:

| Status | Description | UI Display |
|--------|-------------|------------|
| `pending` | Processing in progress | Skeleton loading state |
| `completed` | Successfully processed | Shows title/summary or filename |
| `failed` | Processing failed | Red indicator + retry button |

### Document States and Visual Indicators

```mermaid
stateDiagram-v2
    [*] --> pending: File uploaded
    
    pending --> completed: Classification success
    pending --> completed: Classification failed (graceful)
    pending --> failed: Hard failure (retryable)
    pending --> failed: Stale timeout (>10 min)
    
    failed --> pending: User retry
    completed --> pending: User retry (unclassified)
    
    note right of pending
        Shows skeleton UI
        Fresh: < 10 minutes
        Stale: > 10 minutes (shows retry)
    end note
    
    note right of completed
        title=null: Amber indicator
        title!=null: Normal display
    end note
    
    note right of failed
        Red indicator
        Retry button shown
    end note
```

### Classification States

| State | processingStatus | title | Visual | User Action |
|-------|-----------------|-------|--------|-------------|
| Processing | `pending` | - | Skeleton | Wait |
| Stale Processing | `pending` (>10 min) | - | Amber + Retry | Click retry |
| Fully Processed | `completed` | Set | Normal | None needed |
| Needs Classification | `completed` | `null` | Amber + Retry | Click retry |
| Failed | `failed` | - | Red + Retry | Click retry |

## Processing Flow

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Job Architecture

### Job Hierarchy

| Job | Parent | Purpose | Timeout |
|-----|--------|---------|---------|
| `process-document` | - | Orchestrates document processing | 10 min |
| `classify-document` | process-document | AI text classification | 90 sec |
| `classify-image` | process-document | AI vision classification | 90 sec + 60 sec download |
| `embed-document-tags` | classify-* | Generate tag embeddings | 30 sec |

### Job Deduplication

Jobs use deterministic IDs to prevent duplicate processing:

```typescript
// Pattern: {action}_{teamId}_{identifier}
jobId: `process-doc_${teamId}_${filePath.join("/")}`
jobId: `classify-doc_${teamId}_${fileName}`
jobId: `classify-img_${teamId}_${fileName}`
jobId: `embed-tags_${teamId}_${documentId}`
```

**Benefits:**
- Prevents race conditions when same file triggers multiple uploads
- Safe to retry - duplicate jobs are rejected by BullMQ
- Traceable job lineage in logs

### Queue Configuration

```typescript
const documentsQueueConfig = {
  name: "documents",
  concurrency: 10,            // Conservative for memory + API rate limits
  lockDuration: 660_000,      // 11 minutes (> process timeout)
  stalledInterval: 720_000,   // 12 minutes (> lock duration)
  limiter: {
    max: 20,                  // 20 jobs/second max - prevents API burst
    duration: 1000,
  },
};

// Sharp memory optimization (in image-processing.ts)
sharp.cache({ memory: 256, files: 20, items: 100 }); // 256MB cache limit
sharp.concurrency(2); // Limit internal parallelism

// File size limit for HEIC
const MAX_HEIC_FILE_SIZE = 15 * 1024 * 1024; // 15MB - larger files skip AI
```

**Why concurrency of 10?**
- HEIC conversion is memory-intensive (~50-100MB per 12MP image)
- AI classification (Gemini) has rate limits - avoid 429 errors
- Matches other API-heavy queues (customers: 5, teams: 5, accounting: 10)
- With 4GB worker memory, 10 concurrent jobs has plenty of headroom

## Error Handling

### Error Categories

| Category | Retryable | Retry Delay | Examples |
|----------|-----------|-------------|----------|
| `ai_content_blocked` | No | - | Content filtered by AI safety |
| `ai_quota` | Yes | 60 sec | Quota exceeded, model overloaded |
| `rate_limit` | Yes | 30 sec | Too many requests |
| `timeout` | Yes | 5 sec | Operation timed out |
| `network` | Yes | 5 sec | Connection failed |
| `validation` | No | - | Invalid input |
| `unsupported_file_type` | No | - | ZIP, video, etc. |

### Graceful Degradation Strategy

The pipeline is designed so documents **always reach a usable state**:

```mermaid
flowchart TD
    A[Start Processing] --> B{Content Extraction}
    B -->|Success| C{AI Classification}
    B -->|Failure| D[Complete with null values]
    
    C -->|Success| E[Complete with metadata]
    C -->|Failure| D
    
    D --> F[User can access file]
    E --> F
    
    F --> G{User satisfied?}
    G -->|Yes| H[Done]
    G -->|No| I[Click Retry]
    I --> A
```

**Key Principle:** A document should never be stuck. Even if AI fails:
1. Document status → `completed`
2. Title → `null` (UI shows filename + amber indicator)
3. User can download/view file
4. User can click "Retry classification"

### Failure Handling

```typescript
// In documents.config.ts - onFailed handler
onFailed: async (job, err) => {
  // Handle unsupported file types (not a failure)
  if (err instanceof UnsupportedFileTypeError) {
    await markAsCompleted(job, filename);
    return;
  }
  
  // Only mark failed on final attempt
  if (job.attemptsMade >= job.opts.attempts) {
    await markAsFailed(job);
  }
}
```

## Reprocessing Flow

### User-Initiated Retry

```mermaid
sequenceDiagram
    participant User
    participant UI as VaultItem/DataTable
    participant API as reprocessDocument
    participant DB as Database
    participant Queue as BullMQ
    
    User->>UI: Click "Retry" button
    UI->>UI: Set isReprocessing = true
    UI->>API: mutate({ id })
    
    API->>DB: Get document by ID
    API->>API: Validate pathTokens exist
    API->>API: Check mimetype supported
    
    alt Unsupported mimetype
        API->>DB: Set status = completed
        API-->>UI: { skipped: true }
    else Supported
        API->>DB: Set status = pending
        API->>Queue: Trigger process-document
        API-->>UI: { success: true, jobId }
    end
    
    UI->>UI: Show skeleton (isReprocessing || isPending)
    
    Note over Queue: Job processes...
    
    Queue->>DB: Update document
    DB-->>UI: React Query invalidation
    UI->>UI: Clear isReprocessing
    UI->>UI: Show result
```

### UI State Management

```typescript
// VaultItem component state management
const [isReprocessing, setIsReprocessing] = useState(false);

// Clear local state when document updates
useEffect(() => {
  if (isReprocessing) {
    if (isCompleted || isFailed || isLoading) {
      setIsReprocessing(false);
    }
  }
}, [isReprocessing, isLoading, isFailed, data.processingStatus]);

// Handle mutation errors
const reprocessMutation = useMutation({
  onSuccess: () => invalidateQueries(),
  onError: () => setIsReprocessing(false), // Allow retry
});
```

## Stale Document Detection

Documents pending >10 minutes are considered "stale" and show retry option in the UI:

```typescript
const isStaleProcessing =
  data.processingStatus === "pending" &&
  data.createdAt &&
  Date.now() - new Date(data.createdAt).getTime() > 10 * 60 * 1000;

// Show skeleton only for fresh pending (not stale)
const isLoading = data.processingStatus === "pending" && !isStaleProcessing;

// Show retry for stale processing
const showRetry = isFailed || needsClassification || isStaleProcessing;
```

This client-side detection allows users to manually retry documents that appear stuck without requiring a server-side cleanup job.

## Image Optimization

All images are resized before AI processing to optimize for speed, cost, and OCR quality.

### Why 2048px?

The `IMAGE_SIZES.MAX_DIMENSION` constant (2048px) was chosen based on research:

| Factor | Consideration |
|--------|---------------|
| **OCR Quality** | Text x-height ≥20px required for accurate OCR. 2048px preserves legibility for receipt small print (~400 DPI equivalent) |
| **AI Model Limits** | Within optimal ranges: Gemini (≤3072), GPT-4V (≤2048), Claude (≤1568) |
| **Performance** | Smaller images = fewer tokens = faster response + lower costs |
| **Aspect Ratio** | Uses `fit: "inside"` to maintain proportions without cropping |

### Image Processing Flow

```mermaid
flowchart TD
    A[Image Uploaded] --> B{Is HEIC?}
    B -->|Yes| C[convertHeicToJpeg]
    B -->|No| D[resizeImage]
    
    C --> E[Two-stage conversion]
    E --> F{Try Sharp}
    F -->|Success| G[JPEG @ 2048px]
    F -->|Failure| H[heic-convert fallback]
    H --> I[Sharp resize]
    I --> G
    
    D --> J{Size > 2048px?}
    J -->|Yes| K[Resize to fit 2048px]
    J -->|No| L[Keep original]
    K --> M[Continue to AI]
    L --> M
    G --> M
```

### Implementation

```
/* Detailed source-code truncated for AI context efficiency. */
```

### Supported Image Types

| Mimetype | Resize | HEIC Conversion |
|----------|--------|-----------------|
| `image/jpeg` | ✅ | - |
| `image/png` | ✅ | - |
| `image/webp` | ✅ | - |
| `image/gif` | ✅ | - |
| `image/tiff` | ✅ | - |
| `image/heic` | Via conversion | ✅ |
| `image/heif` | Via conversion | ✅ |

## Timeout Configuration

```typescript
// timeout.ts - Centralized timeout constants
export const TIMEOUTS = {
  DOCUMENT_PROCESSING: 600_000,  // 10 minutes - full pipeline
  AI_CLASSIFICATION: 90_000,     // 90 seconds - AI calls
  CLASSIFICATION_JOB_WAIT: 180_000, // 3 minutes - parent waiting for child
  FILE_DOWNLOAD: 60_000,         // 1 minute - storage downloads
  FILE_UPLOAD: 60_000,           // 1 minute - storage uploads
  EMBEDDING: 30_000,             // 30 seconds - embedding generation
} as const;

// Image size constants
export const IMAGE_SIZES = {
  MAX_DIMENSION: 2048,  // Optimal for vision models + OCR
} as const;

// Usage with timeout wrapper
const result = await withTimeout(
  classifier.classifyDocument({ content }),
  TIMEOUTS.AI_CLASSIFICATION,
  `Classification timed out after ${TIMEOUTS.AI_CLASSIFICATION}ms`
);
```

**Timeout Hierarchy:**
```
CLASSIFICATION_JOB_WAIT (180s) > AI_CLASSIFICATION (90s) + FILE_DOWNLOAD (60s)
```

This ensures parent jobs don't timeout while child jobs are still valid.

## Key Files Reference

| File | Purpose |
|------|---------|
| [`apps/dashboard/src/components/vault/vault-item.tsx`](../apps/dashboard/src/components/vault/vault-item.tsx) | Document card with status indicators and retry button |
| [`apps/dashboard/src/components/tables/vault/columns.tsx`](../apps/dashboard/src/components/tables/vault/columns.tsx) | Table columns with status styling and dropdown retry |
| [`apps/dashboard/src/components/tables/vault/data-table.tsx`](../apps/dashboard/src/components/tables/vault/data-table.tsx) | Table with reprocess mutation |
| [`apps/api/src/trpc/routers/documents.ts`](../apps/api/src/trpc/routers/documents.ts) | tRPC router with reprocessDocument endpoint |
| [`apps/worker/src/processors/documents/process-document.ts`](../apps/worker/src/processors/documents/process-document.ts) | Main orchestrator job |
| [`apps/worker/src/processors/documents/classify-document.ts`](../apps/worker/src/processors/documents/classify-document.ts) | AI text classification with graceful degradation |
| [`apps/worker/src/processors/documents/classify-image.ts`](../apps/worker/src/processors/documents/classify-image.ts) | AI vision classification with graceful degradation |
| [`apps/worker/src/processors/documents/embed-document-tags.ts`](../apps/worker/src/processors/documents/embed-document-tags.ts) | Tag embedding generation |
| [`apps/worker/src/queues/documents.config.ts`](../apps/worker/src/queues/documents.config.ts) | Queue configuration and failure handlers |
| [`apps/worker/src/utils/image-processing.ts`](../apps/worker/src/utils/image-processing.ts) | Image resize and HEIC conversion utilities |
| [`apps/worker/src/utils/document-update.ts`](../apps/worker/src/utils/document-update.ts) | Document update with retry for race conditions |
| [`apps/worker/src/utils/error-classification.ts`](../apps/worker/src/utils/error-classification.ts) | Error categorization and retry strategies |
| [`apps/worker/src/utils/timeout.ts`](../apps/worker/src/utils/timeout.ts) | Timeout constants and wrapper utility |
| [`packages/documents/src/classifier.ts`](../packages/documents/src/classifier.ts) | AI classification implementation |

## Design Decisions

### Why graceful degradation?

Documents should **never** be stuck in an inaccessible state. Even if AI fails:
- Users can still view/download their files
- The filename is displayed (not "Processing...")
- A clear retry option is provided
- No data is lost

This prioritizes user access over perfect metadata.

### Why mark AI failures as "completed" instead of "failed"?

We distinguish between:
- **Hard failures**: File doesn't exist, unsupported format, storage errors → `failed`
- **Soft failures**: AI classification failed → `completed` with `title=null`

Soft failures still result in a usable document. The UI shows these with an amber indicator and "Retry classification" button, differentiating them from hard failures (red indicator, "Retry processing" button).

### Why use deterministic job IDs?

Without deduplication, the same file could be processed multiple times due to:
- Supabase storage trigger retry
- User clicking retry rapidly
- Network issues causing duplicate API calls

Deterministic IDs (`process-doc:${teamId}:${path}`) ensure BullMQ rejects duplicate jobs automatically.

### Why 10-minute stale threshold?

The processing pipeline has these timeouts:
- Full pipeline: 10 minutes
- AI classification: 90 seconds
- File operations: 60 seconds each

If a document is still "pending" after 10 minutes, something went wrong. The threshold gives ample time for legitimate processing while catching stuck jobs.

### Why separate classify-document and classify-image jobs?

Different processing requirements:
- **Documents**: Text extraction → AI text classification
- **Images**: Direct vision API classification (no text extraction)

Separating them allows:
- Different timeout configurations
- Different error handling
- Independent scaling
- Clearer job logs

### Why fire-and-forget for embed-document-tags?

Tag embedding is an **enrichment** step, not a critical path:
- Document is already classified and usable
- Tag embedding improves search but isn't required
- Failure shouldn't mark the document as failed
- Can be retried independently in the future

The failure handler explicitly skips status updates for `documentId`-based jobs (embed-document-tags).

---

### Inbox Matching (`docs/inbox-matching.md`)

# Inbox Matching Algorithm (V2)

## Overview

The inbox matching system links inbox documents (receipts/invoices) to bank transactions using a deterministic scoring model optimized on real production feedback.

The current algorithm is **embedding-free for inbox matching**. It relies on:

- high-quality financial/date signals,
- robust name similarity,
- team-specific threshold calibration,
- historical alias learning,
- hard-negative memory from declines/unmatches.

This design keeps matching explainable, fast, and stable in production.

## Scope

This document covers:

- core matching in `packages/db/src/queries/transaction-matching.ts`,
- scoring utilities in `packages/db/src/utils/transaction-matching.ts`,
- orchestration from inbox processing jobs.

It does not cover unrelated transaction embedding features used elsewhere.

## High-Level Flow

```mermaid
graph TD
  A[New Inbox Item] --> B[process-attachment]
  B --> C[batch-process-matching]
  C --> D[findMatches]

  E[New Transaction] --> F[match-transactions-bidirectional]
  F --> G[findInboxMatches]

  D --> H[scoreMatch]
  G --> H
  H --> I{Team thresholds}
  I -->|auto threshold| J[Auto-match + confirm]
  I -->|suggested threshold| K[Create suggestion]
  I -->|below threshold| L[No match yet]
```

## Candidate Retrieval

Candidate search is SQL-first and efficient:

- Team-bounded (`team_id`) and status-bounded records.
- Date window filter around document/transaction date.
- Excludes already-attached and duplicate pending suggestion scenarios.
- Uses `pg_trgm` text similarity (`word_similarity`) for name-driven retrieval.
- Uses financial filters:
  - same-currency amount proximity, and/or
  - base-currency/base-amount proximity for cross-currency cases.

`pg_trgm` is used for retrieval speed and relevance, while final ranking is done by the custom scorer.

## Scoring Model

Final confidence is produced by `scoreMatch()` from:

- `nameScore` from normalized token similarity and containment logic,
- `amountScore` with strict same-currency behavior and base-amount cross-currency handling,
- `currencyScore` (same currency strongest; shared base currency next),
- `dateScore` with invoice/expense-aware timing logic.

Confidence receives additional guarded adjustments:

- exact-amount boosts (especially with supporting name/date evidence),
- cross-currency boost only when name+amount+date align,
- zero-name penalty,
- hard-negative penalty from decline memory.

## Learning Layers

### 1) Alias Memory

For a normalized `(inboxName, transactionName)` pair:

- if a team has repeated confirmations, alias score boosts name matching.

This improves recurring merchant variant matching (e.g. legal entity vs card statement name). Alias learning is scoped per-team — one team's data never influences another team's matching.

### 2) Hard Negative Memory

Declines/unmatches are converted into a **decayed penalty**:

- recent negatives weigh more than old negatives,
- unmatched contributes as a weaker negative than explicit decline,
- confirmations offset negatives,
- strong recent confirmations can override stale negatives,
- penalty is capped to avoid over-suppression.

This prevents repeated bad suggestions while remaining recoverable.

### 3) Dismissal Protection

Previously dismissed exact inbox/transaction pairs are not re-suggested.

## Team Calibration and Thresholding

Calibration is computed from recent labeled outcomes and cached briefly in memory.

- Uses team-specific recent history (confirmed/declined/unmatched).
- Computes baseline heuristics (accuracy/confidence gap).
- Runs threshold optimization sweep and blends with heuristics for stability.
- Produces:
  - `calibratedSuggestedThreshold`
  - `calibratedAutoThreshold` (strict, derived above suggested threshold)

This avoids one global threshold for all teams and improves precision/recall balance per team.

## Auto-Match Policy

Auto-match is conservative and requires both:

1) Confidence above calibrated auto threshold  
2) Pattern safety gate from historical pair behavior (not just one-off confidence)

Pattern gate expectations include repeated confirmations and high historical reliability, with low negative evidence.

If not eligible for auto-match but above suggested threshold, a pending suggestion is created instead.

## Status Model

Inbox:

`new -> analyzing -> pending -> suggested_match/done` (or later `no_match`)

Suggestion:

`pending -> confirmed/declined/unmatched/expired`

`unmatched` is treated as negative feedback for future calibration/penalties.

## Observability and Verification

### Read-Only Evaluation CLI

`packages/db/src/scripts/matching-eval-db.ts` provides safe verification against real DB data.

Key properties:

- read-only transaction (`BEGIN TRANSACTION READ ONLY`),
- statement timeout,
- forced rollback,
- no write path.

Main command:

`bun run eval:matching:db`

Useful options:

- `--team-id <uuid>`
- `--from-days-ago <n> --to-days-ago <n>`
- `--fixed-threshold <n>`
- `--show-review-list true --review-limit <n>`

Review list output highlights:

- likely false positives (declined/unmatched but scored high),
- likely false negatives (confirmed but scored low),
- IDs needed for quick in-app spot checks.

## Why This Version

Compared to the legacy embedding-driven matcher, V2 is:

- simpler to reason about,
- cheaper and faster at runtime,
- easier to debug and verify,
- better aligned with observed production feedback.

## Files of Interest

- `packages/db/src/queries/transaction-matching.ts`
- `packages/db/src/queries/inbox-matching.ts`
- `packages/db/src/queries/transactions.ts`
- `packages/db/src/queries/inbox.ts`
- `packages/db/src/utils/transaction-matching.ts`
- `packages/db/src/scripts/matching-eval-db.ts`

---

### Invoice Recurring (`docs/invoice-recurring.md`)

# Invoice Recurring System

## Overview

The recurring invoice system automates the generation and delivery of invoices on a scheduled basis. Unlike one-time invoices (created manually) or scheduled invoices (sent once at a future date), recurring invoices represent an ongoing series that generates new invoices automatically based on a defined frequency until an end condition is met.

### Key Concepts

- **Recurring Series**: A configuration that defines how and when invoices should be generated
- **Generated Invoices**: Individual invoices created from the series, each linked back via `invoiceRecurringId`
- **Sequence Number**: Each generated invoice has a unique sequence number within its series for idempotency

## Architecture

```mermaid
graph TB
    subgraph dashboard [Dashboard]
        UI[RecurringConfigPanel]
        Form[Invoice Form]
    end
    
    subgraph api [API Layer]
        Router[invoice-recurring router]
        Schema[Validation Schemas]
    end
    
    subgraph db [Database]
        RecurringTable[(invoice_recurring)]
        InvoicesTable[(invoices)]
    end
    
    subgraph worker [Worker]
        Scheduler[RecurringScheduler]
        Generator[GenerateInvoice]
        EmailSender[SendInvoiceEmail]
    end
    
    subgraph queue [Job Queue]
        BullMQ[BullMQ]
    end
    
    UI --> Form
    Form --> Router
    Router --> Schema
    Router --> RecurringTable
    
    BullMQ -->|"every 2 hours"| Scheduler
    Scheduler -->|"query due series"| RecurringTable
    Scheduler -->|"create invoice"| InvoicesTable
    Scheduler -->|"queue job"| BullMQ
    BullMQ --> Generator
    Generator --> EmailSender
    
    InvoicesTable -->|"invoiceRecurringId"| RecurringTable
```

## Data Model

### invoice_recurring Table

The `invoice_recurring` table stores the configuration and state for each recurring series:

```mermaid
erDiagram
    invoice_recurring {
        uuid id PK
        uuid team_id FK
        uuid user_id FK
        uuid customer_id FK
        
        enum frequency "weekly|biweekly|monthly_date|..."
        int frequency_day "Day of week (0-6) or day of month (1-31)"
        int frequency_week "Week of month (1-5) for monthly_weekday"
        int frequency_interval "Custom: every X days"
        
        enum end_type "never|on_date|after_count"
        timestamp end_date "When end_type=on_date"
        int end_count "When end_type=after_count"
        
        enum status "active|paused|completed|canceled"
        int invoices_generated "Counter"
        int consecutive_failures "For auto-pause"
        timestamp next_scheduled_at "Next generation time"
        timestamp last_generated_at
        
        string timezone
        int due_date_offset "Days until due"
        jsonb template "Invoice template config"
        jsonb line_items
        numeric amount
        string currency
    }
    
    invoices {
        uuid id PK
        uuid invoice_recurring_id FK
        int recurring_sequence "1, 2, 3..."
    }
    
    invoice_recurring ||--o{ invoices : generates
```

### Frequency Options

| Frequency | Description | Fields Used |
|-----------|-------------|-------------|
| `weekly` | Same day each week | `frequencyDay` (0=Sun, 6=Sat) |
| `biweekly` | Every 2 weeks | `frequencyDay` |
| `monthly_date` | Same date each month | `frequencyDay` (1-31) |
| `monthly_weekday` | Nth weekday of month | `frequencyDay` + `frequencyWeek` |
| `monthly_last_day` | Last day of month | - |
| `quarterly` | Every 3 months | `frequencyDay` |
| `semi_annual` | Every 6 months | `frequencyDay` |
| `annual` | Once per year | `frequencyDay` |
| `custom` | Every X days | `frequencyInterval` |

### End Conditions

| End Type | Description | Fields Used |
|----------|-------------|-------------|
| `never` | Runs indefinitely | - |
| `on_date` | Stops after date | `endDate` |
| `after_count` | Stops after N invoices | `endCount` |

## State Machine

```mermaid
stateDiagram-v2
    [*] --> active: Create series
    
    active --> active: Generate invoice
    active --> paused: User pause
    active --> paused: 3 consecutive failures
    active --> completed: End condition met
    active --> canceled: User delete
    
    paused --> active: User resume
    paused --> completed: Resume when end condition already met
    paused --> canceled: User delete
    
    completed --> [*]
    canceled --> [*]
    
    note right of active
        nextScheduledAt is set
        Scheduler picks up due series
    end note
    
    note right of paused
        nextScheduledAt preserved
        Failures reset on resume
    end note
    
    note right of completed
        nextScheduledAt = null
        Generated invoices kept
    end note
```

### State Transitions

| From | To | Trigger | Notes |
|------|-----|---------|-------|
| - | `active` | Series created | Initial state, `nextScheduledAt` calculated |
| `active` | `active` | Invoice generated | Counter incremented, next date calculated |
| `active` | `paused` | User action | Manual pause via API |
| `active` | `paused` | 3 failures | Auto-pause after consecutive failures |
| `active` | `completed` | End condition | Date passed or count reached |
| `active` | `canceled` | User delete | Soft delete, invoices preserved |
| `paused` | `active` | User resume | Next date recalculated from now |
| `paused` | `completed` | Resume attempt | If end condition met while paused |
| `paused` | `canceled` | User delete | Soft delete |

## Generation Flow

```mermaid
sequenceDiagram
    participant Cron as Cron Trigger
    participant BullMQ as Job Queue
    participant Scheduler as RecurringScheduler
    participant DB as Database
    participant Generator as GenerateInvoice
    participant Email as SendInvoiceEmail
    
    Cron->>BullMQ: Every 2 hours
    BullMQ->>Scheduler: Process job
    
    Scheduler->>DB: getDueInvoiceRecurring()
    Note over DB: WHERE status='active'<br/>AND next_scheduled_at <= now
    DB-->>Scheduler: Due series list
    
    loop For each due series
        Scheduler->>DB: checkInvoiceExists(recurringId, sequence)
        alt Already exists
            Scheduler->>Scheduler: Skip (idempotent)
        else New invoice
            Scheduler->>DB: BEGIN TRANSACTION
            Scheduler->>DB: draftInvoice()
            Scheduler->>DB: updateInvoice(recurringId, sequence)
            Scheduler->>DB: markInvoiceGenerated()
            Note over DB: Increment counter<br/>Calculate next date<br/>Check end condition
            Scheduler->>DB: COMMIT
            
            Scheduler->>BullMQ: Queue generate-invoice
            BullMQ->>Generator: Process
            Generator->>Generator: Generate PDF
            Generator->>BullMQ: Queue send-invoice-email
            BullMQ->>Email: Process
            Email->>Email: Send to customer
            Email->>DB: Update sentAt, status
        end
    end
```

### Idempotency Guarantees

The system prevents duplicate invoice generation through multiple mechanisms:

1. **Scheduler Level**: BullMQ's `upsertJobScheduler` ensures only one scheduler job runs
2. **Invoice Level**: `checkInvoiceExists(recurringId, sequence)` check before creation
3. **Transaction**: Invoice creation and counter update are atomic

### Failure Handling

```mermaid
flowchart TD
    A[Generation Attempt] --> B{Success?}
    B -->|Yes| C[Reset consecutiveFailures to 0]
    B -->|No| D[Increment consecutiveFailures]
    D --> E{failures >= 3?}
    E -->|Yes| F[Auto-pause series]
    E -->|No| G[Keep active, retry next cycle]
    F --> H[Send notification to team]
```

### Kill Switch

The scheduler respects an environment variable for emergency disable:

```
DISABLE_RECURRING_INVOICES=true
```

When set, the scheduler returns immediately without processing any series.

### Batch Limits

To prevent overwhelming the system when many invoices are due at once, processing is batched:

| Processor | Batch Size | Description |
|-----------|------------|-------------|
| Recurring invoice generation | 50 | Max invoices generated per scheduler run |
| Upcoming notifications | 100 | Max notifications sent per scheduler run |

**Design rationale:**
- Prevents memory pressure from processing thousands of series at once
- Distributes load over time (scheduler runs every 2 hours)
- Older due invoices are processed first (ordered by `nextScheduledAt`)
- Jobs return `hasMore: true` when additional items remain for the next run

## Notifications

### Upcoming Invoice Notification

A separate scheduler runs every 2 hours (offset from the main scheduler) to send 24-hour advance notifications:

```mermaid
sequenceDiagram
    participant Cron as Cron (odd hours)
    participant Worker as UpcomingNotification
    participant DB as Database
    participant Email as Email Service
    
    Cron->>Worker: Trigger job
    Worker->>DB: Find series due within 24h
    Note over DB: WHERE next_scheduled_at <= now + 24h<br/>AND upcoming_notification_sent_at IS NULL
    
    loop For each series
        Worker->>Email: Send upcoming invoice email
        Worker->>DB: Set upcoming_notification_sent_at
    end
```

The `upcoming_notification_sent_at` field prevents duplicate notifications and is reset when a new invoice is generated.

### Activity Notifications

The system creates in-app activity notifications for:

| Event | Activity Type | Priority |
|-------|---------------|----------|
| Series created | `recurring_series_started` | 3 |
| Series completed | `recurring_series_completed` | 3 |
| Series auto-paused | `recurring_series_paused` | 4 (higher) |
| Upcoming invoice | `recurring_invoice_upcoming` | 3 |

## API Endpoints

The `invoice-recurring` tRPC router exposes these procedures:

| Procedure | Type | Description |
|-----------|------|-------------|
| `create` | Mutation | Create new recurring series |
| `update` | Mutation | Update series configuration |
| `delete` | Mutation | Cancel series (soft delete) |
| `getById` | Query | Get series details |
| `getList` | Query | List series with pagination |
| `pause` | Mutation | Pause an active series |
| `resume` | Mutation | Resume a paused series |
| `getUpcoming` | Query | Preview upcoming invoices |

### Creating a Recurring Series

When creating a recurring series from a draft invoice:

1. Validate customer has email (required for auto-send)
2. Create `invoice_recurring` record
3. Link the draft invoice as sequence #1
4. Calculate `nextScheduledAt` from the invoice's issue date
5. Send notification to team

## Key Files Reference

| File | Purpose |
|------|---------|
| [`apps/dashboard/src/components/invoice/recurring-config.tsx`](../apps/dashboard/src/components/invoice/recurring-config.tsx) | UI panel for configuring frequency, end conditions, and preview |
| [`apps/dashboard/src/components/invoice/submit-button.tsx`](../apps/dashboard/src/components/invoice/submit-button.tsx) | Invoice form submission with recurring option |
| [`apps/dashboard/src/components/sheets/edit-recurring-sheet.tsx`](../apps/dashboard/src/components/sheets/edit-recurring-sheet.tsx) | Sheet for editing existing recurring series |
| [`apps/api/src/trpc/routers/invoice-recurring.ts`](../apps/api/src/trpc/routers/invoice-recurring.ts) | tRPC router with all API endpoints |
| [`apps/api/src/schemas/invoice-recurring.ts`](../apps/api/src/schemas/invoice-recurring.ts) | Zod validation schemas |
| [`apps/worker/src/processors/invoices/generate-recurring.ts`](../apps/worker/src/processors/invoices/generate-recurring.ts) | Scheduled job that generates invoices |
| [`apps/worker/src/processors/invoices/upcoming-notification.ts`](../apps/worker/src/processors/invoices/upcoming-notification.ts) | 24-hour advance notification scheduler |
| [`packages/db/src/queries/invoice-recurring.ts`](../packages/db/src/queries/invoice-recurring.ts) | Database queries (CRUD, state transitions) |
| [`packages/db/src/utils/invoice-recurring.ts`](../packages/db/src/utils/invoice-recurring.ts) | Date calculation utilities |
| [`packages/invoice/src/utils/recurring.ts`](../packages/invoice/src/utils/recurring.ts) | Shared utilities (labels, preview calculations, date handling) |

## Date Handling and Timezone Considerations

### Storage Format

All date-only fields (issue date, due date, end date) are stored as **UTC midnight timestamps** in `TIMESTAMPTZ` columns. For example, "January 15, 2024" is stored as `2024-01-15T00:00:00.000Z`.

This approach provides a canonical, timezone-agnostic representation while working with the existing database schema.

### The Timezone Problem

When a user in a timezone behind UTC (e.g., EST = UTC-5) has a date stored as UTC midnight:

```
Stored: 2024-01-15T00:00:00.000Z (January 15 at midnight UTC)
```

If we naively convert this to local time:
- In EST (UTC-5): This becomes `2024-01-14T19:00:00` (January 14!)
- The calendar would show the wrong date

### Solution: TZDate for Display

We use `TZDate` from `@date-fns/tz` to interpret stored UTC dates correctly:

```typescript
import { TZDate } from "@date-fns/tz";

// Display: Interpret the stored UTC date for calendar display
const selectedDate = new TZDate(dueDate, "UTC");
// "2024-01-15T00:00:00.000Z" → Shows January 15 in calendar ✓
```

### Solution: localDateToUTCMidnight for Storage

When a user selects a date in a calendar picker, the browser returns a `Date` object at local midnight. We convert this to UTC midnight using the **local** date components:

```typescript
import { localDateToUTCMidnight } from "@midday/invoice/recurring";

// Storage: Convert local date selection to UTC midnight
const handleSelect = (date: Date) => {
  setValue("dueDate", localDateToUTCMidnight(date));
};
```

**How it works:**

```typescript
export function localDateToUTCMidnight(date: Date): string {
  return new Date(
    Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()),
  ).toISOString();
}
```

This extracts the **local** year/month/day and creates UTC midnight for that date.

### Why Not Use getStartOfDayUTC?

The codebase has two similar-looking functions with different purposes:

| Function | Purpose | Uses |
|----------|---------|------|
| `getStartOfDayUTC(date)` | Normalize UTC date to UTC midnight | `date.getUTCFullYear()`, `date.getUTCMonth()`, `date.getUTCDate()` |
| `localDateToUTCMidnight(date)` | Convert local selection to UTC midnight | `date.getFullYear()`, `date.getMonth()`, `date.getDate()` |

**Example showing the difference:**

```
User in UTC+14 selects January 15
Calendar returns: new Date(2024, 0, 15) → 2024-01-14T10:00:00.000Z (UTC)

getStartOfDayUTC():       2024-01-14T00:00:00.000Z ✗ (wrong date!)
localDateToUTCMidnight(): 2024-01-15T00:00:00.000Z ✓ (correct!)
```

### Timezone Handling in Invoice Generation

When the scheduler generates invoices, dates are calculated in the **user's configured timezone**:

1. The `timezone` field on `invoice_recurring` stores the user's IANA timezone (e.g., `America/New_York`)
2. Date calculations (next invoice date, due date) respect this timezone
3. Generated invoice dates are stored as UTC midnight

### Date Comparison (Due Date Status)

The `getDueDateStatus()` function in `apps/dashboard/src/utils/format.ts` compares due dates to the current date:

```typescript
// Parse due date as UTC (it's stored as UTC midnight)
const due = new TZDate(dueDate, "UTC");

// Get current date in UTC for consistent comparison
const now = new Date();
const nowUTC = new TZDate(now.toISOString(), "UTC");

// Compare at the day level in UTC
const diffDays = differenceInDays(startOfDay(due), startOfDay(nowUTC));
```

### Recurring Config Date Calculations

When determining recurring patterns from the issue date, we use UTC methods:

```typescript
// Use UTC methods since issue date is stored as UTC midnight
const dayOfWeek = issueDate.getUTCDay();
const dayOfMonth = issueDate.getUTCDate();
```

### Files Implementing Date Handling

| File | Purpose |
|------|---------|
| [`packages/invoice/src/utils/recurring.ts`](../packages/invoice/src/utils/recurring.ts) | `localDateToUTCMidnight()`, `getStartOfDayUTC()` utilities |
| [`apps/dashboard/src/components/invoice/due-date.tsx`](../apps/dashboard/src/components/invoice/due-date.tsx) | Due date picker with correct display/storage |
| [`apps/dashboard/src/components/invoice/issue-date.tsx`](../apps/dashboard/src/components/invoice/issue-date.tsx) | Issue date picker with correct display/storage |
| [`apps/dashboard/src/components/invoice/recurring-config.tsx`](../apps/dashboard/src/components/invoice/recurring-config.tsx) | End date picker for recurring series + UTC day calculations |
| [`apps/dashboard/src/components/invoice/submit-button.tsx`](../apps/dashboard/src/components/invoice/submit-button.tsx) | Schedule date handling with `localDateToUTCMidnight()` |
| [`apps/dashboard/src/components/sheets/edit-recurring-sheet.tsx`](../apps/dashboard/src/components/sheets/edit-recurring-sheet.tsx) | Edit recurring end date with `TZDate` display |
| [`apps/dashboard/src/components/invoice-details.tsx`](../apps/dashboard/src/components/invoice-details.tsx) | Invoice details display with `TZDate` |
| [`apps/dashboard/src/components/invoice-success.tsx`](../apps/dashboard/src/components/invoice-success.tsx) | Invoice success display with `TZDate` |
| [`apps/dashboard/src/components/tables/invoices/columns.tsx`](../apps/dashboard/src/components/tables/invoices/columns.tsx) | Invoice table columns with `formatDateUTC()` helper |
| [`apps/dashboard/src/components/customer-details.tsx`](../apps/dashboard/src/components/customer-details.tsx) | Customer invoice list with `TZDate` |
| [`apps/dashboard/src/components/select-attachment.tsx`](../apps/dashboard/src/components/select-attachment.tsx) | Invoice attachment display with `TZDate` |
| [`apps/dashboard/src/utils/format.ts`](../apps/dashboard/src/utils/format.ts) | `getDueDateStatus()` with UTC comparison |
| [`apps/worker/src/processors/invoices/generate-recurring.ts`](../apps/worker/src/processors/invoices/generate-recurring.ts) | Server-side generation using `getStartOfDayUTC()` |

## Design Decisions

### Why soft delete?

Recurring series are "canceled" rather than hard-deleted to preserve the relationship with generated invoices. This maintains audit history and allows querying which series an invoice came from.

### Why auto-pause after 3 failures?

Consecutive failures typically indicate a systemic issue (customer email invalid, template broken, etc.). Auto-pausing prevents:
- Accumulating failed jobs in the queue
- Spamming error notifications
- Wasting processing resources

The team is notified so they can fix the issue and resume manually.

### Why calculate next date from now on resume?

When a paused series resumes, the next invoice generates based on the current date, not the missed schedule. This prevents a flood of "catch-up" invoices and maintains predictable billing cycles going forward.

### Why require customer email?

Recurring invoices auto-send via email. Without a valid email destination, the invoice would generate but fail to deliver. Validating upfront provides immediate user feedback rather than silent failures.

---

### Runway Burn Rate Analysis (`docs/runway-burn-rate-analysis.md`)

# Financial Calculations

## Data Flow

### Provider Sync Flow

When a bank account is connected, data flows through the following pipeline:

```mermaid
sequenceDiagram
    participant Provider as Banking Provider API
    participant Engine as Engine Transform
    participant DB as Database
    participant Worker as Background Worker

    Provider->>Engine: Raw account data
    Note over Engine: Normalize account type
    Note over Engine: Normalize balance sign
    Engine->>DB: Store normalized data
    
    Provider->>Engine: Raw transactions
    Note over Engine: Normalize amount sign
    Note over Engine: Map category/method
    Engine->>DB: Store transactions
    
    Worker->>Provider: Periodic sync request
    Provider->>Engine: Updated balances
    Note over Engine: Re-normalize if needed
    Engine->>DB: Update account balances
```

### Query Flow (Dashboard)

When displaying financial metrics:

```mermaid
sequenceDiagram
    participant UI as Dashboard Widget
    participant TRPC as tRPC Router
    participant Query as DB Query Function
    participant DB as Database

    UI->>TRPC: Request (teamId, currency)
    TRPC->>Query: getCashBalance / getNetPosition
    Query->>DB: Filter by account type
    Note over Query: CASH_ACCOUNT_TYPES only
    DB->>Query: Account balances
    Note over Query: Math.abs for credit
    Note over Query: Currency conversion
    Query->>TRPC: Calculated result
    TRPC->>UI: Formatted response
```

---

## Account Types

| Type | Description | Examples |
|------|-------------|----------|
| `depository` | Liquid cash accounts | Checking, Savings |
| `other_asset` | Other liquid assets | Treasury, Money Market |
| `credit` | Credit card debt | Credit cards |
| `loan` | Loan accounts | Business loans, Lines of credit |
| `other_liability` | Other liabilities | - |

---

## Runway

```
Runway (months) = Cash Balance / Median Monthly Burn Rate (last 3 completed months)
```

**Cash Balance includes:**
- `depository` ✅
- `other_asset` ✅
- `credit` ❌
- `loan` ❌

**Rationale:** Runway shows how long you can operate with available cash. Debt is not cash.

---

## Burn Rate

Sum of expenses (negative transactions) per month.

**Excluded from burn rate:**
- `internal: true` transactions
- `status: 'excluded'` transactions
- Categories with `excluded: true`:
  - `internal-transfer`
  - `credit-card-payment`

**Why `credit-card-payment` is excluded:**

When a credit card is synced, both card transactions AND payments from checking are synced:

```
CC purchase: $5k (expense on CC account)
CC payment:  $5k (expense on checking)
```

Actual spending is $5k, not $10k. Excluding payments prevents double-counting.

---

## Net Position

```
Net Position = Cash - Credit Card Debt
```

**Cash includes:**
- `depository` ✅
- `other_asset` ✅

**Debt includes:**
- `credit` ✅
- `loan` ❌ (shown separately in Balance Sheet)

**Design decision:** Net Position provides a simple "cash vs credit card" view. Loans are handled separately in Balance Sheet for a complete picture.

**Provider balance conventions:**

| Provider | Raw API Response | Stored In Database |
|----------|------------------|-------------------|
| Plaid | Positive (`1000` = $1000 owed) | Positive |
| GoCardless | Negative (`-1500` = $1500 owed) | **Normalized to positive** |
| EnableBanking | Positive (ISO 20022 `current`) | Positive (normalized as safety) |
| Teller | Positive | Positive (normalized as safety) |

**Normalization Strategy:**

1. **Ingestion-time normalization (all providers):** All provider transforms normalize negative credit balances to positive at sync time.
2. **Query-time safety net:** `Math.abs()` ensures any legacy data with inconsistent signs is handled correctly.

This dual approach ensures data consistency while maintaining backwards compatibility.

---

## Balance Sheet

Full financial picture including all account types:

**Assets:**
- Cash (depository + other_asset accounts)
- Accounts Receivable (unpaid invoices)

**Liabilities:**
- Credit Card Debt (credit accounts) - uses `Math.abs()`
- Loan Account Debt (loan accounts) - uses `Math.abs()`
- Short-term Loans (loan-proceeds < 12 months)
- Long-term Loans (loan-proceeds > 12 months)

---

## Account Type Usage by Report

| Report | depository | other_asset | credit | loan |
|--------|------------|-------------|--------|------|
| Runway (cash) | ✅ | ✅ | ❌ | ❌ |
| Cash Balance | ✅ | ✅ | ❌ | ❌ |
| Net Position (cash) | ✅ | ✅ | - | - |
| Net Position (debt) | - | - | ✅ | ❌ |
| Balance Sheet | ✅ | ✅ | ✅ | ✅ |
| Burn Rate | Via transactions | Via transactions | Via transactions | Via transactions |
| Cash Flow | Via transactions | Via transactions | Via transactions | Via transactions |

---

## Provider Account Type Mapping

Each banking provider returns account types differently. The engine normalizes them:

| Provider | Source Type | Maps To |
|----------|-------------|---------|
| **Plaid** | `depository` | `depository` |
| | `credit` | `credit` |
| | `loan` | `loan` |
| | `investment`, `brokerage`, other | `other_asset` |
| **GoCardless** (ISO 20022) | `CACC`, `SVGS`, `TRAN`, `CASH` | `depository` |
| | `CARD` | `credit` |
| | `LOAN` | `loan` |
| **Enable Banking** (ISO 20022) | `CACC`, `SVGS`, `CASH` | `depository` |
| | `CARD` | `credit` |
| | `LOAN` | `loan` |
| **Teller** | `depository` | `depository` |
| | `credit` | `credit` |

Mapping logic: `apps/engine/src/utils/account.ts`

---

## Shared Constants

Account type constants ensure consistent classification across the codebase:

```typescript
// packages/banking/src/utils/account.ts
import { CASH_ACCOUNT_TYPES, DEBT_ACCOUNT_TYPES } from "@midday/banking/account";

export const CASH_ACCOUNT_TYPES = ["depository", "other_asset"] as const;
export const DEBT_ACCOUNT_TYPES = ["credit", "loan"] as const;
```

Use these constants instead of hardcoding account type strings.

---

## Key Files

| File | Functions / Exports |
|------|-----------|
| `apps/engine/src/utils/account.ts` | `getType()`, `CASH_ACCOUNT_TYPES`, `DEBT_ACCOUNT_TYPES` |
| `apps/engine/src/providers/gocardless/transform.ts` | `transformAccountBalance()` - normalizes negative credit balances |
| `apps/engine/src/providers/enablebanking/transform.ts` | `transformBalance()` - normalizes credit balances (safety) |
| `apps/engine/src/providers/teller/transform.ts` | `transformAccountBalance()` - normalizes credit balances (safety) |
| `apps/engine/src/providers/plaid/transform.ts` | `transformAccountBalance()` - uses `current` for credit (Plaid returns positive) |
| `packages/db/src/queries/reports.ts` | `getRunway()`, `getBurnRate()`, `getBalanceSheet()`, `getCashFlow()` |
| `packages/db/src/queries/bank-accounts.ts` | `getNetPosition()`, `getCashBalance()` |
| `packages/categories/src/categories.ts` | Category definitions with `excluded` flag |

---

## Troubleshooting

### Runway Shows 0 or Incorrect Value

**Symptoms:** Runway widget displays 0 months or a value that doesn't match expectations.

**Possible Causes:**

1. **No burn rate data**
   - Check that transactions exist in the selected date range
   - Verify transactions are categorized (uncategorized expenses still count)

2. **All accounts disabled**
   - Check that at least one cash account (`depository` or `other_asset`) is enabled
   - Verify accounts are synced and have balance data

3. **Currency mismatch**
   - Ensure `baseBalance` is populated for foreign currency accounts
   - Check team's base currency is set correctly

**Debug Query:**

```sql
-- Check cash accounts
SELECT name, type, balance, enabled 
FROM bank_accounts 
WHERE team_id = 'YOUR_TEAM_ID' 
  AND type IN ('depository', 'other_asset');
```

---

### Credit Card Balance Shows as Positive (Expected Negative)

**This is correct behavior.** Credit card balances are stored as positive values representing amount owed.

**How it works:**
- Plaid returns positive values natively
- GoCardless returns negative values, normalized to positive at ingestion
- `Math.abs()` is used as a safety net in queries

**If you see unexpected values:**
1. Check the raw `balance` column in `bank_accounts` table
2. Verify the `type` is `credit` (not `depository`)
3. For legacy data, `Math.abs()` handles both conventions

---

### Net Position Shows Wrong Cash Amount

**Symptoms:** Net Position cash doesn't match sum of bank accounts.

**Check these:**

1. **Account types** - Only `depository` and `other_asset` count as cash

```sql
SELECT name, type, balance 
FROM bank_accounts 
WHERE team_id = 'YOUR_TEAM_ID' AND enabled = true;
```

2. **Disabled accounts** - Disabled accounts are excluded
   
3. **Treasury/Money Market** - Should be `other_asset` type, not `depository`

4. **Credit accounts incorrectly typed** - Verify credit cards have `type = 'credit'`

---

### Double-Counted Expenses in Burn Rate

**Symptoms:** Burn rate appears ~2x higher than expected.

**Root Cause:** Credit card payments not excluded.

**Verify exclusion is working:**

1. Check `credit-card-payment` category has `excluded: true`:

```sql
SELECT slug, excluded 
FROM transaction_categories 
WHERE slug = 'credit-card-payment';
```

2. Check transactions are categorized correctly:

```sql
SELECT name, category_slug, amount 
FROM transactions 
WHERE category_slug = 'credit-card-payment' 
  AND team_id = 'YOUR_TEAM_ID';
```

3. Verify `internal-transfer` is also excluded for internal movements

---

### Provider Sync Issues

**GoCardless showing negative balances after sync:**
- This was fixed with ingestion-time normalization
- Legacy data is handled by `Math.abs()` in queries
- Re-syncing won't update existing records (upsert ignores duplicates)

**Plaid loan accounts showing as assets:**
- Fixed: `getType()` now correctly maps `loan` to `loan`
- Legacy data may need manual type correction

**To force re-sync balance normalization:**

```sql
-- Update legacy negative credit balances
UPDATE bank_accounts 
SET balance = ABS(balance) 
WHERE type = 'credit' AND balance < 0;
```

---

### Widget Not Updating

1. Check browser console for API errors
2. Verify tRPC endpoint is returning data
3. Check widget polling is enabled (see `WIDGET_POLLING_CONFIG`)
4. Clear browser cache and refresh

---

### Subscription Cancellation Flow (`docs/subscription-cancellation-flow.md`)

# Subscription Cancellation Flow

Internal documentation covering how trial and paid subscription cancellation works with Polar webhooks, the full event lifecycle, and how each scenario is handled.

---

## Polar Webhook Lifecycle

Polar sends different webhook event sequences depending on how a subscription is canceled. This is documented at https://polar.sh/docs/integrate/webhooks/events.

### End-of-Period Cancellation (our default)

When `cancelAtPeriodEnd: true` is used (which is what our `cancelSubscription` TRPC mutation does):

**Phase 1 -- fires IMMEDIATELY when user clicks cancel:**

```
subscription.updated   (status: active/trialing, cancel_at_period_end: true)
subscription.canceled  (status: active/trialing, cancel_at_period_end: true)
```

The subscription is still active/trialing at this point. The user has not lost access on Polar's side. These events signal that cancellation has been **scheduled**, not executed.

**Phase 2 -- fires at the END of the billing period (or trial end date):**

```
subscription.updated   (status: canceled)
subscription.revoked   (status: canceled)
```

This is when the subscription is definitively over. Benefits are revoked and billing stops.

### Immediate Revocation (admin/merchant force-cancel)

All events fire at once:

```
subscription.updated   (status: canceled)
subscription.canceled  (status: canceled)
subscription.revoked   (status: canceled)
```

### Reactivation (user un-cancels before period end)

```
subscription.updated   (status: active/trialing, cancel_at_period_end: false)
subscription.uncanceled
```

---

## Our Webhook Handlers

### `subscription.created` (status: trialing)

Sets `plan` from product, `subscriptionStatus: "trialing"`, clears `canceledAt`. This fires when a user completes the Polar checkout during onboarding with a trial.

### `subscription.active`

Sets `plan` from product, `subscriptionStatus: "active"`, clears `canceledAt`. This fires when a trial converts to paid, or when a user subscribes directly without a trial.

### `subscription.canceled`

Only records that cancellation has been scheduled. Sets `canceledAt` on the team. Does **not** change `plan` or `subscriptionStatus` because the subscription is still active/trialing on Polar's side at this point. Also triggers the personal outreach cancellation email ("Thanks for being a customer...") and schedules the 3-day follow-up.

### `subscription.revoked`

Handles the actual end of a subscription:
- If `status === "past_due"`: keeps the plan active, marks `subscriptionStatus: "past_due"` (payment issue -- user keeps access while fixing payment method)
- Otherwise: downgrades to `plan: "trial"`, clears `subscriptionStatus`, sets `canceledAt`

### `subscription.past_due`

Sets `subscriptionStatus: "past_due"` without changing the plan. Triggers payment issue email.

### `subscription.uncanceled`

Not currently handled. The in-app reactivation path uses the TRPC `reactivateSubscription` mutation which directly clears `canceledAt` and sets `cancelAtPeriodEnd: false` on Polar.

---

## End-to-End Flow

```
User clicks "Cancel subscription" in dashboard
  |
  v
TRPC cancelSubscription mutation
  |-- Calls Polar API: subscriptions.update({ cancelAtPeriodEnd: true })
  |-- Updates DB: team.canceledAt = now
  |
  v
Polar fires subscription.canceled IMMEDIATELY
  |
  v
Webhook handler:
  |-- Sets canceledAt = now (plan and subscriptionStatus left intact)
  |-- Sends cancellation outreach email + schedules 3-day follow-up
  |
  v
User's next page load:
  |-- Server fetches user.me -> plan is still "starter"/"pro"
  |-- User keeps full access to dashboard
  |-- Billing page shows "Reactivate subscription" button
  |-- Copy: "Your subscription/trial has been canceled and will end
  |    at the end of your billing/trial period."
  |
  v
[Days/weeks pass -- user has access through remaining period]
  |
  v
Polar fires subscription.revoked (at actual period end)
  |
  v
Webhook handler:
  |-- Sets plan = "trial"
  |-- Sets subscriptionStatus = null
  |-- Sets canceledAt = now
  |
  v
User's next page load:
  |-- Server fetches user.me -> plan is "trial", canceledAt is set
  |-- Layout skips onboarding redirect (canceledAt is set, so this
  |   is a returning user, not a fresh signup)
  |-- TrialGuard shows UpgradeContent ("Continue with Midday" + plans)
  |-- User can subscribe as a paying customer (no trial)
```

---

## Scenario Matrix

### Scenario 1: Trial user cancels during 14-day trial

| Step | DB State After | User Access |
|------|---------------|-------------|
| Signs up, starts trial | plan=starter, status=trialing, canceledAt=null | Full access |
| Clicks cancel | plan=starter, status=trialing, canceledAt=now | Full access |
| `subscription.canceled` fires | plan=starter, status=trialing, canceledAt=now | Full access |
| During remaining trial days | (unchanged) | Full access |
| Trial ends, `subscription.revoked` fires | plan=trial, status=null, canceledAt=now | UpgradeContent |
| User re-subscribes (paid, no trial) | plan=starter/pro, status=active, canceledAt=null | Full access |

### Scenario 2: Paid (active) user cancels mid-billing-cycle

| Step | DB State After | User Access |
|------|---------------|-------------|
| Active subscriber | plan=pro, status=active, canceledAt=null | Full access |
| Clicks cancel | plan=pro, status=active, canceledAt=now | Full access |
| `subscription.canceled` fires | plan=pro, status=active, canceledAt=now | Full access |
| During remaining billing period | (unchanged) | Full access |
| Billing period ends, `subscription.revoked` fires | plan=trial, status=null, canceledAt=now | UpgradeContent |

### Scenario 3: Merchant/admin immediately revokes

| Step | DB State After | User Access |
|------|---------------|-------------|
| Admin revokes from Polar dashboard | -- | -- |
| `subscription.canceled` fires | canceledAt=now (plan/status unchanged) | Full access (briefly) |
| `subscription.revoked` fires (simultaneously) | plan=trial, status=null, canceledAt=now | UpgradeContent |

Both events fire at once. The `revoked` handler performs the downgrade. Immediate loss of access as expected for forced revocation.

### Scenario 4: User cancels, then reactivates before period end

| Step | DB State After | User Access |
|------|---------------|-------------|
| Clicks cancel | plan=starter, status=trialing, canceledAt=now | Full access |
| `subscription.canceled` fires | plan=starter, status=trialing, canceledAt=now | Full access |
| Clicks "Reactivate" (TRPC mutation) | plan=starter, status=trialing, canceledAt=null | Full access |
| Polar: cancelAtPeriodEnd set back to false | -- | -- |

Reactivation works because the Polar-side subscription is still active/trialing. The `reactivateSubscription` TRPC mutation finds the subscription with `cancelAtPeriodEnd: true` and `status: active/trialing`, sets `cancelAtPeriodEnd: false`, and clears `canceledAt` in the DB.

### Scenario 5: Payment fails (past_due)

The `subscription.past_due` handler sets `subscriptionStatus: "past_due"` without changing the plan. User keeps access while fixing their payment method. If payment retries are exhausted, `subscription.revoked` fires with `status: "past_due"` and the plan stays active (separate handling).

### Scenario 6: Trial converts to paid (happy path, no cancellation)

When the trial ends and Polar charges the card, `subscription.active` fires. Handler sets `subscriptionStatus: "active"`, plan from product, clears `canceledAt`. No interaction with cancellation logic.

### Scenario 7: Re-subscription after cancellation/revocation

After revocation: team has `plan: "trial"`, `canceledAt: set`. User sees UpgradeContent with plan selection. The `Plans` component calls `createCheckout` without `requireTrial`, so the checkout proceeds as a regular paid subscription (no trial period, immediate charge). On success, `subscription.active` fires and restores full access.

Note: The user cannot get another free trial because trial eligibility requires `canceledAt == null`.

---

## Dependent Systems

### Bank sync eligibility (`apps/api/src/utils/check-team-eligibility.ts`)

Checks `plan === "pro" || plan === "starter"`. During the remaining period after cancellation, plan stays "starter"/"pro", so sync continues. After revocation, plan becomes "trial" and sync eligibility falls back to the 14-day-from-creation window.

### Email eligibility (`packages/jobs/src/utils/check-team-plan.ts`)

Checks `plan === "trial" || subscription_status === "trialing"`. During the remaining trial period after cancellation, `subscription_status` is still "trialing", so marketing emails continue as appropriate.

### Onboarding task -- trial expiring email (`packages/jobs/src/tasks/team/onboarding.ts`)

The day-12 "trial ends tomorrow" email checks:
```typescript
freshTeam?.subscription_status === "trialing" && !freshTeam.canceled_at
```

A user who canceled during their trial has `subscription_status = "trialing"` but `canceled_at` IS set. So `!freshTeam.canceled_at` is false, and the email is correctly **not sent** -- they already know they're leaving, no need to warn about upcoming billing.

### Layout onboarding redirect (`apps/dashboard/.../layout.tsx`)

Redirects `plan === "trial"` teams (created after the enforcement date) to `/onboarding?s=start-trial`, but only when `canceledAt` is null. Teams that previously had a subscription (canceledAt is set) skip this redirect and land on `TrialGuard`/`UpgradeContent` instead, where they can re-subscribe as paying customers.

### Billing settings page (`apps/dashboard/.../settings/billing/page.tsx`)

- `plan !== "trial"` -- shows `ManageSubscription` (with Reactivate button when `canceledAt` is set)
- `plan === "trial"` -- shows `Plans` component for re-subscription

### Cancellation dialog copy

- Trial users: "Your trial will remain active until it ends. You won't be charged."
- Paid users: "Your plan will remain active until the end of your current billing period. You won't be charged again."

### Manage subscription card copy

- Trial + canceled: "Your trial has been canceled and will end when your trial period expires. You won't be charged."
- Paid + canceled: "Your subscription has been canceled and will end at the end of your billing period."

---

## Key Design Decisions

### `subscription.canceled` does not downgrade

Because this event fires immediately (not at period end), we only record the cancellation intent (`canceledAt`). The plan and subscription status remain intact so the user keeps access through their paid/trial period.

### `subscription.revoked` performs the actual downgrade

This event fires at the actual end of the subscription period. This is where `plan` is set to `"trial"` and `subscriptionStatus` is cleared.

### Cancellation email fires on `subscription.canceled` (immediately)

The personal outreach email ("Thanks for being a customer, what didn't work?") is sent when the user cancels, not when the period ends. This is the right time for this kind of outreach -- while the decision is fresh. The 3-day follow-up checks `isTeamStillCanceled` before sending.

### Layout redirect guards on `canceledAt`

The onboarding enforcement redirect (`/onboarding?s=start-trial`) only applies to fresh teams that have never had a subscription. Teams with `canceledAt` set have already been through onboarding and are returning users who should see the upgrade page.

### Re-subscription skips trial

The `Plans` component (shown on UpgradeContent and billing settings) calls `createCheckout` without `requireTrial`. This means re-subscribing users pay immediately -- no second free trial. Trial eligibility also explicitly requires `canceledAt == null`.

---

## Risks and Mitigations

### `subscription.revoked` webhook not delivered

If Polar fails to deliver `subscription.revoked`, the team would keep access beyond their paid period. Polar has built-in webhook retry logic. This is the safer failure mode (temporary over-access vs. premature access loss). A periodic reconciliation job could be added later to check Polar's actual subscription state.

### `subscription.uncanceled` not handled

If a user reactivates from the Polar customer portal directly (not our dashboard), `subscription.uncanceled` fires but we don't process it. `canceledAt` would remain set, making the dashboard show "Reactivate" even though the subscription is no longer pending cancellation. The in-app reactivation path handles this correctly via the TRPC mutation.

### Race condition between TRPC mutation and webhook

The TRPC `cancelSubscription` mutation calls the Polar API, then updates `canceledAt` in the DB. The webhook could theoretically arrive between these two operations. Both the mutation and the webhook handler only set `canceledAt`, so processing order doesn't matter -- the result is the same regardless.

---

## Files Involved

| File | Role |
|------|------|
| `apps/api/src/rest/routers/webhooks/polar/index.ts` | Webhook handlers for all Polar subscription events |
| `apps/api/src/trpc/routers/billing.ts` | TRPC mutations: createCheckout, cancelSubscription, reactivateSubscription, getActiveSubscription |
| `apps/dashboard/src/app/[locale]/(app)/(sidebar)/layout.tsx` | Layout with onboarding redirect guard and TrialGuard wrapper |
| `apps/dashboard/src/utils/trial.ts` | Trial expiry calculation and upgrade content decision |
| `apps/dashboard/src/components/trial-guard.tsx` | Client component that shows UpgradeContent when trial is expired |
| `apps/dashboard/src/components/upgrade-content.tsx` | "Continue with Midday" page with plan selection |
| `apps/dashboard/src/components/cancellation-dialog.tsx` | Multi-step cancellation dialog with trial-specific copy |
| `apps/dashboard/src/components/manage-subscription.tsx` | Subscription card with cancel/reactivate and status copy |
| `apps/dashboard/src/components/plans.tsx` | Plan selection + Polar checkout (no requireTrial) |
| `packages/db/src/queries/users.ts` | `getUserById` query returning team data including subscriptionStatus |
| `packages/db/src/queries/teams.ts` | Team queries including `isTeamStillCanceled` |
| `apps/api/src/utils/check-team-eligibility.ts` | Bank sync eligibility based on plan |
| `packages/jobs/src/utils/check-team-plan.ts` | Email eligibility based on plan/status |
| `packages/jobs/src/tasks/team/onboarding.ts` | Onboarding task with trial-expiring email guard |
| `apps/worker/src/processors/teams/cancellation-emails.ts` | Immediate cancellation outreach email |
| `apps/worker/src/processors/teams/cancellation-email-followup.ts` | 3-day follow-up email with reactivation check |

---

### Weekly Insights (`docs/weekly-insights.md`)

# Weekly Insights

## Overview

The Weekly Insights system generates AI-powered summaries of business financial performance. It analyzes transactions, invoices, and historical patterns to create actionable weekly reports for SMB owners.

## Key Features

- **🤖 AI-Powered Summaries**: Natural language summaries using GPT-4.1-mini with optimized prompts
- **📊 Smart Metric Selection**: Automatically selects the 4 most relevant metrics based on changes and importance
- **🎯 Actionable Recommendations**: Prioritized actions based on overdue invoices, expense anomalies, and concentration risk
- **📈 Trend Detection**: Identifies streaks (profitable weeks, revenue decline) and year-over-year comparisons
- **⏰ Runway Projections**: Specific exhaustion dates like "cash lasts until March 3, 2026"
- **🔮 Quarter Pace**: Projects quarterly revenue vs same quarter last year
- **⚠️ Payment Anomalies**: Detects unusual payment delays from historically fast-paying customers
- **✅ Data Consistency**: Multi-layer validation ensures numbers are mathematically correct

## Architecture Overview

### Core Components

1. **InsightsService**: Main orchestrator that fetches data and coordinates generation
2. **MetricsCalculator**: Computes financial metrics with period comparisons
3. **MetricsAnalyzer**: Selects top metrics and detects anomalies
4. **ContentGenerator**: Generates AI content using split prompts
5. **Slots System**: Pre-computes all data for AI consumption

### Data Flow

```mermaid
sequenceDiagram
    participant Job as Weekly Job
    participant Service as InsightsService
    participant DB as Database
    participant Calc as MetricsCalculator
    participant Gen as ContentGenerator
    participant AI as OpenAI

    Job->>Service: generate(teamId, periodType, date)
    Service->>DB: Fetch metrics, transactions, invoices
    Service->>Calc: Calculate current & previous period
    Calc->>Service: MetricData with changes
    Service->>Gen: Generate content
    Gen->>Gen: Compute slots (pre-processed data)
    Gen->>AI: Title prompt
    Gen->>AI: Summary prompt (parallel)
    Gen->>AI: Actions prompt (parallel)
    AI->>Gen: Generated content
    Gen->>AI: Story prompt (with context)
    AI->>Gen: Final story
    Gen->>Service: InsightContent
    Service->>DB: Store insight
```

## Content Generation

### Split Prompt Strategy

Content is generated using focused, single-purpose prompts for maximum quality:

| Prompt | Purpose | Dependencies |
|--------|---------|--------------|
| `title.ts` | 15-25 word headline | Slots only |
| `summary.ts` | 40-60 word overview | Slots only |
| `actions.ts` | 3-5 prioritized recommendations | Slots only |
| `story.ts` | Highlight narrative | Title + Summary |

### Slots System

The `computeSlots()` function pre-processes all data before AI generation:

```typescript
type InsightSlots = {
  // Core financials
  profit: string;           // "117,061 kr"
  profitRaw: number;        // 117061
  revenue: string;
  expenses: string;
  margin: string;           // "97.4"
  runway: number;           // 8
  runwayExhaustionDate?: string; // "March 3, 2026"
  
  // Changes
  profitChange: number;     // 96
  profitDirection: "up" | "down" | "flat";
  profitChangeDescription: string; // Pre-computed semantic description
  
  // Context
  weekType: "great" | "good" | "quiet" | "challenging";
  highlight: { type: string; description?: string };
  streak?: { type: string; count: number; description: string };
  
  // Money on table
  overdue: OverdueSlot[];
  overdueTotal: string;
  
  // Projections
  quarterPace?: string;     // "On pace for 450,000 kr this Q1 — 18% ahead"
  yoyRevenue?: string;      // "up 35% vs last year"
  yoyProfit?: string;
};
```

### Semantic Profit Descriptions

To prevent AI misinterpretation, profit changes are pre-computed with semantic accuracy:

| Previous | Current | Change | Description |
|----------|---------|--------|-------------|
| -189,000 | -7,000 | +96% | "Loss decreased from -189,000 to -7,000" |
| -50,000 | 10,000 | +120% | "Turned from -50,000 loss to 10,000 profit" |
| 100,000 | 80,000 | -20% | "Down 20% vs last week" |
| 0 | 50,000 | - | "New profit of 50,000" |

This prevents the AI from saying "profit nearly doubled" when a loss merely decreased.

## Metric Selection

### Algorithm

The `selectTopMetrics()` function chooses the 4 most relevant metrics:

1. **Score each metric** based on:
   - Absolute change magnitude (higher = more interesting)
   - Category importance (profit > revenue > expenses)
   - Anomaly detection boost

2. **Ensure diversity**:
   - Maximum 2 metrics from same category
   - At least 1 core financial metric (profit, revenue, margin)

3. **Fill gaps with state metrics** (for quiet weeks):
   - Cash Balance
   - Total Overdue Amount
   - Runway

### Categories

| Category | Metrics |
|----------|---------|
| `profitability` | Profit, Profit Margin |
| `revenue` | Revenue, Cash Flow |
| `expenses` | Total Expenses |
| `operational` | Runway |
| `receivables` | Overdue Amount |
| `balance` | Cash Balance |

## Advanced Features

### Runway Exhaustion Date

Calculates specific date when cash runs out:

```typescript
// Uses period end date for historical accuracy
const baseDate = context?.periodEnd ?? new Date();
const exhaustionDate = new Date(baseDate);
exhaustionDate.setDate(exhaustionDate.getDate() + Math.round(runway * 30));
// Result: "March 3, 2026"
```

**Conditions:**
- Runway > 0 months
- Runway < 24 months (too far out is meaningless)

### Quarter Pace Projection

Projects full-quarter revenue based on current pace:

```typescript
// Example: 3 weeks into Q1 with 150,000 kr revenue
// Q1 has 13 weeks total
const projectedRevenue = (qtdRevenue / daysElapsed) * totalQuarterDays;
// "On pace for 450,000 kr this Q1 — 18% ahead of Q1 last year"
```

**Requirements:**
- At least 7 days into the quarter
- Non-zero QTD revenue
- Prior year comparison data (optional)

### Payment Anomaly Detection

Flags unusual payment delays from typically fast-paying customers:

```typescript
// Customer typically pays in 5 days, now 15 days overdue
const unusualThreshold = Math.max(typicalPayDays * 1.5, typicalPayDays + 7);
const isUnusual = daysOverdue > unusualThreshold;
// Result: "⚠️ UNUSUAL - typically pays in 5 days"
```

**Requirements:**
- Customer has 2+ paid invoices (to establish pattern)
- Current delay exceeds threshold

### Streak Detection

Identifies consecutive week patterns:

| Streak Type | Trigger | Example |
|-------------|---------|---------|
| `profitable` | 3+ profitable weeks | "Third consecutive profitable week" |
| `revenue_growth` | 3+ weeks of growth | "Revenue up for 4 weeks straight" |
| `revenue_decline` | 2+ weeks of decline | "Second week of declining revenue" |
| `invoices_paid_on_time` | 3+ weeks, requires actual invoices | "All invoices paid on time for 5 weeks" |

## Data Consistency

### Multi-Layer Validation

The system ensures mathematical consistency at multiple points:

#### Layer 1: Fetch Time (`fetchMetricData`)
```typescript
// If spending query returns 0 but profit is negative, derive expenses
if (expenses === 0 && profit < 0 && revenue >= 0) {
  expenses = revenue - profit;
}
```

#### Layer 2: Slots Computation (`computeSlots`)
```typescript
// If revenue is 0 but profit + expenses > 0, derive revenue
if (revenueRaw === 0 && profitRaw > 0 && expensesRaw > 0) {
  revenueRaw = profitRaw + expensesRaw;
}
```

#### Layer 3: Prompt Accuracy Rules
```xml
<accuracy>
- If profit is negative, expenses MUST be mentioned
- Profit of 0 is "no activity", NOT a loss
- If profit is NEGATIVE, never say it "improved" or "doubled"
</accuracy>
```

## Configuration

### Model Selection

```typescript
// Default model optimized for instruction following + cost
const model = "gpt-4.1-mini";
```

### Prompt Constraints

| Constraint | Title | Summary | Story | Actions |
|------------|-------|---------|-------|---------|
| Word count | 15-25 | 40-60 | 25-40 | N/A |
| Banned words | solid, healthy, strong... | same | same | N/A |
| Required elements | profit, runway | profit, revenue, margin, runway | highlight | overdue invoices first |

### Thresholds

```typescript
// Anomaly detection
const SIGNIFICANT_CHANGE_THRESHOLD = 20; // 20% change is significant
const LOW_RUNWAY_WARNING = 6; // months
const CRITICAL_RUNWAY_ALERT = 3; // months

// Streak detection
const MIN_STREAK_FOR_HIGHLIGHT = 3; // weeks (except revenue_decline = 2)

// Concentration risk
const CONCENTRATION_WARNING_THRESHOLD = 0.7; // 70% from one customer
```

## Evaluation Testing

### Evalite Framework

The insights package uses Evalite for AI output testing:

```bash
cd packages/insights
bun run eval
```

### Scorers

| Scorer | Type | What it checks |
|--------|------|----------------|
| `titleWordCount` | Deterministic | 15-25 words |
| `summaryWordCount` | Deterministic | 40-60 words |
| `bannedPhrases` | Deterministic | No filler words |
| `profitAccuracy` | Deterministic | Negative profit mentions expenses |
| `runwayDateMentioned` | Deterministic | Short runway includes date |
| `quarterPaceMentioned` | Deterministic | Quarter pace in summary when available |
| `overallQuality` | LLM-as-judge | Natural, actionable, accurate |

### Fixtures

14 test fixtures covering various scenarios:
- Great week, good week, quiet week, challenging week
- First insight (no comparison data)
- Recovery after losses
- Loss scenarios (with/without revenue)
- Short runway urgency
- Quarter pace projection
- Payment anomaly detection
- Concentration risk

## Key Files

| File | Purpose |
|------|---------|
| `packages/insights/src/index.ts` | Main InsightsService orchestrator |
| `packages/insights/src/content/generator.ts` | AI content generation |
| `packages/insights/src/content/prompts/slots.ts` | Data pre-processing |
| `packages/insights/src/content/prompts/title.ts` | Title prompt |
| `packages/insights/src/content/prompts/summary.ts` | Summary prompt |
| `packages/insights/src/content/prompts/actions.ts` | Actions prompt |
| `packages/insights/src/content/prompts/story.ts` | Story prompt |
| `packages/insights/src/metrics/calculator.ts` | Metric calculations |
| `packages/insights/src/metrics/analyzer.ts` | Metric selection & anomaly detection |
| `packages/db/src/queries/insights.ts` | Database queries & historical context |
| `packages/insights/evals/insight.eval.ts` | Evalite test definitions |
| `packages/insights/evals/fixtures.ts` | Test data fixtures |
| `packages/insights/evals/scorers.ts` | Custom eval scorers |

## Troubleshooting

### Summary Says "No Expenses" When Profit is Negative

**Symptom:** Summary claims no expenses despite negative profit.

**Cause:** Spending query returned 0 but mathematical consistency wasn't enforced.

**Fix:** Data consistency layers should catch this. If still occurring:
1. Check `fetchMetricData` consistency logic
2. Check `computeSlots` secondary validation
3. Verify profit/revenue/expenses relationship: `profit = revenue - expenses`

### Runway Date Not Showing

**Symptom:** Summary mentions "1 month runway" but no specific date.

**Possible Causes:**
1. Runway is 0 or negative
2. Runway is >= 24 months (too far out)
3. `periodEnd` not passed to slots computation

**Debug:**
```typescript
console.log({
  runway: slots.runway,
  runwayExhaustionDate: slots.runwayExhaustionDate,
  periodEnd: context?.periodEnd
});
```

### Quarter Pace Not Appearing

**Symptom:** No quarter pace projection in summary.

**Requirements not met:**
1. Less than 7 days into the quarter
2. Zero QTD revenue
3. Historical context not computed

**Verify:**
```typescript
console.log({
  quarterPace: slots.quarterPace,
  historicalContext: context?.quarterPace
});
```

### AI Says "Profit Doubled" for Loss Decrease

**Symptom:** AI uses growth language when loss merely decreased.

**Fix:** Ensure `profitChangeDescription` is being used in prompts:
```typescript
// Should see pre-computed description like:
"Loss decreased from -189,000 to -7,000"
// Not calculated percentage like:
"+96%"
```

### Metrics Show "-100% vs last week" for Zero Activity

**Symptom:** Key metrics display shows "-100% vs last week" for quiet weeks.

**This is expected behavior.** The raw percentage is mathematically correct. The summary handles this with natural language like "no financial activity this week."

### Eval Score Below 90%

**Common issues:**
1. Word count outside range → Check prompt constraints
2. Banned phrases used → AI may be ignoring instructions
3. Accuracy violations → Data consistency issue

**Debug:**
```bash
cd packages/insights
bun run eval
# Check individual scorer failures in output
```

## Performance

### Generation Time

Typical insight generation: 2-4 seconds

| Phase | Time |
|-------|------|
| Data fetch | 200-500ms |
| Metric calculation | 50-100ms |
| Title + Summary (parallel) | 800-1200ms |
| Actions (parallel with above) | 600-800ms |
| Story | 600-1000ms |
| Total | 2-4s |

### Cost

Using `gpt-4.1-mini`:
- ~1,500 input tokens per insight
- ~300 output tokens per insight
- Estimated cost: ~$0.002 per insight

---

### Apps Website Src App Docs Content Accept Online Payments (`apps/website/src/app/docs/content/accept-online-payments.mdx`)

---
title: Accept Online Payments
description: Let customers pay invoices online with credit cards via Stripe.
section: invoicing
order: 4
---

Enable online payments to let your customers pay invoices instantly with a credit card. Powered by Stripe.

## How it works

When online payments are enabled:

1. Your customer receives the invoice
2. They click the **Pay** button
3. They enter their card details on a secure Stripe page
4. Payment is processed and the invoice is marked as paid

Funds are deposited to your connected bank account.

## Set up Stripe

1. Go to [Settings](https://app.midday.ai/settings)
2. Find the Invoice section
3. Click **Connect Stripe**
4. Sign in to your Stripe account (or create one)
5. Complete the connection

Once connected, all invoices will have a Pay button.

## What customers see

When a customer clicks Pay:

1. They see the invoice amount and your business name
2. They enter card details on Stripe's secure checkout
3. They receive a payment confirmation

The entire experience is hosted by Stripe—professional and secure.

## Fees

Stripe charges payment processing fees:

- Typically 2.9% + $0.30 per transaction (varies by country)
- Fees are deducted from the payment before deposit
- See [Stripe's pricing](https://stripe.com/pricing) for details

Midday doesn't charge additional fees for payment processing.

## View payments

Payments are tracked in Midday:

1. Go to [Invoices](https://app.midday.ai/invoices)
2. Paid invoices show a **Paid** status
3. Click an invoice to see payment details

For detailed payment history, log into your Stripe dashboard.

## Disconnect Stripe

To stop accepting online payments:

1. Go to [Settings](https://app.midday.ai/settings)
2. Find the Invoice section
3. Click **Disconnect Stripe**

Existing invoices will no longer have a Pay button.

## Tips

- Customers can still pay via bank transfer—online payments are an additional option
- Some customers prefer traditional payment; make sure your bank details are on the invoice
- Monitor your Stripe dashboard for disputes or failed payments

---

### Apps Website Src App Docs Content Account Settings (`apps/website/src/app/docs/content/account-settings.mdx`)

---
title: Account Settings
description: Manage your connected bank accounts and their impact on financial metrics.
section: settings
order: 1
---

Your connected bank accounts are the foundation of Midday. This guide explains how to manage accounts and how your settings affect financial metrics.

## View your accounts

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. See all connected accounts with their:
   - Account name and bank
   - Current balance
   - Account type
   - Connection status
   - Enabled/disabled state

## Account types

Midday categorizes accounts by type, which affects how they're used in financial calculations:

| Type | Examples | Counts as Cash | Used in Reports |
|------|----------|----------------|-----------------|
| Depository | Checking, Savings | Yes | Cash balance, Runway |
| Other Asset | Money Market, Treasury | Yes | Cash balance, Runway |
| Credit | Credit Cards | No | Net position (debt), Balance sheet |
| Loan | Business Loans | No | Balance sheet |

### Why account types matter

Your **runway** calculation uses only cash accounts (depository + other asset). Credit cards and loans don't extend your runway—they're debt.

Your **burn rate** includes transactions from all enabled accounts.

Your **net position** shows cash minus credit card debt, giving you a realistic picture of your financial position.

[Learn more about metrics →](/docs/understanding-metrics)

## Enable and disable accounts

You can enable or disable any connected account.

### To disable an account

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. Find the account
3. Toggle the **Enabled** switch off

### Effects of disabling

When you disable an account:

- **Balance excluded**: The account's balance won't count toward cash balance or runway
- **Transactions hidden**: Transactions from this account won't appear in lists by default
- **Reports affected**: Financial reports exclude this account's data
- **History preserved**: Past data remains; re-enable anytime to restore

### When to disable accounts

Common reasons to disable:

- **Closed accounts**: Keep historical data but exclude from current metrics
- **Personal accounts**: If you accidentally connected a personal account
- **Inactive accounts**: Accounts you no longer use actively
- **Test accounts**: Sandbox or test accounts

## Connect a new account

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. Click **Connect bank**
3. Search for your bank
4. Complete the secure authentication
5. Select which accounts to connect

### What gets synced

When you connect an account:

- **Historical transactions**: Usually 90 days of past transactions
- **Current balance**: Updated with each sync
- **Account details**: Name, type, currency

New transactions sync automatically, typically within minutes of clearing your bank.

[Full guide: Connect bank account →](/docs/connect-bank-account)

## Reconnect an account

Bank connections sometimes need re-authentication. You'll see a notification when this happens.

### To reconnect

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. Find the account with a connection issue
3. Click **Reconnect**
4. Complete the authentication with your bank

### Why reconnection is needed

- **Security**: Banks require periodic re-authorization (often every 90 days)
- **Password changes**: If you changed your bank password
- **Bank updates**: Banks sometimes update their security systems

## Manual accounts

For banks that aren't supported for automatic sync, you can create manual accounts.

### Create a manual account

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. Click **Add manual account**
3. Enter:
   - Account name
   - Account type
   - Currency
   - Current balance
4. Save

### Managing manual accounts

- **Update balance**: Manually update the balance periodically
- **Import transactions**: Import via CSV
- **Same features**: Manual accounts work like connected accounts for reporting

[Import transactions from CSV →](/docs/import-transactions-csv)

## Delete a connection

To completely remove a bank connection:

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. Find the connection
3. Click **Delete connection**
4. Confirm deletion

### What happens when you delete

- **Transaction history**: Option to keep or delete historical transactions
- **Connection removed**: The bank link is severed
- **Cannot undo**: You'll need to reconnect to restore

Consider **disabling** instead if you want to keep historical data but exclude from current metrics.

## Currency settings

### Account currency

Each account has a currency based on the bank account's denomination. This is set automatically when you connect.

### Base currency

Your team's base currency is used for:

- Displaying totals across accounts
- Converting foreign currency transactions
- Financial reports and metrics

Set your base currency in [Settings](https://app.midday.ai/settings).

### Multi-currency handling

If you have accounts in different currencies:

1. Each account's balance is shown in its native currency
2. Totals are converted to your base currency
3. Exchange rates are updated regularly
4. Transaction amounts show both original and converted values

## Troubleshooting

### Account balance doesn't match bank

- **Pending transactions**: Some transactions may not have cleared
- **Sync delay**: Wait a few minutes and check again
- **Force sync**: Click the sync button to refresh

### Missing transactions

- **Sync timing**: Transactions appear when they clear (not when pending)
- **Account disabled**: Check the account is enabled
- **Date range**: Check you're looking at the right date range

### Connection keeps failing

- **Bank maintenance**: Try again later
- **Credentials changed**: You may need to reconnect with new credentials
- **Bank not supported**: Some smaller banks have limited API support

### Wrong account type

If an account is categorized incorrectly:

1. The type is set automatically based on bank data
2. Contact support if it's clearly wrong
3. This affects which metrics the account contributes to

## Best practices

1. **Connect all business accounts**: Complete data means accurate metrics
2. **Disable, don't delete**: Keep historical data by disabling inactive accounts
3. **Check connections regularly**: Reconnect before they expire
4. **Use manual accounts sparingly**: Automatic sync is more reliable
5. **Set base currency correctly**: Ensures accurate multi-currency totals

## Privacy and security

- **Read-only access**: Midday can only view transactions, never move money
- **Bank-level security**: OAuth authentication with major banks
- **Encrypted storage**: Account credentials and data are encrypted
- **You control access**: Disconnect anytime

[Back to settings →](/docs/notification-settings)

---

### Apps Website Src App Docs Content Add Customer (`apps/website/src/app/docs/content/add-customer.mdx`)

---
title: Add and Manage Customers
description: Store customer details for invoicing and project tracking.
section: customers
order: 1
---

Customers in Midday store the information you need for invoicing and tracking work.

## Add a customer

1. Go to [Customers](https://app.midday.ai/customers)
2. Click **Add customer**
3. Enter their details:
   - **Name**: Business or individual name
   - **Email**: For sending invoices
   - **Address**: For invoices (optional)
   - **VAT number**: For tax purposes (optional)
   - **Contact person**: Who to address (optional)
4. Save

## Customer details

### Required

- **Name**: How you identify them
- **Email**: Where invoices are sent

### Optional

- **Address**: Appears on invoices
- **Country**: For currency and tax handling
- **VAT number**: For EU/international tax compliance
- **Contact person**: Individual's name at the company
- **Website**: For reference
- **Notes**: Internal notes

## Customer enrichment

Midday can automatically enrich customer profiles:

1. Add a customer with their email domain
2. Click **Enrich**
3. Company info is pulled automatically (when available):
   - Logo
   - Address
   - Industry
   - Company size

## Edit a customer

1. Go to [Customers](https://app.midday.ai/customers)
2. Click on the customer
3. Edit their details
4. Save

Changes apply to future invoices. Past invoices keep their original details.

## Delete a customer

Customers can only be deleted if they have no invoices or projects. To remove:

1. Delete or reassign any invoices
2. Remove from any projects
3. Then delete the customer

Usually it's better to just keep old customers for records.

## Using customers

Once added, customers appear in:

- **Invoices**: Select when creating an invoice
- **Projects**: Link projects to customers
- **Reports**: Filter by customer

## Tips

- Add customers before your first invoice to them
- Keep email addresses current for delivery
- Use notes for payment preferences or special terms

---

### Apps Website Src App Docs Content Api Reference (`apps/website/src/app/docs/content/api-reference.mdx`)

---
title: API Reference
description: Access your Midday data programmatically through the REST API.
section: developer
order: 1
---

The Midday API lets you access your financial data programmatically. Build custom integrations, automate workflows, or connect Midday to your own tools.

## Full API documentation

For complete API documentation including all endpoints, parameters, and examples:

**[api.midday.ai](https://api.midday.ai)**

## Overview

The API provides access to:

- Transactions
- Invoices
- Customers
- Time tracking projects and entries
- Financial reports
- Documents
- Bank accounts

## Authentication

The Midday API supports two authentication methods:

### API Keys (for your own data)

For accessing your own Midday data in scripts, automations, or personal tools.

1. Go to [Settings → Developer](https://app.midday.ai/settings/developer)
2. Click **Create API key**
3. Give it a descriptive name
4. Copy the key immediately (you won't see it again)

Include your API key in the `Authorization` header:

```
Authorization: Bearer your-api-key-here
```

### OAuth 2.0 (for user-facing apps)

For building apps that access other users' Midday data with their permission.

1. Create an OAuth application in [Settings → Developer](https://app.midday.ai/settings/developer)
2. Implement the OAuth authorization flow
3. Exchange authorization codes for access tokens
4. Use access tokens in API requests

[Build an OAuth app →](/docs/build-oauth-app)

## Base URL

All API endpoints use:

```
https://api.midday.ai/v1
```

## Rate limits

- **Standard**: 100 requests per minute
- **Burst**: Up to 200 requests in short bursts

If you exceed rate limits, you'll receive a `429 Too Many Requests` response. Wait and retry with exponential backoff.

## SDKs

### NPM package

Install the Midday SDK:

```bash
npm install @midday-ai/sdk
```

### MCP package

For AI tool integrations:

```bash
npx @midday-ai/mcp
```

[MCP documentation →](/docs/assistant-mcp)

## Support

- **Email**: [support@midday.ai](mailto:support@midday.ai)

---

### Apps Website Src App Docs Content App Review Process (`apps/website/src/app/docs/content/app-review-process.mdx`)

---
title: App Review Process
description: Get your OAuth app verified and listed in the Midday app directory.
section: developer
order: 5
---

Once your OAuth app is working, you can submit it for review. Verified apps display a trust badge and can be listed in the Midday app directory for users to discover.

## Why get verified?

### For users

- **Trust badge**: Verified apps show users that Midday has reviewed the app
- **Confidence**: Users know the app meets security and quality standards
- **Discoverability**: Listed apps are easier to find

### For developers

- **Increased adoption**: Users trust verified apps more
- **App directory listing**: Reach Midday's user base
- **Partnership opportunities**: Connect with the Midday team

## Verification levels

### Unverified (default)

All new OAuth apps start unverified:

- Apps work fully
- Users see a warning: "This app hasn't been verified by Midday yet"
- Not listed in the app directory

### Verified

After passing review:

- No warning displayed to users
- Listed in the app directory (optional)
- Trust badge on consent screen

## Submission requirements

Before submitting, ensure your app has:

### 1. Complete app information

Fill out all fields in your OAuth application settings:

| Field | Required | Description |
|-------|----------|-------------|
| Name | Yes | Clear, recognizable app name |
| Description | Yes | Brief description (1-2 sentences) |
| Overview | Recommended | Detailed description with features |
| Developer name | Yes | Your name or company name |
| Website | Yes | Your app's homepage |
| Logo | Recommended | Square image, at least 256x256px |
| Screenshots | Recommended | Up to 4 screenshots showing your app |

### 2. Working integration

Your app must:

- Successfully complete the OAuth flow
- Handle tokens correctly (refresh, expiration)
- Use requested scopes appropriately
- Handle errors gracefully

### 3. Privacy policy

- Link to your privacy policy from your website
- Explain what data you collect and how it's used
- Describe data retention and deletion policies

### 4. Security requirements

- Use HTTPS for all endpoints
- Store tokens securely
- Implement PKCE for public clients
- Never log or expose access tokens

## Submitting for review

### Step 1: Prepare your app

1. Go to [Settings → Developer](https://app.midday.ai/settings/developer)
2. Click on your OAuth application
3. Fill in all required fields
4. Add screenshots if you have them

### Step 2: Test thoroughly

Before submitting, verify:

- [ ] Authorization flow works end-to-end
- [ ] Token refresh works correctly
- [ ] Error handling covers edge cases
- [ ] All requested scopes are actually used
- [ ] App works with different Midday accounts

### Step 3: Submit for review

1. In your OAuth application settings, click **Submit for review**
2. Your app status changes to "Pending"
3. You'll receive an email confirming submission

## Review process

### What we check

Our team reviews:

| Area | What we look for |
|------|------------------|
| **Security** | HTTPS, secure token handling, PKCE for public clients |
| **Functionality** | OAuth flow works, errors handled, scopes used appropriately |
| **User experience** | Clear app name, accurate description, working website |
| **Privacy** | Privacy policy exists, data handling is clear |
| **Quality** | App does what it claims, no misleading information |

### Timeline

- **Initial review**: 3-5 business days
- **If changes needed**: We'll email specific feedback
- **Re-review after changes**: 2-3 business days

### Possible outcomes

| Outcome | Description |
|---------|-------------|
| **Approved** | Your app is verified and can be listed |
| **Changes requested** | We'll explain what needs to be fixed |
| **Rejected** | Doesn't meet requirements (with explanation) |

## After approval

### Verification badge

Approved apps display verification status on the consent screen, removing the "not verified" warning.

### App directory listing

After approval, you can opt to list your app in the Midday app directory:

1. Go to your OAuth application settings
2. Enable **List in app directory**
3. Ensure your logo and screenshots are uploaded

### Maintaining verification

To keep your verified status:

- Keep your app functional
- Respond to user issues
- Update your app when APIs change
- Don't change scope usage without notification

## Guidelines

### Naming

- Use your app's real name
- Don't include "Midday" unless you have permission
- Avoid misleading names that imply official Midday features

### Description

- Accurately describe what your app does
- List key features
- Be clear about any costs or limitations

### Screenshots

- Show your app's actual UI
- Demonstrate key features
- Use high-quality images
- Don't use misleading mockups

### Scopes

- Only request scopes you actually use
- Explain to users why you need each scope
- Don't request broad scopes for simple features

## Common rejection reasons

| Issue | How to fix |
|-------|------------|
| Missing privacy policy | Add one to your website |
| Non-working OAuth flow | Test thoroughly before submitting |
| Unused scopes | Remove scopes you don't use |
| Incomplete app info | Fill in all required fields |
| HTTP endpoints | Use HTTPS everywhere |
| Misleading description | Accurately describe your app |
| No error handling | Handle authorization denials gracefully |

## Updating verified apps

After verification, you can still update your app:

### No re-review needed

- Updating logo or screenshots
- Changing website URL
- Editing description
- Adding redirect URIs

### May trigger re-review

- Adding new scopes (users must re-authorize anyway)
- Significant functionality changes
- Changing app name

## Contact us

Questions about the review process?

- **Email**: [support@midday.ai](mailto:support@midday.ai)
- **Discord**: [go.midday.ai/discord](https://go.midday.ai/discord)

## Related

- [Build an OAuth App](/docs/build-oauth-app) — Getting started guide
- [OAuth Scopes Reference](/docs/oauth-scopes) — Available permissions
- [OAuth API Endpoints](/docs/oauth-api-endpoints) — Technical reference

---

### Apps Website Src App Docs Content Assistant Connectors (`apps/website/src/app/docs/content/assistant-connectors.mdx`)

---
title: Connected Apps
description: Let the assistant interact with Gmail, Slack, Google Calendar, Notion, GitHub, and 50+ other services.
section: assistant
order: 3
---

The Midday assistant can interact with external services you connect — reading data, taking actions, and pulling context from the tools you already use, all from the chat.

## How it works

1. You connect an app from the Connectors settings in your dashboard
2. The assistant automatically discovers what that app can do
3. When your question or request involves that app, the assistant uses it

No API keys or manual configuration required. Authentication is handled through the Connectors UI.

## Connecting an app

1. Open [Midday](https://app.midday.ai)
2. Go to **Apps** in the sidebar
3. Browse available connectors or search by name
4. Click **Connect** and authenticate with the service
5. The app is now available to the assistant

## Available apps

### Communication and email

| App | What the assistant can do |
|-----|--------------------------|
| Gmail | Search and read emails, draft replies |
| Outlook | Search emails, manage calendar, draft messages |
| Slack | Send and read messages, search channels |
| Microsoft Teams | Send messages, manage channels, access shared files |

**Example**: "Search my Gmail for the latest invoice from Acme Corp"

### Calendar and scheduling

| App | What the assistant can do |
|-----|--------------------------|
| Google Calendar | View, create, and manage events, check availability |
| Calendly | View upcoming bookings, manage event types |
| Cal.com | Check bookings, manage event types |
| Google Meet | View and manage meeting details |
| Zoom | View meetings, manage recordings |

**Example**: "What meetings do I have tomorrow?"

### Documents and storage

| App | What the assistant can do |
|-----|--------------------------|
| Google Drive | Search and access files, manage folders |
| Google Docs | Read and search documents |
| Google Sheets | Read and query spreadsheet data |
| Notion | Search pages, create content, manage databases |
| Airtable | Read and manage records |
| Dropbox | Search and access files |
| OneDrive | Browse and manage files |
| Confluence | Search and read pages |
| Coda | Read and manage documents |

**Example**: "Find the Q1 budget spreadsheet in Google Drive"

### Project management

| App | What the assistant can do |
|-----|--------------------------|
| Linear | View and manage issues, search projects |
| Jira | Read and manage issues, search projects |
| Asana | View tasks and projects |
| ClickUp | Manage tasks and spaces |
| Monday.com | View and update boards and items |
| Trello | Manage cards and boards |
| Wrike | View and manage tasks |
| Shortcut | Manage stories and projects |
| Basecamp | Access projects and messages |
| Todoist | Manage tasks and projects |
| Google Tasks | View and manage tasks |

**Example**: "What's assigned to me in Linear this week?"

### CRM and sales

| App | What the assistant can do |
|-----|--------------------------|
| HubSpot | Access contacts, deals, and company data |
| Pipedrive | View deals and contacts |
| Salesforce | Query contacts, opportunities, and accounts |
| Attio | Access contacts and records |
| Zoho | View and manage CRM records |
| Apollo | Search contacts and companies |
| Close | Manage leads and contacts |
| Capsule | Access contacts and cases |
| Affinity | View relationship data |

**Example**: "Show me the latest deals in HubSpot"

### Design

| App | What the assistant can do |
|-----|--------------------------|
| Figma | Access files and project data |
| Canva | Manage designs and folders |
| Miro | Access boards and content |
| Webflow | Manage sites and CMS data |

**Example**: "What Figma files were updated this week?"

### Developer tools

| App | What the assistant can do |
|-----|--------------------------|
| GitHub | View and manage issues, PRs, repos |

**Example**: "Show me open pull requests on our main repo"

### Finance and accounting

| App | What the assistant can do |
|-----|--------------------------|
| Stripe | View payments, customers, and subscription data |
| FreshBooks | Access invoices and expenses |
| Brex | View transactions and card data |
| QuickBooks | Access accounting data |
| Zoho Books | View invoices and financial data |
| Xero | Access accounting records |
| Sage | View financial data |
| Wave | Access invoicing and accounting data |

**Example**: "Show my latest Stripe payments"

### HR and recruiting

| App | What the assistant can do |
|-----|--------------------------|
| BambooHR | Access employee data and time-off |
| Workable | View candidates and job postings |

### Marketing and analytics

| App | What the assistant can do |
|-----|--------------------------|
| Google Ads | View campaign performance and metrics |
| Meta Ads | Access ad data and insights |
| Semrush | View SEO and marketing data |

### Customer support

| App | What the assistant can do |
|-----|--------------------------|
| Intercom | Access conversations and contact data |
| Zendesk | View tickets and customer data |
| Freshdesk | Access support tickets |

### Other

| App | What the assistant can do |
|-----|--------------------------|
| Shopify | View orders, products, and store data |
| DocuSign | Access documents and signing status |
| Fireflies | View meeting transcriptions and notes |

## Disconnecting an app

1. Go to **Apps** in the sidebar
2. Find the connected app
3. Click **Disconnect**

The assistant will no longer have access to that service. No data from the service is stored in Midday after disconnection.

## Security and privacy

- Each connection uses secure OAuth — Midday never stores your passwords for connected services
- The assistant only accesses data when you ask it to in a conversation
- You control which apps are connected and can disconnect at any time
- Connected app data is not used to train AI models

[Learn about the assistant →](/docs/using-assistant)
[AI Tools and MCP →](/docs/assistant-mcp)

---

### Apps Website Src App Docs Content Assistant Mcp (`apps/website/src/app/docs/content/assistant-mcp.mdx`)

---
title: Assistant and MCP Integration
description: Manage your entire business from AI tools via MCP — transactions, invoices, time tracking, exports, and more.
section: assistant
order: 2
---

The Midday MCP server gives AI tools like Claude, Cursor, ChatGPT, and Raycast full access to your business data — not just reading, but creating invoices, managing transactions, tracking time, and exporting to your accountant.

## What is MCP?

Model Context Protocol (MCP) is an open standard for connecting AI assistants to external data sources. Instead of copying and pasting data, MCP lets AI tools work with your Midday account directly and securely.

## Supported tools

Midday MCP works with:

- **Claude Desktop** - Anthropic's desktop app
- **Cursor** - AI-powered code editor
- **ChatGPT** - Via plugins/actions
- **Raycast** - Mac productivity launcher
- **Any MCP-compatible client**

## Getting started

### Step 1: Get your API key

1. Go to [Settings → Developer](https://app.midday.ai/settings/developer)
2. Click **Create API key**
3. Give it a descriptive name (e.g., "Claude Desktop")
4. Select the scopes you need (read-only by default, enable write scopes for full capabilities)
5. Copy the key immediately (you won't see it again)

### Step 2: Configure your client

Choose your tool below for specific setup instructions.

---

## Claude Desktop setup

Add Midday to your Claude Desktop configuration.

### Find your config file

- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`

### Add Midday configuration

Add or update the `mcpServers` section:

```json
{
  "mcpServers": {
    "midday": {
      "command": "npx",
      "args": ["-y", "@midday-ai/mcp"],
      "env": {
        "MIDDAY_API_KEY": "your-api-key-here"
      }
    }
  }
}
```

### Restart Claude Desktop

After saving, restart Claude Desktop. You should see Midday in your available tools.

### Test it

Ask Claude something like:
- "What was my revenue last month?"
- "Create an invoice for Acme Corp"
- "Export Q1 transactions to my accountant"

---

## Cursor setup

Use Midday data while coding in Cursor.

### Open Cursor settings

1. Open Cursor
2. Go to Settings (Cmd/Ctrl + ,)
3. Find the MCP configuration section

### Add Midday

Add to your MCP servers:

```json
{
  "midday": {
    "command": "npx",
    "args": ["-y", "@midday-ai/mcp"],
    "env": {
      "MIDDAY_API_KEY": "your-api-key-here"
    }
  }
}
```

### Use in conversations

In Cursor's AI chat, you can now ask about your financial data while working on your projects.

---

## ChatGPT setup

Use Midday with ChatGPT via custom GPTs or plugins.

### Custom GPT approach

1. Create a Custom GPT in ChatGPT
2. Add actions that call the Midday API
3. Configure authentication with your API key

### Configuration

Use the Midday API endpoints with your API key in the Authorization header:

```
Authorization: Bearer your-api-key-here
```

See the [API reference](/docs/api-reference) for available endpoints.

---

## Raycast setup

Access Midday from Raycast on Mac.

### Install the extension

1. Open Raycast
2. Search for "Midday" in the Store
3. Install the extension

### Configure

1. Open Raycast preferences
2. Find the Midday extension
3. Enter your API key

### Use it

Trigger Raycast and use Midday commands to query your financial data.

---

## Available tools

The MCP server exposes 107 tools covering your entire business workflow.

### Transactions

| Tool | Description |
|------|-------------|
| `transactions_list` | List transactions with filters (date, category, amount, status, tags) |
| `transactions_get` | Get full details of a specific transaction |
| `transactions_create` | Create a manual transaction |
| `transactions_create_bulk` | Create up to 100 manual transactions at once |
| `transactions_update` | Update a transaction (category, status, note, tags, tax) |
| `transactions_update_bulk` | Bulk update multiple transactions |
| `transactions_delete` | Delete a manual transaction |
| `transactions_delete_bulk` | Delete up to 1,000 manual transactions |

**Example queries**:
- "Show transactions from last month"
- "What did I spend on software this quarter?"
- "Categorize all my Stripe transactions as income"
- "Tag these transactions as tax-deductible"

### Export & Accounting

| Tool | Description |
|------|-------------|
| `transactions_export` | Export transactions as a ZIP (CSV/XLSX + receipts), optionally email to accountant |
| `transactions_export_to_accounting` | Push transactions to Xero, QuickBooks, or Fortnox |
| `export_job_status` | Poll the progress of an export job |
| `accounting_connections` | List connected accounting providers |
| `accounting_sync_status` | Check sync status of transactions with accounting software |

**Example queries**:
- "Export my January transactions to my accountant at lisa@accounting.com"
- "Push last month's expenses to QuickBooks"
- "Which accounting software do I have connected?"
- "Download all Q1 transactions as CSV"

### Invoices

| Tool | Description |
|------|-------------|
| `invoices_list` | List invoices with filters (status, customer, date) |
| `invoices_get` | Get full invoice details with line items and payment history |
| `invoices_summary` | Get totals by status (draft, unpaid, paid, overdue) |
| `invoices_search_number` | Search for an invoice by number |
| `invoices_payment_status` | Get overall invoice payment health score |
| `invoices_analytics` | Invoice analytics: avg days to payment, top clients, trends |
| `invoices_create` | Create a new invoice (saved as draft by default) |
| `invoices_update_draft` | Edit a draft invoice's line items, customer, dates, or discount |
| `invoices_send` | Send a draft invoice to the customer via email |
| `invoices_remind` | Send a payment reminder for an unpaid/overdue invoice |
| `invoices_update` | Update invoice status or internal note |
| `invoices_mark_paid` | Mark an invoice as paid |
| `invoices_cancel` | Cancel an invoice |
| `invoices_duplicate` | Duplicate an existing invoice with a new number |
| `invoices_delete` | Permanently delete an invoice |
| `invoices_create_from_tracker` | Create an invoice from tracked time entries on a project |

**Example queries**:
- "Create an invoice for Acme Corp for 10 hours of consulting at $150/hr"
- "Send the draft invoice to Acme Corp"
- "Remind Beta Inc about their overdue invoice"
- "Mark invoice INV-0042 as paid"
- "What invoices are overdue?"
- "Invoice the time I tracked on Project X this month"

### Invoice Templates

| Tool | Description |
|------|-------------|
| `invoice_template_list` | List all invoice templates |
| `invoice_template_get` | Get a template's labels and settings |
| `invoice_template_update` | Update template labels, tax rates, currency, or payment details |

### Invoice Products

| Tool | Description |
|------|-------------|
| `invoice_products_list` | List saved line item products (product catalog) |
| `invoice_products_get` | Get product details (name, price, currency, tax rate) |
| `invoice_products_create` | Create a reusable invoice line item product |
| `invoice_products_update` | Update a product |
| `invoice_products_delete` | Delete a product |

### Recurring Invoices

| Tool | Description |
|------|-------------|
| `invoice_recurring_list` | List recurring invoice schedules |
| `invoice_recurring_get` | Get recurring schedule details |
| `invoice_recurring_upcoming` | Preview upcoming scheduled invoices |
| `invoice_recurring_create` | Set up a recurring invoice schedule |
| `invoice_recurring_pause` | Pause a recurring schedule |
| `invoice_recurring_resume` | Resume a paused schedule |
| `invoice_recurring_delete` | Delete a recurring schedule |

### Customers

| Tool | Description |
|------|-------------|
| `customers_list` | List customers with search and sorting |
| `customers_get` | Get full customer details (contact, address, VAT, tags) |
| `customers_create` | Create a new customer |
| `customers_update` | Update customer information |
| `customers_delete` | Delete a customer |

**Example queries**:
- "List all my customers"
- "Add a new customer: Acme Corp, acme@example.com"
- "Update Acme Corp's billing address"

### Time tracking

| Tool | Description |
|------|-------------|
| `tracker_projects_list` | List time tracking projects with filters |
| `tracker_projects_get` | Get project details with total tracked time |
| `tracker_projects_create` | Create a new project |
| `tracker_projects_update` | Update project settings (rate, status, estimate) |
| `tracker_projects_delete` | Delete a project and its entries |
| `tracker_entries_list` | List time entries for a date range |
| `tracker_entries_create` | Log a time entry |
| `tracker_entries_update` | Update a time entry |
| `tracker_entries_delete` | Delete a time entry |
| `tracker_timer_start` | Start a live timer |
| `tracker_timer_stop` | Stop the running timer |
| `tracker_timer_status` | Check if a timer is running |

**Example queries**:
- "How many hours did I track this week?"
- "Start a timer on Project X"
- "Log 2 hours for the client meeting yesterday"
- "What's unbilled for Project X?"

### Reports & Analytics

| Tool | Description |
|------|-------------|
| `reports_revenue` | Revenue for a period with comparison |
| `reports_profit` | Profit (revenue minus expenses) |
| `reports_burn_rate` | Monthly burn rate |
| `reports_runway` | Estimated months of cash remaining |
| `reports_expenses` | Expense totals with recurring breakdown |
| `reports_spending` | Spending by category |
| `reports_tax_summary` | Tax summary for a date range |
| `reports_growth_rate` | Revenue or profit growth rate |
| `reports_profit_margin` | Profit margin analysis |
| `reports_cash_flow` | Cash flow (income vs expenses over time) |
| `reports_recurring_expenses` | Detected recurring expenses |
| `reports_revenue_forecast` | Revenue forecast based on historical data |
| `reports_balance_sheet` | Balance sheet snapshot (assets, liabilities, equity) |

**Example queries**:
- "What's my revenue this quarter?"
- "Calculate my runway"
- "Show spending by category for last month"
- "What's my growth rate compared to last quarter?"
- "Give me a balance sheet as of today"

### Categories

| Tool | Description |
|------|-------------|
| `categories_list` | List all transaction categories as a tree |
| `categories_get` | Get a category with children, color, and tax settings |
| `categories_create` | Create a custom category |
| `categories_update` | Update a category |
| `categories_delete` | Delete a custom category |

### Tags

| Tool | Description |
|------|-------------|
| `tags_list` | List all tags |
| `tags_get` | Get a tag by ID |
| `tags_create` | Create a new tag |
| `tags_update` | Rename a tag |
| `tags_delete` | Delete a tag |

### Bank Accounts

| Tool | Description |
|------|-------------|
| `bank_accounts_list` | List connected accounts with balance, currency, and connection info |
| `bank_accounts_balances` | Get current balances for all accounts |
| `bank_accounts_currencies` | List all unique currencies across accounts |
| `bank_accounts_details` | Get banking details (IBAN, account number, routing number) |

**Example queries**:
- "What's my current cash balance?"
- "Show all connected accounts"
- "What currencies do my accounts use?"

### Documents

| Tool | Description |
|------|-------------|
| `documents_list` | List documents in the vault with search and tag filters |
| `documents_get` | Get document details with signed download URL |
| `documents_delete` | Delete a document from the vault |
| `document_tags_list` | List all document tags |
| `document_tags_create` | Create a document tag |
| `document_tags_delete` | Delete a document tag |
| `document_tags_assign` | Add a tag to a document |
| `document_tags_unassign` | Remove a tag from a document |

### Inbox

| Tool | Description |
|------|-------------|
| `inbox_list` | List inbox items (receipts, invoices pending processing) |
| `inbox_get` | Get inbox item details with matched transaction |
| `inbox_update` | Update an inbox item's status, name, or amount |
| `inbox_delete` | Delete an inbox item |
| `inbox_match_transaction` | Link an inbox item to a transaction |
| `inbox_unmatch_transaction` | Remove the link between an inbox item and a transaction |
| `inbox_confirm_match` | Confirm an AI-suggested match |
| `inbox_decline_match` | Decline an AI-suggested match |

**Example queries**:
- "Show my unprocessed receipts"
- "Match this receipt to the Stripe transaction"

### Search & Team

| Tool | Description |
|------|-------------|
| `search_global` | Full-text search across all data types |
| `team_get` | Get team info (name, currency, plan, fiscal year) |
| `team_members` | List team members with roles and user IDs |

---

## Example conversations

### Financial overview

> **You**: What's my financial health looking like?
>
> **Assistant**: Based on your Midday data:
> - Cash balance: $45,230
> - Monthly burn rate: $8,500
> - Runway: 5.3 months
> - Revenue this month: $12,400
> - Profit margin: 31%

### Create and send an invoice

> **You**: Create an invoice for Acme Corp for website redesign - 40 hours at $125/hr
>
> **Assistant**: I've created draft invoice INV-0051 for Acme Corp:
> - Website Redesign: 40 × $125.00 = $5,000.00
> - Total: $5,000.00
>
> Would you like me to send it?
>
> **You**: Yes, send it
>
> **Assistant**: Invoice INV-0051 has been sent to billing@acmecorp.com.

### Export to accountant

> **You**: Export my January transactions to my accountant at lisa@accounting.com
>
> **Assistant**: I've started the export of 142 transactions from January. Your accountant at lisa@accounting.com will receive a download link with the CSV, XLSX, and receipt attachments once it's ready.

### Push to accounting software

> **You**: Push last month's expenses to QuickBooks
>
> **Assistant**: You have QuickBooks connected (Acme Corp LLC). I'll export 87 expense transactions from last month. The sync is in progress — transactions and their receipts are being pushed to QuickBooks now.

### Time tracking

> **You**: How many hours did I work on the mobile app this week?
>
> **Assistant**: You tracked 18.5 hours on "Mobile App Redesign" this week:
> - Monday: 4h (UI design)
> - Tuesday: 3.5h (API integration)
> - Wednesday: 5h (Testing)
> - Thursday: 3h (Bug fixes)
> - Friday: 3h (Documentation)

---

## Scopes & Permissions

Every tool is gated by scopes. When you create an API key or authorize via OAuth, you choose exactly which data and actions are accessible. For example, `transactions.read` enables listing and searching transactions, while `transactions.write` additionally allows creating, updating, and deleting them.

See the full [OAuth Scopes Reference](/docs/oauth-scopes) for the complete list of available scopes.

---

## Security

### API key permissions

- API keys have **configurable scopes** — choose exactly what each key can access
- Read scopes allow querying data; write scopes allow creating, modifying, and exporting
- Keys never expose sensitive credentials, passwords, or payment card details

### Managing keys

1. Go to [Settings → Developer](https://app.midday.ai/settings/developer)
2. View all active API keys
3. See last used date for each key
4. Revoke keys you no longer need

### Best practices

- Create separate keys for different tools
- Use descriptive names for keys
- Only grant the scopes each tool actually needs
- Revoke unused keys
- Don't share keys publicly

---

## Troubleshooting

### "Tool not found" errors

- Ensure you've restarted your client after configuration
- Check that the API key is valid and has the required scopes
- Verify the configuration syntax

### No data returned

- Check your API key has access to the team
- Verify transactions/invoices exist for the queried period
- Try a broader query

### Write operations failing

- Ensure your API key has the appropriate write scopes enabled
- Check that required fields are provided (e.g., customer ID for invoices)

### Connection issues

- Ensure you have internet access
- Check that `npx` is available in your PATH
- Try running `npx @midday-ai/mcp` manually to test

---

## Tips for best results

1. **Be specific**: "Revenue last month" works better than "How am I doing?"
2. **Include dates**: "Expenses in Q1 2025" helps narrow results
3. **Use follow-ups**: The assistant remembers context in a conversation
4. **Combine actions**: "Create an invoice for Acme Corp and send it" works in one go
5. **Ask about connections first**: "What accounting software do I have connected?" before exporting

[Learn about the in-app assistant →](/docs/using-assistant)
[API reference →](/docs/api-reference)

---

### Apps Website Src App Docs Content Auto Categorization (`apps/website/src/app/docs/content/auto-categorization.mdx`)

---
title: Automatic Categorization
description: How Midday uses AI to categorize your transactions and how categories affect your metrics.
section: banking
order: 4
---

Every transaction that flows into Midday is automatically categorized using AI. This saves hours of manual work and keeps your books organized for accounting.

## How it works

When a new transaction arrives, Midday's AI:

1. **Analyzes the merchant**: Identifies the company from the transaction description
2. **Checks your history**: Looks at how you've categorized similar transactions
3. **Considers patterns**: Factors in amount, frequency, and context
4. **Assigns a category**: Picks the most likely category

The AI learns from your corrections. The more you use Midday, the more accurate it becomes for your specific business.

## Category structure

Midday uses a two-level category system designed for business accounting:

### Parent categories (14 total)

1. **Revenue** - Income from your business
2. **Cost of Goods Sold** - Direct costs of products sold
3. **Sales & Marketing** - Customer acquisition costs
4. **Operations** - Day-to-day running costs
5. **Professional Services** - External expertise
6. **Human Resources** - Team costs
7. **Travel & Entertainment** - Business travel
8. **Technology** - Software and subscriptions
9. **Banking & Finance** - Financial transactions
10. **Assets** - Capital purchases
11. **Liabilities & Debt** - Obligations
12. **Taxes & Government** - Tax payments
13. **Owner / Equity** - Founder transactions
14. **System** - Uncategorized and transfers

### Subcategories

Each parent category contains specific subcategories. For example, Technology includes:
- Software
- Non-Software Subscriptions

[See all categories and subcategories →](/docs/categories-reference)

## Excluded categories

Some categories are "excluded" from burn rate calculations to prevent double-counting:

| Category | Why it's excluded |
|----------|-------------------|
| Credit Card Payment | The purchases on your card already count; the payment would be double-counting |
| Internal Transfer | Moving money between accounts isn't spending |

This happens automatically. Your burn rate and runway calculations stay accurate.

[Learn more about how categories affect metrics →](/docs/understanding-metrics)

## Changing a category

If the AI gets it wrong:

1. Click on the transaction
2. Select the correct category from the dropdown
3. Save

### What happens when you correct

- The transaction is recategorized immediately
- The AI learns from your correction
- Future similar transactions are more likely to be categorized correctly
- Your financial metrics update to reflect the change

## Bulk categorization

For multiple similar transactions:

1. Go to [Transactions](https://app.midday.ai/transactions)
2. Use filters to find the transactions (e.g., filter by merchant name)
3. Select multiple transactions using checkboxes
4. Choose **Categorize** from the bulk actions menu
5. Select the category to apply
6. Confirm

This is useful for:
- Fixing multiple miscategorized transactions at once
- Categorizing a backlog of transactions
- Applying consistent categories to recurring expenses

## How the AI learns

### Merchant patterns

The AI builds patterns based on merchant names:

- "SPOTIFY" → Technology > Software
- "UBER" → Travel & Entertainment > Travel
- "AMAZON" → Depends on your history (could be office supplies, software, etc.)

### Your corrections

When you correct a category:
- That specific merchant pattern is updated
- Similar merchant names benefit from the learning
- The AI weighs your choices higher over time

### Team patterns

The AI learns from your entire team's categorization choices, building a model specific to your business.

## Categories and financial metrics

How you categorize transactions directly affects your reports:

| Metric | Affected by |
|--------|-------------|
| Revenue | Transactions in Revenue categories |
| Burn Rate | All expense categories except excluded ones |
| Profit | Revenue minus expenses |
| Cash Flow | All income and expense categories |

Accurate categorization means accurate metrics.

## Categories and accounting

### Export to CSV

When you [export transactions](/docs/export-transactions-csv), categories are included. Your accountant can map them to their chart of accounts.

### Export to accounting software

If you connect [Xero](/docs/connect-xero), [QuickBooks](/docs/connect-quickbooks), or [Fortnox](/docs/connect-fortnox):

- Categories map to account codes
- You configure the mapping during setup
- Transactions export with the correct account mappings

## Tips for better categorization

### Review regularly

- Check new transactions weekly
- Correct any miscategorized items promptly
- The AI learns faster with quick feedback

### Be consistent

- Use the same category for the same type of expense
- Don't switch between similar categories (pick one)
- Consistency helps both AI learning and your reports

### Use tags for detail

Categories are intentionally broad (for accounting). Use [tags](/docs/use-tags) for additional detail:

- Project names
- Client names
- Tax deduction status
- Department codes

### Handle ambiguous merchants

Some merchants (like Amazon) could be many categories:

- Office supplies
- Software
- Equipment
- Personal (shouldn't be here!)

Correct these carefully. The AI will learn that Amazon transactions from your account might be different from another business.

## Common questions

### Can I create custom categories?

Midday uses a standard category set that maps to accounting charts of accounts. This ensures consistency for exports and integrations. Use tags for custom groupings.

### What if a transaction fits multiple categories?

Choose the primary purpose. For example:
- A laptop is Equipment, not Technology
- Conference travel is Travel, not Marketing

### How do I handle personal transactions?

If a personal transaction accidentally appears:
1. Categorize it appropriately (or use "Other")
2. Consider excluding it from reports
3. Discuss with your accountant for proper handling

### Why did the AI categorize this wrong?

Common reasons:
- New merchant the AI hasn't seen
- Ambiguous merchant name
- Similar merchant to one you categorized differently

Correct it once, and the AI will learn.

[Full category reference →](/docs/categories-reference)
[How categories affect metrics →](/docs/understanding-metrics)

---

### Apps Website Src App Docs Content Build Oauth App (`apps/website/src/app/docs/content/build-oauth-app.mdx`)

---
title: Build an OAuth App
description: Create applications that integrate with Midday using OAuth 2.0.
section: developer
order: 2
---

Build applications that let users connect their Midday accounts. OAuth apps can access financial data on behalf of users, enabling integrations, automations, and custom tools.

## What you can build

With OAuth, you can build:

- **Browser extensions** that display Midday data
- **Mobile apps** with Midday integration
- **Automation tools** that sync data between services
- **Custom dashboards** for specific workflows
- **Developer tools** like IDE extensions
- **Public apps** listed in the Midday app directory

## How OAuth works

OAuth 2.0 lets your app request access to a user's Midday data without handling their password:

1. Your app redirects users to Midday's authorization page
2. Users log in and approve the requested permissions
3. Midday redirects back with an authorization code
4. Your app exchanges the code for access tokens
5. Use access tokens to call the Midday API

## Getting started

### Step 1: Create an OAuth application

1. Go to [Settings → Developer](https://app.midday.ai/settings/developer)
2. Click **Create OAuth app**
3. Fill in your application details:
   - **Name**: Your app's name (shown to users)
   - **Description**: Brief description
   - **Website**: Your app's homepage
   - **Redirect URIs**: URLs to redirect after authorization
4. Select the **scopes** your app needs
5. Click **Create**

You'll receive:
- **Client ID**: Public identifier for your app
- **Client Secret**: Keep this secret (shown once)

### Step 2: Implement the authorization flow

Redirect users to start authorization:

```
https://app.midday.ai/oauth/authorize?
  response_type=code&
  client_id=YOUR_CLIENT_ID&
  redirect_uri=YOUR_REDIRECT_URI&
  scope=transactions.read%20invoices.read&
  state=RANDOM_STATE_VALUE
```

**Parameters:**

| Parameter | Required | Description |
|-----------|----------|-------------|
| `response_type` | Yes | Must be `code` |
| `client_id` | Yes | Your application's client ID |
| `redirect_uri` | Yes | Must match a registered redirect URI |
| `scope` | Yes | Space-separated list of scopes |
| `state` | Recommended | Random string to prevent CSRF attacks |
| `code_challenge` | For public clients | PKCE code challenge |
| `code_challenge_method` | For public clients | Must be `S256` |

### Step 3: Handle the callback

After the user authorizes, Midday redirects to your `redirect_uri`:

```
https://yourapp.com/callback?code=AUTH_CODE&state=YOUR_STATE
```

If the user denies access:

```
https://yourapp.com/callback?error=access_denied&error_description=User%20denied%20access
```

### Step 4: Exchange code for tokens

Make a POST request to exchange the authorization code:

```bash
curl -X POST https://api.midday.ai/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "AUTH_CODE",
    "redirect_uri": "YOUR_REDIRECT_URI",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET"
  }'
```

**Response:**

```json
{
  "access_token": "mid_at_xxxxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "mid_rt_xxxxx",
  "scope": "transactions.read invoices.read"
}
```

### Step 5: Make API requests

Install the Midday SDK:

```bash
npm install @midday-ai/sdk
```

Use the access token with the SDK:

```typescript
import { Midday } from "@midday-ai/sdk";

const midday = new Midday({
  token: "mid_at_xxxxx", // Access token from OAuth flow
});

// List transactions
const transactions = await midday.transactions.list({
  pageSize: 50,
});

// Get invoices
const invoices = await midday.invoices.list({
  pageSize: 20,
});

// Get financial metrics
const revenue = await midday.metrics.revenue({
  from: "2024-01-01",
  to: "2024-12-31",
});
```

## PKCE for public clients

For mobile apps, SPAs, or any client that can't securely store secrets, use PKCE (Proof Key for Code Exchange).

### Generate code verifier and challenge

```typescript
function base64UrlEncode(buffer: Uint8Array): string {
  return btoa(String.fromCharCode(...buffer))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

// Generate a random code verifier
function generateCodeVerifier(): string {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64UrlEncode(array);
}

// Create SHA-256 hash for code challenge
async function generateCodeChallenge(verifier: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest("SHA-256", data);
  return base64UrlEncode(new Uint8Array(hash));
}
```

### Include in authorization request

```
https://app.midday.ai/oauth/authorize?
  response_type=code&
  client_id=YOUR_CLIENT_ID&
  redirect_uri=YOUR_REDIRECT_URI&
  scope=transactions.read&
  code_challenge=CHALLENGE&
  code_challenge_method=S256&
  state=STATE
```

### Include verifier in token exchange

```bash
curl -X POST https://api.midday.ai/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "AUTH_CODE",
    "redirect_uri": "YOUR_REDIRECT_URI",
    "client_id": "YOUR_CLIENT_ID",
    "code_verifier": "YOUR_CODE_VERIFIER"
  }'
```

Note: Public clients should not send `client_secret`.

## Refreshing tokens

Access tokens expire after 1 hour. Use the refresh token to get new tokens:

```bash
curl -X POST https://api.midday.ai/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "mid_rt_xxxxx",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET"
  }'
```

Refresh tokens are valid for 30 days and rotate on each use.

## Revoking tokens

Allow users to disconnect your app by revoking tokens:

```bash
curl -X POST https://api.midday.ai/v1/oauth/revoke \
  -H "Content-Type: application/json" \
  -d '{
    "token": "mid_at_xxxxx",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET"
  }'
```

## Example: TypeScript implementation

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Managing your app

### Edit application settings

1. Go to [Settings → Developer](https://app.midday.ai/settings/developer)
2. Click on your OAuth application
3. Update settings as needed
4. Click **Save**

### Regenerate client secret

If your client secret is compromised:

1. Go to your OAuth application settings
2. Click **Regenerate secret**
3. Update your app with the new secret immediately

The old secret stops working immediately.

### Monitor usage

Track your application's usage:

- Number of authorized users
- API calls per day
- Error rates

## Security best practices

### Store secrets securely

- Never commit `client_secret` to version control
- Use environment variables or secret management
- Rotate secrets if potentially exposed

### Validate state parameter

Always verify the `state` parameter matches what you sent:

```typescript
if (req.query.state !== storedState) {
  throw new Error("Invalid state parameter");
}
```

### Use HTTPS everywhere

- All redirect URIs must use HTTPS (except localhost for development)
- Never send tokens over unencrypted connections

### Request minimal scopes

Only request the scopes your app actually needs. Users are more likely to authorize apps that request limited access.

### Handle token expiration

- Check for 401 responses and refresh tokens automatically
- Implement proper token refresh logic
- Handle refresh token expiration gracefully

## Next steps

- [OAuth Scopes Reference](/docs/oauth-scopes) — See all available scopes
- [OAuth API Endpoints](/docs/oauth-api-endpoints) — Technical endpoint reference
- [App Review Process](/docs/app-review-process) — Get your app verified
- [API Reference](/docs/api-reference) — Full API documentation

---

### Apps Website Src App Docs Content Categories Reference (`apps/website/src/app/docs/content/categories-reference.mdx`)

---
title: Transaction Categories Reference
description: Complete guide to all transaction categories in Midday and how they affect your financial metrics.
section: banking
order: 5
---

Midday uses a structured category system to organize your transactions. Understanding these categories helps you maintain accurate books and ensures your financial metrics are calculated correctly.

## How categories work

Every transaction in Midday is assigned to a category. The AI automatically categorizes transactions when they sync, and you can adjust categories at any time. Categories are organized in a two-level hierarchy: parent categories contain more specific subcategories.

Categories affect your financial reports:

- **Revenue categories** count toward your income
- **Expense categories** count toward your burn rate and reduce profit
- **Excluded categories** are intentionally left out of burn rate calculations to prevent double-counting

## Excluded categories

Two categories are marked as "excluded" and won't count toward your burn rate:

- **Credit Card Payment**: When you pay your credit card bill from your checking account, this transaction is excluded. Why? Because the individual purchases on your credit card already count as expenses. Counting the payment would double-count those expenses.

- **Internal Transfer**: Moving money between your own accounts isn't an expense—it's just reorganizing your cash. These transfers are excluded from burn rate.

This exclusion logic is automatic. You don't need to do anything special.

---

## Revenue

Income flowing into your business. These transactions increase your revenue metrics.

| Subcategory | Use for |
|-------------|---------|
| Income | General income that doesn't fit other categories |
| Product Sales | Revenue from selling physical or digital products |
| Service Revenue | Revenue from providing services |
| Consulting Revenue | Revenue from consulting engagements |
| Subscription Revenue | Recurring subscription income (SaaS, memberships) |
| Interest Income | Interest earned on bank accounts or investments |
| Other Income | Miscellaneous income sources |
| Customer Refunds | Refunds issued to customers (reduces revenue) |
| Chargebacks & Disputes | Payment disputes and chargebacks (reduces revenue) |

**Tip**: Customer Refunds and Chargebacks reduce your net revenue. Categorize them correctly for accurate revenue reporting.

---

## Cost of Goods Sold

Direct costs of producing what you sell. Important for businesses selling products.

| Subcategory | Use for |
|-------------|---------|
| Inventory | Purchasing inventory for resale |
| Manufacturing | Production and manufacturing costs |
| Shipping (Inbound) | Freight and shipping to receive goods |
| Duties & Customs | Import duties and customs fees |

**Tip**: COGS is separated from operating expenses because it scales with sales. This helps you understand your gross margin.

---

## Sales & Marketing

Spending to acquire customers and grow your business.

| Subcategory | Use for |
|-------------|---------|
| Marketing | General marketing activities |
| Advertising | Paid ads (Google, Meta, LinkedIn, etc.) |
| Website | Website hosting, domains, design services |
| Events | Conferences, trade shows, hosted events |
| Promotional Materials | Swag, printed materials, branded items |

---

## Operations

Day-to-day costs of running your business.

| Subcategory | Use for |
|-------------|---------|
| Office Supplies | Supplies, stationery, small equipment |
| Rent | Office or workspace rent |
| Utilities | Electricity, water, gas |
| Facilities Expenses | Cleaning, maintenance, repairs |
| Equipment | Larger equipment purchases |
| Internet & Telephone | Internet service, phone plans |
| Shipping | Outbound shipping and postage |

---

## Professional Services

External expertise and protection.

| Subcategory | Use for |
|-------------|---------|
| Professional Services Fees | Accountants, lawyers, consultants |
| Contractors | Freelancers and contract workers |
| Insurance | Business insurance premiums |

---

## Human Resources

Team-related costs.

| Subcategory | Use for |
|-------------|---------|
| Salary | Employee salaries and wages |
| Training | Training programs, courses, certifications |
| Benefits | Health insurance, retirement contributions, perks |

---

## Travel & Entertainment

Business travel and client entertainment.

| Subcategory | Use for |
|-------------|---------|
| Travel | Flights, hotels, car rentals, transportation |
| Meals | Business meals and client entertainment |
| Activity | Team activities, client entertainment |

**Tip**: Keep receipts for travel and meals—these are commonly audited expense categories.

---

## Technology

Software and digital subscriptions.

| Subcategory | Use for |
|-------------|---------|
| Software | SaaS subscriptions, software licenses |
| Non-Software Subscriptions | Publications, memberships, non-software services |

**Tip**: The AI is particularly good at recognizing software subscriptions from merchants like AWS, Vercel, GitHub, Notion, etc.

---

## Banking & Finance

Financial transactions and fees.

| Subcategory | Use for | Excluded? |
|-------------|---------|-----------|
| Transfer | General transfers | No |
| Credit Card Payment | Paying your credit card bill | **Yes** |
| Banking Fees | Bank service charges, wire fees | No |
| Loan Proceeds | Money received from loans | No |
| Loan Principal Repayment | Paying back loan principal | No |
| Interest Expense | Interest paid on loans or credit | No |
| Payouts | Payouts from payment processors (Stripe, PayPal) | No |
| Processor Fees | Payment processing fees | No |
| Fees | Other financial fees | No |

**Important**: Credit Card Payment is excluded from burn rate. When you sync both your checking account and credit card, you'll see both the individual credit card purchases AND the payment from checking. The payment is excluded to prevent counting those expenses twice.

---

## Assets

Capital expenditures and prepaid items.

| Subcategory | Use for |
|-------------|---------|
| Fixed Assets | Major equipment, vehicles, property |
| Prepaid Expenses | Annual subscriptions paid upfront, prepaid rent |

**Tip**: Fixed assets may be depreciated over time for accounting purposes. Your accountant can help with this.

---

## Liabilities & Debt

Obligations and deferred items.

| Subcategory | Use for |
|-------------|---------|
| Leases | Lease payments for equipment or property |
| Deferred Revenue | Payments received for services not yet delivered |

---

## Taxes & Government

Tax payments and government fees.

| Subcategory | Use for |
|-------------|---------|
| VAT/GST/PST/QST Payments | Value-added tax and sales tax payments |
| Sales & Use Tax Payments | Sales tax remittances |
| Income Tax Payments | Business income tax payments |
| Payroll Tax Remittances | Employer payroll tax payments |
| Employer Taxes | Employer portion of employment taxes |
| Government Fees | Business licenses, permits, filing fees |

**Tip**: Accurate tax categorization makes year-end much easier. Review these categories before exporting to your accountant.

---

## Owner / Equity

Transactions between the business and owners.

| Subcategory | Use for |
|-------------|---------|
| Owner Draws | Money taken out by owners |
| Capital Investment | Money put into the business by owners |
| Charitable Donations | Donations to charitable organizations |

**Tip**: Owner draws aren't business expenses—they're distributions of profit. Categorize them correctly to avoid inflating your burn rate.

---

## System

Special categories for uncategorized and internal transactions.

| Subcategory | Use for | Excluded? |
|-------------|---------|-----------|
| Uncategorized | Transactions not yet categorized | No |
| Other | Transactions that don't fit elsewhere | No |
| Internal Transfer | Transfers between your own accounts | **Yes** |

**Important**: Internal Transfer is excluded from burn rate. Moving money from checking to savings isn't spending—it's just moving your cash around.

---

## How categories affect your metrics

| Metric | What counts |
|--------|-------------|
| Revenue | All transactions in Revenue categories |
| Expenses | All expense categories except excluded ones |
| Burn Rate | Monthly expenses (excludes Credit Card Payment and Internal Transfer) |
| Profit | Revenue minus expenses |
| Profit Margin | Profit divided by revenue |

## Changing categories

If the AI gets a category wrong:

1. Click on the transaction
2. Select the correct category
3. Save

The AI learns from your corrections and improves over time for similar transactions.

## Bulk categorization

For multiple similar transactions:

1. Filter transactions (e.g., by merchant name)
2. Select multiple transactions
3. Choose **Categorize** from bulk actions
4. Apply the category to all selected

[Learn more about auto-categorization →](/docs/auto-categorization)

---

### Apps Website Src App Docs Content Check Runway (`apps/website/src/app/docs/content/check-runway.mdx`)

---
title: Check Your Runway
description: See how long your cash will last based on current spending and what affects this calculation.
section: reports
order: 3
---

Runway tells you how many months you can keep operating based on your current cash and spending rate. It's one of the most important metrics for any founder.

## Why runway matters

Knowing your runway helps you:

- **Plan ahead**: Make hiring and investment decisions with confidence
- **Time fundraising**: Start conversations before you're desperate
- **Manage stress**: Replace uncertainty with data
- **Communicate with stakeholders**: Give investors and advisors clear visibility

## View your runway

1. Go to [Overview](https://app.midday.ai)
2. Find the Runway card

You'll see:

- **Current cash**: Total balance across connected cash accounts
- **Monthly burn rate**: Average monthly net outflow
- **Runway**: How many months of cash you have

## How runway is calculated

The formula is straightforward:

```
Runway (months) = Cash Balance ÷ Average Monthly Burn Rate
```

### Example

| Input | Value |
|-------|-------|
| Cash on hand | $120,000 |
| Monthly burn rate | $20,000 |
| **Runway** | **6 months** |

### What counts as cash

Only liquid assets count toward your cash balance:

| Account Type | Examples | Included in Cash? |
|-------------|----------|-------------------|
| Depository | Checking, Savings | Yes |
| Other Asset | Money Market, Treasury | Yes |
| Credit | Credit Cards | No |
| Loan | Business Loans | No |

**Why credit cards and loans don't count**: They represent borrowed money, not cash you have. A $50,000 credit limit doesn't extend your runway—it's debt you'll need to repay.

### Burn rate calculation

Burn rate is the **median** of your monthly expenses over the last 3 completed months. Using the median (middle value) instead of a simple average means that a single outlier month — like a large tax payment or annual subscription — won't dramatically skew your runway.

Excluded from burn rate:
- Credit card payments (prevents double-counting)
- Internal transfers between accounts

[Learn more about burn rate →](/docs/view-burn-rate)

## Runway benchmarks

| Runway | Status | Action |
|--------|--------|--------|
| 12+ months | Comfortable | Keep building, you have time to experiment |
| 6-12 months | Healthy | Normal operating range for growing businesses |
| 3-6 months | Caution | Time to focus on extending runway |
| Under 3 months | Critical | Immediate action needed |

These benchmarks depend on your situation:

- **Profitable businesses** can operate with less runway since they're cash-flow positive
- **Pre-revenue startups** should aim for 12-18 months to have time to find product-market fit
- **Seasonal businesses** need enough runway to cover slow periods

## Multi-currency considerations

If you have bank accounts in different currencies:

1. Midday converts all balances to your base currency
2. Exchange rates are updated regularly
3. Set your base currency in [Settings](https://app.midday.ai/settings)

Your runway is shown in your base currency for a unified view.

## Disabled accounts

Accounts you've disabled in settings don't count toward:
- Cash balance (runway numerator)
- Burn rate (if transactions come from disabled accounts)

Only disable accounts you want to exclude from your financial picture entirely.

## Special runway states

### Infinite runway

If your burn rate is zero or negative (you're profitable), runway shows as infinite. You're making more than you spend—your cash grows over time.

### Zero or missing runway

This means either:
- Cash balance is zero
- No expense data to calculate burn rate
- All cash accounts are disabled

Check that your accounts are connected and transactions are syncing.

## Improving runway

You have two levers: increase cash or decrease burn.

### Increase cash
- **Accelerate receivables**: Follow up on unpaid invoices, offer early payment discounts
- **Raise prices**: Even small increases compound over time
- **Find new revenue**: New customers, upsells, new products
- **Fundraise**: External capital extends runway (but comes with dilution)

### Decrease burn
- **Audit subscriptions**: Cancel unused software and services
- **Negotiate contracts**: Ask vendors for discounts, especially for annual payment
- **Delay non-essential spending**: Push discretionary purchases to later
- **Review team costs**: Your largest expense is likely people

### The math of runway extension

| Scenario | Cash | Monthly Burn | Runway |
|----------|------|--------------|--------|
| Current | $60,000 | $10,000 | 6 months |
| Reduce burn by $2,000 | $60,000 | $8,000 | 7.5 months |
| Add $20,000 cash | $80,000 | $10,000 | 8 months |
| Both | $80,000 | $8,000 | 10 months |

Small changes in burn rate can significantly extend runway.

## Monitoring runway

- **Weekly glance**: Quick check that nothing unexpected happened
- **Monthly review**: Deep dive into what's changing and why
- **Quarterly planning**: Use runway to inform hiring, investment, and strategy

### Set up alerts

Configure notifications to alert you when:
- Cash drops below a threshold
- Burn rate spikes unexpectedly
- Runway falls below a comfortable level

[Configure notifications →](/docs/notification-settings)

## Runway in conversations

### With investors
Runway is the first metric investors ask about. Being able to say "We have 8 months of runway and are growing 15% monthly" is much better than "We have money for a while."

### With your team
Sharing runway (at least directionally) helps everyone make better decisions about spending and prioritization.

### With advisors
Runway context helps advisors give relevant advice. Different strategies apply at 12 months vs. 3 months.

## Related metrics

- [Burn rate](/docs/view-burn-rate): The denominator in runway calculation
- [Cash balance](/docs/understanding-metrics): The numerator in runway calculation
- [Revenue and profit](/docs/view-revenue-profit): Affects your burn rate

[Understand all metrics →](/docs/understanding-metrics)

---

### Apps Website Src App Docs Content Connect Bank Account (`apps/website/src/app/docs/content/connect-bank-account.mdx`)

---
title: Connect Bank Account
description: Link your bank accounts to automatically sync transactions.
section: banking
order: 1
---

Connecting your bank is the foundation of Midday. Once linked, your transactions sync automatically, giving you a complete picture of your business finances.

## How it works

Midday uses secure, read-only connections to sync your transactions. We partner with leading banking providers to support over 20,000 banks worldwide.

- **Read-only access**: We can only view transactions, never move money
- **Bank-level security**: Connections use OAuth or secure credential encryption
- **Automatic sync**: New transactions appear within minutes

## Connect your bank

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. Click **Connect bank**
3. Search for your bank by name
4. Select your bank and follow the authentication flow
5. Choose which accounts to connect

Most banks use secure OAuth, which means you authenticate directly with your bank—Midday never sees your login credentials.

## What gets synced

Once connected, Midday pulls:

- **Transaction history**: Usually 90 days of past transactions
- **Account balance**: Updated with each sync
- **Transaction details**: Date, amount, merchant name, category

New transactions typically appear within a few minutes of clearing your bank.

## Multiple accounts and currencies

You can connect multiple bank accounts, even from different banks:

- Business checking and savings accounts
- Credit card accounts
- Accounts in different currencies

Each account syncs independently. Multi-currency transactions are converted using the exchange rate from the transaction date.

## Troubleshooting

### Bank not found

If you can't find your bank:

1. Try searching by the full official name
2. Check if your bank has a separate business banking platform
3. Use [manual import via CSV](/docs/import-transactions-csv) as a fallback

### Connection issues

If your bank connection stops syncing:

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. Click **Reconnect** on the affected account
3. Re-authenticate with your bank

Some banks require re-authentication every 90 days for security.

### Missing transactions

If transactions are missing:

1. Allow a few minutes for sync to complete
2. Check your bank's online portal to confirm the transaction has cleared
3. Try clicking **Sync** to force an immediate refresh

## Supported banks

We support 20,000+ banks across North America, Europe, and other regions. Coverage includes:

- All major US and Canadian banks
- Most European banks (UK, Germany, France, Netherlands, Sweden, and more)
- Many banks in Australia, Singapore, and other markets

---

### Apps Website Src App Docs Content Connect Fortnox (`apps/website/src/app/docs/content/connect-fortnox.mdx`)

---
title: Connect Fortnox
description: Export your transactions to Fortnox accounting software.
section: export
order: 4
---

Connect Midday to Fortnox to export your categorized transactions. Popular with Swedish businesses using Fortnox for accounting and year-end preparation.

## How it works

Once connected:

1. Transactions arrive from your bank and receipts are matched automatically
2. Ready transactions appear in the **Review** tab (the export queue)
3. Export to Fortnox — categories map to your BAS accounts
4. Receipts export as verification files
5. Exported transactions are marked as **Exported** in Midday

[Learn about the full export flow →](/docs/export-transactions-csv)

## Connect Fortnox

1. Go to [Apps](https://app.midday.ai/apps)
2. Find **Fortnox**
3. Click **Connect**
4. Sign in to your Fortnox account
5. Authorize Midday to access your organization

You'll need admin access to your Fortnox account to authorize the connection.

## Category mapping

Midday categories map to Fortnox account codes following Swedish BAS accounting standards.

### Default mappings

Common mappings include:

| Midday Category | Fortnox Account (BAS) |
|-----------------|----------------------|
| Revenue | 3000-series |
| Office Supplies | 5460 |
| Software | 5420 |
| Travel | 5800-series |
| Professional Services | 6500-series |

### Customize mappings

1. Go to [Apps](https://app.midday.ai/apps)
2. Click **Settings** on your Fortnox connection
3. Review and adjust category mappings
4. Save changes

Work with your accountant to ensure mappings match your chart of accounts.

## What gets exported

### Transaction data

- Transaction date
- Amount (in original currency)
- Description/merchant name
- Category (mapped to Fortnox account)
- VAT/moms if applicable

### Attachments

- Attached receipts export as verification files
- Supports PDF, images, and common document formats
- Files appear in Fortnox linked to the transaction

## Exporting transactions

1. Go to the **Review** tab in [Transactions](https://app.midday.ai/transactions)
2. Select the transactions you want to export (or export all)
3. Click **Export** and choose **Fortnox**
4. Transactions and receipts are sent to Fortnox with your category mappings and verification files
5. Exported transactions are marked as **Exported** in Midday

## Swedish accounting considerations

### VAT/Moms handling

- Midday tracks VAT amounts when available
- Maps to appropriate Fortnox VAT codes
- Review VAT settings in your Fortnox configuration

### BAS account structure

Fortnox uses the Swedish BAS standard:

- **1xxx**: Assets
- **2xxx**: Liabilities and equity
- **3xxx**: Revenue
- **4xxx**: Cost of goods sold
- **5-6xxx**: Operating expenses
- **7xxx**: Depreciation and financial items
- **8xxx**: Financial income/expenses

Midday's category structure aligns with this for straightforward mapping.

### Fiscal year

Ensure your fiscal year settings match between Midday and Fortnox for accurate reporting.

## Troubleshooting

### Export errors

If transactions fail to export:

- Check that the Fortnox connection is active in [Apps](https://app.midday.ai/apps)
- Verify transactions are categorized (uncategorized transactions can't export)
- Check category mappings are complete

### Mapping errors

If transactions appear in the wrong account:

1. Review category mappings in Fortnox settings
2. Re-categorize affected transactions in Midday
3. Export again

### Reconnecting

If the connection expires:

1. Go to [Apps](https://app.midday.ai/apps)
2. Click **Reconnect** on Fortnox
3. Re-authorize with your Fortnox credentials

Fortnox connections may need periodic re-authorization for security.

## Disconnect

To remove the connection:

1. Go to [Apps](https://app.midday.ai/apps)
2. Click **Disconnect** on Fortnox
3. Confirm disconnection

Previously exported data remains in Fortnox. New transactions won't export.

## Working with your accountant

### Before connecting

- Discuss category mappings with your accountant
- Agree on which accounts transactions should flow to
- Set up the appropriate BAS accounts in Fortnox

### Ongoing workflow

1. Transactions arrive and receipts are matched automatically
2. Review the **Review** tab — it's your export queue
3. Export to Fortnox when ready (weekly, monthly, or as needed)
4. Your accountant reviews in Fortnox for final adjustments

### Year-end

- Export all transactions before year-end close
- All transactions and receipts are in Fortnox
- Your accountant has clean, organized data
- Reduces year-end preparation time significantly

## Tips

- **Categorize promptly**: Keep transactions categorized so exports are ready
- **Attach receipts**: They export automatically as verification
- **Review mappings**: Work with your accountant to verify
- **Export regularly**: Weekly or monthly keeps Fortnox current

[Back to all integrations →](/docs/apps-overview)

---

### Apps Website Src App Docs Content Connect Gmail (`apps/website/src/app/docs/content/connect-gmail.mdx`)

---
title: Connect Gmail
description: Automatically capture receipts from your Gmail inbox.
section: inbox
order: 2
---

Connect your Gmail account to automatically capture receipts and invoices from your email. No manual forwarding required.

## How it works

Once connected, Midday:

1. Scans your inbox for emails that look like receipts and invoices
2. Uses AI to identify transaction-related emails
3. Extracts attachments and relevant data
4. Sends them to your Midday inbox for matching

The scanning happens automatically on a regular schedule.

## Connect Gmail

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Click **Connect Gmail**
3. Sign in to your Google account
4. Review the permissions and click **Allow**

You'll be redirected back to Midday once connected.

## What emails are scanned

### Emails we process

The AI looks for emails that are likely receipts or invoices:

- **Order confirmations**: Amazon, Shopify stores, e-commerce purchases
- **Digital receipts**: Uber, Lyft, Airbnb, travel bookings
- **Subscription receipts**: SaaS tools, streaming services, subscriptions
- **Invoice emails**: Bills, vendor invoices, payment confirmations
- **Bank notifications**: Transaction alerts with receipt information

### Emails we ignore

The AI filters out non-receipt emails:

- **Marketing emails**: Newsletters, promotions, sales announcements
- **Social notifications**: LinkedIn, Twitter, Facebook updates
- **Personal emails**: Messages from friends and family
- **Spam**: Anything in your spam folder
- **General business emails**: Meeting invites, project discussions

The AI is trained to distinguish between "someone sent me a receipt" and "someone sent me an email."

## What Midday accesses

Midday has **read-only** access to your Gmail:

| Permission | What it allows | What we do |
|------------|---------------|------------|
| Read emails | View email content | Scan for receipt-like patterns |
| Read attachments | View email attachments | Extract PDFs and images |

### What we cannot do

- Send emails from your account
- Delete or modify emails
- Access contacts or calendar
- Read drafts

## Privacy and security

### Data handling

- **Selective processing**: Only emails matching receipt patterns are processed
- **Secure storage**: Extracted receipts are encrypted in your Midday account
- **No training data**: Your emails are not used to train AI models
- **Team isolation**: Only your team can access your receipts

### What we store

- Extracted receipt/invoice files (PDFs, images)
- Extracted data (merchant, amount, date)
- Email metadata for deduplication (subject line, date, sender)

We don't store the full content of non-receipt emails.

### Compliance

- OAuth 2.0 authentication (we never see your password)
- Google Cloud verified application
- GDPR compliant data handling

## Scan settings

### Scan frequency

Midday scans your inbox periodically for new receipts. You can trigger a manual scan:

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Click **Sync now** next to Gmail

### Historical receipts

When first connected, Midday looks back at recent emails (typically the last 30-90 days) to capture existing receipts. After that, it monitors for new emails.

## Troubleshooting

### Missing receipts

If some receipts aren't being captured:

- **Check spam/trash**: We don't scan those folders
- **Unusual format**: Some receipt formats aren't recognized
- **Manual forward**: Forward specific receipts to your inbox email address
- **Wait for sync**: New emails may take a few minutes to process

### Too many non-receipts

If non-receipt emails are appearing:

- The AI sometimes misidentifies promotional emails as receipts
- Delete unwanted items from your Midday inbox
- Add the sender to your blocklist in [Inbox → Settings](https://app.midday.ai/inbox/settings)

### Duplicate receipts

Duplicates can occur if you:

- Forward manually and have auto-capture on
- Receive the same receipt to multiple email addresses

Midday attempts to deduplicate, but delete any that slip through.

### Connection issues

If the connection stops working:

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Click **Reconnect** next to Gmail
3. Re-authorize the connection

Google may require re-authorization periodically for security.

## Disconnect Gmail

To stop automatic capture:

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Click **Disconnect** next to Gmail

### Revoke from Google

You can also revoke access from your [Google Account settings](https://myaccount.google.com/permissions):

1. Find "Midday" in your connected apps
2. Click **Remove Access**

Receipts already in your Midday inbox remain; no new receipts will sync.

## Tips

- **Use alongside forwarding**: Belt and suspenders approach for maximum capture
- **Review weekly**: Check your Midday inbox for unmatched items
- **Confirm matches**: The AI learns from your confirmations
- **Use blocklist**: Block senders of unwanted non-receipt emails

[Set up receipt matching →](/docs/receipt-matching)

---

### Apps Website Src App Docs Content Connect Outlook (`apps/website/src/app/docs/content/connect-outlook.mdx`)

---
title: Connect Outlook
description: Automatically capture receipts from your Outlook inbox.
section: inbox
order: 3
---

Connect your Outlook account to automatically capture receipts and invoices from your email. Works with Outlook.com, Microsoft 365, and work/school accounts.

## How it works

Once connected, Midday:

1. Scans your Outlook inbox for emails that look like receipts and invoices
2. Uses AI to identify transaction-related emails
3. Extracts attachments and relevant data
4. Sends them to your Midday inbox for matching

The scanning happens automatically on a regular schedule.

## Connect Outlook

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Click **Connect Outlook**
3. Sign in to your Microsoft account
4. Review the permissions and click **Accept**

### Supported account types

- **Outlook.com**: Personal Microsoft accounts
- **Microsoft 365**: Business and enterprise accounts
- **Work/School**: Organizational accounts (if permitted by your IT admin)

## What emails are scanned

### Emails we process

The AI looks for emails that are likely receipts or invoices:

- **Order confirmations**: E-commerce purchases, online orders
- **Digital receipts**: Uber, travel bookings, services
- **Subscription receipts**: SaaS tools, subscriptions, memberships
- **Invoice emails**: Bills, vendor invoices, payment confirmations
- **Bank notifications**: Transaction alerts with receipt information

### Emails we ignore

The AI filters out non-receipt emails:

- **Marketing emails**: Newsletters, promotions, announcements
- **Social notifications**: LinkedIn, Teams messages
- **Personal emails**: Non-business correspondence
- **Spam/Junk**: Anything in your junk folder
- **General business emails**: Meeting invites, project discussions

### Focused Inbox consideration

If you use Outlook's Focused Inbox feature, receipts may land in either Focused or Other. Midday scans both.

## What Midday accesses

Midday has **read-only** access to your Outlook:

| Permission | What it allows | What we do |
|------------|---------------|------------|
| Read mail | View email content | Scan for receipt-like patterns |
| Read attachments | View email attachments | Extract PDFs and images |

### What we cannot do

- Send emails from your account
- Delete or modify emails
- Access contacts or calendar
- Read OneDrive files

## Privacy and security

### Data handling

- **Selective processing**: Only emails matching receipt patterns are processed
- **Secure storage**: Extracted receipts are encrypted in your Midday account
- **No training data**: Your emails are not used to train AI models
- **Team isolation**: Only your team can access your receipts

### What we store

- Extracted receipt/invoice files (PDFs, images)
- Extracted data (merchant, amount, date)
- Email metadata for deduplication (subject line, date, sender)

We don't store the full content of non-receipt emails.

### Enterprise considerations

For Microsoft 365 and work accounts:

- Your organization's policies may affect what apps can connect
- Check with your IT admin if you have connection issues
- Admin consent may be required for organizational accounts

## Scan settings

### Scan frequency

Midday scans your inbox periodically. You can trigger a manual scan:

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Click **Sync now** next to Outlook

### Historical receipts

When first connected, Midday looks back at recent emails (typically 30-90 days) to capture existing receipts.

## Troubleshooting

### Missing receipts

If some receipts aren't being captured:

- **Check junk folder**: We don't scan junk mail
- **Check Other inbox**: If using Focused Inbox, check both views
- **Manual forward**: Forward specific receipts to your inbox email address
- **Wait for sync**: New emails may take a few minutes to process

### Permission issues

If connection fails:

- **Wrong account**: Ensure you're signing in with the correct Microsoft account
- **Organization policy**: Your IT admin may need to allow third-party apps
- **Admin consent**: Work accounts may require admin approval

Contact your IT admin if using a work account and experiencing issues.

### Connection expired

Microsoft may require periodic re-authorization:

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Click **Reconnect** next to Outlook
3. Sign in again to reauthorize

## Disconnect Outlook

To stop automatic capture:

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Click **Disconnect** next to Outlook

### Revoke from Microsoft

You can also revoke access from your [Microsoft Account settings](https://account.live.com/consent/Manage):

1. Find "Midday" in your connected applications
2. Click **Remove permissions**

For work/school accounts, use the [Microsoft 365 app permissions page](https://myapps.microsoft.com/).

Receipts already in your Midday inbox remain; no new receipts will sync.

## Tips

- **Works great with forwarding**: Use both for comprehensive capture
- **Review weekly**: Check your Midday inbox for unmatched items
- **Confirm matches**: The AI learns from your confirmations
- **Use blocklist**: Block unwanted senders in inbox settings

[Set up receipt matching →](/docs/receipt-matching)

---

### Apps Website Src App Docs Content Connect Quickbooks (`apps/website/src/app/docs/content/connect-quickbooks.mdx`)

---
title: Connect QuickBooks
description: Export your transactions to QuickBooks accounting software.
section: export
order: 3
---

Connect Midday to QuickBooks Online to export your categorized transactions to your accounting software.

## How it works

Once connected:

1. Transactions arrive from your bank and receipts are matched automatically
2. Ready transactions appear in the **Review** tab (the export queue)
3. Export to QuickBooks — categories map to your chart of accounts
4. Exported transactions are marked as **Exported** in Midday

[Learn about the full export flow →](/docs/export-transactions-csv)

## Connect QuickBooks

1. Go to [Apps](https://app.midday.ai/apps)
2. Find **QuickBooks**
3. Click **Connect**
4. Sign in to your QuickBooks account
5. Select your QuickBooks company
6. Authorize Midday

## Category mapping

Midday categories map to QuickBooks accounts:

- Match each Midday category to the appropriate QuickBooks account
- Mapping is configured once during setup
- You can update mappings in settings

Work with your accountant to ensure the mappings match your chart of accounts.

## What gets exported

- Transaction date
- Amount
- Description/merchant name
- Category (mapped to QuickBooks account)
- Attached receipts (as files)

## Exporting transactions

1. Go to the **Review** tab in [Transactions](https://app.midday.ai/transactions)
2. Select the transactions you want to export (or export all)
3. Click **Export** and choose **QuickBooks**
4. Transactions and receipts are sent to QuickBooks with your category mappings applied
5. Exported transactions are marked as **Exported** in Midday

## Disconnect

To remove the connection:

1. Go to [Apps](https://app.midday.ai/apps)
2. Click **Disconnect** on QuickBooks

Previously exported data remains in QuickBooks.

## Tips

- Work with your accountant to set up correct category mappings
- Review the first few exported transactions in QuickBooks to verify accuracy
- Keep receipts attached in Midday—they export with transactions
- Export regularly (weekly or monthly) to keep QuickBooks current

---

### Apps Website Src App Docs Content Connect Slack (`apps/website/src/app/docs/content/connect-slack.mdx`)

---
title: Connect Slack
description: Share receipts and documents from Slack directly to your Midday inbox.
section: integrations
order: 5
---

Connect your Slack workspace to share receipts and documents directly to your Midday inbox. Great for teams where receipts often come through Slack.

## How it works

Once connected:

1. Share any file to a designated Slack channel
2. The file appears in your Midday inbox
3. AI processes and attempts to match it to transactions
4. Review and confirm matches as usual

## Connect Slack

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Find Slack in the integrations section
3. Click **Connect Slack**
4. Authorize Midday to access your workspace
5. Select or create a channel for receipts

## Choose a channel

You can use:

- **An existing channel**: Use a channel your team already shares receipts in
- **A new channel**: Create a dedicated `#receipts` or `#expenses` channel

Only files shared in the connected channel will be sent to Midday.

## Sharing files

Once connected, sharing is simple:

1. Open Slack
2. Go to the connected channel
3. Share a file (drag and drop, or use the attachment button)
4. The file automatically appears in your Midday inbox

### Supported files

- Images (photos of receipts)
- PDFs (digital receipts, invoices)
- Common document formats

### What happens next

1. The file appears in your [Inbox](https://app.midday.ai/inbox)
2. AI extracts merchant, amount, and date
3. Matching against transactions begins
4. You'll see it as "pending" or "suggested match"

## Use cases

### Team expense capture

Multiple team members can share receipts to one channel. All receipts flow into the same Midday inbox for processing.

### Mobile receipt capture

Take a photo of a receipt on your phone, share it to Slack, and it's in Midday—no need to open the Midday app.

### Vendor communication

If vendors send receipts or invoices via Slack, forward them to the connected channel.

## Multiple workspaces

If you have multiple Slack workspaces, you can connect one per Midday team. Switch teams in Midday to manage different workspace connections.

## Disconnect Slack

To stop the integration:

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Find the Slack connection
3. Click **Disconnect**

Files already in your inbox remain; new files won't sync.

## Troubleshooting

### Files not appearing in Midday

- Verify the file was shared in the connected channel
- Check that the Slack connection is still active
- Try sharing a different file to test

### Can't find the channel

- Make sure you have permission to access the channel
- The channel must be one Midday was authorized to access

### Reconnecting

If the connection breaks:

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Click **Reconnect** next to Slack
3. Re-authorize the connection

## Privacy and permissions

- Midday only receives files shared in the connected channel
- We can't read other channels or direct messages
- The integration is read-only—Midday can't post to Slack
- You can disconnect anytime

---

### Apps Website Src App Docs Content Connect Xero (`apps/website/src/app/docs/content/connect-xero.mdx`)

---
title: Connect Xero
description: Export your transactions to Xero accounting software.
section: export
order: 2
---

Connect Midday to Xero to export your categorized transactions to your accounting software.

## How it works

Once connected:

1. Transactions arrive from your bank and receipts are matched automatically
2. Ready transactions appear in the **Review** tab (the export queue)
3. Export to Xero — categories map to your chart of accounts
4. Exported transactions are marked as **Exported** in Midday

[Learn about the full export flow →](/docs/export-transactions-csv)

## Connect Xero

1. Go to [Apps](https://app.midday.ai/apps)
2. Find **Xero**
3. Click **Connect**
4. Sign in to your Xero account
5. Authorize Midday to access your organization

## Category mapping

Midday categories map to Xero accounts:

- Match each Midday category to the appropriate Xero account code
- Mapping is configured once during setup
- You can update mappings in settings

Work with your accountant to ensure the mappings match your chart of accounts.

## What gets exported

- Transaction date
- Amount
- Description/merchant name
- Category (mapped to Xero account)
- Attached receipts (as files)

## Exporting transactions

1. Go to the **Review** tab in [Transactions](https://app.midday.ai/transactions)
2. Select the transactions you want to export (or export all)
3. Click **Export** and choose **Xero**
4. Transactions and receipts are sent to Xero with your category mappings applied
5. Exported transactions are marked as **Exported** in Midday

## Disconnect

To remove the connection:

1. Go to [Apps](https://app.midday.ai/apps)
2. Click **Disconnect** on Xero

Previously exported data remains in Xero.

## Tips

- Work with your accountant to set up correct category mappings
- Review the first few exported transactions in Xero to verify accuracy
- Keep receipts attached in Midday—they export with transactions
- Export regularly (weekly or monthly) to keep Xero current

---

### Apps Website Src App Docs Content Create Invoice (`apps/website/src/app/docs/content/create-invoice.mdx`)

---
title: Create an Invoice
description: Send professional invoices to your clients.
section: invoicing
order: 1
---

Midday makes it easy to create professional invoices that look great and help you get paid faster.

## Create a new invoice

1. Go to [Invoices](https://app.midday.ai/invoices)
2. Click **Create invoice**
3. Fill in the details (see below)
4. Choose **Send** to email immediately, or **Save as draft**

## Invoice details

### Customer

Select an existing customer or create a new one. You'll need:

- Customer name
- Email address (for sending)
- Optional: Address, VAT number, contact person

### Line items

Add what you're billing for:

- **Description**: What the item or service is
- **Quantity**: Number of units
- **Price**: Unit price
- **Tax**: Optional tax rate

Add as many line items as needed. The total calculates automatically.

### Invoice settings

- **Invoice number**: Auto-generated, but you can customize
- **Issue date**: When the invoice is created
- **Due date**: When payment is expected
- **Currency**: Defaults to your base currency, can change per invoice
- **Notes**: Optional message to your customer

## Sending options

### Send by email

Select **Create & Send** as your delivery method to email the invoice directly to your customer. They receive a professional email that includes:

- Your logo and company name
- The invoice amount, due date, and invoice number
- A button to view the full invoice online
- A PDF attachment (if enabled)
- A link to pay online (if Stripe is connected)

#### Preview the email before sending

When **Create & Send** is selected, an email icon appears in the toolbar. Click it to open a preview of the exact email your customer will receive. You can customize the subject line, heading, body text, and button label directly from the preview — just click on any text to edit. Changes are saved to your template automatically.

[Learn more about email settings →](/docs/invoice-settings#email-settings)

### Share a link

Copy the invoice link to share via any channel. Your customer can view and pay online.

### Download PDF

Download a PDF to send manually or keep for your records.

## After sending

Track invoice status from the [Invoices](https://app.midday.ai/invoices) list:

- **Draft**: Not sent yet
- **Sent**: Delivered to customer
- **Viewed**: Customer opened the invoice
- **Paid**: Payment received
- **Overdue**: Past due date, not yet paid

[Learn more about tracking invoice status →](/docs/track-invoice-status)

## Tips

- Set up your logo and business details in [Settings → Invoice](https://app.midday.ai/settings) for professional branding
- Use [recurring invoices](/docs/set-up-recurring-invoice) for retainer clients
- Enable [online payments](/docs/accept-online-payments) to get paid faster

---

### Apps Website Src App Docs Content Create Project (`apps/website/src/app/docs/content/create-project.mdx`)

---
title: Create a Project
description: Set up projects to organize your tracked time.
section: time-tracking
order: 1
---

Projects help you organize time tracking by client, engagement, or type of work.

## Create a project

1. Go to [Time Tracking](https://app.midday.ai/tracker)
2. Click **New project**
3. Enter project details:
   - **Name**: Something recognizable (e.g., "Acme Corp - Website Redesign")
   - **Client**: Optional; link to a customer
   - **Rate**: Hourly rate for billing
   - **Budget**: Optional estimate in hours
4. Click **Create**

## Project settings

### Billable vs. non-billable

Mark projects as billable or non-billable:

- **Billable**: Time can be invoiced at the project rate
- **Non-billable**: Internal work, not invoiced (e.g., admin, learning)

### Hourly rate

Set a rate for the project:

- Used when creating invoices from tracked time
- Can differ from your default rate
- Override per-entry if needed

### Budget / Estimate

Set an expected total hours:

- Track progress toward the estimate
- Get alerts when approaching the budget
- Helpful for fixed-price projects

## Link to a customer

Connecting a project to a customer:

- Streamlines invoicing (customer info pre-fills)
- Lets you see total time for a customer across projects
- Keeps things organized

## Edit a project

1. Go to [Time Tracking](https://app.midday.ai/tracker)
2. Find the project
3. Click **Edit** (or the settings icon)
4. Make changes
5. Save

## Archive or delete

When a project is complete:

- **Archive**: Hides from active list but keeps history
- **Delete**: Removes project and all time entries (careful!)

Archive is usually the better choice.

## Tips

- Use consistent naming: "Client - Project" works well
- Set realistic budgets based on past similar work
- Review projects monthly; archive completed ones

---

### Apps Website Src App Docs Content Customer Portal (`apps/website/src/app/docs/content/customer-portal.mdx`)

---
title: Customer Portal
description: Give your customers a self-service portal to view invoices and make payments.
section: customers
order: 2
---

The customer portal gives your customers a dedicated page to view their invoices, make payments, and access their history.

## How it works

Each customer can access their portal via a unique link:

1. They see all their invoices in one place
2. They can pay outstanding invoices
3. They can download past invoices as PDF

## Enable the customer portal

The portal is available automatically for customers with invoices.

### Share the portal link

1. Go to [Customers](https://app.midday.ai/customers)
2. Select a customer
3. Copy their portal link
4. Share it with them

### Include in invoices

Portal links are included in invoice emails automatically.

## What customers see

### Invoice list

All invoices for this customer:

- Invoice number and date
- Amount and status
- Pay button for unpaid invoices

### Invoice details

Click any invoice to see:

- Full invoice with line items
- Payment status
- Download as PDF

### Payment

For unpaid invoices (when online payments are enabled):

- Click Pay
- Enter card details via Stripe
- Receive confirmation

## Customization

The portal uses your branding:

- Your logo
- Your business name
- Colors match your invoice settings

## Security

- Each portal link is unique and private
- Customers only see their own invoices
- Links can be regenerated if needed

## Tips

- Include the portal link in your email signature
- Mention it when onboarding new clients
- Great for clients who need to access past invoices for their records

---

### Apps Website Src App Docs Content Customize Invoice Template (`apps/website/src/app/docs/content/customize-invoice-template.mdx`)

---
title: Customize Invoice Template
description: Set up your invoice branding and default settings.
section: invoicing
order: 2
---

Customize your invoice template to reflect your brand and save time on each invoice.

## Access template settings

Template settings are available directly in the invoice editor. Click the **gear icon** in the bottom toolbar to open the settings menu, organized into categories:

- **Invoice** — Size, currency, date format, payment terms
- **Tax & Pricing** — Tax, VAT, discounts, decimals, units, QR code
- **Email** — PDF attachment, send copy
- **Payments** — Stripe online payments

You can also manage templates (rename, duplicate, set as default, delete) from the same menu.

## Branding

### Logo

Upload your company logo:

- Appears at the top of every invoice
- Recommended size: 400x200 pixels
- PNG or JPG format

### Colors

Customize accent colors to match your brand.

## Business details

Set your default business information:

- **Company name**: Your legal business name
- **Address**: Your business address
- **Email**: Contact email shown on invoices
- **Phone**: Optional contact phone
- **Website**: Optional website URL

These appear on every invoice automatically.

## Tax settings

### Tax identification

- **VAT number**: Your VAT/GST number
- **Tax ID**: Other tax identification numbers

These appear on invoices for compliance.

### Default tax rate

Set a default tax rate:

- Applied to new invoices automatically
- Can be overridden per invoice
- Set to 0% if you don't charge tax

## Payment details

### Bank details

Add your bank information for wire transfers:

- Bank name
- Account number / IBAN
- Routing number / SWIFT

### Payment terms

Set default payment terms:

- **Due date**: Net 14, Net 30, etc.
- **Late fees**: Optional late payment terms
- **Notes**: Standard payment instructions

## Invoice numbering

Configure invoice number format:

- **Prefix**: Optional prefix (e.g., "INV-")
- **Starting number**: Where to begin numbering
- **Format**: Number of digits

Example: INV-00001, INV-00002, etc.

## Default notes

Add standard text that appears on all invoices:

- Payment instructions
- Thank you message
- Terms and conditions
- Return policy

## Email template

Each template stores its own email content. When you send an invoice via **Create & Send**, the customer receives an email based on these settings.

### Customize the email

Click the email preview button in the invoice editor toolbar (visible when **Create & Send** is selected) to open a live preview. You can edit:

- **Subject line** — The email subject your customer sees in their inbox (defaults to "[Company Name] sent you an invoice")
- **Heading** — The title inside the email body (defaults to "Invoice from [Company Name]")
- **Body text** — The message below the invoice details
- **Button label** — The call-to-action button text (defaults to "View invoice")

The subject line and heading are independent — customizing one does not affect the other. Click directly on any text in the preview to edit it. Changes save automatically to the current template.

### What the email shows

The email automatically includes data from the invoice:

- Your template logo
- Invoice amount and currency
- Due date (using your template's date format and label)
- Invoice number (using your template's label)
- A sign-off with your company name

These values update automatically based on each invoice — you don't need to edit them manually.

### Email options

In the **Email** settings category:

- **Attach PDF** — Include the invoice as a PDF attachment
- **Send copy (BCC)** — Send yourself a copy of every invoice email

## Tips

- Set up your template before creating your first invoice
- Use a professional logo—it makes a big difference
- Include clear payment instructions to get paid faster
- Customize your email template once — it applies to all invoices using that template
- Review and update annually or when business details change

---

### Apps Website Src App Docs Content Desktop App (`apps/website/src/app/docs/content/desktop-app.mdx`)

---
title: Desktop App
description: Use Midday's native desktop app for macOS.
section: getting-started
order: 3
---

Midday is available as a native desktop app for macOS. Get quick access to your finances without opening a browser.

## Download

Download the desktop app from:

**[midday.ai/download](https://midday.ai/download)**

Available for:
- **macOS** (Apple Silicon and Intel)

## Features

The desktop app provides the full Midday experience:

- All dashboard features
- Time tracking with native notifications
- Quick access from your menu bar
- Offline access to recently viewed data
- Native keyboard shortcuts

## Installation

1. Download the `.dmg` file
2. Open the downloaded file
3. Drag Midday to your Applications folder
4. Open Midday from Applications
5. If prompted about an unidentified developer, go to System Preferences → Security & Privacy and click "Open Anyway"

## Sign in

When you first open the app:

1. Click **Sign in**
2. You'll be redirected to sign in via your browser
3. After signing in, you'll be returned to the desktop app
4. Your session stays active until you sign out

## Menu bar

The desktop app runs in your menu bar for quick access:

- Click the Midday icon in the menu bar
- Quick view of today's tracked time
- Start/stop timer without opening the full app

## Time tracking

The desktop app is great for time tracking:

### Start tracking

1. Click the Midday menu bar icon
2. Select a project
3. Click **Start**

Or open the full app and use the timer as usual.

### Notifications

Get notified when:
- You've been tracking for a long time (optional reminder)
- A timer is still running at end of day

Configure notifications in the app settings.

## Keyboard shortcuts

| Action | Shortcut |
|--------|----------|
| Open Midday | `Cmd + Shift + M` |
| Start/Stop Timer | `Cmd + Shift + T` |
| New Invoice | `Cmd + N` |
| Search | `Cmd + K` |

Shortcuts can be customized in Settings → Keyboard.

## Updates

The desktop app updates automatically. When a new version is available:

1. You'll see a notification
2. Click to download and install
3. Restart the app to apply

You can also check for updates manually in the app menu.

## Troubleshooting

### App won't open

If macOS blocks the app:

1. Open **System Preferences → Security & Privacy**
2. Click the **General** tab
3. Click **Open Anyway** next to the Midday message

### Sync issues

If data seems out of sync:

1. Check your internet connection
2. Try signing out and back in
3. The app syncs automatically when online

### High memory usage

If the app uses too much memory:

1. Quit and restart the app
2. Check for updates (newer versions often improve performance)

## Uninstall

1. Quit Midday
2. Open Finder → Applications
3. Drag Midday to Trash
4. Empty Trash

## vs. Web app

Both the desktop and web app provide the same features. Choose based on your preference:

| Feature | Desktop App | Web App |
|---------|-------------|---------|
| Access | Menu bar | Browser tab |
| Notifications | Native OS notifications | Browser notifications |
| Offline | Limited offline access | Requires internet |
| Updates | Auto-updates | Always current |

Many users use both—the desktop app for daily work and time tracking, and the web app when on shared computers.

---

### Apps Website Src App Docs Content Export Transactions Csv (`apps/website/src/app/docs/content/export-transactions-csv.mdx`)

---
title: Export Transactions
description: How the transaction export flow works in Midday.
section: export
order: 1
---

Midday tracks each transaction through a clear lifecycle so you always know what needs attention and what's been exported.

## Transaction statuses

Every transaction has a status that tells you where it is in the flow:

| Status | Meaning |
|--------|---------|
| **No receipt** | Default state. No receipt attached yet. |
| **Receipt found** | Midday found a possible receipt match. Confirm or dismiss it. |
| **Ready to export** | Receipt attached. This transaction is in the export queue. |
| **Exporting** | Export is in progress. |
| **Exported** | Successfully exported. Final state. |
| **Export failed** | Something went wrong. Needs attention before retry. |
| **Excluded** | Excluded from analytics and the export queue. |
| **Archived** | No longer part of active workflows. |

## The export flow

Transactions move through this lifecycle:

1. A transaction arrives from your bank connection (status: **No receipt**)
2. Midday finds a matching receipt automatically (status: **Receipt found**)
3. You confirm the match — or attach a receipt manually (status: **Ready to export**)
4. You export from the Review tab (status: **Exporting** → **Exported**)

If an export fails, the transaction moves to **Export failed** and stays in the Review tab so you can fix the issue and retry.

## Tabs

### All

Shows every transaction across all statuses. Use filters to narrow by status, date, category, account, and more. Excluded and Archived transactions are hidden by default but available via the status filter.

### Review

The export queue. Shows only transactions that are **Ready to export** or have an **Export failed** status. No filters apply here — it always shows the full queue so it can reliably reach zero.

When the Review tab is empty, you're done.

## Exporting

### To accounting software (Xero, QuickBooks, Fortnox)

If you've connected an accounting provider:

1. Go to the **Review** tab
2. Select transactions (or export all)
3. Click **Export** and choose your provider
4. Transactions and receipts are sent to your accounting software

[Connect Xero →](/docs/connect-xero) · [Connect QuickBooks →](/docs/connect-quickbooks) · [Connect Fortnox →](/docs/connect-fortnox)

### To CSV

If you export to a file instead:

1. Go to the **Review** tab (or use filters in **All**)
2. Select transactions
3. Click **Export** → **CSV**
4. Download the file

The CSV includes date, description, amount, category, account, tags, receipt links, and notes.

## Actions

You can manage transactions with these actions:

| Action | What it does |
|--------|-------------|
| **Mark ready** | Moves a transaction to the export queue (Review tab) |
| **Unmark ready** | Removes from the export queue |
| **Mark exported** | Manually marks as exported (use only if exported outside Midday) |
| **Unmark exported** | Moves an exported transaction back to the Review tab |
| **Exclude** | Removes from analytics and the export queue (bulk action) |
| **Include** | Reverses an exclusion |
| **Archive** | Hides from active workflows (bulk action) |

## Tips

- Export regularly (weekly or monthly) to keep your accounting software current
- Attach receipts before exporting — they export with the transaction
- Use the Review tab as your to-do list: when it's empty, you're caught up
- If your accountant needs a file, use CSV export. If they use Xero, QuickBooks, or Fortnox, use the direct integration instead

---

### Apps Website Src App Docs Content Forward Receipts Email (`apps/website/src/app/docs/content/forward-receipts-email.mdx`)

---
title: Forward Receipts via Email
description: Send receipts to your unique Midday inbox email address.
section: inbox
order: 4
---

The simplest way to get receipts into Midday: forward them to your unique inbox email address.

## Get your inbox email

1. Go to [Inbox](https://app.midday.ai/inbox)
2. Click **Settings** or the gear icon
3. Copy your unique inbox email address

It looks like: `inbox-abc123@midday.ai`

## Forward receipts

When you receive a receipt:

1. Forward the email to your inbox address
2. Midday extracts the receipt automatically
3. AI matches it to your transactions

That's it. No apps to open, no files to upload.

## Set up automatic forwarding

### Gmail

1. Open Gmail Settings → Forwarding
2. Add your Midday inbox address
3. Create a filter to auto-forward receipts

### Outlook

1. Open Settings → Mail → Forwarding
2. Enable forwarding to your Midday inbox
3. Or use rules to forward specific emails

### Other email providers

Most email providers support forwarding rules. Search for "auto forward" in your email settings.

## What gets extracted

From forwarded emails, Midday extracts:

- PDF attachments
- Image attachments (receipts, photos)
- Embedded receipt images

Plain text emails without attachments are processed but may have limited data.

## Tips

- Forward receipts the same day for best matching
- Include the original email (not just attachments) for more context
- Works great alongside connected email for maximum capture

---

### Apps Website Src App Docs Content Import Transactions Csv (`apps/website/src/app/docs/content/import-transactions-csv.mdx`)

---
title: Import Transactions from CSV
description: Manually import transactions when bank connection isn't available.
section: banking
order: 3
---

If your bank isn't supported for automatic sync, you can import transactions manually via CSV.

## When to use CSV import

- Your bank isn't in our supported list
- You have transactions from before connecting your bank
- You're migrating from another tool
- You need to import credit card statements

## Prepare your CSV

Your CSV file should include:

- **Date**: Transaction date
- **Description**: Merchant or transaction description
- **Amount**: Transaction amount (positive for income, negative for expenses—or vice versa)

Optional columns:

- Category
- Notes
- Reference number

Most bank exports work directly. Midday maps common formats automatically.

## Import transactions

1. Go to [Transactions](https://app.midday.ai/transactions)
2. Click **Import** or the import icon
3. Select your CSV file
4. Map columns to Midday fields
5. Review the preview
6. Confirm import

## Column mapping

If Midday doesn't recognize your columns automatically:

1. Match each CSV column to a Midday field
2. Specify date format if needed
3. Indicate if amounts are inverted (positive = expense)

## After import

Imported transactions:

- Appear in your transaction list
- Are categorized by AI automatically
- Can be edited like any other transaction
- Won't duplicate if you later connect the same bank

## Tips

- Export from your bank in CSV format, not PDF
- Include as much history as useful (usually 1 year)
- Review categories after import—AI needs to learn your patterns
- Consider connecting the bank after import to avoid future manual work

## Troubleshooting

### Date parsing errors

- Check your date format matches what you selected
- Common formats: MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD

### Duplicate transactions

- Midday checks for duplicates based on date, amount, and description
- Exact duplicates are skipped
- Near-duplicates might import—delete manually if needed

### Missing columns

- Only Date, Description, and Amount are required
- Other data can be added to transactions after import

---

### Apps Website Src App Docs Content Introduction (`apps/website/src/app/docs/content/introduction.mdx`)

---
title: Introduction
description: What is Midday and how it helps you run your business finances.
section: getting-started
order: 1
---

Midday is your business's financial operating system. It brings together banking, invoicing, time tracking, and bookkeeping preparation in one place—so you can spend less time on admin and more time building your business.

## Who is Midday for?

Midday is built for:

- **Solo founders** running a business and wanting real-time financial visibility
- **SMB owners** who need to understand cash flow, runway, and profitability
- **Startups** scaling their operations and preparing for growth or fundraising
- **Small teams** managing multiple clients, projects, and team members

If you're tired of juggling spreadsheets, multiple apps, and manual data entry, Midday brings everything together.

## What makes Midday different

### Automation-first

Transactions sync automatically. Receipts match to transactions automatically. Categories are assigned automatically. You review and refine—not do manual data entry.

### Built for founders

Financial metrics that matter: runway, burn rate, profit margins. Not just transaction lists, but insights into your business health.

### AI-powered

From categorizing transactions to matching receipts to answering questions about your finances, AI handles the tedious work.

### Accountant-ready

When it's time for taxes or year-end, export clean, categorized data to your accountant or directly to accounting software.

## What can you do with Midday?

### Connect your bank accounts

Link your business bank accounts to automatically sync transactions. Midday supports over 20,000 banks worldwide through secure connections. Your transactions flow in automatically, categorized by AI.

[Learn how to connect your bank →](/docs/connect-bank-account)

### Understand your financial metrics

See your revenue, profit, burn rate, and runway at a glance. Midday calculates these in real-time based on your actual transactions—not estimates or manual inputs.

[Understand your metrics →](/docs/understanding-metrics)

### Send professional invoices

Create and send invoices that look great on any device. Set up recurring invoices for retainer clients, schedule delivery in advance, and accept online payments via Stripe.

[Learn how to create invoices →](/docs/create-invoice)

### Track your time

Use the built-in timer or log hours manually. Organize work by project and client. When you're ready, turn tracked time into invoices with one click.

[Learn how to track time →](/docs/track-time-timer)

### Match receipts automatically

Forward receipts to your Midday inbox or connect Gmail/Outlook. Our AI matches documents to transactions automatically—no more manual filing at tax time.

[Learn how receipt matching works →](/docs/receipt-matching)

### Store and organize documents

Keep contracts, statements, and business documents in the Vault. Everything searchable, organized, and connected to your transactions.

[Learn about the Vault →](/docs/vault-file-storage)

### Export for your accountant

When it's time for taxes or year-end, export clean, categorized transactions to CSV or directly to Xero, QuickBooks, or Fortnox.

[Learn how to export →](/docs/export-transactions-csv)

### Ask the AI assistant

Ask questions about your finances in plain language: "What was my revenue last quarter?" or "Show my top expenses this month." Get instant answers from your real data.

[Learn about the assistant →](/docs/using-assistant)

## Getting started

The fastest way to get started:

1. [Sign up for Midday](https://app.midday.ai)
2. [Connect your primary bank account](/docs/connect-bank-account)
3. [Review your transactions and categories](/docs/auto-categorization)

Or, follow the [Quick Start guide](/docs/quick-start) for a 5-minute walkthrough.

## For developers

Midday has a full API and developer platform for technical founders:

- **REST API**: Access your data programmatically with API keys
- **OAuth 2.0**: Build apps that integrate with Midday for other users
- **MCP**: Manage your business from AI tools like Claude, Cursor, and ChatGPT — 80+ tools

[API documentation →](/docs/api-reference) | [Build an OAuth app →](/docs/build-oauth-app) | [MCP setup →](/docs/assistant-mcp)

## Getting help

- **Ask the assistant**: Built-in AI can answer most questions
- **Documentation**: You're in the right place
- **Email support**: [support@midday.ai](mailto:support@midday.ai)
- **Community**: Join us on [Discord](https://go.midday.ai/discord)

---

### Apps Website Src App Docs Content Invite Team Member (`apps/website/src/app/docs/content/invite-team-member.mdx`)

---
title: Invite Team Members
description: Add team members to your Midday account.
section: team
order: 1
---

Invite colleagues to your Midday team so they can help manage finances, track time, or create invoices.

## Invite a team member

1. Go to [Settings → Members](https://app.midday.ai/settings/members)
2. Click **Invite**
3. Enter their email address
4. Select their role
5. Send the invitation

They'll receive an email with instructions to join your team.

## Roles

### Owner

Full access to everything:

- Manage billing and subscription
- Invite and remove members
- Access all financial data
- Delete the team

There's always at least one owner.

### Member

Access to day-to-day features:

- View transactions
- Create and send invoices
- Track time
- View reports

Cannot manage team settings or billing.

## Pending invitations

View outstanding invitations:

1. Go to [Settings → Members](https://app.midday.ai/settings/members)
2. See pending invites
3. Resend or cancel if needed

Invitations expire after 7 days.

## Remove a member

1. Go to [Settings → Members](https://app.midday.ai/settings/members)
2. Find the member
3. Click **Remove**
4. Confirm

Removed members immediately lose access. Their past activity (time entries, invoices created) remains.

## Change a role

1. Go to [Settings → Members](https://app.midday.ai/settings/members)
2. Find the member
3. Click to change their role
4. Save

## Tips

- Keep the number of owners limited
- Use member roles for day-to-day users
- Review member access periodically

---

### Apps Website Src App Docs Content Invoice Settings (`apps/website/src/app/docs/content/invoice-settings.mdx`)

---
title: Invoice Settings
description: Configure your invoice templates, branding, and payment details.
section: invoicing
order: 5
---

Customize your invoices to match your brand and include all the details your customers need. These settings apply to all invoices you create.

## Access invoice settings

Invoice settings are available directly in the invoice editor. Click the **gear icon** in the bottom toolbar to open the settings menu. Settings are organized into four categories:

- **Invoice** — Size, currency, date format, payment terms
- **Tax & Pricing** — Sales tax, VAT, line item tax, discount, decimals, units, QR code
- **Email** — PDF attachment, send copy (BCC)
- **Payments** — Accept online payments via Stripe

Each category opens a sub-menu with the relevant options. Changes are saved automatically to your current template.

## Branding

### Logo

Add your company logo to appear on all invoices:

1. Click **Upload logo**
2. Select an image file (PNG, JPG, or SVG recommended)
3. Your logo appears in the header of all invoices

**Tips for logos**:
- Use a high-resolution image (at least 200px wide)
- Transparent PNG works best
- Square or horizontal logos display well

### Company details

Your business information appears on every invoice:

- **Company name**: Your legal business name
- **Address**: Your business address
- **Email**: Contact email for invoice queries
- **Phone**: Optional contact number
- **Website**: Optional company website
- **VAT/Tax ID**: Your tax identification number

Set these in [Settings](https://app.midday.ai/settings) under company information.

## Invoice numbering

### Automatic numbering

Midday auto-generates invoice numbers sequentially:

- Format: INV-0001, INV-0002, etc.
- Increments automatically with each invoice
- Ensures no duplicate numbers

### Custom prefix

Customize the invoice number prefix:

1. Go to invoice settings
2. Change the prefix (e.g., "MID-" instead of "INV-")
3. All new invoices use this prefix

### Manual override

You can change individual invoice numbers when creating:

1. Create or edit an invoice
2. Click on the invoice number
3. Enter your custom number
4. Ensure it's unique

## Payment details

### Bank transfer details

Add your bank account information for wire/transfer payments:

- **Bank name**: Your bank's name
- **Account number**: Your account number
- **Routing number**: Bank routing/sort code
- **IBAN**: For international transfers
- **SWIFT/BIC**: For international transfers

These details appear on the invoice so customers know where to send payment.

### Multiple payment methods

You can include:

- Bank transfer details
- Online payment via Stripe (if connected)
- Other payment instructions in the notes

## Online payments (Stripe)

Connect Stripe to let customers pay invoices online with a credit card.

### Connect Stripe

1. Go to invoice settings
2. Click **Connect Stripe**
3. Sign in to your Stripe account (or create one)
4. Complete the connection

### How it works

Once connected:

- All invoices include a **Pay** button
- Customers pay via Stripe's secure checkout
- The invoice automatically marks as paid
- Funds deposit to your bank account (minus Stripe fees)

### Fees

Stripe charges standard processing fees (typically 2.9% + $0.30 per transaction). Midday doesn't charge additional payment fees.

[Full guide: Accept online payments →](/docs/accept-online-payments)

## Default settings

### Payment terms

Set the default due date for new invoices:

- **Due on receipt**: Payment expected immediately
- **Net 7**: Due 7 days from invoice date
- **Net 15**: Due 15 days from invoice date
- **Net 30**: Due 30 days from invoice date (most common)
- **Net 60**: Due 60 days from invoice date
- **Custom**: Set your own number of days

You can change this per invoice when needed.

### Currency

Set your default invoice currency:

- New invoices use this currency by default
- You can change currency per invoice
- Multiple currencies are supported

### Tax/VAT

Configure default tax settings:

- **Tax rate**: Default percentage to apply
- **Tax name**: What to call it (VAT, GST, Sales Tax, etc.)
- **Tax ID display**: Show your tax ID on invoices

These can be overridden per invoice or per line item.

## Invoice templates

### Built-in templates

Midday includes professional templates that work for most businesses. Your branding (logo, colors, details) applies automatically.

### Template customization

Customize how your invoices look:

- **Color scheme**: Match your brand colors
- **Layout preferences**: Adjust what information displays where
- **Footer content**: Custom text at the bottom

## Notes and terms

### Default notes

Add text that appears on every invoice:

- Thank you messages
- Payment instructions
- Terms and conditions
- Late payment policies

Example:
> Thank you for your business. Payment is due within 30 days. Late payments may incur a 1.5% monthly fee.

### Per-invoice notes

You can add or edit notes on individual invoices for customer-specific messages.

## Products/Services catalog

Save commonly used line items:

1. Go to [Invoices → Products](https://app.midday.ai/invoices/products)
2. Add products or services you invoice frequently
3. Include name, description, and default price
4. Select from the catalog when creating invoices

This saves time if you invoice the same items repeatedly.

## Email settings

### What the email includes

When you send an invoice via **Create & Send**, the customer receives an email with:

- Your invoice template logo (or the Midday logo if none is set)
- A heading (customizable, defaults to "Invoice from [Company Name]")
- The invoice amount, due date, and invoice number
- A **View invoice** button linking to the online invoice
- A body message with your sign-off
- A PDF attachment (if enabled in settings)
- A **Pay** button (if Stripe is connected)

All labels (like "Due Date" and "Invoice No") match the labels configured in your invoice template.

### Preview and customize the email

When **Create & Send** is selected as your delivery method, an email preview button appears in the invoice editor toolbar. Click it to open a preview of the exact email your customer will receive.

From the preview, you can click on any text to edit it directly:

- **Subject line** — The email subject your customer sees in their inbox (defaults to "[Company Name] sent you an invoice")
- **Heading** — The title inside the email body (defaults to "Invoice from [Company Name]")
- **Body text** — Customize the message below the invoice details
- **Button label** — Change the call-to-action text (defaults to "View invoice")

The subject line and heading are independent — customizing one does not affect the other.

Changes save automatically to your current template, so they apply to all future invoices using that template.

### PDF attachment

Toggle **Attach PDF** in the **Email** settings category to include or exclude the invoice PDF as an email attachment. When enabled, the email preview shows the attached filename.

### Send copy (BCC)

Toggle **Send copy (BCC)** to receive a copy of every invoice email sent to customers. The copy is sent to your account email address.

## Reminders

### Automatic reminders

Set up reminders for unpaid invoices:

- Before due date (e.g., 3 days before)
- On due date
- After due date (e.g., 7 days overdue)

Configure in invoice settings.

### Manual reminders

Send reminders manually from any unpaid invoice:

1. Open the invoice
2. Click **Send reminder**
3. Customer receives a reminder email

## Best practices

1. **Complete your profile first**: Set up branding and payment details before sending invoices
2. **Use consistent numbering**: Don't skip numbers or use duplicates
3. **Include payment details**: Make it easy for customers to pay
4. **Enable online payments**: Customers pay faster when it's easy
5. **Set appropriate terms**: Net 30 is standard; adjust for your industry
6. **Add clear notes**: Include any terms or late payment policies

## Troubleshooting

### Logo not displaying

- Check file format (PNG, JPG, SVG)
- Ensure file size isn't too large
- Try re-uploading

### Invoice numbering reset

- Invoice numbers are unique and don't reset
- Contact support if you need to adjust the sequence

### Payment details not showing

- Verify bank details are entered in settings
- Check that the invoice isn't set to online-only payment

[Create an invoice →](/docs/create-invoice)
[Set up recurring invoices →](/docs/set-up-recurring-invoice)

---

### Apps Website Src App Docs Content Invoice Tracked Time (`apps/website/src/app/docs/content/invoice-tracked-time.mdx`)

---
title: Invoice Tracked Time
description: Turn your time entries into a professional invoice.
section: time-tracking
order: 3
---

Convert tracked time directly into invoices. No manual calculation needed.

## Create an invoice from time

1. Go to [Time Tracking](https://app.midday.ai/tracker)
2. Select the project or time entries to invoice
3. Click **Create invoice** (or select the entries to include)
4. Review the pre-filled invoice:
   - Customer from project
   - Line items from time entries
   - Amounts calculated from hours × rate
5. Edit if needed
6. Send

## What's included

Each time entry becomes a line item:

- **Description**: From your entry description
- **Hours**: Duration of the entry
- **Rate**: From the project or entry
- **Amount**: Hours × rate

Multiple entries can be combined or kept separate.

## Grouping options

When creating the invoice, choose how to present time:

- **Detailed**: Each entry as a separate line item
- **Grouped**: Combine entries by date or description
- **Summary**: Single line with total hours

Pick what makes sense for your client relationship.

## Partial invoicing

You don't have to invoice all time at once:

1. Select only the entries to invoice
2. Remaining time stays available for future invoices

Useful for:

- Billing portions of ongoing work
- Separating billable phases
- Matching client purchase orders

## Mark time as invoiced

When you create an invoice from time:

- Entries are marked as invoiced
- They won't appear in future "uninvoiced" filters
- You can still see them in the project history

## Tips

- Review time entries before invoicing—clean up descriptions
- Confirm rates are correct for the project
- Add a note on the invoice if billing terms differ from tracked time

---

### Apps Website Src App Docs Content Manage Subscription (`apps/website/src/app/docs/content/manage-subscription.mdx`)

---
title: Manage Your Subscription
description: Upgrade, change plans, or manage billing for your Midday account.
section: account
order: 1
---

Manage your Midday subscription and billing settings.

## View your plan

1. Go to [Settings → Billing](https://app.midday.ai/settings/billing)
2. See your current plan and usage

## Upgrade to Pro

1. Go to [Settings → Billing](https://app.midday.ai/settings/billing)
2. Click **Upgrade**
3. Select your plan
4. Enter payment details
5. Confirm

Your new features are available immediately.

## Change plans

1. Go to [Settings → Billing](https://app.midday.ai/settings/billing)
2. Click **Change plan**
3. Select a new plan
4. Confirm

When upgrading, you're charged the prorated difference. When downgrading, credit is applied to your next billing cycle.

## Update payment method

1. Go to [Settings → Billing](https://app.midday.ai/settings/billing)
2. Click **Payment method**
3. Enter new card details
4. Save

## View invoices

1. Go to [Settings → Billing](https://app.midday.ai/settings/billing)
2. See your billing history
3. Download any invoice as PDF

## Cancel subscription

1. Go to [Settings → Billing](https://app.midday.ai/settings/billing)
2. Click **Cancel subscription**
3. Confirm

You'll retain access until the end of your billing period. Your data is preserved if you decide to resubscribe.

## Questions about billing

Contact [support@midday.ai](mailto:support@midday.ai) for:

- Questions about charges
- Special pricing for non-profits
- Enterprise plans

---

### Apps Website Src App Docs Content Multi Currency (`apps/website/src/app/docs/content/multi-currency.mdx`)

---
title: Multi-Currency
description: How Midday handles multiple currencies and exchange rates.
section: banking
order: 7
---

Midday supports businesses operating in multiple currencies. Connect accounts in different currencies, track international transactions, and see unified reports in your base currency.

## Base currency

Your base currency is the primary currency for your business. All reports and metrics are displayed in this currency.

### Set your base currency

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. Find **Base Currency**
3. Select your currency from the dropdown
4. Save

Choose the currency you use for:
- Most of your revenue
- Tax reporting
- Day-to-day operations

### When to change base currency

Only change your base currency if your business fundamentally shifts operations. Changing it affects how all historical data is displayed in reports.

## Multi-currency accounts

You can connect bank accounts in any currency Midday supports.

### Connecting accounts in different currencies

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. Click **Connect bank**
3. Select your bank and authenticate
4. Choose which accounts to connect

Each account retains its native currency. A EUR account shows EUR transactions and balances.

### Viewing account balances

In your accounts list, each account shows:
- Balance in the account's native currency
- Converted balance in your base currency (for reference)

## Exchange rates

Midday uses market exchange rates to convert currencies.

### How conversion works

When displaying multi-currency data in reports:

1. **Transactions**: Converted using the exchange rate on the transaction date
2. **Balances**: Converted using the current exchange rate
3. **Reports**: All amounts converted to your base currency

### Exchange rate sources

Midday uses reliable market rates updated daily. Rates are for display purposes and may differ slightly from your bank's rates.

## Reports with multiple currencies

All reports display in your base currency for easy comparison.

### Revenue and profit

- Revenue from all currencies is converted and summed
- Expenses in foreign currencies are converted
- Profit shows the combined result in base currency

### Burn rate and runway

- Calculated using converted amounts
- Gives you one unified view of cash consumption
- Runway uses total cash across all currencies

### Transaction list

In the transaction list, you can:
- See the original amount and currency
- Filter by currency
- View the converted amount in base currency

## Invoicing in multiple currencies

Create invoices in any currency your customers prefer.

### Setting invoice currency

1. Create a new invoice
2. Select the customer
3. Choose the invoice currency
4. Enter amounts in that currency

The invoice displays in the selected currency. When paid, the revenue converts to your base currency for reports.

### Customer currency preferences

When you add a customer, you can set their default currency. New invoices for that customer will use this currency automatically.

## Tips for multi-currency businesses

### Organize by currency

Use tags to organize transactions by currency or region:
- `EUR transactions`
- `US operations`
- `UK clients`

### Monitor exchange rate impact

If you have significant foreign currency holdings, exchange rate changes affect your reported cash position. Review regularly.

### Match currencies when possible

Where practical, invoice customers in currencies that match your expenses in that region. This creates a natural hedge.

### Keep records

For tax purposes, you may need to track original currency amounts. Midday stores both the original and converted amounts for every transaction.

## Supported currencies

Midday supports all major world currencies including:

- USD, EUR, GBP, JPY, CHF
- CAD, AUD, NZD
- SEK, NOK, DKK
- And many more

The available currencies depend on your connected banks and payment processors.

---

### Apps Website Src App Docs Content Notification Settings (`apps/website/src/app/docs/content/notification-settings.mdx`)

---
title: Notification Settings
description: Control what notifications you receive from Midday.
section: account
order: 2
---

Manage your notification preferences to stay informed without being overwhelmed.

## Access notification settings

1. Go to [Settings → Notifications](https://app.midday.ai/settings/notifications)
2. Toggle notifications on or off
3. Save changes

## Notification types

### Invoice notifications

- **Invoice paid**: When a customer pays an invoice
- **Invoice viewed**: When a customer opens an invoice
- **Invoice overdue**: When an invoice passes its due date

### Transaction notifications

- **Large transactions**: Alerts for transactions above a threshold
- **New transactions**: Daily or weekly summaries of new transactions

### Bank notifications

- **Connection issues**: When a bank connection needs attention
- **Low balance**: When account balance drops below a threshold

### Team notifications

- **New member joined**: When an invited member accepts
- **Member activity**: Summaries of team activity

## Delivery methods

Choose how to receive notifications:

- **Email**: Traditional email notifications
- **In-app**: Notifications within Midday

## Frequency settings

For some notifications, set frequency:

- **Real-time**: As events happen
- **Daily digest**: Once per day summary
- **Weekly digest**: Weekly summary

## Tips

- Enable invoice paid notifications to know immediately when you get paid
- Set low balance alerts to avoid overdrafts
- Use digests for less urgent items to reduce inbox noise
- Review settings periodically as your needs change

---

### Apps Website Src App Docs Content Oauth Api Endpoints (`apps/website/src/app/docs/content/oauth-api-endpoints.mdx`)

---
title: OAuth API Endpoints
description: Technical reference for Midday OAuth 2.0 endpoints.
section: developer
order: 4
---

Complete technical reference for Midday's OAuth 2.0 implementation. These endpoints follow the [OAuth 2.0 RFC 6749](https://tools.ietf.org/html/rfc6749) and [PKCE RFC 7636](https://tools.ietf.org/html/rfc7636) specifications.

## Base URLs

| Environment | URL |
|-------------|-----|
| Authorization | `https://app.midday.ai/oauth` |
| Token & API | `https://api.midday.ai/v1` |

## Authorization endpoint

Initiates the OAuth flow by redirecting users to log in and authorize your app.

```
GET https://app.midday.ai/oauth/authorize
```

### Request parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `response_type` | string | Yes | Must be `code` |
| `client_id` | string | Yes | Your application's client ID |
| `redirect_uri` | string | Yes | URI to redirect after authorization (must be registered) |
| `scope` | string | Yes | Space-separated list of scopes |
| `state` | string | Recommended | Opaque value for CSRF protection |
| `code_challenge` | string | PKCE | Base64-URL-encoded SHA-256 hash of code verifier |
| `code_challenge_method` | string | PKCE | Must be `S256` |

### Example request

```
https://app.midday.ai/oauth/authorize?
  response_type=code&
  client_id=mid_client_abc123&
  redirect_uri=https://yourapp.com/callback&
  scope=transactions.read%20invoices.read&
  state=xyz789&
  code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
  code_challenge_method=S256
```

### Success response

Redirects to your `redirect_uri` with:

| Parameter | Description |
|-----------|-------------|
| `code` | Authorization code (valid for 10 minutes) |
| `state` | Same value you sent (verify this!) |

```
https://yourapp.com/callback?code=AUTH_CODE_HERE&state=xyz789
```

### Error response

Redirects to your `redirect_uri` with:

| Parameter | Description |
|-----------|-------------|
| `error` | Error code |
| `error_description` | Human-readable description |
| `state` | Same value you sent |

```
https://yourapp.com/callback?error=access_denied&error_description=User%20denied%20access&state=xyz789
```

### Error codes

| Code | Description |
|------|-------------|
| `invalid_request` | Missing or invalid parameter |
| `unauthorized_client` | Client not authorized for this grant type |
| `access_denied` | User denied authorization |
| `invalid_scope` | Invalid or unknown scope |
| `server_error` | Internal server error |

---

## Token endpoint

Exchange authorization codes for access tokens, or refresh existing tokens.

```
POST https://api.midday.ai/v1/oauth/token
```

### Content types

Accepts both:
- `application/json`
- `application/x-www-form-urlencoded`

### Authorization code grant

Exchange an authorization code for tokens.

#### Request body

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `grant_type` | string | Yes | Must be `authorization_code` |
| `code` | string | Yes | Authorization code from callback |
| `redirect_uri` | string | Yes | Same URI used in authorization |
| `client_id` | string | Yes | Your application's client ID |
| `client_secret` | string | Confidential clients | Your client secret |
| `code_verifier` | string | PKCE | Original code verifier |

#### Example request (confidential client)

```bash
curl -X POST https://api.midday.ai/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "AUTH_CODE",
    "redirect_uri": "https://yourapp.com/callback",
    "client_id": "mid_client_abc123",
    "client_secret": "mid_secret_xyz789"
  }'
```

#### Example request (public client with PKCE)

```bash
curl -X POST https://api.midday.ai/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "AUTH_CODE",
    "redirect_uri": "https://yourapp.com/callback",
    "client_id": "mid_client_abc123",
    "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
  }'
```

#### Success response

```json
{
  "access_token": "mid_at_xxxxxxxxxxxxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "mid_rt_xxxxxxxxxxxxx",
  "scope": "transactions.read invoices.read"
}
```

| Field | Description |
|-------|-------------|
| `access_token` | Token for API requests |
| `token_type` | Always `Bearer` |
| `expires_in` | Seconds until expiration (3600 = 1 hour) |
| `refresh_token` | Token to get new access tokens |
| `scope` | Granted scopes (space-separated) |

### Refresh token grant

Get a new access token using a refresh token.

#### Request body

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `grant_type` | string | Yes | Must be `refresh_token` |
| `refresh_token` | string | Yes | Current refresh token |
| `client_id` | string | Yes | Your application's client ID |
| `client_secret` | string | Confidential clients | Your client secret |
| `scope` | string | No | Request subset of original scopes |

#### Example request

```bash
curl -X POST https://api.midday.ai/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "mid_rt_xxxxxxxxxxxxx",
    "client_id": "mid_client_abc123",
    "client_secret": "mid_secret_xyz789"
  }'
```

#### Response

Same format as authorization code grant. The refresh token may rotate (new token returned).

### Token endpoint errors

```json
{
  "error": "invalid_grant",
  "error_description": "The authorization code has expired"
}
```

| Error | Description |
|-------|-------------|
| `invalid_request` | Missing required parameter |
| `invalid_client` | Invalid client credentials |
| `invalid_grant` | Invalid, expired, or used code/token |
| `unauthorized_client` | Client not authorized for grant type |
| `unsupported_grant_type` | Grant type not supported |

---

## Revocation endpoint

Revoke an access token or refresh token.

```
POST https://api.midday.ai/v1/oauth/revoke
```

### Request body

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `token` | string | Yes | Token to revoke |
| `client_id` | string | Yes | Your application's client ID |
| `client_secret` | string | Confidential clients | Your client secret |

### Example request

```bash
curl -X POST https://api.midday.ai/v1/oauth/revoke \
  -H "Content-Type: application/json" \
  -d '{
    "token": "mid_at_xxxxxxxxxxxxx",
    "client_id": "mid_client_abc123",
    "client_secret": "mid_secret_xyz789"
  }'
```

### Response

Always returns success, even if token was already invalid:

```json
{
  "success": true
}
```

---

## Rate limits

OAuth endpoints have specific rate limits to prevent abuse:

| Endpoint | Limit |
|----------|-------|
| `/oauth/authorize` | 20 requests per 15 minutes per IP |
| `/oauth/token` | 20 requests per 15 minutes per IP |
| `/oauth/revoke` | 20 requests per 15 minutes per IP |

Exceeding limits returns `429 Too Many Requests`.

---

## Token lifetimes

| Token type | Lifetime | Notes |
|------------|----------|-------|
| Authorization code | 10 minutes | Single use |
| Access token | 1 hour | Use refresh token to renew |
| Refresh token | 30 days | Rotates on use |

---

## PKCE implementation

PKCE adds security for public clients (mobile apps, SPAs).

### 1. Generate code verifier

Create a random string (43-128 characters, URL-safe):

```typescript
function base64UrlEncode(buffer: Uint8Array): string {
  return btoa(String.fromCharCode(...buffer))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

function generateCodeVerifier(): string {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64UrlEncode(array);
}
```

### 2. Create code challenge

SHA-256 hash of the verifier, base64-URL encoded:

```typescript
async function generateCodeChallenge(verifier: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest("SHA-256", data);
  return base64UrlEncode(new Uint8Array(hash));
}
```

### 3. Use in flow

1. Store `code_verifier` securely (session storage)
2. Send `code_challenge` in authorization request
3. Send `code_verifier` in token exchange

---

## Security considerations

### State parameter

Always use and validate the `state` parameter:

```typescript
// Generate
const state = crypto.randomUUID();
sessionStorage.setItem("oauth_state", state);

// Validate on callback
const storedState = sessionStorage.getItem("oauth_state");
if (callbackState !== storedState) {
  throw new Error("State mismatch - possible CSRF attack");
}
```

### Redirect URI validation

- Register all redirect URIs in your app settings
- Use exact match validation (no wildcards)
- Always use HTTPS in production

### Token storage

- Store tokens securely (encrypted, server-side preferred)
- Never expose tokens in URLs or logs
- Clear tokens on logout

### Client secret protection

- Never include client secrets in client-side code
- Use environment variables on servers
- Rotate secrets if compromised

---

## Error handling examples

### Handle authorization errors

```typescript
app.get("/callback", (req, res) => {
  const { error, error_description, code, state } = req.query;
  
  if (error) {
    console.error(`OAuth error: ${error} - ${error_description}`);
    return res.redirect("/connect?error=" + encodeURIComponent(error as string));
  }
  
  // Verify state
  if (state !== req.session.oauthState) {
    return res.status(400).send("Invalid state");
  }
  
  // Exchange code for tokens
  // ...
});
```

### Handle token errors

```typescript
async function exchangeCode(code: string) {
  const response = await fetch("https://api.midday.ai/v1/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      grant_type: "authorization_code",
      code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
    }),
  });
  
  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Token error: ${error.error} - ${error.error_description}`);
  }
  
  return response.json();
}
```

### Handle refresh failures

```typescript
async function refreshTokens(refreshToken: string) {
  try {
    const response = await fetch("https://api.midday.ai/v1/oauth/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        grant_type: "refresh_token",
        refresh_token: refreshToken,
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET,
      }),
    });
    
    const tokens = await response.json();
    
    if (tokens.error) {
      // Refresh token expired or revoked
      // Redirect user to re-authorize
      return null;
    }
    
    return tokens;
  } catch (error) {
    console.error("Refresh failed:", error);
    return null;
  }
}
```

---

## Using the SDK with OAuth tokens

Once you have an access token, use the Midday SDK for API requests:

```typescript
import { Midday } from "@midday-ai/sdk";

const midday = new Midday({
  token: accessToken, // OAuth access token
});

// List transactions
const transactions = await midday.transactions.list({
  pageSize: 50,
});

// Get invoices
const invoices = await midday.invoices.list({
  statuses: ["unpaid", "overdue"],
});

// Get financial metrics
const profit = await midday.metrics.profit({
  from: "2024-01-01",
  to: "2024-12-31",
});
```

See the [SDK documentation](https://github.com/midday-ai/midday-ts) for all available methods.

---

## Related

- [Build an OAuth App](/docs/build-oauth-app) — Getting started guide
- [OAuth Scopes Reference](/docs/oauth-scopes) — Available permissions
- [App Review Process](/docs/app-review-process) — Get your app verified
- [API Reference](/docs/api-reference) — Full API documentation

---

### Apps Website Src App Docs Content Oauth Scopes (`apps/website/src/app/docs/content/oauth-scopes.mdx`)

---
title: OAuth Scopes Reference
description: Complete list of OAuth scopes for Midday API access.
section: developer
order: 3
---

Scopes define what data and actions your OAuth application can access. Request only the scopes your app needs—users are more likely to authorize apps with limited, focused permissions.

## How scopes work

- Scopes are requested during the authorization flow
- Users see the requested permissions before authorizing
- Your app can only access data allowed by granted scopes
- Scopes cannot be upgraded without re-authorization

## Scope format

Scopes follow the pattern `resource.permission`:

- `transactions.read` — Read transaction data
- `invoices.write` — Create, update, delete invoices

## Available scopes

### Transactions

| Scope | Description |
|-------|-------------|
| `transactions.read` | View transactions, categories, and attachments |
| `transactions.write` | Update transaction categories, notes, and attachments |

**Use cases**: Financial dashboards, expense tracking, receipt management

### Invoices

| Scope | Description |
|-------|-------------|
| `invoices.read` | View invoices and their status |
| `invoices.write` | Create, update, send, and delete invoices |

**Use cases**: Invoice automation, payment reminders, accounting sync

### Customers

| Scope | Description |
|-------|-------------|
| `customers.read` | View customer information |
| `customers.write` | Create, update, and delete customers |

**Use cases**: CRM integration, customer portals, contact sync

### Bank Accounts

| Scope | Description |
|-------|-------------|
| `bank-accounts.read` | View connected bank accounts and balances |
| `bank-accounts.write` | Manage bank account settings |

**Use cases**: Cash position monitoring, balance alerts

### Documents

| Scope | Description |
|-------|-------------|
| `documents.read` | View documents in the vault |
| `documents.write` | Upload and organize documents |

**Use cases**: Document management, backup tools, OCR integrations

### Inbox

| Scope | Description |
|-------|-------------|
| `inbox.read` | View inbox items (uploaded receipts, pending matches) |
| `inbox.write` | Process and match inbox items |

**Use cases**: Receipt processing, automated matching

### Tracker Projects

| Scope | Description |
|-------|-------------|
| `tracker-projects.read` | View time tracking projects |
| `tracker-projects.write` | Create, update, and delete projects |

**Use cases**: Project management integration, resource planning

### Tracker Entries

| Scope | Description |
|-------|-------------|
| `tracker-entries.read` | View time entries |
| `tracker-entries.write` | Create, update, and delete time entries |

**Use cases**: Time tracking apps, timesheets, billing automation

### Teams

| Scope | Description |
|-------|-------------|
| `teams.read` | View team information and settings |
| `teams.write` | Update team settings |

**Use cases**: Team management, onboarding tools

### Users

| Scope | Description |
|-------|-------------|
| `users.read` | View user information within the team |
| `users.write` | Update user settings |

**Use cases**: User management, access control

### Tags

| Scope | Description |
|-------|-------------|
| `tags.read` | View tags used for organizing data |
| `tags.write` | Create, update, and delete tags |

**Use cases**: Custom categorization, workflow automation

### Reports

| Scope | Description |
|-------|-------------|
| `reports.read` | Access financial reports (revenue, profit, runway, burn rate) |

**Use cases**: Financial dashboards, investor updates, forecasting

### Search

| Scope | Description |
|-------|-------------|
| `search.read` | Search across all data |

**Use cases**: Global search, data discovery tools

### Notifications

| Scope | Description |
|-------|-------------|
| `notifications.read` | View notifications |
| `notifications.write` | Mark notifications as read, manage settings |

**Use cases**: Notification aggregators, alert systems

## Meta scopes

For apps that need broad access, meta scopes provide convenient shortcuts:

| Scope | Description |
|-------|-------------|
| `apis.read` | Read-only access to all resources |
| `apis.all` | Full read and write access to all resources |

Use meta scopes sparingly. Most apps should request specific scopes.

## Requesting scopes

Include scopes in the authorization URL as a space-separated list:

```
https://app.midday.ai/oauth/authorize?
  response_type=code&
  client_id=YOUR_CLIENT_ID&
  redirect_uri=YOUR_REDIRECT_URI&
  scope=transactions.read%20invoices.read%20customers.read&
  state=STATE
```

URL-encode the scope parameter (spaces become `%20`).

## Scope validation

When users authorize your app:

1. Midday validates requested scopes against your app's registered scopes
2. Invalid or unregistered scopes cause authorization to fail
3. Users see exactly what permissions they're granting

If you need additional scopes later, users must re-authorize your app.

## Scope combinations

### Financial dashboard

```
transactions.read invoices.read bank-accounts.read reports.read
```

### Invoice automation

```
invoices.read invoices.write customers.read customers.write
```

### Time tracking integration

```
tracker-projects.read tracker-projects.write tracker-entries.read tracker-entries.write
```

### Accounting export

```
transactions.read invoices.read customers.read documents.read
```

### Full read-only access

```
apis.read
```

## Best practices

### Request minimal scopes

Only request what you need. Users trust apps that ask for limited permissions.

**Good**: `transactions.read` for a spending tracker

**Avoid**: `apis.all` when you only need to read transactions

### Separate read and write

If your app only displays data, don't request write scopes:

```
transactions.read invoices.read
```

### Group related scopes

If you need invoices, you likely need customers too:

```
invoices.read invoices.write customers.read
```

### Document your requirements

Tell users why you need each scope in your app's description or onboarding flow.

## Checking granted scopes

The token response includes the granted scopes:

```json
{
  "access_token": "mid_at_xxxxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "mid_rt_xxxxx",
  "scope": "transactions.read invoices.read"
}
```

Check this against your requested scopes to confirm what was granted.

## Related

- [Build an OAuth App](/docs/build-oauth-app) — Getting started guide
- [OAuth API Endpoints](/docs/oauth-api-endpoints) — Technical reference
- [API Reference](/docs/api-reference) — Full API documentation

---

### Apps Website Src App Docs Content Quick Start (`apps/website/src/app/docs/content/quick-start.mdx`)

---
title: Quick Start
description: Get running with Midday in 5 minutes.
section: getting-started
order: 2
---

This guide walks you through setting up Midday in about 5 minutes. By the end, you'll have your bank connected and be ready to track your finances.

## Step 1: Create your account

1. Go to [app.midday.ai](https://app.midday.ai)
2. Sign up with your email or Google account
3. Enter your business name and select your base currency

Your base currency is used for financial reports. You can still handle transactions in multiple currencies.

## Step 2: Connect your bank

This is the most important step. Once connected, your transactions flow in automatically.

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. Click **Connect bank**
3. Search for your bank (we support 20,000+ banks worldwide)
4. Complete the secure authentication

Transactions typically sync within a few minutes. Historical transactions (usually 90 days) are pulled automatically.

[Full guide: Connect bank account →](/docs/connect-bank-account)

## Step 3: Review your transactions

Go to [Transactions](https://app.midday.ai/transactions) to see everything that's synced:

- Transactions are automatically categorized by AI
- Click any transaction to add notes, tags, or change the category
- Use filters to find specific transactions

Tip: Spend a few minutes checking the AI categories. The more you correct, the smarter it gets for your business.

## Step 4: Set up your inbox (optional but recommended)

The inbox captures receipts and matches them to transactions automatically.

**Option A: Email forwarding**
Forward receipts to your unique inbox email address. Find it in [Inbox → Settings](https://app.midday.ai/inbox/settings).

**Option B: Connect email**
Connect Gmail or Outlook to automatically capture receipts from your existing email.

[Full guide: Receipt matching →](/docs/receipt-matching)

## Step 5: Create your first invoice

1. Go to [Invoices](https://app.midday.ai/invoices)
2. Click **Create invoice**
3. Add your customer and line items
4. Choose **Send** to email it, or **Save as draft** to finish later

[Full guide: Create invoice →](/docs/create-invoice)

## What's next?

Now that you're set up, explore these features:

- [Set up recurring invoices](/docs/set-up-recurring-invoice) for retainer clients
- [Track time on projects](/docs/track-time-timer) and bill clients accurately
- [Check your runway](/docs/check-runway) to understand your cash position
- [Export transactions](/docs/export-transactions-csv) for your accountant

## Need help?

- Ask the [Midday assistant](/docs) any question
- Email us at [support@midday.ai](mailto:support@midday.ai)
- Join the community on [Discord](https://go.midday.ai/discord)

---

### Apps Website Src App Docs Content Receipt Matching (`apps/website/src/app/docs/content/receipt-matching.mdx`)

---
title: Receipt Matching
description: How Midday automatically matches receipts and invoices to the right transactions, and what to expect in daily use.
section: inbox
order: 1
---

Midday captures receipts and invoices, then matches them to your bank transactions so reconciliation takes minutes instead of hours.

The goal is simple: get the obvious matches done automatically, and show you only the few that need review.

## How it works

1. A receipt arrives in your inbox (via email forward, connected email, or upload)
2. Midday extracts merchant, amount, date, and document details
3. The system searches your transactions for matches
4. High-confidence matches are attached automatically
5. Uncertain matches are flagged for your review

## Matching intelligence

Midday uses multiple signals together:

- **Amount**: Receipt total vs transaction amount (exact matches score highest)
- **Date**: Purchase date vs transaction clearing date (accounting for banking delays)
- **Merchant**: Company name on receipt vs bank description
- **Currency context**: Handles same-currency and cross-currency scenarios
- **Your patterns**: Learns from confirmed, declined, and unmatched feedback

This is why you may see:

- an instant auto-match for very obvious cases,
- a suggestion when Midday is likely right but wants confirmation,
- or "pending" when there isn't enough evidence yet.

## Confidence levels

Matches fall into three categories:

### Auto-matched

Midday is highly confident this is correct and attaches the receipt automatically.

This happens when:
- Amount matches exactly
- Merchant signals are strong
- Date is within expected range
- Similar matches have been reliable before

### Suggested match

Midday thinks this is likely correct, but asks you to confirm.

Review and either:
- **Confirm**: Receipt is attached and matching gets smarter for similar cases
- **Reject**: Midday records this as negative feedback

### No match yet

No transaction currently meets the quality threshold. The receipt stays in "Pending."

You can:
- Wait for the transaction to sync (it might not have cleared yet)
- Manually match to any transaction
- Leave it unmatched if it doesn't need matching

## How it improves over time

Midday continuously improves through your team's actions:

### Merchant pattern learning

When your team repeatedly confirms the same merchant pairings, Midday starts treating those as safer and faster to match.

### Team-specific calibration

The system adapts to your team's patterns:
- If suggestions are frequently confirmed, matching can become more automatic
- If suggestions are often rejected, matching becomes more conservative

### Dismissed match memory

When you reject a match, Midday remembers that pair and avoids repeating it.

### Post-match learning

Even after confirmation, unmatching later is treated as negative feedback so the system avoids repeating similar mistakes.

## Getting receipts into Midday

### Email forwarding

Every Midday account has a unique inbox email address:

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Copy your inbox email address
3. Forward receipts to this address

**Pro tip**: Set up automatic forwarding rules in your email client to send receipts automatically.

### Connected email

Connect Gmail or Outlook to automatically capture receipts:

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Click **Connect email**
3. Authorize your email account
4. Midday scans for receipts and invoices automatically

The AI identifies receipt emails and ignores newsletters, marketing, and other non-receipt content.

[Set up Gmail →](/docs/connect-gmail)
[Set up Outlook →](/docs/connect-outlook)

### Slack integration

Share receipts directly from Slack:

1. Connect your Slack workspace in [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Share any file to the connected channel
3. It appears in your Midday inbox automatically

Great for teams where receipts come through Slack.

[Set up Slack →](/docs/connect-slack)

### Manual upload

Drag and drop files directly:

1. Go to [Inbox](https://app.midday.ai/inbox)
2. Click **Upload** or drag files into the window
3. PDFs and images are processed automatically

## Review matches

Items needing your review appear in the inbox:

1. Go to [Inbox](https://app.midday.ai/inbox)
2. Items with suggested matches show a confidence indicator
3. Click to see the suggested transaction
4. Review the match:
   - **Confirm** if it's correct
   - **Select different** to match to another transaction
   - **Dismiss** if it shouldn't match anything

### What you're confirming

When you confirm a match, you're saying:
- "This receipt documents this transaction"
- The receipt attaches to the transaction permanently
- The AI learns from your confirmation

## Unmatched items

If a receipt doesn't match any transaction:

### Transaction hasn't cleared yet

Bank transactions can take 1-3 days to appear. The receipt will match once the transaction syncs.

### Transaction is in a different account

The purchase might be on an account you haven't connected. Check your connected accounts in [Settings → Accounts](https://app.midday.ai/settings/accounts).

### Manual matching

Match any receipt to any transaction:

1. Open the unmatched receipt
2. Click **Match manually**
3. Search for the transaction
4. Select and confirm

## Inbox settings

### Blocklist

Block senders whose emails you don't want in your inbox:

1. Go to [Inbox → Settings](https://app.midday.ai/inbox/settings)
2. Add email addresses or domains to the blocklist
3. Future emails from these senders are ignored

Useful for blocking:
- Marketing emails that slip through
- Newsletters
- Automated notifications that aren't receipts

### Sync settings

Control how connected email accounts are scanned:

- Frequency of scanning
- How far back to look
- Which folders to scan

## Tips for reliable matching

### Best practices

- **Forward receipts promptly**: Same-day is best, while the transaction is fresh
- **Include the full receipt**: Not cropped photos or partial screenshots
- **Keep bank connections healthy**: Transactions need to be available for matching
- **Review suggestions regularly**: Don't let the inbox pile up

### What makes a good receipt

The AI works best with receipts that clearly show:
- Merchant name
- Total amount
- Date of purchase
- (Optional) Line items

Poor quality images, partial receipts, or handwritten notes are harder to match.

### When matching fails

If matching consistently fails for certain merchants:

1. Check if transactions are syncing from that account
2. Manually confirm a few correct matches so Midday learns your pattern
3. Ensure receipt images are clear and complete

## Privacy and security

- Receipts are processed securely and stored encrypted
- Only your team can access your inbox
- Connected email access is read-only—Midday can't send emails
- You can disconnect email accounts anytime

[Learn about data security →](/policy)

---

### Apps Website Src App Docs Content Revenue Forecast (`apps/website/src/app/docs/content/revenue-forecast.mdx`)

---
title: Revenue Forecast
description: See projected revenue for the coming months based on your recurring income, outstanding invoices, and tracked work.
section: reports
order: 6
---

Your revenue forecast shows how much money you can expect to receive in the coming months. Unlike simple projections that just extend past trends, Midday builds your forecast from real, known sources of incoming revenue.

## Why forecasting matters

Knowing what's coming in helps you:

- **Plan cash flow**: Match expected income against upcoming expenses
- **Make hiring decisions**: Know if you can afford to grow the team
- **Time big purchases**: Align spending with expected revenue
- **Spot gaps early**: See slow months before they happen

## View your forecast

1. Go to [Overview](https://app.midday.ai)
2. Find the Revenue Forecast card
3. Hover over any month to see the breakdown

The forecast shows:

- **Historical revenue**: What you actually received (solid line)
- **Projected revenue**: What Midday expects you'll receive (dashed line)
- **Confidence score**: How reliable the projection is

## How the forecast works

Midday calculates your forecast by adding up known and expected revenue sources. This "bottom-up" approach is more accurate than simply assuming past trends will continue.

### Revenue sources included

Your forecast is built from these sources, ordered by reliability:

**High confidence (most reliable)**
- **Recurring invoices**: Active recurring invoices you've set up that will automatically generate
- **Recurring deposits**: Bank deposits that happen regularly (like retainer payments)
- **Scheduled invoices**: Invoices you've created but scheduled for future dates

**Medium confidence**
- **Outstanding invoices**: Unpaid invoices adjusted by your historical collection rate
- **Billable hours**: Tracked time that hasn't been invoiced yet

**Low confidence**
- **New business baseline**: A conservative estimate based on your typical non-recurring revenue

### What's excluded

To keep forecasts realistic, Midday automatically excludes:

- **Credit card payments**: These are payments to your debt, not revenue
- **Internal transfers**: Moving money between your own accounts
- **Refunds and chargebacks**: These reduce revenue, not add to it
- **One-time windfalls**: Unusual large deposits that won't repeat

## Understanding confidence scores

Each forecast month shows a confidence score (0-100%). Higher scores mean the projection relies more on known, reliable sources.

| Confidence | Meaning |
|------------|---------|
| 80-100% | Mostly recurring revenue—very reliable |
| 60-80% | Good mix of recurring and expected collections |
| 40-60% | Significant portion from new business estimates |
| Below 40% | Mostly projections—treat as rough estimate |

Confidence naturally decreases for months further in the future since there's more uncertainty.

## Improving forecast accuracy

The more information Midday has, the better your forecast:

### Set up recurring invoices

If you bill clients the same amount regularly, [create recurring invoices](/docs/set-up-recurring-invoice). These are the most reliable source for forecasting.

### Track your time

When you [track billable hours](/docs/track-time-timer), Midday can estimate unbilled revenue. Link your tracked time to projects with hourly rates.

### Keep invoices updated

- Mark invoices as paid when you receive payment
- Cancel invoices that won't be collected
- Send scheduled invoices on time

### Connect all revenue accounts

Make sure all accounts where you receive payments are connected and enabled. Missing accounts mean missing data.

## Multi-currency support

If you receive payments in different currencies:

- All amounts are converted to your base currency
- Exchange rates are updated regularly
- Your forecast shows everything in one unified view

Set your base currency in [Settings](https://app.midday.ai/settings).

## Forecast breakdown

Hover over any forecast month to see exactly where the projected revenue comes from:

- **Recurring invoices**: From your active recurring invoice schedules
- **Recurring deposits**: From bank transactions marked as recurring
- **Scheduled**: From invoices you've scheduled for that month
- **Collections**: From unpaid invoices expected to be paid
- **Billable hours**: From tracked time at project rates
- **New business**: Conservative baseline from historical patterns

## Forecast vs. reality

The forecast updates as new information comes in:

- **New invoice created**: Forecast increases
- **Invoice paid**: Moves from forecast to actual
- **Invoice canceled**: Forecast decreases
- **Recurring invoice stopped**: Future months decrease

Check your forecast weekly to see how projections are tracking against reality.

## Using forecast for planning

### Cash flow planning

Compare your forecast against expected expenses:

```
Expected Cash Position = Current Cash + Forecast Revenue - Expected Expenses
```

If projected revenue won't cover expenses, you'll know ahead of time.

### Capacity planning

Use the forecast to answer:
- Can we afford to hire next month?
- Is there enough coming in to cover that software purchase?
- When is our next slow period?

### Client concentration

If one client dominates your recurring revenue, that's worth noting. Losing that client would significantly impact future months.

## Warnings and notes

Midday may show warnings on your forecast:

**"Double-counting possible"**: You have both recurring invoices and recurring bank deposits that might represent the same revenue (e.g., a retainer billed via invoice that also shows as a recurring bank deposit).

**"Limited history"**: Not enough historical data to calculate reliable baselines. Accuracy improves as you use Midday longer.

## Limitations

The forecast is a projection, not a guarantee:

- **New clients**: Future clients you haven't signed yet aren't included
- **One-time projects**: Large projects without recurring patterns aren't predicted
- **Market changes**: External factors that affect your business aren't modeled
- **Payment delays**: Clients may pay late or not at all

Use the forecast as one input for planning, not the only one.

## Related features

- [Recurring invoices](/docs/set-up-recurring-invoice): Set up automatic billing
- [Time tracking](/docs/track-time-timer): Track billable work
- [Invoice status](/docs/track-invoice-status): Monitor outstanding payments
- [Understanding metrics](/docs/understanding-metrics): Learn about all financial metrics

---

### Apps Website Src App Docs Content Set Up Recurring Invoice (`apps/website/src/app/docs/content/set-up-recurring-invoice.mdx`)

---
title: Set Up Recurring Invoices
description: Automatically send invoices on a schedule for retainer clients.
section: invoicing
order: 3
---

Recurring invoices save time for ongoing client relationships. Set it once, and Midday sends invoices automatically.

## When to use recurring invoices

- Monthly retainer clients
- Subscription services you provide
- Regular consulting arrangements
- Any predictable, repeating billing

## Create a recurring invoice

1. Go to [Invoices](https://app.midday.ai/invoices) → **Create invoice**
2. Fill in customer and line items as usual
3. Enable **Recurring**
4. Set the schedule:
   - **Frequency**: Weekly, monthly, quarterly, or yearly
   - **Start date**: When the first invoice should send
   - **End date**: Optional; leave blank for indefinite
5. Review and confirm

The first invoice is created immediately or on the start date.

## What happens automatically

On each scheduled date:

1. A new invoice is generated with updated dates
2. Invoice number increments automatically
3. The invoice is sent to your customer
4. You receive a confirmation

## Managing recurring invoices

View all recurring invoices:

1. Go to [Invoices](https://app.midday.ai/invoices)
2. Filter by **Recurring**

### Edit the schedule

1. Open a recurring invoice
2. Click **Edit recurring**
3. Change frequency, amount, or line items
4. Save

Changes apply to future invoices only.

### Pause or stop

To stop a recurring invoice:

1. Open the recurring invoice
2. Click **Edit recurring**
3. Set an end date or disable recurring
4. Save

Past invoices are not affected.

## Tips

- Review recurring invoices monthly to ensure amounts are still correct
- Update for price changes at contract renewal time
- Use clear descriptions so customers know what they're paying for

---

### Apps Website Src App Docs Content Share Report (`apps/website/src/app/docs/content/share-report.mdx`)

---
title: Share Reports
description: Share financial reports with your team, investors, or accountant.
section: reports
order: 4
---

Share financial reports with stakeholders who need visibility into your business performance.

## Share a report

1. Go to [Overview](https://app.midday.ai) or the specific report you want to share
2. Click **Share**
3. Choose sharing method:
   - **Link**: Generate a shareable URL
   - **PDF**: Download to send manually
   - **Email**: Send directly from Midday

## Shareable link

Generate a link that anyone can view:

- Link is private by default (only people with the link can access)
- Optional password protection
- Set expiration date

### Link settings

- **Password**: Require a password to view
- **Expires**: Auto-disable after a date
- **Permissions**: View only (no editing)

## PDF export

Download a PDF for:

- Sending via your own email
- Printing for meetings
- Archiving for records

PDFs include:

- Report data as of export time
- Your business branding
- Date range and filters applied

## What you can share

### Overview dashboard

High-level metrics:

- Revenue
- Profit
- Burn rate
- Runway

### Specific reports

- Revenue by period
- Expense breakdown
- Profit and loss
- Cash flow

## Managing shared links

View and manage your shared reports:

1. Go to [Settings](https://app.midday.ai/settings)
2. See all active shared links
3. Revoke or update as needed

## Use cases

### For investors

Monthly or quarterly overview showing:

- Revenue growth
- Burn rate
- Runway

### For accountants

Detailed transaction exports and reports:

- Categorized transactions
- Profit and loss
- Use [direct integrations](/docs/connect-xero) for automated sync

### For partners

Specific project reports:

- Time tracked
- Revenue generated
- Outstanding invoices

## Tips

- Set expiration dates for sensitive reports
- Use passwords for extra security
- Review shared links periodically and revoke unused ones

---

### Apps Website Src App Docs Content Supported Banks (`apps/website/src/app/docs/content/supported-banks.mdx`)

---
title: Supported Banks
description: Overview of banks and financial institutions Midday supports.
section: banking
order: 2
---

Midday connects to over 20,000 banks and financial institutions worldwide.

## Coverage by region

### North America

- All major US banks (Chase, Bank of America, Wells Fargo, Citi, etc.)
- Credit unions
- Canadian banks (TD, RBC, Scotiabank, etc.)
- Business banking platforms

### Europe

- UK banks (Barclays, HSBC, Lloyds, Monzo, Starling, etc.)
- German banks (Deutsche Bank, Commerzbank, N26, etc.)
- French banks (BNP Paribas, Société Générale, etc.)
- Dutch banks (ING, ABN AMRO, Rabobank, etc.)
- Swedish banks (SEB, Nordea, Handelsbanken, Swedbank)
- And many more across the EU

### Other regions

- Australia (Commonwealth, ANZ, Westpac, NAB)
- Singapore
- And expanding to more markets

## Connection methods

### Open Banking (OAuth)

Most banks use secure OAuth connections:

- You authenticate directly with your bank
- Midday never sees your login credentials
- Read-only access to transactions

### Credential-based

Some banks require credentials:

- Encrypted and stored securely
- Used only to sync transactions
- Can be removed at any time

## Check your bank

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. Click **Connect bank**
3. Search for your bank by name

If you don't find your bank:

- Try the full official name
- Check for separate business banking
- Use [CSV import](/docs/import-transactions-csv) as a fallback

## Business vs. personal banking

Some banks have separate platforms for business accounts:

- Search for "Business" or "Commercial" versions
- Business accounts may have different connection requirements
- Contact your bank if unsure which platform you use

## Connection limits

Depending on your plan:

- Free: Limited number of connected accounts
- Pro: More connected accounts
- Check your plan details for specifics

## Requesting a bank

If your bank isn't supported:

1. Email [support@midday.ai](mailto:support@midday.ai) with bank name and country
2. We'll check if it can be added
3. Some regional banks have limited API availability

---

### Apps Website Src App Docs Content Track Invoice Status (`apps/website/src/app/docs/content/track-invoice-status.mdx`)

---
title: Track Invoice Status
description: Monitor sent invoices and follow up on unpaid ones.
section: invoicing
order: 5
---

Keep track of your invoices from sent to paid. Know exactly who owes you money and when to follow up.

## Invoice statuses

| Status | Meaning |
|--------|---------|
| Draft | Created but not sent |
| Sent | Delivered to customer |
| Viewed | Customer opened the invoice |
| Paid | Payment received |
| Overdue | Past due date, unpaid |

## View invoice status

1. Go to [Invoices](https://app.midday.ai/invoices)
2. See status badges on each invoice
3. Filter by status to focus on specific invoices

### Filter by status

Click status filters to see:

- **Unpaid**: All outstanding invoices
- **Overdue**: Need immediate attention
- **Paid**: Completed invoices

## Mark as paid

When you receive payment outside of Stripe:

1. Open the invoice
2. Click **Mark as paid**
3. Optionally add payment date and method

This updates your records without requiring online payment.

## Send reminders

For overdue invoices:

1. Open the overdue invoice
2. Click **Send reminder**
3. A reminder email is sent to your customer

Be professional but persistent. A friendly reminder often does the trick.

## Best practices

### Before due date
- Send invoices promptly
- Set reasonable due dates (Net 14 or Net 30 are common)
- Enable online payments for faster collection

### When overdue
- Send a reminder within a few days
- Follow up by phone or email for large amounts
- Consider payment plans for long-standing invoices

### General tips
- Review unpaid invoices weekly
- Note customers with slow payment history
- Update payment terms if needed

---

### Apps Website Src App Docs Content Track Time Timer (`apps/website/src/app/docs/content/track-time-timer.mdx`)

---
title: Track Time
description: Use the timer or log hours manually to track billable work.
section: time-tracking
order: 2
---

Track your work hours accurately to bill clients and understand where your time goes.

## Start the timer

1. Go to [Time Tracking](https://app.midday.ai/tracker)
2. Select a project
3. Click **Start timer**
4. Work on your task
5. Click **Stop** when done

The timer runs in the background. You can navigate away—it keeps tracking.

## Log time manually

For time you forgot to track:

1. Go to [Time Tracking](https://app.midday.ai/tracker)
2. Click **Add entry**
3. Select the project
4. Enter the date and duration
5. Add an optional description
6. Save

## Time entries

Each entry records:

- **Project**: Which project the time is for
- **Date**: When the work was done
- **Duration**: How long (in hours or hours:minutes)
- **Description**: Optional note about what you worked on

## Edit or delete entries

1. Find the entry in the time tracking list
2. Click to edit
3. Change project, duration, or description
4. Save or delete

## View your time

### By day

See entries grouped by date. Good for reviewing your workday.

### By project

See total time per project. Good for checking progress toward estimates.

### By week or month

Use date filters to see totals for any period.

## Tips for accurate tracking

- **Start the timer immediately**: Even if you're just "checking one thing"
- **Add descriptions**: Future you will thank present you
- **Log forgotten time the same day**: Memory fades quickly
- **Review weekly**: Catch any gaps before invoicing

## Turn time into invoices

When you're ready to bill:

[Learn how to invoice tracked time →](/docs/invoice-tracked-time)

---

### Apps Website Src App Docs Content Troubleshooting (`apps/website/src/app/docs/content/troubleshooting.mdx`)

---
title: Troubleshooting
description: Solutions to common issues and frequently asked questions.
section: getting-started
order: 4
---

Quick solutions to common issues you might encounter with Midday.

## Bank connection issues

### Bank not syncing

If your bank connection stops syncing:

1. Go to [Settings → Accounts](https://app.midday.ai/settings/accounts)
2. Check if the account shows a warning icon
3. Click **Reconnect** and re-authenticate with your bank
4. Wait a few minutes for transactions to sync

**Why this happens**: Banks require re-authentication periodically (often every 90 days) for security.

### Bank not found

If you can't find your bank when connecting:

1. Try searching by the full official name
2. Check if your bank has a separate business banking platform (search for that instead)
3. Some smaller banks may not be supported yet
4. Use [CSV import](/docs/import-transactions-csv) as an alternative

### Missing transactions

If recent transactions aren't appearing:

1. **Wait**: Transactions can take 1-3 days to appear after clearing your bank
2. **Check the date range**: Make sure you're viewing the correct period
3. **Sync manually**: Click **Sync** in your account settings to force a refresh
4. **Verify in your bank**: Confirm the transaction has actually cleared

### Duplicate transactions

If you see duplicate transactions:

1. Check if you connected the same account twice
2. Look for pending vs. cleared versions of the same transaction
3. You can hide duplicates by marking them as excluded

## Receipt matching issues

### Receipts not matching

If receipts aren't matching to transactions:

1. **Transaction hasn't cleared**: Wait for the bank transaction to appear
2. **Different account**: The purchase might be on a different connected account
3. **Amount mismatch**: Tips, taxes, or currency conversion can cause slight differences
4. **Manual match**: You can always match manually from the inbox

### Receipt not processing

If an uploaded receipt stays stuck:

1. Check the file format (PDF, JPG, PNG work best)
2. Ensure the image is clear and readable
3. Try re-uploading the receipt
4. Very long receipts may need to be split into pages

### Gmail/Outlook not capturing receipts

If connected email isn't finding receipts:

1. Check the connection is still active in [Inbox Settings](https://app.midday.ai/inbox/settings)
2. Receipts in spam/junk folders may not be captured
3. Some receipt formats may not be recognized automatically
4. Forward important receipts manually as a backup

## Invoice issues

### Customer not receiving invoice

If a customer says they didn't get your invoice:

1. Check the email address is correct in customer details
2. Ask them to check spam/junk folders
3. Resend the invoice from the invoice detail page
4. Share the invoice link directly as a backup

### Online payment not working

If customers can't pay online:

1. Verify Stripe is connected in [Invoice Settings](https://app.midday.ai/settings/invoice)
2. Check that online payments are enabled for that invoice
3. Have the customer try a different card or browser
4. Check your Stripe dashboard for any declined payment details

### Invoice showing wrong currency

To fix invoice currency:

1. Edit the invoice
2. Change the currency setting
3. Re-enter amounts if needed
4. Save and resend

Set a default currency per customer to avoid this in the future.

## Report issues

### Numbers seem wrong

If your reports show unexpected numbers:

1. **Check date range**: Make sure you're viewing the intended period
2. **Review categories**: Miscategorized transactions affect reports
3. **Check excluded transactions**: Some transactions might be excluded
4. **Multiple currencies**: Exchange rates affect converted amounts

### Burn rate is too high

If burn rate seems inflated:

1. Check for **credit card payments** categorized incorrectly (should be "Credit Card Payment" to be excluded)
2. Look for **internal transfers** between your accounts
3. Review large one-time expenses that might skew the average

### Runway showing zero

If runway shows zero or seems wrong:

1. Verify you have **cash accounts** connected (checking/savings)
2. Check that accounts are **enabled** in settings
3. Make sure transactions are **categorized** (uncategorized expenses don't count toward burn rate)

## Time tracking issues

### Timer not saving

If tracked time isn't saving:

1. Make sure you selected a **project** before starting
2. Check your internet connection
3. Don't close the browser tab while timer is running
4. Use the desktop app for more reliable tracking

### Can't delete time entry

If you can't delete a time entry:

1. Check if it's been **invoiced** (invoiced time can't be deleted)
2. You may need to remove it from the invoice first
3. Archived projects may have restrictions

## Account and billing

### Can't access account

If you can't sign in:

1. Try **resetting your password**
2. Check you're using the correct email
3. Clear browser cache and cookies
4. Try a different browser or incognito mode

### Subscription issues

For billing questions:

1. Go to [Settings → Billing](https://app.midday.ai/settings/billing)
2. View current plan and payment method
3. Update payment details if needed
4. Contact support for invoice or refund requests

## Performance issues

### App running slowly

If Midday feels slow:

1. **Clear browser cache**: Old cached data can cause issues
2. **Check internet**: Slow connection affects load times
3. **Try a different browser**: Chrome and Firefox work best
4. **Close other tabs**: Free up browser memory

### Data not loading

If pages aren't loading data:

1. Check your internet connection
2. Try refreshing the page
3. Sign out and back in
4. Clear browser cache

## Still need help?

If you can't find a solution:

1. **Email**: [support@midday.ai](mailto:support@midday.ai)
2. **Response time**: Usually within 24 hours on business days

When contacting support, include:
- What you were trying to do
- What happened instead
- Any error messages you saw
- Screenshots if helpful

---

### Apps Website Src App Docs Content Understanding Metrics (`apps/website/src/app/docs/content/understanding-metrics.mdx`)

---
title: Understanding Your Metrics
description: How Midday calculates your financial metrics and what affects each number.
section: reports
order: 0
---

Midday gives you real-time visibility into your business finances. This guide explains how each metric is calculated and what factors affect them.

## Overview

Your financial dashboard shows several key metrics:

- **Cash Balance**: Money available in your accounts
- **Burn Rate**: How fast you're spending money
- **Runway**: How long your cash will last
- **Revenue**: Money coming in
- **Profit**: What's left after expenses
- **Net Position**: Cash minus credit card debt

Understanding what goes into each calculation helps you make better decisions.

---

## Cash Balance

Cash balance is the total money available across all your connected cash accounts.

### What counts as cash

| Account Type | Examples | Included in Cash? |
|-------------|----------|-------------------|
| Depository | Checking, Savings | Yes |
| Other Asset | Money Market, Treasury | Yes |
| Credit | Credit Cards | No |
| Loan | Business Loans | No |

### Why credit cards don't count

Credit card "available balance" isn't your money—it's borrowed money you'll need to pay back. Midday shows only the cash you actually have.

### Multi-currency accounts

If you have accounts in different currencies, Midday converts them to your base currency using current exchange rates. Set your base currency in [Settings](https://app.midday.ai/settings).

### Disabled accounts

If you disable an account in settings, its balance won't be included in your cash total. This is useful if you have accounts you want to track separately or legacy accounts you no longer use.

---

## Burn Rate

Burn rate measures how much cash you're consuming each month. It's calculated from your expense transactions.

### The formula

```
Monthly Burn Rate = Total Expenses in Month
```

Midday calculates averages over 3-6 months to smooth out one-time spikes.

### What's included

All expense transactions from enabled accounts, except:

- **Credit Card Payments**: Excluded to prevent double-counting (see below)
- **Internal Transfers**: Moving money between your own accounts isn't spending

### Why credit card payments are excluded

This is important to understand. When you connect both a checking account and a credit card:

1. Your credit card shows individual purchases (coffee, software, etc.)
2. Your checking account shows the payment to your credit card

If both counted as expenses, you'd be counting the same money twice:
- Once when you bought the item (on the credit card)
- Again when you paid the credit card bill (from checking)

Midday excludes credit card payments from burn rate automatically. The actual expenses on your credit card still count.

### Manually excluding transactions

Some transactions shouldn't count as burn rate even if they're categorized as expenses. You can exclude individual transactions by marking them as excluded in the transaction details.

Common exclusions:
- One-time equipment purchases
- Security deposits (you'll get these back)
- Founder loans to the company

---

## Runway

Runway tells you how many months you can operate before running out of cash.

### The formula

```
Runway = Cash Balance ÷ Average Monthly Burn Rate
```

### Example

- Cash on hand: $120,000
- Average monthly burn: $20,000
- Runway: 6 months

### What affects runway

**Cash Balance** (increases runway):
- Revenue coming in
- Investment or loans
- Collecting receivables faster

**Burn Rate** (decreases runway when higher):
- Operating expenses
- Hiring
- Marketing spend

### Healthy runway benchmarks

| Runway | Status |
|--------|--------|
| 12+ months | Comfortable |
| 6-12 months | Healthy |
| 3-6 months | Caution—time to act |
| Under 3 months | Critical—immediate action needed |

These benchmarks vary by business stage and industry. An established profitable business might operate with less runway than a startup still finding product-market fit.

### If runway shows zero or infinite

- **Zero**: Your cash balance is zero, or burn rate couldn't be calculated (no expense data)
- **Infinite**: Your business is profitable—you're making more than you spend

---

## Revenue

Revenue is the total money flowing into your business.

### What counts

All transactions categorized under Revenue:
- Income
- Product Sales
- Service Revenue
- Consulting Revenue
- Subscription Revenue
- Interest Income
- Other Income

### What reduces revenue

- Customer Refunds
- Chargebacks & Disputes

These are categorized under Revenue but with negative amounts, reducing your net revenue.

### Revenue vs. cash received

Revenue is based on when money hits your bank account, not when you invoice. If you send an invoice in January but get paid in February, it counts as February revenue.

For accrual-based accounting, work with your accountant on adjustments.

---

## Profit

Profit is what remains after subtracting expenses from revenue.

### The formula

```
Profit = Revenue - Expenses
```

Expenses include all non-excluded expense categories.

### Profit margin

```
Profit Margin = (Profit ÷ Revenue) × 100%
```

Profit margin shows how efficiently you turn revenue into profit. A 20% margin means you keep $0.20 of every dollar earned.

### Gross vs. net profit

Midday shows net profit (all expenses included). For gross profit (revenue minus only cost of goods sold), filter your reports to exclude operating expenses.

---

## Revenue Forecast

The revenue forecast shows projected incoming revenue for the coming months.

### How it works

Unlike simple trend projections, Midday builds forecasts from known revenue sources:

- **Recurring invoices**: Active recurring invoices you've set up
- **Recurring deposits**: Bank deposits marked as recurring
- **Scheduled invoices**: Invoices scheduled for future dates
- **Outstanding invoices**: Unpaid invoices adjusted by your collection history
- **Billable hours**: Tracked time that hasn't been invoiced yet

### Confidence scores

Each forecast month shows a confidence score. Higher scores mean the forecast relies more on reliable, known sources like recurring invoices. Lower scores mean more of the projection comes from estimates.

### What's excluded

To keep forecasts realistic, certain transactions are automatically excluded:
- Credit card payments
- Internal transfers
- Refunds and chargebacks

[Learn more about revenue forecast →](/docs/revenue-forecast)

---

## Net Position

Net position gives you a quick view of cash minus outstanding credit card debt.

### The formula

```
Net Position = Cash Balance - Credit Card Balances
```

### Why this matters

If you have $50,000 in checking but $15,000 on credit cards, your real position is $35,000. Net position shows you this at a glance.

### What's included

**Cash side**:
- Depository accounts (checking, savings)
- Other asset accounts (money market)

**Debt side**:
- Credit card balances

**Not included in net position**:
- Loans (shown separately in Balance Sheet)
- Accounts receivable (unpaid invoices)
- Accounts payable (bills you owe)

---

## Account types and their impact

Different account types affect different metrics:

| Account Type | Cash Balance | Burn Rate | Net Position (Cash) | Net Position (Debt) | Balance Sheet |
|-------------|--------------|-----------|---------------------|---------------------|---------------|
| Depository | Yes | Via transactions | Yes | No | Assets |
| Other Asset | Yes | Via transactions | Yes | No | Assets |
| Credit | No | Via transactions | No | Yes | Liabilities |
| Loan | No | Via transactions | No | No | Liabilities |

### Enabling and disabling accounts

When you disable an account:
- Its balance is excluded from cash calculations
- Historical transactions still appear
- You can re-enable it anytime

This is useful for:
- Closed accounts you want to keep for records
- Personal accounts accidentally connected
- Test or sandbox accounts

---

## Date ranges and comparisons

All metrics can be viewed for different time periods:

- This month / Last month
- This quarter / Last quarter
- This year / Last year
- Custom date range

Comparisons show you:
- Absolute change ($5,000 more revenue)
- Percentage change (+12% from last period)
- Trend direction (improving or declining)

---

## Tips for accurate metrics

1. **Connect all business accounts**: Missing accounts mean incomplete data

2. **Review auto-categorization**: Spend 5 minutes weekly checking that categories are correct

3. **Exclude one-time items**: Large one-time expenses can skew burn rate

4. **Set your base currency**: Ensures multi-currency accounts convert correctly

5. **Keep accounts enabled**: Only disable accounts you truly want excluded

6. **Categorize promptly**: Uncategorized transactions affect accuracy

[Learn about categories →](/docs/categories-reference)
[Check your runway →](/docs/check-runway)
[View burn rate →](/docs/view-burn-rate)
[Revenue forecast →](/docs/revenue-forecast)

---

### Apps Website Src App Docs Content Use Tags (`apps/website/src/app/docs/content/use-tags.mdx`)

---
title: Use Tags
description: Organize transactions with custom tags for flexible categorization.
section: banking
order: 4
---

Tags let you add custom labels to transactions beyond the standard categories. Use them to track projects, clients, tax categories, or anything else.

## What tags are for

Tags are flexible labels you define:

- **Projects**: Tag expenses by project for cost tracking
- **Clients**: Tag transactions related to specific clients
- **Tax categories**: Tag deductible expenses
- **Departments**: Track spending by department or team

Unlike categories (which are predefined), tags are completely customizable.

## Add tags to transactions

1. Go to [Transactions](https://app.midday.ai/transactions)
2. Click on a transaction
3. Click **Add tag** or the tag icon
4. Select existing tag or create new one
5. Save

A transaction can have multiple tags.

## Create tags

Tags are created as you use them:

1. Click **Add tag** on any transaction
2. Type a new tag name
3. Press Enter to create it

Or manage all tags from settings.

## Filter by tags

Find transactions with specific tags:

1. Open the filter panel
2. Select **Tags**
3. Choose one or more tags
4. View filtered results

## Tag reports

Use tags to generate custom reports:

- Export transactions filtered by tag
- See spending by tag
- Track project profitability

## Tips

- Use consistent naming: "Client: Acme" not "Acme client"
- Keep tags focused—too many becomes unwieldy
- Review and clean up unused tags periodically
- Tags are great for things categories don't cover

---

### Apps Website Src App Docs Content Using Assistant (`apps/website/src/app/docs/content/using-assistant.mdx`)

---
title: Using the Assistant
description: Ask questions, take actions, and manage your business from a single conversation.
section: assistant
order: 1
---

The Midday assistant lets you ask questions, take actions, and manage your business in plain language — directly from the dashboard.

## Access the assistant

1. Open [Midday](https://app.midday.ai)
2. The assistant is available from the home screen
3. Type your question or request
4. Get an instant response — or watch the assistant take action for you

## Chat modes

The assistant supports three modes you can switch between:

- **Auto** — The default. Automatically picks the best approach for your question — fast for simple lookups, more thorough for complex analysis.
- **Instant** — Optimized for speed. Best for quick lookups like "show my balance" or "list overdue invoices".
- **Thinking** — Deep reasoning mode for complex questions like financial comparisons, forecasts, or multi-step analysis. Available on Pro plans.

## What you can do

The assistant can both answer questions and take real actions on your behalf.

### Ask about your finances

- "What was my revenue last month?"
- "Show me revenue by customer this quarter"
- "How does this month compare to last month?"
- "What did I spend on software last quarter?"
- "What's my current balance?"
- "How long will my cash last?"

### Create and manage invoices

- "Create an invoice for Acme Corp for 10 hours of consulting at $150/hr"
- "Send the draft invoice to Acme Corp"
- "Remind Beta Inc about their overdue invoice"
- "Mark invoice INV-0042 as paid"
- "What invoices are overdue?"

### Manage transactions

- "Categorize all my Stripe transactions as income"
- "Tag these transactions as tax-deductible"
- "Delete the manual transaction from yesterday"
- "Show transactions from last month"

### Track time

- "Start a timer on Project X"
- "Log 2 hours for the client meeting yesterday"
- "How many hours did I track this week?"
- "What's unbilled for Project X?"

### Export and report

- "Export my January transactions to my accountant at lisa@accounting.com"
- "What's my growth rate compared to last quarter?"
- "Give me a balance sheet as of today"
- "Show spending by category for last month"

## Web search

The assistant can search the web for real-time information and combine it with your business data:

- "Can I afford a Tesla Model 3?" — looks up the current price and checks your balance or runway.
- "What's the VAT rate in Germany?" — finds the rate and checks your relevant transactions.
- "How does my revenue compare to industry average?" — searches for benchmarks and pulls your actual numbers.

This is useful for questions involving prices, exchange rates, tax rules, or industry benchmarks.

## Connected apps

The assistant can interact with external tools you've connected — Gmail, Slack, Google Calendar, Notion, GitHub, Linear, and more.

- "Check my calendar for tomorrow"
- "Send a Slack message to the team channel"
- "Look up the latest GitHub issue on our repo"
- "Create a Notion page with this month's revenue summary"

Connect apps from the **Connectors** settings in your dashboard. See [Connected Apps](/docs/assistant-connectors) for the full list and setup instructions.

## Tips

- **Be specific**: "Revenue last month" works better than "How am I doing?"
- **Include time periods**: "this month," "last quarter," "in 2025"
- **Ask follow-ups**: The assistant remembers context within a conversation
- **Combine actions**: "Create an invoice for Acme Corp and send it" works in one go
- **Use natural language**: You don't need special syntax — just describe what you want

## Limitations

The assistant cannot:

- Send emails (other than invoice send/remind)
- Connect bank accounts or modify account settings
- Manage billing or subscriptions
- Upload files

For these, use the corresponding pages in the Midday dashboard.

## Privacy

Your conversations with the assistant are private. We don't use your financial data or conversation history to train AI models.

---

### Apps Website Src App Docs Content Vault File Storage (`apps/website/src/app/docs/content/vault-file-storage.mdx`)

---
title: Vault - File Storage
description: Store and organize all your business documents in one secure place.
section: vault
order: 1
---

The Vault is your centralized document storage in Midday. Store contracts, receipts, statements, and any business documents—all searchable and organized.

## What is the Vault

The Vault is a secure file storage system built into Midday. Unlike your inbox (which captures receipts for transaction matching), the Vault is for any document you want to keep:

- Contracts and agreements
- Tax documents
- Business licenses and permits
- Bank statements
- Legal documents
- Team documents
- Any file you need to access later

## Access the Vault

1. Go to [Vault](https://app.midday.ai/vault)
2. You'll see all your stored documents
3. Use search and filters to find what you need

## Upload documents

### Drag and drop

1. Open the Vault
2. Drag files directly into the window
3. Files upload automatically

### Click to upload

1. Click the **Upload** button
2. Select files from your computer
3. Files upload automatically

### Supported file types

The Vault accepts common business document formats:

- **Documents**: PDF, DOC, DOCX
- **Spreadsheets**: XLS, XLSX, CSV
- **Images**: JPG, PNG, HEIC
- **Other**: Most common file formats

## Organize with tags

Tags help you categorize and find documents later.

### Add tags

1. Click on a document
2. Click **Add tag** or the tag icon
3. Select existing tags or create new ones
4. Save

### Create a tagging system

Consistent tags make documents easier to find:

- **By type**: `contract`, `receipt`, `tax`, `legal`
- **By year**: `2024`, `2025`
- **By client**: `acme-corp`, `client-b`
- **By status**: `signed`, `draft`, `expired`

### Filter by tags

1. Use the filter panel
2. Select one or more tags
3. View matching documents

## Search documents

The Vault search looks through:

- File names
- Tags
- Document metadata

Type in the search box to find documents instantly.

## Connect documents to transactions

Link Vault documents to specific transactions for a complete audit trail:

1. Open the document in the Vault
2. Click **Link to transaction**
3. Search for and select the transaction
4. The document is now attached

This is useful for:
- Attaching contracts to related payments
- Linking statements to reconciliation
- Connecting any supporting document to a transaction

## Vault vs. Inbox

| Feature | Vault | Inbox |
|---------|-------|-------|
| Purpose | Long-term document storage | Receipt capture and matching |
| Matching | Manual linking to transactions | Automatic AI matching |
| Organization | Tags and folders | Status-based (pending, matched) |
| Use case | Contracts, statements, any documents | Receipts, invoices for bookkeeping |

Use both:
- **Inbox** for receipts that need to match transactions
- **Vault** for documents you want to keep and organize

## Download documents

1. Click on any document
2. Click **Download**
3. The file downloads to your computer

You can also download multiple files by selecting them and using bulk actions.

## Delete documents

1. Click on the document
2. Click **Delete**
3. Confirm deletion

Deleted documents are removed permanently. Make sure you have backups of important files before deleting.

## Storage and limits

Your storage limit depends on your plan. Check your current usage in [Settings → Billing](https://app.midday.ai/settings/billing).

If you're approaching your limit:
- Delete documents you no longer need
- Compress large files before uploading
- Upgrade your plan for more storage

## Security

Your documents are stored securely:

- **Encryption**: Files are encrypted at rest and in transit
- **Access control**: Only your team members can access your Vault
- **No public access**: Documents are never publicly accessible

## Tips

- **Upload regularly**: Don't let documents pile up—upload as you receive them
- **Tag consistently**: A good tagging system saves time later
- **Use descriptive names**: Rename files before uploading for easier search
- **Back up critical documents**: The Vault is secure, but keep copies of irreplaceable documents

## Coming from another tool

If you're migrating from Dropbox, Google Drive, or another storage system:

1. Download your business documents
2. Organize them by type or date
3. Upload to the Vault in batches
4. Add tags as you upload

---

### Apps Website Src App Docs Content View Burn Rate (`apps/website/src/app/docs/content/view-burn-rate.mdx`)

---
title: View Your Burn Rate
description: Understand your monthly cash consumption and what affects this critical metric.
section: reports
order: 2
---

Burn rate shows how quickly you're consuming cash. It's essential for financial planning, fundraising conversations, and understanding your business health.

## What is burn rate

Burn rate is your net cash outflow over a period:

- **Gross burn**: Total money going out
- **Net burn**: Money out minus money in

Net burn is more useful—it accounts for revenue offsetting expenses.

## View your burn rate

1. Go to [Overview](https://app.midday.ai)
2. Find the burn rate card

You'll see:

- Average monthly burn
- Trend over recent months
- How it affects your runway

## How burn rate is calculated

Midday calculates burn rate from your expense transactions across all enabled accounts.

### What's included

All expense transactions except those in excluded categories.

### What's excluded

Two categories are automatically excluded from burn rate to prevent double-counting:

| Category | Why it's excluded |
|----------|-------------------|
| Credit Card Payment | Prevents counting expenses twice (once on the card, once when paying the bill) |
| Internal Transfer | Moving money between your own accounts isn't spending |

### The double-counting problem

When you connect both a checking account and credit card, you'll see:

1. **On your credit card**: Individual purchases ($50 for software, $30 for meals, etc.)
2. **On your checking account**: Payment to credit card ($1,500)

Without exclusions, Midday would count:
- $50 + $30 + ... = expenses on credit card
- $1,500 = payment from checking
- Total = double the actual spending

By excluding credit card payments, only the actual purchases count. This is handled automatically.

### Which accounts affect burn rate

| Account Type | Transactions count toward burn? |
|-------------|--------------------------------|
| Depository (checking, savings) | Yes |
| Other Asset (money market) | Yes |
| Credit (credit cards) | Yes |
| Loan | Yes |

All account types contribute transactions to burn rate. The exclusion logic applies to specific transaction categories, not account types.

## Understanding the numbers

### Positive burn rate

You're spending more than you're earning. Normal for:

- Early-stage startups investing in growth
- Seasonal slow periods
- Planned investment phases (hiring, expansion)

### Negative burn rate

You're earning more than spending. You're profitable—your runway is essentially infinite.

### Zero burn rate

Breaking even. Income matches expenses.

## Excluding specific transactions

Sometimes a transaction shouldn't count toward burn rate even if it's categorized as an expense:

- One-time equipment purchases
- Security deposits (you'll get them back)
- Loans to the company from founders

To exclude a transaction:

1. Click on the transaction
2. Find the exclude option
3. The transaction will no longer affect burn rate

Use this sparingly—only for truly exceptional items.

## Trends matter

A single month's burn rate can be misleading due to one-time expenses or income. Look at:

- **3-month average**: Recent trend, shows if things are changing
- **6-month average**: Overall pattern, smooths out anomalies
- **Year-over-year**: For seasonal businesses, compare to the same period last year

## Burn rate and runway

Burn rate directly affects your runway:

```
Runway = Cash Balance ÷ Monthly Burn Rate
```

Reducing burn rate by $5,000/month on $60,000 cash extends runway from 6 months to 10 months.

[Learn more about runway →](/docs/check-runway)

## Reducing burn rate

If burn rate is too high:

- **Review subscriptions**: Audit software and services you're paying for
- **Negotiate contracts**: Vendors often offer discounts for annual payment or loyalty
- **Delay non-essential purchases**: Push discretionary spending to later
- **Focus on efficiency**: Higher-margin work or automation can help

## Common burn rate issues

### Burn rate seems too high

Check for:
- Credit card payments not categorized correctly (should be "Credit Card Payment")
- Internal transfers between accounts not categorized correctly
- One-time expenses skewing the average

### Burn rate seems too low

Check for:
- Missing accounts (expenses on unconnected accounts)
- Transactions incorrectly categorized as transfers
- Disabled accounts that should be enabled

### Burn rate is zero or missing

This usually means:
- No expense transactions in the selected period
- All accounts are disabled
- Transactions aren't categorized yet

## Tips

- Review burn rate monthly—catch issues early
- Compare to revenue: high burn with high revenue growth might be the right strategy
- Plan for spikes: quarterly taxes, annual renewals, seasonal hiring
- Share with your team: financial visibility helps everyone make better decisions

[See all categories →](/docs/categories-reference)
[Understand all metrics →](/docs/understanding-metrics)

---

### Apps Website Src App Docs Content View Revenue Profit (`apps/website/src/app/docs/content/view-revenue-profit.mdx`)

---
title: View Revenue and Profit
description: See how much you're making and keeping.
section: reports
order: 1
---

Understanding your revenue and profit helps you make informed business decisions.

## Access financial reports

1. Go to [Overview](https://app.midday.ai)

You'll see cards showing your key metrics.

## Revenue

Revenue is the total money coming in:

- Invoice payments
- Sales
- Other income transactions

### View revenue

- Current period total
- Comparison to previous period
- Trend over time

Revenue is based on **income** transactions in your connected accounts.

## Profit

Profit is revenue minus expenses:

- **Revenue**: All income
- **Expenses**: All outgoing transactions (excluding transfers)
- **Profit**: What's left

### View profit

- Current period profit
- Profit margin (profit as % of revenue)
- Trend over time

## Date ranges

Switch between:

- This month / Last month
- This quarter / Last quarter
- This year / Last year
- Custom date range

## Filters

Filter to see specific views:

- By account (see profit from a specific bank account)
- By category (see revenue from certain categories)

## Share reports

1. Open the report you want to share
2. Click **Export** or **Share**
3. Get a link or download PDF

[Learn more about sharing reports →](/docs/share-report)

## Tips

- Review weekly to catch issues early
- Compare periods to spot trends
- Use profit margin to understand efficiency

---

### Apps Website Src App Docs Content Weekly Insights (`apps/website/src/app/docs/content/weekly-insights.mdx`)

---
title: Weekly Insights
description: Get an AI-powered summary of your business each week with key metrics, trends, and recommended actions.
section: reports
order: 7
---

Weekly Insights gives you a clear picture of how your business performed each week. Instead of digging through transactions and reports yourself, you get a concise summary with the numbers that matter and specific actions to take.

## Why weekly insights matter

Running a business means juggling many things. Weekly Insights helps you:

- **Stay informed in 30 seconds**: Know if your week was good or bad without spreadsheets
- **Catch problems early**: Spot declining revenue or rising expenses before they become crises
- **Know exactly who owes you money**: See every overdue invoice with amounts and company names
- **Understand your runway**: Not just "3 months" but "until April 15"—a date you can plan around
- **Get actionable next steps**: Specific recommendations, not generic advice

## What you'll see each week

Your weekly insight includes several sections:

### Summary

A 2-3 sentence overview of your week covering:

- Profit, revenue, and expenses
- Your margin percentage
- How long your cash will last (with a specific date)
- Any outstanding receivables worth noting

**Example:**
> "You earned $5,644 profit on $7,500 revenue this week, with a margin of 75.3% and expenses at $1,855, giving you a runway lasting until February 24. Note that $22,500 remains overdue from three clients."

### Key Metrics

The four most important numbers for your week, automatically selected based on what changed and what matters most:

| Metric | What it tells you |
|--------|-------------------|
| Profit | What you kept after expenses |
| Revenue | What came in |
| Expenses | What went out |
| Profit Margin | How efficiently you turn revenue into profit |
| Cash Flow | Net money movement |
| Runway | How long your cash lasts |

Each metric shows:
- The current value
- Change from last week (percentage and direction)

### Recommended Actions

A prioritized list of 3-5 things to focus on. Actions are ordered by importance:

1. **Overdue invoices first**: Collecting money you're already owed
2. **Expense concerns**: Unusual spending spikes to investigate
3. **Business health**: Diversification, unbilled work, or other strategic items

**Example actions:**
- Collect $7,500 from Acme Corp
- Review Travel expenses ($2,500, +100%)
- Diversify—100% revenue from one customer

### Highlight Story

A brief narrative about the most notable aspect of your week:

- Milestones: "Best profit week since October"
- Streaks: "Third consecutive profitable week"
- Recoveries: "Back in profit after two down weeks"
- Payment wins: "All invoices paid on time for 5 weeks"

## Understanding your runway date

One of the most useful parts of Weekly Insights is seeing exactly when your cash runs out—not just "3 months" but a specific date like "March 15, 2026."

This helps you:
- **Set real deadlines**: "I need to close a deal by March 1"
- **Plan fundraising**: Know exactly when conversations need to conclude
- **Prioritize collections**: See how collecting one invoice extends your date

The date updates each week based on your current cash and spending rate.

## Trends and patterns

Weekly Insights tracks patterns across multiple weeks:

### Streaks

When something happens consistently, you'll see it called out:

| Streak | When it appears |
|--------|-----------------|
| Profitable weeks | 3+ consecutive weeks with positive profit |
| Revenue growth | 3+ consecutive weeks of increasing revenue |
| Revenue decline | 2+ consecutive weeks of declining revenue (early warning) |
| Invoices paid on time | 3+ weeks where all invoices were paid on time |

Revenue decline is highlighted after just 2 weeks because catching this early matters.

### Year-over-year comparison

When you have enough historical data, you'll see how this week compares to the same week last year:

> "Revenue up 35% vs last year"

This helps you understand if changes are seasonal or represent real growth.

### Quarter pace

Midway through a quarter, you'll see projections:

> "On pace for $450,000 this Q1—18% ahead of Q1 last year"

This helps you gauge progress toward quarterly goals.

## Payment alerts

If a customer who normally pays quickly is unusually late, you'll see a flag:

> "Acme Corp: $7,500 overdue—unusual delay, they typically pay in 5 days"

This helps you prioritize follow-ups. A normally reliable customer being late might indicate a problem worth a phone call, while a habitually slow payer being a few days late is less concerning.

## Different week types

Weekly Insights adapts its tone and focus based on how your week went:

| Week Type | What you'll see |
|-----------|-----------------|
| Great week | Celebration of wins, forward-looking suggestions |
| Good week | Balanced overview, areas for improvement |
| Quiet week | Reassurance that slow weeks are normal, focus on existing receivables |
| Challenging week | Honest assessment, emphasis on runway, prioritized recovery actions |

For quiet weeks with no activity, you won't see misleading "down 100% from last week" language. Instead, the summary acknowledges it was slow and focuses on what you can control (like collecting receivables).

## Listen instead of read

Your weekly insight is also available as a short audio summary you can listen to instead of reading. This is useful when you:

- Want a quick update while commuting or exercising
- Prefer hands-free consumption of information
- Are checking in from your phone

The audio summary covers the same key points—profit, runway, overdue invoices, and recommended actions—in a natural, conversational tone. Just tap play on the audio player in your insight.

## When insights are generated

New insights are generated weekly, typically Sunday evening or Monday morning. You'll receive:

- A notification in Midday
- An email summary (if enabled in notification settings)
- An audio version you can play anytime

## Getting the most from insights

### Keep your data current

The better your data, the better your insights:

- **Connect all business accounts**: Missing accounts mean incomplete picture
- **Categorize transactions**: Helps identify expense patterns
- **Mark invoices as paid**: Keeps receivables accurate
- **Update invoice status**: Cancel invoices that won't be collected

### Review weekly

Spend 2 minutes each Monday:

1. Read the summary—any surprises?
2. Check the key metrics—anything unusual?
3. Review actions—can you tackle the top one today?

### Act on recommendations

The actions aren't generic advice—they're specific to your business this week. Following up on an overdue invoice often takes just one email.

## First week insights

Your first insight won't have comparison data, so you'll see:

- Current metrics without percentage changes
- "This period" instead of "vs last week"
- A welcome message explaining what to expect

After a few weeks, you'll start seeing trends, comparisons, and pattern detection.

## Multi-currency support

If you operate in multiple currencies:

- All metrics are shown in your base currency
- Invoice amounts appear in their original currency
- Exchange rates are current as of insight generation

Set your base currency in [Settings](https://app.midday.ai/settings).

## Privacy and data

Your insights are generated using AI, but:

- Data never leaves Midday's secure infrastructure
- No third parties see your financial details
- Insights are only visible to your team members

## Related features

- [Understanding metrics](/docs/understanding-metrics): Learn how each number is calculated
- [Check your runway](/docs/check-runway): Deep dive into runway calculations
- [Revenue forecast](/docs/revenue-forecast): See projected future revenue
- [Notification settings](/docs/notification-settings): Control how you receive insights

---

--- METRICS ---
- Files Extracted: 64
- Estimated Token Budget: ~91337 tokens
- Canonical Reference: https://codewiki.google/github.com/midday-ai/midday
