Skip to content

Reconciliation API

Match bank transactions with journal entries.

Endpoints

Method Endpoint Description
POST /api/reconciliation/run Run matching
GET /api/reconciliation/pending Get pending reviews
POST /api/reconciliation/matches/{id}/accept Accept match
POST /api/reconciliation/matches/{id}/reject Reject match
POST /api/reconciliation/batch-accept Batch accept
GET /api/reconciliation/stats Get statistics
GET /api/reconciliation/unmatched Get unmatched

Run Matching

POST /api/reconciliation/run

Executes the matching algorithm for a statement or account.

Request Body

Field Type Required Description
statement_id UUID ❌* Statement to reconcile
account_id UUID ❌* Account to reconcile
start_date date Filter from date
end_date date Filter to date

*Either statement_id or account_id is required.

Request

{
  "statement_id": "aa0e8400-e29b-41d4-a716-446655440000"
}

Response

{
  "run_id": "bb0e8400-e29b-41d4-a716-446655440001",
  "status": "completed",
  "summary": {
    "total_transactions": 50,
    "auto_accepted": 35,
    "pending_review": 10,
    "unmatched": 5
  },
  "completed_at": "2026-01-10T12:30:00Z"
}

Get Pending Reviews

GET /api/reconciliation/pending

Query Parameters

Parameter Type Description
account_id UUID Filter by account
min_score integer Minimum score (default: 60)
max_score integer Maximum score (default: 84)
page integer Page number
per_page integer Items per page

Response

{
  "items": [
    {
      "id": "cc0e8400-e29b-41d4-a716-446655440002",
      "status": "pending_review",
      "score": 78,
      "score_breakdown": {
        "amount": 100,
        "date": 100,
        "description": 45,
        "business": 80,
        "history": 60
      },
      "bank_transaction": {
        "id": "dd0e8400-e29b-41d4-a716-446655440003",
        "date": "2026-01-06",
        "description": "WHOLE FOODS #1234",
        "amount": "87.50",
        "type": "debit"
      },
      "journal_entries": [
        {
          "id": "770e8400-e29b-41d4-a716-446655440000",
          "entry_date": "2026-01-06",
          "memo": "Groceries - Whole Foods",
          "total_amount": "87.50"
        }
      ],
      "created_at": "2026-01-10T12:00:00Z"
    }
  ],
  "total": 10,
  "page": 1,
  "per_page": 20
}

Accept Match

POST /api/reconciliation/matches/{id}/accept

Response

{
  "id": "cc0e8400-e29b-41d4-a716-446655440002",
  "status": "accepted",
  "accepted_at": "2026-01-10T12:35:00Z"
}

Errors

Code Description
400 Match already processed
404 Match not found

Reject Match

POST /api/reconciliation/matches/{id}/reject

Request

{
  "reason": "Wrong transaction - different payee"
}

Response

{
  "id": "cc0e8400-e29b-41d4-a716-446655440002",
  "status": "rejected",
  "reject_reason": "Wrong transaction - different payee",
  "rejected_at": "2026-01-10T12:36:00Z"
}

Batch Accept

POST /api/reconciliation/batch-accept

Accept all pending matches above a score threshold.

Request

{
  "min_score": 80,
  "account_id": "550e8400-e29b-41d4-a716-446655440000"
}

Response

{
  "accepted_count": 25,
  "match_ids": [
    "cc0e8400-e29b-41d4-a716-446655440002",
    "cc0e8400-e29b-41d4-a716-446655440003",
    ...
  ]
}

Get Statistics

GET /api/reconciliation/stats

Query Parameters

Parameter Type Description
account_id UUID Filter by account
start_date date Stats from date
end_date date Stats to date

Response

{
  "total_transactions": 150,
  "reconciled": 142,
  "pending_review": 5,
  "unmatched": 3,
  "reconciliation_rate": 94.67,
  "average_score": 88.5,
  "score_distribution": {
    "85_100": 120,
    "60_84": 22,
    "0_59": 8
  },
  "by_account": [
    {
      "account_id": "550e8400-e29b-41d4-a716-446655440000",
      "account_name": "Chase Checking",
      "reconciled": 100,
      "pending": 3,
      "unmatched": 2
    }
  ]
}

Get Unmatched

GET /api/reconciliation/unmatched

Query Parameters

Parameter Type Description
account_id UUID Filter by account
type enum bank_transaction, journal_entry
page integer Page number
per_page integer Items per page

Response

{
  "items": [
    {
      "type": "bank_transaction",
      "transaction": {
        "id": "ee0e8400-e29b-41d4-a716-446655440004",
        "date": "2026-01-08",
        "description": "BANK FEE",
        "amount": "15.00",
        "type": "debit"
      },
      "suggested_action": "create_entry",
      "suggested_accounts": [
        {
          "id": "ff0e8400-e29b-41d4-a716-446655440005",
          "name": "Bank Fees",
          "confidence": 0.85
        }
      ]
    }
  ],
  "total": 3,
  "page": 1,
  "per_page": 20
}

Match Schema

interface ReconciliationMatch {
  id: string;                    // UUID
  status: MatchStatus;
  score: number;                 // 0-100
  score_breakdown: ScoreBreakdown;
  bank_transaction: BankTransaction;
  journal_entries: JournalEntry[];
  version: number;               // For optimistic locking
  superseded_by_id?: string;     // If replaced
  created_at: string;
  updated_at: string;
  accepted_at?: string;
  rejected_at?: string;
  reject_reason?: string;
}

interface ScoreBreakdown {
  amount: number;      // 0-100, weight 40%
  date: number;        // 0-100, weight 25%
  description: number; // 0-100, weight 20%
  business: number;    // 0-100, weight 10%
  history: number;     // 0-100, weight 5%
}

type MatchStatus = 
  | "pending"
  | "auto_accepted"
  | "pending_review"
  | "accepted"
  | "rejected"
  | "superseded";

Scoring Details

Dimension Weight 100 Score 0 Score
Amount 40% Exact match >$50 difference
Date 25% Same day >30 days apart
Description 20% Exact text match No similarity
Business 10% Valid account types Type mismatch
History 5% Known pattern New payee

Thresholds

See: docs/ssot/reconciliation.md#thresholds for the authoritative SSOT definition.

Score Action
≥ 85 Auto-accept
60-84 Pending review
< 60 Unmatched

Environment Override

Thresholds can be configured via environment variables:

  • RECONCILIATION_AUTO_ACCEPT_THRESHOLD
  • RECONCILIATION_REVIEW_THRESHOLD

Error Codes

Status Code Description
400 ALREADY_PROCESSED Match already accepted/rejected
400 SUPERSEDED Match was replaced
404 NOT_FOUND Match not found
409 VERSION_CONFLICT Concurrent modification