POST /api/pricer/quote
Evaluate a loan scenario against every applicable Non-QM and FlexPoint program and receive ranked eligible offers plus structured rejection reasons.
Endpoint
POST /api/pricer/quote
Content-Type: application/json
Authorization: Bearer <FLEXRATE_INTERNAL_TOKEN>A single call evaluates the scenario against every program on the most recent rate sheet of every lender (Non-QM + FlexPoint today), returns priced offers ranked best-price-first across both lenders, and structured rejection reasons for every (lender, program, doc_type) tuple that didn't qualify. Every eligible offer and rejection is tagged with lender_slug so the consumer can group or filter by lender. Omit program_slug and doc_type to evaluate the full catalog.
Request Body
All fields are JSON. Required fields are marked with *. Defaults reflect what the API substitutes when a field is omitted — they are the production defaults, not placeholders.
Program filters
| Field | Type | Values | Default | Description |
|---|---|---|---|---|
program_slug | string | See /api/pricer/programs | null | Limit evaluation to one program. Omit to evaluate all. |
doc_type | string | e.g. full_doc, bank_statements, dscr | null | Limit to one doc type within the selected program. |
doc_seasoning | string | 12mo, 24mo | null | Doc seasoning for Full Doc / Jumbo. null preserves the legacy behavior of matching the 12-month row. |
Loan product
| Field | Type | Values | Default | Description |
|---|---|---|---|---|
term * | integer | 10, 15, 20, 25, 30, 40 | — | Amortisation term in years. |
purpose * | string | purchase, rate_term_refi, cash_out_refi | — | Loan purpose. |
comp_type | string | lender_paid, borrower_paid | lender_paid | Compensation type. |
margin_bps | integer | 0–1000 | 0 | Compensation margin in basis points. Applied as a rate-side adjustment (does not move price). |
Borrower
| Field | Type | Values | Default | Description |
|---|---|---|---|---|
credit_score * | integer | 500–850 | — | Representative credit score (middle of tri-merge). |
purchase_price * | number | ≥ 50000 | — | Purchase price or appraised value in whole dollars. |
loan_amount * | number | ≥ 50000 | — | Loan amount in whole dollars. |
dti | number | 0–60 | 40.0 | Debt-to-income ratio as a percentage. Default 40.0 sits below every Non-QM DTI tier (lowest tier starts at 43.01%) so scenarios that don't specify DTI take no tier adjustment. |
mtg_lates_12mo | integer | 0, 1, 2, 3 | 0 | Mortgage late payments in the last 12 months. 3 means "3 or more". |
housing_event | string | null | bk_chapter_7, bk_chapter_13, foreclosure, short_sale, deed_in_lieu | null | Most recent adverse housing event, if any. |
housing_event_months_ago | integer | null | ≥ 0 | null | Months since the housing event was discharged or completed. Should be set if housing_event is set. |
citizenship | string | us_citizen, permanent_resident, non_permanent_resident, itin, foreign_national | us_citizen | Borrower citizenship / documentation status. |
lien_position | string | 1st, 2nd | 1st | Lien position. |
Property
| Field | Type | Values | Default | Description |
|---|---|---|---|---|
occupancy | string | owner_occupied, second_home, non_owner_occupied, investment | owner_occupied | Property occupancy type. |
property_type | string | sfr, 2_unit, 3_4_unit, 5_8_unit, condo, warrantable_condo, non_warrantable_condo, condotel, townhome, mixed_use, manufactured | sfr | Property type. |
state | string | 2-letter US state code | CA | Property state. Uppercased server-side. |
zip | string | 5-digit ZIP | "" | Property ZIP code. |
Pricing options
| Field | Type | Values | Default | Description |
|---|---|---|---|---|
ppp_term | string | No PPP, 1YR PPP, 2YR PPP, 3YR PPP, 5YR PPP | No PPP | Prepayment penalty period. Must match a term in the program's PPP table. |
ppp_table_name | string | null | e.g. 5% Fixed Fee, 6 mo Interest | null | PPP table selector when a program ships multiple tables (Full Doc / Expanded Doc / DSCR ship both). null uses the first table found. |
lock_days | integer | 15, 30, 45 | 30 | Rate lock period in days. |
DSCR (investment / rental properties)
| Field | Type | Values | Default | Description |
|---|---|---|---|---|
dscr_ratio | number | null | 0–10 | null | Debt-service coverage ratio. Required for DSCR, Multi-Unit, and Foreign National programs. Pass null for income-qualified programs (Full Doc, Expanded Doc, ITIN). |
The DSCR ratio determines which LTV tier applies and which DSCR-tier LLPA row fires. DSCR ≥ 1.00 unlocks higher LTV limits and better pricing than DSCR < 1.00. Passing the wrong value (or omitting it on a DSCR program) produces an inaccurate result.
Risk & adjustment toggles
These fields each map 1:1 to a row in the "Miscellaneous Adjustments" / "Other Adjustments" LLPA grids. Multiple toggles fire independently on the same scenario, so a borrower can incur, say, escrow_waiver + interest_only + housing_history=1x30x12 adjustments simultaneously.
| Field | Type | Values | Default | Fires LLPA row |
|---|---|---|---|---|
escrow_waiver | boolean | true, false | false | "Escrow waiver" |
interest_only | boolean | true, false | false | "Interest Only" (note: term=40 already covers "40 Year Maturity") |
housing_history | string | null | 1x30x12, 0x60x12, 0x90x12 | null | Matches verbatim by row label |
recent_credit_event | boolean | true, false | false | FC/SS/DIL/BK<48M |
short_term_rental | boolean | true, false | false | "Short-Term Rental" (DSCR programs) |
If any toggle hits an NA cell in the LLPA grid, the offer is rejected with key: "llpa_na" rather than priced — NA is a hard eligibility cut-off, not a zero adjustment.
Response
{
"rate_sheet_date": "2026-05-20",
"eligible_count": 25,
"rejected_count": 8,
"eligible": [ /* OfferObject[] */ ],
"rejected": [ /* RejectionObject[] */ ]
}| Field | Type | Description |
|---|---|---|
rate_sheet_date | string | null | effective_at date of the rate sheet that produced this quote (YYYY-MM-DD). |
eligible_count | integer | Number of eligible offers across all programs. |
rejected_count | integer | Number of (program, doc_type) pairs that didn't produce any eligible offers. |
eligible[] | array | Priced offers, sorted descending by final_price — the first offer is the most favourably priced for the borrower. |
rejected[] | array | One entry per (program, doc_type) that produced no offers, with structured rejection reasons. |
eligible[] — Offer object
Each entry is one note rate on one program's ladder (or one rate-block row for FlexPoint) that passed every eligibility check.
{
"lender_slug": "non-qm",
"program_slug": "dscr",
"program_name": "DSCR",
"doc_type": "dscr",
"rate": 9.125,
"final_rate": 9.125,
"base_price": 100.500,
"price_adjustment": -1.250,
"final_price": 99.250,
"max_ltv": "85.00",
"max_cltv": "85.00",
"adjusters": [
{ "category": "FICO/CLTV", "row_label": "740 - 759", "band": "70.01 - 75%", "value": -0.25, "source": "llpa" },
{ "category": "Purpose", "row_label": "Cash-Out Refi", "band": null, "value": -1.00, "source": "llpa" },
{ "category": "Miscellaneous Adjustments", "row_label": "Escrow waiver", "band": null, "value": 0.00, "source": "llpa" }
],
"caps_applied": [],
"term": 30,
"lock_days": 30,
"ppp_term": "No PPP"
}| Field | Type | Description |
|---|---|---|
lender_slug | string | null | Lender that owns the program: "non-qm" or "flexpoint". |
program_slug | string | Program identifier. |
program_name | string | Human-readable program name. |
doc_type | string | Doc type within the program. FlexPoint agency programs default to "full_doc" because no doc-type registry is shipped on the sheet. |
rate | number | Note rate from the ladder (e.g. 9.125). |
final_rate | number | rate plus any rate-side adjustments (currently margin_bps). |
base_price | number | Base price for rate from rate_ladder_entries. |
price_adjustment | number | Net of every LLPA cell that fired (positive = add-on, negative = discount). |
final_price | number | base_price + price_adjustment, after pricing caps and floors. Higher is better. |
max_ltv | string | Maximum LTV from the matrix row that matched this scenario (decimal as string). |
max_cltv | string | Maximum CLTV from the matrix row that matched this scenario. |
adjusters[] | array | Every LLPA cell that contributed to price_adjustment. Order is insertion order from the engine — meaningful for debugging but not contractual. |
adjusters[].category | string | LLPA grid name (e.g. FICO/CLTV, Purpose, Doc Type, Property Type, Occupancy, Product, DSCR, Miscellaneous Adjustments). |
adjusters[].row_label | string | Exact row_label from llpa_adjustments — useful for cross-referencing the lender's published rate sheet. |
adjusters[].band | string | null | Column band when the grid has bands (e.g. "70.01 - 75%"); null for single-column grids. |
adjusters[].value | number | Adjustment in points. |
adjusters[].source | string | "llpa" (LLPA grid cell), "ppp" (Non-QM PPP table), "rate_block" (FlexPoint block + lock-day column selection), or "state_adjustment" (FlexPoint per-state Fannie/Freddie adjuster). |
caps_applied[] | array | Pricing caps or floors that mutated final_price. Empty when no cap was hit. See Pricing caps & floors. |
term | integer | Amortisation term echoed from the request. |
lock_days | integer | Lock period echoed from the request. |
ppp_term | string | PPP term echoed from the request. |
rejected[] — Rejection object
{
"lender_slug": "non-qm",
"program_slug": "expanded_doc",
"program_name": "Expanded Doc",
"doc_type": "bank_statements",
"rejections": [
{
"key": "no_ltv_matrix_cell",
"threshold": null,
"provided": {
"fico": 740,
"loan_amount": 500000,
"occupancy": "investment",
"purpose": "purchase"
},
"source_page": null
}
]
}The engine emits a rejection at one of two levels:
- Program-level — eligibility check failed before pricing began.
rejections[]carries one entry per failing rule. - Rate-level (rolled up) — every rate on the ladder was rate-rejected (e.g.
NALLPA cell, min-price floor breach). The engine bubbles up a single entry summarising what happened, withrateandfinal_priceon eachrejections[]row plus adetailobject describing the cell that triggered it.
Rejection keys
| Key | Level | threshold | provided | Meaning |
|---|---|---|---|---|
doc_type_not_available | program | null | submitted doc_type | Requested doc_type isn't registered for the program. |
min_fico | program | minimum FICO | submitted credit_score | Credit score below program minimum. |
max_dti | program | maximum DTI | submitted dti | DTI exceeds program maximum. |
min_loan_amount | program | minimum loan amount | submitted loan_amount | Loan amount too small. |
max_loan_amount | program | maximum loan amount | submitted loan_amount | Loan amount too large. |
dscr_ratio_required | program | minimum DSCR | null | Program requires a DSCR ratio and none was provided. |
dscr_min_ratio | program | minimum DSCR | submitted dscr_ratio | DSCR below program floor. |
eligible_citizenship | program | accepted types | submitted citizenship | Citizenship not accepted by this program. |
eligible_states | program | eligible states or "all" | submitted state | Property state not eligible. |
no_ltv_matrix_cell | program | null | {fico, loan_amount, occupancy, purpose} | No LTV matrix row covers this combination. |
max_ltv_exceeded | program | max_ltv from matrix | computed LTV | Computed LTV exceeds the matrix maximum. |
max_cltv_exceeded | program | max_cltv from matrix | computed CLTV | Computed CLTV exceeds the matrix maximum. |
llpa_na | rate | null | rate that hit the NA cell | An LLPA cell required by this scenario is marked NA (hard ineligible at that intersection). |
min_price_floor | rate | floor price | priced value | Final price would fall below the program's minimum price. |
state_not_offered | rate | null | {state} | FlexPoint program ships per-state adjusters but no row matched the scenario state — the lender does not offer the program in that state. |
Pricing caps & floors
After LLPA adjustments are summed, every offer is clamped against per-program price caps and a minimum-price floor sourced from programs.cap_* columns. When a cap clamps a price, caps_applied[] records what happened:
{
"caps_applied": [
{ "key": "Primary / 2nd Homes", "threshold": 102, "provided": 102.875 }
]
}The cap selected for a scenario depends on occupancy, PPP term, and loan amount. Order of evaluation: tightest threshold first.
| Cap | Applies when |
|---|---|
Primary / 2nd Homes | Occupancy is owner_occupied or second_home |
Investments >= 3YR PPP | Occupancy is investment with PPP term ≥ 3 years |
Investments w/ < 3YR PPP | Occupancy is investment with PPP term < 3 years |
No PPP or State Restricted | Occupancy is investment with No PPP or in a state that bans PPP |
Loan Amounts >$1.5m | loan_amount > 1,500,000 |
Maximum Price | Program-wide hard ceiling |
Minimum Price | Program-wide hard floor — breaching it produces a min_price_floor rate rejection rather than an offer |
Examples
DSCR investment purchase
curl -X POST https://api.example.com/api/pricer/quote \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"term": 30,
"purpose": "purchase",
"credit_score": 740,
"purchase_price": 625000,
"loan_amount": 500000,
"state": "CA",
"occupancy": "investment",
"property_type": "sfr",
"dscr_ratio": 1.15
}'What happens internally:
- DSCR program is found on the latest rate sheet.
- Scalar rules pass: FICO 740 ≥ 640 minimum, loan $500K within $100K–$3.5M range.
- LTV matrix lookup: FICO 740 falls in the 740–850 premium band, loan $500K ≤ $1M →
max_ltv = 85,max_cltv = 85(DSCR ≥ 1.00 tier matchesdscr_ratio = 1.15). - Computed LTV = $500K / $625K = 80% ≤ 85% ✓
- Every non-NA rate on the ladder is priced; LLPA cells across FICO/CLTV, Purpose, Occupancy, Property Type, DSCR, and Product grids are summed into
price_adjustment. - Pricing caps applied (e.g.
Investments >= 3YR PPPcap of 102 for a 3-year PPP scenario). - Offers sorted descending by
final_price.
Full Doc primary residence, 24-month seasoning
curl -X POST https://api.example.com/api/pricer/quote \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"program_slug": "full_doc",
"doc_seasoning": "24mo",
"term": 30,
"purpose": "purchase",
"credit_score": 720,
"purchase_price": 800000,
"loan_amount": 640000,
"state": "TX",
"occupancy": "owner_occupied",
"property_type": "sfr",
"dti": 42.0
}'doc_seasoning: "24mo" causes the Doc Type LLPA grid to resolve to the "24 mo Full Doc" row instead of the default "12 mo Full Doc". Omit the field to preserve legacy behaviour.
ITIN borrower
curl -X POST https://api.example.com/api/pricer/quote \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"term": 30,
"purpose": "purchase",
"credit_score": 700,
"purchase_price": 500000,
"loan_amount": 400000,
"state": "FL",
"occupancy": "owner_occupied",
"property_type": "sfr",
"citizenship": "itin",
"dti": 45.0
}'Foreign National investment with PPP
curl -X POST https://api.example.com/api/pricer/quote \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"term": 30,
"purpose": "purchase",
"credit_score": 720,
"purchase_price": 900000,
"loan_amount": 675000,
"state": "CA",
"occupancy": "investment",
"property_type": "sfr",
"citizenship": "foreign_national",
"dscr_ratio": 1.10,
"ppp_term": "3YR PPP",
"ppp_table_name": "5% Fixed Fee"
}'DSCR short-term rental with escrow waiver & interest-only
curl -X POST https://api.example.com/api/pricer/quote \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"program_slug": "dscr",
"term": 30,
"purpose": "cash_out_refi",
"credit_score": 720,
"purchase_price": 800000,
"loan_amount": 560000,
"state": "FL",
"occupancy": "investment",
"property_type": "sfr",
"dscr_ratio": 1.25,
"short_term_rental": true,
"escrow_waiver": true,
"interest_only": true
}'Each toggle adds its own row to adjusters[] on every priced offer (or rejects the offer if the corresponding cell is NA).
FlexPoint Conventional Fixed (30Y, owner-occupied, CA)
curl -X POST https://api.example.com/api/pricer/quote \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"program_slug": "2_conventional_fixed",
"doc_type": "full_doc",
"term": 30,
"purpose": "purchase",
"credit_score": 740,
"purchase_price": 500000,
"loan_amount": 400000,
"state": "CA",
"occupancy": "owner_occupied",
"property_type": "sfr",
"lock_days": 30
}'Returns offers from FlexPoint's Conventional 30 Yr Fixed rate block, lock_30 column. Each offer carries lender_slug: "flexpoint"; the adjuster log includes:
{
"lender_slug": "flexpoint",
"program_slug": "2_conventional_fixed",
"program_name": "Conventional Fixed",
"doc_type": "full_doc",
"rate": 5.250,
"base_price": 95.8826,
"adjusters": [
{ "source": "rate_block", "block": "Conventional 30 Yr Fixed", "lock_days": 30 },
{ "source": "state_adjustment", "state": "CA", "agency": "fannie", "value": 0.0 }
]
}Submit the same payload with state: "HI" (or any state outside FlexPoint's 19-state Conv Fixed footprint) and the program appears under rejected[] with a single state_not_offered rejection instead — agency-shape eligibility lets the program survive doc-type/LTV-matrix gates so the state diagnostic is the one the broker sees.
Switching loan_amount above $766,550 (the 2026 conforming limit) automatically routes to the Conventional 30 Yr Fixed HB block when one exists for the term; HomeReady, HomePossible, RefiNow, FHA, VA, and USDA work the same way, each routing to its own block-name pattern.
Program & doc type reference
program_slug | doc_type | Occupancy | Notes |
|---|---|---|---|
full_doc | full_doc | All | W2 / traditional income. Pair with doc_seasoning to target the 12 mo vs 24 mo row. |
expanded_doc | bank_statements | All | 12 or 24 months personal/business bank statements. |
expanded_doc | 1099 | All | 1099-only income. |
expanded_doc | asset_utilization | All | Asset depletion income calculation. |
expanded_doc | pl_statement | All | CPA-prepared P&L. |
dscr | dscr | investment | Requires dscr_ratio. |
multi_unit_mixed_use | dscr_5_8_unit | investment | 5–8 unit / mixed-use; requires dscr_ratio ≥ 1.00. |
itin | itin | owner_occupied, non_owner_occupied | Requires citizenship: "itin". |
fn | foreign_national | investment | Requires citizenship: "foreign_national" + dscr_ratio. |
Use GET /api/pricer/programs to enumerate the live catalog at runtime — it reflects whatever the most recent php artisan rates:daily seeded.
Validation errors
PricerQuoteRequest returns a Laravel-standard 422 Unprocessable Entity when validation fails:
{
"message": "The term field must be one of: 10, 15, 20, 25, 30, 40 years.",
"errors": {
"term": ["The term field must be one of: 10, 15, 20, 25, 30, 40 years."]
}
}Hard ranges enforced before the engine runs: credit_score 500–850, purchase_price/loan_amount ≥ 50000, dti 0–60, dscr_ratio 0–10, margin_bps 0–1000, state exactly 2 letters, zip exactly 5 digits. Enum-valued fields reject anything not in the documented set above.