diff --git a/apps/python/fincall/README.md b/apps/python/fincall/README.md
new file mode 100644
index 0000000..24f76b8
--- /dev/null
+++ b/apps/python/fincall/README.md
@@ -0,0 +1,105 @@
+# FinCall
+
+AI-powered financial operations phone agent for overdue invoice follow-up using CALL-E.
+
+## What it does
+
+FinCall helps finance teams prioritize overdue invoices and conduct structured payment follow-up calls.
+
+The workflow is:
+
+1. Load overdue invoices
+2. Calculate collection priority
+3. Select an invoice
+4. Initiate an authorized CALL-E phone call
+5. Extract payment status and expected payment date
+6. Store the call outcome
+7. Recommend the next finance action
+
+## Features
+
+- Multiple overdue invoices
+- Collection prioritization
+- CALL-E outbound phone calls
+- Structured payment-status extraction
+- Expected payment-date extraction
+- Call evidence
+- Persistent call history
+- Recommended next actions
+- Streamlit dashboard
+
+## Setup
+
+Install the dependencies:
+
+```bash
+pip install -r requirements.txt
+```
+
+Create a .env file:
+
+```bash
+CALLE_API_KEY=your_api_key
+```
+
+Run the application:
+
+```bash
+streamlit run app.py
+```
+
+## Credentials
+
+The CALL-E API key must be provided through the `CALLE_API_KEY` environment variable.
+
+Never commit API keys, credentials, or other secrets.
+
+## Phone numbers
+
+The example invoice data uses masked phone numbers.
+
+For live testing, replace the example phone number with a phone number that you are authorized to call.
+
+Do not use unauthorized or third-party phone numbers.
+
+## Side effects
+
+Live mode places a real outbound phone call through CALL-E.
+
+A phone call is initiated only when the operator explicitly presses the call button.
+
+Review the selected invoice and recipient before initiating a live call.
+
+## Cancellation and follow-up
+
+FinCall does not automatically initiate recurring calls.
+
+If a call requires follow-up, the previous outcome is displayed so that the operator can decide what action to take.
+
+Any additional call should be explicitly initiated by the operator.
+
+## Preview / demo behavior
+
+The invoice prioritization and dashboard workflow can be reviewed without placing a phone call.
+
+Live phone execution occurs only when the operator explicitly initiates the CALL-E action.
+
+## Safety
+
+- Do not commit credentials or API keys.
+- Use authorized phone numbers only.
+- Example data uses masked phone numbers.
+- Review call outcomes before taking consequential financial action.
+- FinCall does not automatically make recurring collection calls.
+
+## Project structure
+
+```text
+fincall/
+├── app.py
+├── fincall.py
+├── requirements.txt
+├── README.md
+└── data/
+ └── invoices.example.json
+```
diff --git a/apps/python/fincall/app.py b/apps/python/fincall/app.py
new file mode 100644
index 0000000..36dc017
--- /dev/null
+++ b/apps/python/fincall/app.py
@@ -0,0 +1,689 @@
+import json
+from datetime import date, datetime
+from pathlib import Path
+
+import streamlit as st
+
+from fincall import FinCall
+
+
+# ============================================================
+# PAGE CONFIG
+# ============================================================
+
+st.set_page_config(
+ page_title="FinCall",
+ page_icon="📞",
+ layout="wide",
+)
+
+
+# ============================================================
+# CUSTOM CSS
+# ============================================================
+
+st.markdown(
+ """
+
+""",
+ unsafe_allow_html=True,
+)
+
+
+# ============================================================
+# PATHS
+# ============================================================
+
+DATA_PATH = Path("data/invoices.example.json")
+HISTORY_PATH = Path("data/call_history.json")
+
+
+# ============================================================
+# DATA FUNCTIONS
+# ============================================================
+
+def load_invoices():
+ with open(DATA_PATH, "r", encoding="utf-8") as file:
+ return json.load(file)
+
+
+def load_call_history():
+ if not HISTORY_PATH.exists():
+ return {}
+
+ try:
+ with open(HISTORY_PATH, "r", encoding="utf-8") as file:
+ return json.load(file)
+ except Exception:
+ return {}
+
+
+def save_call_history(history):
+ with open(HISTORY_PATH, "w", encoding="utf-8") as file:
+ json.dump(history, file, indent=2)
+
+
+def calculate_priority(invoice):
+ """
+ Calculate collection priority using:
+ - Outstanding amount
+ - Number of days overdue
+ """
+
+ due_date = datetime.strptime(
+ invoice["due_date"],
+ "%Y-%m-%d"
+ ).date()
+
+ today = date.today()
+
+ days_overdue = max(
+ 0,
+ (today - due_date).days
+ )
+
+ amount = float(invoice["amount"])
+
+ # Normalize amount roughly to a 0-100 range.
+ amount_score = min(
+ 100,
+ (amount / 150000) * 100
+ )
+
+ # More overdue days = higher priority.
+ overdue_score = min(
+ 100,
+ days_overdue * 15
+ )
+
+ # Amount is weighted slightly more heavily.
+ score = (
+ amount_score * 0.6
+ + overdue_score * 0.4
+ )
+
+ if score >= 65:
+ priority = "HIGH"
+ elif score >= 35:
+ priority = "MEDIUM"
+ else:
+ priority = "LOW"
+
+ return score, priority, days_overdue
+
+
+# ============================================================
+# LOAD DATA
+# ============================================================
+
+try:
+ invoices = load_invoices()
+ call_history = load_call_history()
+
+except Exception as error:
+ st.error(f"Could not load application data: {error}")
+ st.stop()
+
+
+# ============================================================
+# RANK INVOICES
+# ============================================================
+
+ranked_invoices = []
+
+for invoice in invoices:
+
+ score, priority, days_overdue = calculate_priority(
+ invoice
+ )
+
+ invoice_copy = invoice.copy()
+
+ invoice_copy["priority_score"] = score
+ invoice_copy["priority"] = priority
+ invoice_copy["days_overdue"] = days_overdue
+
+ ranked_invoices.append(invoice_copy)
+
+
+ranked_invoices.sort(
+ key=lambda x: x["priority_score"],
+ reverse=True
+)
+
+
+# ============================================================
+# HEADER
+# ============================================================
+
+st.markdown(
+ '
📞 FinCall
',
+ unsafe_allow_html=True,
+)
+
+st.markdown(
+ ''
+ 'AI-powered financial operations phone agent'
+ '
',
+ unsafe_allow_html=True,
+)
+
+st.divider()
+
+
+# ============================================================
+# TOP METRICS
+# ============================================================
+
+total_overdue = sum(
+ invoice["amount"]
+ for invoice in ranked_invoices
+)
+
+high_priority_count = sum(
+ 1
+ for invoice in ranked_invoices
+ if invoice["priority"] == "HIGH"
+)
+
+col1, col2, col3 = st.columns(3)
+
+with col1:
+ st.metric(
+ "Overdue Invoices",
+ len(ranked_invoices)
+ )
+
+with col2:
+ st.metric(
+ "Outstanding Amount",
+ f"₹{total_overdue:,.0f}"
+ )
+
+with col3:
+ st.metric(
+ "High Priority",
+ high_priority_count
+ )
+
+
+st.divider()
+
+
+# ============================================================
+# COLLECTION PRIORITY
+# ============================================================
+
+st.markdown(
+ ''
+ 'COLLECTION PRIORITY'
+ '
',
+ unsafe_allow_html=True,
+)
+
+st.subheader("Who should be called first?")
+
+
+for index, invoice in enumerate(ranked_invoices):
+
+ priority = invoice["priority"]
+
+ if priority == "HIGH":
+ icon = "🔴"
+ priority_class = "priority-high"
+
+ elif priority == "MEDIUM":
+ icon = "🟠"
+ priority_class = "priority-medium"
+
+ else:
+ icon = "🟢"
+ priority_class = "priority-low"
+
+ # Show latest call outcome if available.
+ history = call_history.get(
+ invoice["invoice_id"],
+ {}
+ )
+
+ last_status = history.get(
+ "payment_status",
+ "Not contacted"
+ )
+
+ if last_status != "Not contacted":
+ last_status = last_status.replace(
+ "_",
+ " "
+ ).title()
+
+ st.markdown(
+ f"""
+**{icon} {invoice["customer"]}**
+Invoice `{invoice["invoice_id"]}` ·
+₹{invoice["amount"]:,.0f} ·
+{invoice["days_overdue"]} day(s) overdue ·
+{priority} PRIORITY
+
+Last outcome: **{last_status}**
+""",
+ unsafe_allow_html=True,
+ )
+
+ if index < len(ranked_invoices) - 1:
+ st.divider()
+
+
+st.divider()
+
+
+# ============================================================
+# SELECT CUSTOMER
+# ============================================================
+
+st.markdown(
+ ''
+ 'AI PAYMENT FOLLOW-UP'
+ '
',
+ unsafe_allow_html=True,
+)
+
+invoice_options = {
+ f'{invoice["invoice_id"]} · {invoice["customer"]} · '
+ f'₹{invoice["amount"]:,.0f}': invoice["invoice_id"]
+ for invoice in ranked_invoices
+}
+
+
+selected_label = st.selectbox(
+ "Select an invoice to contact",
+ options=list(invoice_options.keys()),
+)
+
+selected_invoice_id = invoice_options[selected_label]
+
+selected_invoice = next(
+ invoice
+ for invoice in ranked_invoices
+ if invoice["invoice_id"] == selected_invoice_id
+)
+
+
+# ============================================================
+# SELECTED INVOICE
+# ============================================================
+
+st.markdown(
+ f"""
+
+
SELECTED INVOICE
+
{selected_invoice["customer"]}
+
+ Invoice {selected_invoice["invoice_id"]} ·
+ Due {selected_invoice["due_date"]}
+
+
₹{selected_invoice["amount"]:,.0f}
+
+ {selected_invoice["days_overdue"]} day(s) overdue ·
+ {selected_invoice["priority"]} PRIORITY
+
+
+""",
+ unsafe_allow_html=True,
+)
+
+
+# ============================================================
+# PREVIOUS CALL HISTORY
+# ============================================================
+
+previous_call = call_history.get(
+ selected_invoice_id
+)
+
+if previous_call:
+
+ st.write("")
+
+ st.markdown(
+ ''
+ 'PREVIOUS CALL'
+ '
',
+ unsafe_allow_html=True,
+ )
+
+ status = previous_call.get(
+ "payment_status",
+ "unknown"
+ )
+
+ status_display = status.replace(
+ "_",
+ " "
+ ).title()
+
+ expected_payment = previous_call.get(
+ "expected_payment_date",
+ "Unknown"
+ )
+
+ follow_up = previous_call.get(
+ "follow_up_required",
+ False
+ )
+
+ call_time = previous_call.get(
+ "called_at",
+ "Unknown"
+ )
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric(
+ "Payment Status",
+ status_display
+ )
+
+ with col2:
+ st.metric(
+ "Expected Payment",
+ expected_payment
+ )
+
+ with col3:
+ st.metric(
+ "Follow-up",
+ "Required"
+ if follow_up
+ else "Not Required"
+ )
+
+ st.caption(
+ f"Last contacted: {call_time}"
+ )
+
+
+st.write("")
+
+
+# ============================================================
+# CALL CUSTOMER
+# ============================================================
+
+if st.button(
+ "📞 Start AI Payment Follow-up",
+ type="primary",
+ use_container_width=True,
+):
+
+ fincall = FinCall()
+
+ with st.spinner(
+ "CALL-E is calling the customer..."
+ ):
+
+ try:
+
+ result = fincall.call_customer(
+ selected_invoice_id
+ )
+
+ st.success(
+ "Call completed successfully"
+ )
+
+ structured = result.get(
+ "structured_result",
+ {}
+ )
+
+ payment_status = structured.get(
+ "payment_status",
+ "unknown"
+ )
+
+ expected_date = structured.get(
+ "expected_payment_date",
+ "Unknown"
+ )
+
+ follow_up = structured.get(
+ "follow_up_required",
+ False
+ )
+
+ evidence = result.get(
+ "evidence",
+ []
+ )
+
+ # ====================================================
+ # SAVE CALL HISTORY
+ # ====================================================
+
+ call_history[selected_invoice_id] = {
+ "payment_status": payment_status,
+ "expected_payment_date": expected_date,
+ "follow_up_required": follow_up,
+ "evidence": evidence,
+ "called_at": datetime.now().strftime(
+ "%Y-%m-%d %H:%M:%S"
+ )
+ }
+
+ save_call_history(call_history)
+
+ # ====================================================
+ # CALL OUTCOME
+ # ====================================================
+
+ st.divider()
+
+ st.markdown(
+ ''
+ 'CALL OUTCOME'
+ '
',
+ unsafe_allow_html=True,
+ )
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+
+ st.metric(
+ "Payment Status",
+ payment_status.replace(
+ "_",
+ " "
+ ).title(),
+ )
+
+ with col2:
+
+ st.metric(
+ "Expected Payment",
+ expected_date,
+ )
+
+ with col3:
+
+ st.metric(
+ "Follow-up",
+ "Required"
+ if follow_up
+ else "Not Required",
+ )
+
+ # ====================================================
+ # AI EVIDENCE
+ # ====================================================
+
+ st.markdown(
+ ''
+ 'AI CALL EVIDENCE'
+ '
',
+ unsafe_allow_html=True,
+ )
+
+ if evidence:
+
+ for item in evidence:
+
+ st.write(
+ "✓",
+ item
+ )
+
+ else:
+
+ st.write(
+ "No evidence returned."
+ )
+
+ # ====================================================
+ # RECOMMENDED ACTION
+ # ====================================================
+
+ if payment_status == "payment_promised":
+
+ next_action = (
+ "Recommended next action"
+ "
"
+ "Customer committed to payment. "
+ "Follow up after the promised payment "
+ "date if payment has not been confirmed."
+ )
+
+ elif payment_status == "delayed":
+
+ next_action = (
+ "Recommended next action"
+ "
"
+ "Payment is delayed. "
+ "Schedule another follow-up and review "
+ "the customer's stated reason."
+ )
+
+ elif payment_status == "disputed":
+
+ next_action = (
+ "Recommended next action"
+ "
"
+ "Customer disputed the invoice. "
+ "Route the case to the finance team "
+ "for manual review."
+ )
+
+ elif payment_status == "paid":
+
+ next_action = (
+ "Recommended next action"
+ "
"
+ "Payment has been confirmed. "
+ "No further collection call is required."
+ )
+
+ else:
+
+ next_action = (
+ "Recommended next action"
+ "
"
+ "Call outcome is inconclusive. "
+ "Schedule another attempt or manual review."
+ )
+
+ st.markdown(
+ f"""
+
+{next_action}
+
+""",
+ unsafe_allow_html=True,
+ )
+
+ # ====================================================
+ # REFRESH DATA
+ # ====================================================
+
+ st.info(
+ "Call outcome saved to FinCall history."
+ )
+
+ except Exception as error:
+
+ st.error(
+ f"Call failed: {error}"
+ )
+
+
+# ============================================================
+# FOOTER
+# ============================================================
+
+st.divider()
+
+st.caption(
+ "FinCall · AI-powered financial operations "
+ "using CALL-E"
+)
\ No newline at end of file
diff --git a/apps/python/fincall/data/invoices.example.json b/apps/python/fincall/data/invoices.example.json
new file mode 100644
index 0000000..ad49d42
--- /dev/null
+++ b/apps/python/fincall/data/invoices.example.json
@@ -0,0 +1,32 @@
+[
+ {
+ "invoice_id": "INV-1042",
+ "customer": "ABC Industries",
+ "contact_name": "Rahul",
+ "phone": "+91XXXXXXXXXX",
+ "amount": 125000,
+ "currency": "INR",
+ "due_date": "2026-09-10",
+ "status": "overdue"
+ },
+ {
+ "invoice_id": "INV-1043",
+ "customer": "Zenith Solutions",
+ "contact_name": "Priya",
+ "phone": "+91XXXXXXXXXX",
+ "amount": 78000,
+ "currency": "INR",
+ "due_date": "2026-09-12",
+ "status": "overdue"
+ },
+ {
+ "invoice_id": "INV-1044",
+ "customer": "Nova Systems",
+ "contact_name": "Arjun",
+ "phone": "+91XXXXXXXXXX",
+ "amount": 18000,
+ "currency": "INR",
+ "due_date": "2026-09-13",
+ "status": "overdue"
+ }
+]
diff --git a/apps/python/fincall/fincall.py b/apps/python/fincall/fincall.py
new file mode 100644
index 0000000..0b67fbe
--- /dev/null
+++ b/apps/python/fincall/fincall.py
@@ -0,0 +1,114 @@
+import json
+import os
+from pathlib import Path
+
+from dotenv import load_dotenv
+from calle import CalleClient
+
+
+load_dotenv()
+
+
+class FinCall:
+ def __init__(self):
+ api_key = os.getenv("CALLE_API_KEY")
+
+ if not api_key:
+ raise RuntimeError("CALLE_API_KEY not found in .env")
+
+ self.client = CalleClient(api_key=api_key)
+
+ def load_invoice(self, invoice_id: str):
+ data_path = Path("data/invoices.json")
+
+ with open(data_path, "r", encoding="utf-8") as f:
+ invoices = json.load(f)
+
+ for invoice in invoices:
+ if invoice["invoice_id"] == invoice_id:
+ return invoice
+
+ raise ValueError(f"Invoice {invoice_id} not found")
+
+ def call_customer(self, invoice_id: str):
+ invoice = self.load_invoice(invoice_id)
+
+ task = f"""
+You are FinCall, an AI financial operations assistant.
+
+Your goal is to follow up with a customer regarding an overdue invoice.
+
+Customer:
+{invoice["customer"]}
+
+Contact:
+{invoice["contact_name"]}
+
+Invoice:
+{invoice["invoice_id"]}
+
+Outstanding amount:
+{invoice["currency"]} {invoice["amount"]:,}
+
+Original due date:
+{invoice["due_date"]}
+
+Call the customer at:
+{invoice["phone"]}
+
+Start politely.
+
+Explain that you are calling regarding the outstanding invoice.
+
+Ask whether they are aware of the outstanding payment.
+
+Ask when they expect to make the payment.
+
+If they cannot make the payment, politely ask for the reason
+or expected timeline.
+
+Do not pressure, threaten, or misrepresent anything.
+
+The purpose of the call is to understand the payment status
+and obtain a realistic expected payment date if possible.
+
+Before ending the call, make sure you have gathered the
+best available payment status.
+
+Then end the call politely.
+"""
+
+ result = self.client.calls.create_and_wait(
+ task=task,
+ result_schema={
+ "type": "object",
+ "required": [
+ "payment_status",
+ "expected_payment_date",
+ "follow_up_required"
+ ],
+ "properties": {
+ "payment_status": {
+ "type": "string",
+ "enum": [
+ "paid",
+ "payment_promised",
+ "delayed",
+ "disputed",
+ "unknown"
+ ]
+ },
+ "expected_payment_date": {
+ "type": "string"
+ },
+ "reason": {
+ "type": "string"
+ },
+ "follow_up_required": {
+ "type": "boolean"
+ }
+ }
+ }
+ )
+
+ return result
\ No newline at end of file
diff --git a/apps/python/fincall/requirements.txt b/apps/python/fincall/requirements.txt
new file mode 100644
index 0000000..b2fb5da
--- /dev/null
+++ b/apps/python/fincall/requirements.txt
@@ -0,0 +1,3 @@
+calle-ai
+python-dotenv
+streamlit
\ No newline at end of file