Skip to main content
Reconciliation helps you verify that your internal ledger matches external records from payment processors, banks, or third-party systems. Blnk provides powerful matching rules and automated reconciliation workflows.

How reconciliation works

Blnk’s reconciliation process:
  1. Upload external data - Import CSV/JSON files or send via API
  2. Define matching rules - Set criteria for identifying matches
  3. Start reconciliation - Choose strategy (one-to-one, one-to-many, many-to-one)
  4. Review results - See matched and unmatched transactions

Uploading external data

Import transactions from external sources:
1

Upload CSV file

POST /reconciliation/upload
Content-Type: multipart/form-data

file: transactions.csv
source: "stripe"
CSV format:
reference,amount,currency,description,date
ch_abc123,100.00,USD,Payment from customer,2024-01-15T10:30:00Z
ch_def456,250.00,USD,Subscription payment,2024-01-15T11:00:00Z
ch_ghi789,75.50,USD,One-time purchase,2024-01-15T12:30:00Z
2

Response

{
  "upload_id": "upload_xyz789abc",
  "source": "stripe",
  "total_records": 3,
  "status": "completed"
}

Upload via API (instant reconciliation)

Skip file uploads and send transactions directly:
POST /reconciliation/instant
{
  "external_transactions": [
    {
      "reference": "ch_abc123",
      "amount": 100.00,
      "currency": "USD",
      "description": "Payment from customer",
      "date": "2024-01-15T10:30:00Z"
    },
    {
      "reference": "ch_def456",
      "amount": 250.00,
      "currency": "USD",
      "description": "Subscription payment",
      "date": "2024-01-15T11:00:00Z"
    }
  ],
  "strategy": "one_to_one",
  "matching_rule_ids": ["rule_exact_match"],
  "is_dry_run": false
}

Creating matching rules

Define how to match external and internal transactions:
1

Create matching rule

POST /reconciliation/matching-rules
{
  "name": "Exact amount and date match",
  "description": "Match transactions with same amount and date",
  "criteria": [
    {
      "field": "amount",
      "operator": "equals",
      "allowable_drift": 0
    },
    {
      "field": "date",
      "operator": "equals",
      "allowable_drift": 0
    }
  ]
}
2

Response

{
  "rule_id": "rule_abc123xyz",
  "name": "Exact amount and date match",
  "description": "Match transactions with same amount and date",
  "criteria": [
    {
      "field": "amount",
      "operator": "equals",
      "allowable_drift": 0
    },
    {
      "field": "date",
      "operator": "equals",
      "allowable_drift": 0
    }
  ],
  "created_at": "2024-01-15T09:00:00Z"
}

Matching criteria fields

Amount matching

{
  "field": "amount",
  "operator": "equals",
  "allowable_drift": 0.01
}
Allows up to $0.01 difference (useful for currency conversion rounding).

Date matching

{
  "field": "date",
  "operator": "equals",
  "allowable_drift": 86400
}
Allows up to 1 day difference (86400 seconds).

Reference matching

{
  "field": "reference",
  "operator": "contains",
  "allowable_drift": 3
}
Uses fuzzy matching with Levenshtein distance ≤ 3.

Description matching

{
  "field": "description",
  "operator": "contains"
}
Checks if description contains the search string.

Currency matching

{
  "field": "currency",
  "operator": "equals"
}
Strict currency code matching.

Reconciliation strategies

One-to-One

Match each external transaction to one internal transaction:
POST /reconciliation/start
{
  "upload_id": "upload_xyz789abc",
  "strategy": "one_to_one",
  "matching_rule_ids": ["rule_abc123xyz"],
  "is_dry_run": false
}
Use case: Standard payment reconciliation where each external payment maps to one internal transaction.

One-to-Many

Match one external transaction to multiple internal transactions:
POST /reconciliation/start
{
  "upload_id": "upload_xyz789abc",
  "strategy": "one_to_many",
  "group_criteria": "reference",
  "matching_rule_ids": ["rule_abc123xyz"],
  "is_dry_run": false
}
Use case: Bank deposit that combines multiple customer payments.

Many-to-One

Match multiple external transactions to one internal transaction:
POST /reconciliation/start
{
  "upload_id": "upload_xyz789abc",
  "strategy": "many_to_one",
  "group_criteria": "date",
  "matching_rule_ids": ["rule_abc123xyz"],
  "is_dry_run": false
}
Use case: Multiple partial payments from a customer matching one invoice.

Starting reconciliation

1

Start reconciliation process

POST /reconciliation/start
{
  "upload_id": "upload_xyz789abc",
  "strategy": "one_to_one",
  "matching_rule_ids": ["rule_exact_match"],
  "is_dry_run": false
}
2

Response

{
  "reconciliation_id": "recon_abc123def456",
  "status": "started",
  "upload_id": "upload_xyz789abc",
  "started_at": "2024-01-15T10:00:00Z"
}
3

Check progress

GET /reconciliation/recon_abc123def456
{
  "reconciliation_id": "recon_abc123def456",
  "status": "in_progress",
  "matched_transactions": 245,
  "unmatched_transactions": 12,
  "started_at": "2024-01-15T10:00:00Z"
}

Dry run mode

Test reconciliation without recording matches:
POST /reconciliation/start
{
  "upload_id": "upload_xyz789abc",
  "strategy": "one_to_one",
  "matching_rule_ids": ["rule_abc123xyz"],
  "is_dry_run": true
}
Results show what would be matched without actually creating match records.

Viewing results

Get reconciliation details

GET /reconciliation/recon_abc123def456
{
  "reconciliation_id": "recon_abc123def456",
  "status": "completed",
  "upload_id": "upload_xyz789abc",
  "matched_transactions": 250,
  "unmatched_transactions": 5,
  "started_at": "2024-01-15T10:00:00Z",
  "completed_at": "2024-01-15T10:15:00Z"
}

Get matched transactions

GET /reconciliation/recon_abc123def456/matches
[
  {
    "external_transaction_id": "ext_abc123",
    "internal_transaction_id": "txn_xyz789",
    "amount": 100.00,
    "date": "2024-01-15T10:30:00Z",
    "match_confidence": 1.0
  },
  {
    "external_transaction_id": "ext_def456",
    "internal_transaction_id": "txn_ghi012",
    "amount": 250.00,
    "date": "2024-01-15T11:00:00Z",
    "match_confidence": 1.0
  }
]

Get unmatched transactions

GET /reconciliation/recon_abc123def456/unmatched
[
  "ext_unmatched_001",
  "ext_unmatched_002",
  "ext_unmatched_003"
]

Common use cases

Stripe payment reconciliation

// 1. Create matching rule
POST /reconciliation/matching-rules
{
  "name": "Stripe charge matching",
  "criteria": [
    {
      "field": "reference",
      "operator": "contains"
    },
    {
      "field": "amount",
      "operator": "equals",
      "allowable_drift": 0
    },
    {
      "field": "currency",
      "operator": "equals"
    }
  ]
}

// 2. Upload Stripe transactions
POST /reconciliation/upload
{
  "file": "stripe_charges_january_2024.csv",
  "source": "stripe"
}

// 3. Start reconciliation
POST /reconciliation/start
{
  "upload_id": "upload_stripe_jan",
  "strategy": "one_to_one",
  "matching_rule_ids": ["rule_stripe_match"]
}

Bank statement reconciliation

// 1. Create lenient matching rule for bank statements
POST /reconciliation/matching-rules
{
  "name": "Bank transaction matching",
  "criteria": [
    {
      "field": "amount",
      "operator": "equals",
      "allowable_drift": 0.01
    },
    {
      "field": "date",
      "operator": "equals",
      "allowable_drift": 172800
    }
  ]
}

// 2. Upload bank statement
POST /reconciliation/upload
{
  "file": "bank_statement_january.csv",
  "source": "bank_of_america"
}

// 3. Reconcile with date tolerance
POST /reconciliation/start
{
  "upload_id": "upload_bank_jan",
  "strategy": "one_to_one",
  "matching_rule_ids": ["rule_bank_match"]
}

Multi-payment aggregation

// Match multiple customer payments to one bank deposit
POST /reconciliation/start
{
  "upload_id": "upload_bank_deposits",
  "strategy": "many_to_one",
  "group_criteria": "date",
  "matching_rule_ids": ["rule_aggregate_match"]
}

Implementation code (from reconciliation.go:334-369)

The reconciliation workflow:
func (s *Blnk) processReconciliation(ctx context.Context, reconciliation model.Reconciliation, 
    strategy string, groupCriteria string, matchingRuleIDs []string) error {
    
    // Update status to in progress
    if err := s.updateReconciliationStatus(ctx, reconciliation.ReconciliationID, StatusInProgress); err != nil {
        return fmt.Errorf("failed to update reconciliation status: %w", err)
    }

    // Retrieve matching rules
    matchingRules, err := s.getMatchingRules(ctx, matchingRuleIDs)
    if err != nil {
        return fmt.Errorf("failed to get matching rules: %w", err)
    }

    // Initialize progress tracking
    progress, err := s.initializeReconciliationProgress(ctx, reconciliation.ReconciliationID)
    if err != nil {
        return fmt.Errorf("failed to initialize reconciliation progress: %w", err)
    }

    // Create reconciler based on strategy
    reconciler := s.createReconciler(strategy, reconciliation.UploadID, groupCriteria, matchingRules)

    // Create transaction processor
    processor := s.createTransactionProcessor(reconciliation, progress, reconciler)

    // Process transactions
    err = s.processTransactions(ctx, reconciliation.UploadID, processor, strategy)
    if err != nil {
        return fmt.Errorf("failed to process transactions: %w", err)
    }

    // Get results
    matched, unmatched := processor.getResults()

    // Finalize reconciliation
    return s.finalizeReconciliation(ctx, reconciliation, matched, unmatched)
}

Best practices

Start with dry runs

Always test matching rules with is_dry_run: true first

Use multiple rules

Apply multiple matching rules for higher confidence

Allow reasonable drift

Set allowable_drift to handle rounding and timezone differences

Review unmatched

Investigate unmatched transactions to improve matching rules

Automate uploads

Schedule regular uploads for continuous reconciliation

Archive results

Store reconciliation reports for audit trails

Troubleshooting

No matches found

Problem: Reconciliation completes but finds no matches Solutions:
  1. Check date formats match (ISO 8601)
  2. Verify currency codes are identical
  3. Review amount precision (use same decimal places)
  4. Increase allowable_drift for amounts and dates

Too many false positives

Problem: Reconciliation matches unrelated transactions Solutions:
  1. Add more matching criteria
  2. Reduce allowable_drift values
  3. Use stricter operators (“equals” instead of “contains”)
  4. Include reference field matching

Performance issues with large datasets

Problem: Reconciliation takes too long Solutions:
  1. Upload in smaller batches
  2. Use indexed fields (reference, amount, date)
  3. Narrow date ranges
  4. Run during off-peak hours

Next steps

Webhooks

Get notified when reconciliation completes

Bulk Transactions

Process reconciled transactions in batches

Build docs developers (and LLMs) love