Skip to content

Reconciliation API

The Reconciliation API provides endpoints for uploading trustee tapes, running reconciliation comparisons, and managing discrepancy issues.


Overview

CalcBridge's reconciliation engine supports:

  • Trustee Tape Upload: Parse and normalize tapes from US Bank, Deutsche, and Wilmington
  • Automated Comparison: Match loans by CUSIP and detect variances
  • Issue Tracking: Severity-based discrepancy management with resolution workflow
  • Configurable Rules: Tolerance thresholds for numeric and date comparisons

Endpoints

Upload Trustee Tape

Upload and process a trustee tape file.

POST /api/v1/reconciliation/tapes/upload

Request

Content-Type: multipart/form-data

Field Type Required Description
file file Yes Excel file containing trustee tape data
trustee_name string Yes Trustee: us_bank, deutsche, wilmington
statement_date string (date) Yes Date of the trustee statement (YYYY-MM-DD)

Example Request

curl -X POST https://api.calcbridge.io/api/v1/reconciliation/tapes/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@trustee_tape_jan2026.xlsx" \
  -F "trustee_name=us_bank" \
  -F "statement_date=2026-01-31"
import requests

with open("trustee_tape_jan2026.xlsx", "rb") as f:
    response = requests.post(
        "https://api.calcbridge.io/api/v1/reconciliation/tapes/upload",
        headers={"Authorization": f"Bearer {token}"},
        files={"file": ("trustee_tape_jan2026.xlsx", f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
        data={
            "trustee_name": "us_bank",
            "statement_date": "2026-01-31"
        }
    )
tape = response.json()
print(f"Tape ID: {tape['tape_id']} - Job: {tape['job_id']}")
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('trustee_name', 'us_bank');
formData.append('statement_date', '2026-01-31');

const response = await fetch('https://api.calcbridge.io/api/v1/reconciliation/tapes/upload', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${token}` },
  body: formData
});
const tape = await response.json();
console.log('Tape ID:', tape.tape_id, '- Job:', tape.job_id);

Response

{
  "tape_id": "tape_abc123",
  "job_id": "job_xyz789",
  "status": "processing",
  "message": "Trustee tape upload queued for processing"
}

Error Responses

Status Error Description
400 Invalid file format File is not a valid Excel file
400 Invalid trustee Unknown trustee_name value
400 Invalid date statement_date is not a valid date
422 Parse error Tape file could not be parsed
429 Rate limit exceeded Upload quota exceeded

List Trustee Tapes

List trustee tapes for the current tenant.

GET /api/v1/reconciliation/tapes

Query Parameters

Parameter Type Default Description
page integer 1 Page number (1-indexed)
page_size integer 20 Items per page (max 100)
trustee_name string - Filter by trustee
status_filter string - Filter by processing status

Example Request

curl -X GET "https://api.calcbridge.io/api/v1/reconciliation/tapes?trustee_name=us_bank&page_size=50" \
  -H "Authorization: Bearer $TOKEN"
import requests

response = requests.get(
    "https://api.calcbridge.io/api/v1/reconciliation/tapes",
    headers={"Authorization": f"Bearer {token}"},
    params={"trustee_name": "us_bank", "page_size": 50}
)
tapes = response.json()
for tape in tapes["items"]:
    print(f"{tape['id']}: {tape['statement_date']} - {tape['loan_count']} loans")
const response = await fetch(
  'https://api.calcbridge.io/api/v1/reconciliation/tapes?trustee_name=us_bank&page_size=50',
  { headers: { 'Authorization': `Bearer ${token}` } }
);
const tapes = await response.json();
tapes.items.forEach(tape => {
  console.log(`${tape.id}: ${tape.statement_date} - ${tape.loan_count} loans`);
});

Response

{
  "items": [
    {
      "id": "tape_abc123",
      "trustee_name": "us_bank",
      "statement_date": "2026-01-31",
      "file_name": "trustee_tape_jan2026.xlsx",
      "loan_count": 150,
      "total_par": 450000000.00,
      "processing_status": "completed",
      "error_message": null,
      "created_at": "2026-02-01T09:00:00Z"
    },
    {
      "id": "tape_def456",
      "trustee_name": "us_bank",
      "statement_date": "2025-12-31",
      "file_name": "trustee_tape_dec2025.xlsx",
      "loan_count": 148,
      "total_par": 445000000.00,
      "processing_status": "completed",
      "error_message": null,
      "created_at": "2026-01-02T09:00:00Z"
    }
  ],
  "total": 12,
  "page": 1,
  "page_size": 50,
  "total_pages": 1
}

Get Trustee Tape

Get details of a specific trustee tape.

GET /api/v1/reconciliation/tapes/{tape_id}

Path Parameters

Parameter Type Description
tape_id string Trustee tape identifier

Example Request

curl -X GET "https://api.calcbridge.io/api/v1/reconciliation/tapes/tape_abc123" \
  -H "Authorization: Bearer $TOKEN"
import requests

response = requests.get(
    f"https://api.calcbridge.io/api/v1/reconciliation/tapes/{tape_id}",
    headers={"Authorization": f"Bearer {token}"}
)
tape = response.json()
print(f"Trustee: {tape['trustee_name']}")
print(f"Loans: {tape['loan_count']}")
print(f"Total Par: ${tape['total_par']:,.2f}")
const response = await fetch(
  `https://api.calcbridge.io/api/v1/reconciliation/tapes/${tapeId}`,
  { headers: { 'Authorization': `Bearer ${token}` } }
);
const tape = await response.json();
console.log('Trustee:', tape.trustee_name);
console.log('Loans:', tape.loan_count);

Response

{
  "id": "tape_abc123",
  "trustee_name": "us_bank",
  "statement_date": "2026-01-31",
  "file_name": "trustee_tape_jan2026.xlsx",
  "loan_count": 150,
  "total_par": 450000000.00,
  "processing_status": "completed",
  "error_message": null,
  "created_at": "2026-02-01T09:00:00Z"
}

Error Responses

Status Error Description
404 Tape not found Invalid tape_id or access denied

Get Tape Loans

Get loans from a trustee tape with pagination.

GET /api/v1/reconciliation/tapes/{tape_id}/loans

Path Parameters

Parameter Type Description
tape_id string Trustee tape identifier

Query Parameters

Parameter Type Default Description
page integer 1 Page number (1-indexed)
page_size integer 50 Items per page (max 500)

Example Request

curl -X GET "https://api.calcbridge.io/api/v1/reconciliation/tapes/tape_abc123/loans?page=1&page_size=50" \
  -H "Authorization: Bearer $TOKEN"
import requests

response = requests.get(
    f"https://api.calcbridge.io/api/v1/reconciliation/tapes/{tape_id}/loans",
    headers={"Authorization": f"Bearer {token}"},
    params={"page": 1, "page_size": 50}
)
loans = response.json()
for loan in loans["items"]:
    print(f"{loan['cusip']}: {loan['borrower_name']} - ${loan['par_value']:,.2f}")
const response = await fetch(
  `https://api.calcbridge.io/api/v1/reconciliation/tapes/${tapeId}/loans?page=1&page_size=50`,
  { headers: { 'Authorization': `Bearer ${token}` } }
);
const loans = await response.json();
loans.items.forEach(loan => {
  console.log(`${loan.cusip}: ${loan.borrower_name} - $${loan.par_value}`);
});

Response

{
  "items": [
    {
      "id": "loan_abc123",
      "tape_id": "tape_abc123",
      "cusip": "12345ABC",
      "loanx_id": "LX123456",
      "primary_identifier": "12345ABC",
      "borrower_name": "Acme Corp",
      "par_value": 5000000.00,
      "price": 98.50,
      "rating_moodys": "Ba2",
      "rating_sp": "BB",
      "rating_fitch": "BB",
      "spread": 350,
      "base_rate": "SOFR",
      "all_in_rate": 8.75,
      "maturity_date": "2029-06-15",
      "industry": "Technology",
      "country": "US",
      "days_past_due": 0
    },
    {
      "id": "loan_def456",
      "tape_id": "tape_abc123",
      "cusip": "67890XYZ",
      "loanx_id": "LX789012",
      "primary_identifier": "67890XYZ",
      "borrower_name": "Tech Industries",
      "par_value": 7500000.00,
      "price": 97.25,
      "rating_moodys": "B1",
      "rating_sp": "B+",
      "rating_fitch": "B+",
      "spread": 425,
      "base_rate": "SOFR",
      "all_in_rate": 9.50,
      "maturity_date": "2030-03-20",
      "industry": "Software",
      "country": "US",
      "days_past_due": 0
    }
  ],
  "total": 150,
  "page": 1,
  "page_size": 50,
  "total_pages": 3
}

Trigger Reconciliation

Trigger reconciliation between a trustee tape and internal workbook.

POST /api/v1/reconciliation/compare

Request Body

Field Type Required Description
tape_id string Yes Trustee tape to compare
workbook_id string (UUID) Yes Internal workbook to compare against
rules string[] No Specific rules to run (all if omitted)

Example Request

curl -X POST https://api.calcbridge.io/api/v1/reconciliation/compare \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tape_id": "tape_abc123",
    "workbook_id": "550e8400-e29b-41d4-a716-446655440000",
    "rules": ["par_value", "rating", "spread"]
  }'
import requests

response = requests.post(
    "https://api.calcbridge.io/api/v1/reconciliation/compare",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "tape_id": "tape_abc123",
        "workbook_id": "550e8400-e29b-41d4-a716-446655440000",
        "rules": ["par_value", "rating", "spread"]
    }
)
job = response.json()
print(f"Reconciliation started: {job['job_id']}")
const response = await fetch('https://api.calcbridge.io/api/v1/reconciliation/compare', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    tape_id: 'tape_abc123',
    workbook_id: '550e8400-e29b-41d4-a716-446655440000',
    rules: ['par_value', 'rating', 'spread']
  })
});
const job = await response.json();
console.log('Reconciliation started:', job.job_id);

Response

{
  "job_id": "recon_job_abc123",
  "tape_id": "tape_abc123",
  "workbook_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "processing",
  "message": "Reconciliation started"
}

Error Responses

Status Error Description
400 Tape not ready Tape is still processing
400 Invalid rules Unknown rule identifiers
404 Tape not found Invalid tape_id or access denied
404 Workbook not found Invalid workbook_id or access denied

Get Reconciliation Status

Get status of a reconciliation job.

GET /api/v1/reconciliation/compare/{job_id}/status

Path Parameters

Parameter Type Description
job_id string Reconciliation job identifier

Example Request

curl -X GET "https://api.calcbridge.io/api/v1/reconciliation/compare/recon_job_abc123/status" \
  -H "Authorization: Bearer $TOKEN"
import requests

response = requests.get(
    f"https://api.calcbridge.io/api/v1/reconciliation/compare/{job_id}/status",
    headers={"Authorization": f"Bearer {token}"}
)
status = response.json()
print(f"Status: {status['status']}")
if status["status"] == "completed":
    summary = status["summary"]
    print(f"Matched: {summary['matched']}/{summary['total_loans_compared']}")
    print(f"Issues: {summary['issues_found']}")
const response = await fetch(
  `https://api.calcbridge.io/api/v1/reconciliation/compare/${jobId}/status`,
  { headers: { 'Authorization': `Bearer ${token}` } }
);
const status = await response.json();
console.log('Status:', status.status);
if (status.status === 'completed') {
  console.log('Matched:', status.summary.matched);
  console.log('Issues:', status.summary.issues_found);
}

Response

{
  "job_id": "recon_job_abc123",
  "status": "completed",
  "summary": {
    "total_loans_compared": 150,
    "matched": 145,
    "unmatched_trustee": 3,
    "unmatched_internal": 2,
    "issues_found": 12
  },
  "completed_at": "2026-02-01T09:05:00Z"
}

Error Responses

Status Error Description
404 Job not found Invalid job_id or access denied

List Reconciliation Issues

List reconciliation issues for the current tenant.

GET /api/v1/reconciliation/issues

Query Parameters

Parameter Type Default Description
page integer 1 Page number (1-indexed)
page_size integer 50 Items per page (max 200)
tape_id string - Filter by tape
status string - Filter: open, acknowledged, resolved, false_positive
severity string - Filter: critical, error, warning, info
category string - Filter by category

Example Request

curl -X GET "https://api.calcbridge.io/api/v1/reconciliation/issues?severity=critical&status=open" \
  -H "Authorization: Bearer $TOKEN"
import requests

response = requests.get(
    "https://api.calcbridge.io/api/v1/reconciliation/issues",
    headers={"Authorization": f"Bearer {token}"},
    params={"severity": "critical", "status": "open"}
)
issues = response.json()
for issue in issues["items"]:
    print(f"[{issue['severity']}] {issue['cusip']}: {issue['field_name']} - "
          f"trustee={issue['trustee_value']}, internal={issue['internal_value']}")
const response = await fetch(
  'https://api.calcbridge.io/api/v1/reconciliation/issues?severity=critical&status=open',
  { headers: { 'Authorization': `Bearer ${token}` } }
);
const issues = await response.json();
issues.items.forEach(issue => {
  console.log(`[${issue.severity}] ${issue.cusip}: ${issue.field_name}`);
});

Response

{
  "items": [
    {
      "id": "issue_abc123",
      "tape_id": "tape_abc123",
      "cusip": "12345ABC",
      "identifier_type": "cusip",
      "issue_type": "value_mismatch",
      "severity": "critical",
      "category": "calculation",
      "field_name": "par_value",
      "trustee_value": "5000000.00",
      "internal_value": "4950000.00",
      "status": "open",
      "resolution_notes": null,
      "created_at": "2026-02-01T09:05:00Z"
    },
    {
      "id": "issue_def456",
      "tape_id": "tape_abc123",
      "cusip": "67890XYZ",
      "identifier_type": "cusip",
      "issue_type": "value_mismatch",
      "severity": "warning",
      "category": "rating",
      "field_name": "rating_moodys",
      "trustee_value": "Ba3",
      "internal_value": "Ba2",
      "status": "open",
      "resolution_notes": null,
      "created_at": "2026-02-01T09:05:00Z"
    }
  ],
  "total": 12,
  "page": 1,
  "page_size": 50,
  "total_pages": 1
}

Get Issue Summary

Get summary of reconciliation issues by severity and status.

GET /api/v1/reconciliation/issues/summary

Query Parameters

Parameter Type Default Description
tape_id string - Filter by tape

Example Request

curl -X GET "https://api.calcbridge.io/api/v1/reconciliation/issues/summary?tape_id=tape_abc123" \
  -H "Authorization: Bearer $TOKEN"
import requests

response = requests.get(
    "https://api.calcbridge.io/api/v1/reconciliation/issues/summary",
    headers={"Authorization": f"Bearer {token}"},
    params={"tape_id": "tape_abc123"}
)
summary = response.json()
print(f"Total issues: {summary['total']}")
print(f"Critical: {summary['by_severity']['critical']}")
print(f"Open: {summary['by_status']['open']}")
const response = await fetch(
  'https://api.calcbridge.io/api/v1/reconciliation/issues/summary?tape_id=tape_abc123',
  { headers: { 'Authorization': `Bearer ${token}` } }
);
const summary = await response.json();
console.log('Total:', summary.total);
console.log('Critical:', summary.by_severity.critical);

Response

{
  "total": 12,
  "by_severity": {
    "critical": 1,
    "error": 4,
    "warning": 5,
    "info": 2
  },
  "by_status": {
    "open": 8,
    "acknowledged": 2,
    "resolved": 1,
    "false_positive": 1
  }
}

Update Issue

Update a reconciliation issue status or resolution.

PATCH /api/v1/reconciliation/issues/{issue_id}

Path Parameters

Parameter Type Description
issue_id string Issue identifier

Request Body

Field Type Required Description
status string No New status: open, acknowledged, resolved, false_positive
resolution_notes string No Notes about the resolution

Example Request

curl -X PATCH https://api.calcbridge.io/api/v1/reconciliation/issues/issue_abc123 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "resolved",
    "resolution_notes": "Trustee tape contained stale data. Confirmed internal value is correct per trade settlement."
  }'
import requests

response = requests.patch(
    f"https://api.calcbridge.io/api/v1/reconciliation/issues/{issue_id}",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "status": "resolved",
        "resolution_notes": "Trustee tape contained stale data. Confirmed internal value is correct per trade settlement."
    }
)
issue = response.json()
print(f"Issue {issue['id']} updated to: {issue['status']}")
const response = await fetch(
  `https://api.calcbridge.io/api/v1/reconciliation/issues/${issueId}`,
  {
    method: 'PATCH',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      status: 'resolved',
      resolution_notes: 'Trustee tape contained stale data. Confirmed internal value is correct per trade settlement.'
    })
  }
);
const issue = await response.json();
console.log('Issue updated to:', issue.status);

Response

{
  "id": "issue_abc123",
  "tape_id": "tape_abc123",
  "cusip": "12345ABC",
  "identifier_type": "cusip",
  "issue_type": "value_mismatch",
  "severity": "critical",
  "category": "calculation",
  "field_name": "par_value",
  "trustee_value": "5000000.00",
  "internal_value": "4950000.00",
  "status": "resolved",
  "resolution_notes": "Trustee tape contained stale data. Confirmed internal value is correct per trade settlement.",
  "created_at": "2026-02-01T09:05:00Z",
  "updated_at": "2026-02-02T14:30:00Z"
}

Error Responses

Status Error Description
400 Invalid status Unknown status value
404 Issue not found Invalid issue_id or access denied

Severity Levels

Severity Criteria
Critical Par value differs >5% OR missing loan >$1M
Error Par value differs 1-5% OR missing loan \(100K-\)1M
Warning Rating differs OR spread differs >10bps
Info Minor field differences (name formatting, rounding)

Issue Status Flow

Status Description
open Issue detected, pending review
acknowledged Issue reviewed, investigation in progress
resolved Issue resolved with corrective action
false_positive Issue determined to be a false positive

Reconciliation Rules

Rule Description Default Tolerance
par_value Compare par/principal balance 0.01 (absolute)
price Compare market price 0.125 (⅛th point)
rating Compare agency ratings Exact match
spread Compare credit spread 10 bps
maturity_date Compare maturity date 0 days
base_rate Compare base rate type Exact match
all_in_rate Compare all-in coupon rate 5 bps
industry Compare industry classification Exact match
missing_loan Detect loans in one source but not the other N/A

Best Practices

  1. Upload tapes promptly: Process trustee tapes as soon as they are received
  2. Resolve critical issues first: Prioritize critical and error severity issues
  3. Document resolutions: Always include resolution notes for audit trail
  4. Review false positives: Periodically review false positive classifications
  5. Run all rules: Avoid filtering rules unless troubleshooting specific issues
  6. Compare same period: Ensure tape and workbook cover the same reporting period