---
title: "Rank lenders by total cost of a loan (fees + lifetime interest)"
method: POST
path: "/v1/total-cost"
tags: ["Data & Analytics"]
---

# Rank lenders by total cost of a loan (fees + lifetime interest)

`POST /v1/total-cost`

## Request

- Base URL: `https://api.rateapi.dev`
- URL: `https://api.rateapi.dev/v1/total-cost`
- Auth: API key in header `X-API-Key`

Apples-to-apples multi-lender ranking by TOTAL cost of a loan (upfront origination/lender fees + interest paid over the holding period), not just the headline rate. Composes the decision engine's APR-ranked credit-union offers and layers fee + lifetime-interest math on top, re-ranking lenders by total cost over the holding period and flagging when the lowest-rate lender is not the cheapest overall. This route is the single source of truth; the MCP `compare_total_cost` tool and the consumer app call it.

Billing: each call is metered against your API key usage. **Authentication required**: Pass your API key in the `X-API-Key` header.

## Request body

- object
  - `product_type` 'mortgage' | 'auto_loan' | 'personal_loan' | 'heloc' | 'student_loan', required: Loan or deposit product this applies to.
  - `state` string, required: Two-letter US state code.
  - `amount` number, required: Loan principal (> 0)
  - `intent` 'purchase' | 'refinance': Defaults to purchase
  - `term_months` integer: Defaults per product (mortgage 360)
  - `credit_score` integer: Borrower FICO score used to scope which offers apply.
  - `down_payment_percent` number: Down payment as a percentage of purchase price.
  - `holding_period_months` integer: Horizon for the total-cost comparison. Defaults to term_months; clamped to <= term_months.
  - `fees` object: Fee assumptions applied uniformly across lenders so the ranking stays apples-to-apples. Supply `origination_percent` and `flat_fees` for a sharper ranking.
    - `origination_percent` number: Origination fee as a percent of principal, applied uniformly per lender
    - `flat_fees` number: Flat lender fees in dollars, applied uniformly per lender
  - `max_lenders` integer: Cap on ranked rows. Default 10, hard cap 25.

## Response `200`

Lenders re-ranked by total cost over the holding period.

- object
  - `product_type` string: Loan or deposit product this applies to.
  - `state` string: Two-letter US state code.
  - `intent` string: What the borrower is trying to do — for example purchase or refinance.
  - `amount` number: Loan amount in US dollars.
  - `term_months` integer, nullable: Loan term in months.
  - `holding_period_months` integer, nullable: How long the borrower is assumed to hold the loan. Defaults to the term and is clamped to it; null when no term is known.
  - `ranked_by` string: What the ranking is ordered on. Always `total_cost` on this route — that is the point of it.
  - `ranked` object[]: The lenders, cheapest total cost first.
    - `rank` integer: Position in the ranking, 1 being best.
    - `lender` string: Name of the credit union offering this rate.
    - `credit_union_id` string: Canonical identifier of the credit union.
    - `credit_union_url` string, nullable: The credit union's own website.
    - `rateapi_url` string, uri, nullable: Link to this credit union's profile page on rateapi.dev (null if unresolvable)
    - `rate` number: Note rate as an annual percentage, excluding fees.
    - `apr` number: Annual percentage rate, which includes fees and is the figure offers are ranked on.
    - `monthly_payment` number: Monthly principal-and-interest payment in US dollars.
    - `upfront_fees` number: Origination and flat lender fees due at closing, in US dollars.
    - `interest_over_holding` number: Interest paid across the holding period, in US dollars.
    - `principal_paid_over_holding` number: Principal repaid across the holding period, in US dollars. This builds equity and is not counted as a cost.
    - `remaining_balance` number: Balance still owed at the end of the holding period, in US dollars. A balloon, not a cost.
    - `total_cost` number: Upfront fees plus interest over the holding period, in US dollars — the figure this ranking is built on.
    - `total_cost_vs_best` number: How much more this lender costs than the cheapest, in US dollars. Zero for the cheapest.
    - `headline_rank` integer: Where this lender would have placed if ranked on headline APR alone, so you can see what the fee math changed.
  - `cheapest` object, nullable: The lender with the lowest total cost, or null when no offer qualified.
    - `lender` string: Name of the credit union offering this rate.
    - `total_cost` number: That lender’s total cost, in US dollars.
  - `rate_leader` object, nullable: The lender with the lowest headline APR, or null when no offer qualified. It is not always the cheapest.
    - `lender` string: Name of the credit union offering this rate.
    - `apr` number: Annual percentage rate, which includes fees and is the figure offers are ranked on.
  - `reranked` boolean: True when the cheapest lender is not the headline rate leader — that is, when the fee and interest math changed the winner.
  - `note` string: Plain-language explanation of what this result means.
  - `disclosures` string[]: Statements that must be shown to a consumer alongside this result.
  - `next_actions` string[]: Other endpoints or tools that are the natural next step from this result.

## Other responses

- `400`: The request was malformed — a missing required field, an unparseable body, or a parameter outside its allowed range. The `error` field names what was wrong; retrying the identical request will fail identically.
- `401`: No API key, or a key that is unknown or disabled. Send it in the `X-API-Key` header. Free keys are issued instantly by POST /keys with an email address. The response carries `WWW-Authenticate` pointing at this host's protected-resource metadata, and the `RateLimit-*` headers describing the Free-tier budget a key issued now would carry (scope `anonymous`) — a rejected request spends none of it, so `Remaining` equals `Limit`.
- `429`: Quota exhausted for the current window. `Retry-After` gives the seconds to wait; `upgradeInfo` explains how to lift the cap. Retrying before the reset will fail again and still counts as a request.

---

[API](https://skmtc.dev/rateapi/apis/rate-api.md) · [All operations](https://skmtc.dev/rateapi/apis/rate-api/llms.txt) · [OpenAPI document](https://skmtc.dev/rateapi/apis/rate-api/revisions/06a6db76995f?raw)
