Developers
For AI agents: markdown copy, OpenAPI spec, and llms.txt.
Every field the API takes, in one request. Fields you leave out get defaults, listed in assumptions.
POST https://cms.offermarket.us/public/v1/quotes/dscr
POST https://cms.offermarket.us/public/v1/quotes/dscr
POST https://cms.offermarket.us/public/v1/quotes/dscr
POST https://cms.offermarket.us/public/v1/quotes/dscr
POST https://cms.offermarket.us/public/v1/quotes/dscr
POST https://cms.offermarket.us/public/v1/quotes/dscr
POST https://cms.offermarket.us/public/v1/quotes/dscr
POST https://cms.offermarket.us/public/v1/quotes/dscr
POST https://cms.offermarket.us/public/v1/quotes/dscr
POST https://cms.offermarket.us/public/v1/quotes/dscr
POST https://cms.offermarket.us/public/v1/quotes/dscr
POST https://cms.offermarket.us/public/v1/quotes/dscr
POST https://cms.offermarket.us/public/v1/quotes/dscr
How many calls with no login your IP address has left. This call does not use one.
GET https://cms.offermarket.us/public/v1/free-calls
When the free calls run out, send these in order to make a free API login. Each call uses what an earlier call returned. You only type what no call can give you. The tokens stay on this page and are gone when you leave it.
Do this once. It makes your login, accepts the OfferMarket terms, and gives you tokens. Use your user's real email and a password you make up and keep, 6 or more characters. If the email already has a login, the same call signs in with the password instead. Each IP address, and each email, gets 10 tries an hour.
POST https://cms.offermarket.us/public/v1/account
{
"email": "<email>",
"password": "<password>",
"accept_terms": true
}Send the `id_token` on every quote. The body is the same as an open call. A login gets 30 calls a minute.
POST https://cms.offermarket.us/public/v1/quotes/dscr Authorization: Bearer <id_token>
Do this when a call returns `401 INVALID_TOKEN`, or the `id_token` is close to 1 hour old. It is the sign_up call again, with the same email and password. It signs in and gives a new `id_token`. Then send the failed call again, once.
POST https://cms.offermarket.us/public/v1/account
{
"email": "<email>",
"password": "<password>",
"accept_terms": true
}You can skip this step. It does the same job as sign_in, with the `refresh_token` in place of the password. This call goes to Google's login service (Firebase), not to us. Its body is a form, not JSON.
POST https://securetoken.googleapis.com/v1/token?key=AIzaSyDyib9HegXAt3apVxbKlJ_-kYQJ1_dHsQA
grant_type=refresh_token&refresh_token=<refresh_token>Needs refresh_token. Send "Sign up and accept the terms (one call, once)" first.
Every quote has a next_step_url. Log in first, then call it with your login token. It tells our team you want a loan file for that quote, and someone emails your login's address to set it up. Opening a loan file from the API is still being built. With no login it returns 401 with the login steps.
POST https://cms.offermarket.us/public/v1/quotes/<request_id>/loan-file Authorization: Bearer <id_token>
Send any quote on this page first. You have no login on this page yet, so this gets a 401.Sends a token that is not a real login token.
POST https://cms.offermarket.us/public/v1/quotes/dscr Authorization: Bearer not-a-token
GET https://cms.offermarket.us/public/v1/openapi.json
The guide for AI agents.
GET https://cms.offermarket.us/llms.txt
Instant rate estimates for business-purpose real estate investor loans. No name, SSN, or credit pull is needed. The first 10 calls need no login; then log in with a free OfferMarket login. The steps to sign up and stay logged in are in components.securitySchemes.firebase and at https://cms.offermarket.us/llms.txt. Try it in a browser, and copy your own login token, at https://www.offermarket.us/developers#try-it. Estimates are not loan offers. Every call is kept, including the property address and the login it came from.
POST https://cms.offermarket.us/public/v1/quotes/dscrPOST https://cms.offermarket.us/public/v1/quotes/{request_id}/loan-file (the quote's next_step_url)Returns loan options at each loan-to-value level our capital providers offer for this property, rent, and borrower. Fields left out are filled with documented defaults and listed in assumptions.
curl -s https://cms.offermarket.us/public/v1/quotes/dscr \
-H 'Content-Type: application/json' \
-d '{"loan_purpose":"purchase","address":"123 Main St, Indianapolis, IN 46204","property_type":"single_family","purchase_price":300000,"monthly_rent":2600,"credit_score":740}'
Check status first. quoted comes with options, one per loan-to-value level. no_offer comes with reasons, and each reason names the field to look at.
This request sends every field. Every field (a refinance). The portfolio fields (property_count, state, counties) are left out: they replace address and property_type. Every field is described under Request below. Fields you leave out get defaults, listed in assumptions.
curl -s https://cms.offermarket.us/public/v1/quotes/dscr \
-H 'Content-Type: application/json' \
-d @- <<'JSON'
{
"loan_purpose": "refinance",
"address": "123 Main St, Indianapolis, IN 46204",
"property_type": "duplex",
"units": 2,
"unit_size_sqft": 950,
"purchase_price": 280000,
"as_is_value": 400000,
"monthly_rent": 3400,
"leasing_strategy": "long_term",
"tenant_type": "market",
"annual_taxes": 4200,
"annual_insurance": 2100,
"annual_hoa": 0,
"credit_score": 740,
"experience_deals": 5,
"entity": "llc",
"citizenship": "us_citizen",
"refinance_type": "cash_out",
"existing_debt": 150000,
"purchase_date": "2021-06-15",
"capital_improvements": 25000,
"loan_amount": 260000,
"amortization": "fully_amortizing"
}
JSON
The response. Rates here are illustrative, not live.
{
"program": "dscr",
"status": "quoted",
"request_id": "3f9a1c2e7b4d8a60",
"rates_as_of": "2026-10-09T13:00:00.000Z",
"options": [
{
"ltv": 60,
"loan_amount": 240000,
"interest_rate": 7,
"monthly_principal_and_interest": 1596.73,
"monthly_payment_total": 2121.73,
"dscr": 1.6,
"prepayment_penalty": "5-4-3-2-1",
"covers_requested_amount": false
},
{
"ltv": 65,
"loan_amount": 260000,
"interest_rate": 7,
"monthly_principal_and_interest": 1729.79,
"monthly_payment_total": 2254.79,
"dscr": 1.51,
"prepayment_penalty": "5-4-3-2-1",
"covers_requested_amount": true
},
{
"ltv": 70,
"loan_amount": 280000,
"interest_rate": 7.25,
"monthly_principal_and_interest": 1910.09,
"monthly_payment_total": 2435.09,
"dscr": 1.4,
"prepayment_penalty": "5-4-3-2-1",
"covers_requested_amount": true
},
{
"ltv": 75,
"loan_amount": 300000,
"interest_rate": 7.5,
"monthly_principal_and_interest": 2097.64,
"monthly_payment_total": 2622.64,
"dscr": 1.3,
"prepayment_penalty": "5-4-3-2-1",
"covers_requested_amount": true
}
],
"reasons": [],
"assumptions": [],
"disclaimer": "Estimate only. This is not a loan offer, a pre-approval, a commitment to lend, or a guarantee of any rate or term. Rates change daily. Final terms depend on underwriting, the appraisal, and approval by OfferMarket Capital LLC and its capital providers. For business-purpose loans on non-owner-occupied investment property only. Terms: https://offermarket.us/terms-of-service (Section 2.2.5). Licenses: https://offermarket.us/licenses.",
"next_step_url": "https://cms.offermarket.us/public/v1/quotes/3f9a1c2e7b4d8a60/loan-file"
}
Needs a login: send Authorization: Bearer <id_token>. With no login, it returns 401 LOGIN_REQUIRED with the login steps in error.log_in. Opening a loan file from the API is still being built. This call tells OfferMarket you want one for this quote, and someone emails the login's address to set it up. The quote can be one you made before you logged in. GET and POST do the same thing.
curl -s -X POST 'https://cms.offermarket.us/public/v1/quotes/<request_id>/loan-file' \
-H 'Authorization: Bearer <id_token>'
With no login, the call returns 401 LOGIN_REQUIRED with the login steps in error.log_in.
{
"request_id": "3f9a1c2e7b4d8a60",
"status": "received",
"email": "you@example.com",
"message": "Thanks. Opening a loan file from the API is still being built. Someone from OfferMarket will email you at you@example.com to set it up."
}
An OfferMarket login, sent as Authorization: Bearer <id_token>. The id_token is a Firebase ID token. It works for 1 hour, then you sign in again for a new one.
With no login, each caller (one IP address) gets 10 calls in total. Every quote or 400 counts. The next call gets 429 FREE_CALLS_USED. GET https://cms.offermarket.us/public/v1/free-calls tells you how many are left. To keep quoting, log in. A login is free and gets 30 calls a minute.
To test in a browser first, go to https://www.offermarket.us/developers#try-it. You can send requests there, see your free calls, log in, and copy your own id_token and refresh_token.
A login is an email and a password. An agent signs up for its user: send the user's email and a password you make up and keep. One call to us makes the login and accepts the terms. In exchange for free quotes, we may send marketing email to that address; the user can unsubscribe at any time. When the token runs out after 1 hour, send the same call again. Every step is one HTTP call, so no browser is needed. Do the steps in this order:
You send ---> You get back
Quote, no login (calls 1 to 10) ---> 200 quote
Quote, no login (call 11) ---> 429 FREE_CALLS_USED
1. Sign up + accept the terms (once) ---> id_token + refresh_token
2. Quote + Bearer id_token ---> 200 quote (30 a minute)
... 1 hour later, or 401 INVALID_TOKEN ...
3. Send step 1 again (it signs in) ---> new id_token
then go back to step 2
Do this once. It makes your login, accepts the OfferMarket terms, and gives you tokens. Use your user's real email and a password you make up and keep, 6 or more characters. If the email already has a login, the same call signs in with the password instead. Each IP address, and each email, gets 10 tries an hour.
curl -s 'https://cms.offermarket.us/public/v1/account' \
-H 'Content-Type: application/json' \
-d '{"email":"<email>","password":"<password>","accept_terms":true}'
| Reply field | What it is |
|---|---|
id_token |
The login token. Send it on every quote as Authorization: Bearer <id_token>. It works for 1 hour. |
refresh_token |
Save it with your secrets, in place of any older one. The refresh step trades it for a new id_token. |
expires_in |
Seconds until the id_token stops working: 3600 (1 hour). |
login_created |
true when this call made the login. false when it signed in to a login you already had. |
Send the id_token on every quote. The body is the same as an open call. A login gets 30 calls a minute.
curl -s 'https://cms.offermarket.us/public/v1/quotes/dscr' \
-H 'Authorization: Bearer <id_token>' \
-H 'Content-Type: application/json' \
-d '<the same JSON as an open call>'
Do this when a call returns 401 INVALID_TOKEN, or the id_token is close to 1 hour old. It is the sign_up call again, with the same email and password. It signs in and gives a new id_token. Then send the failed call again, once.
curl -s 'https://cms.offermarket.us/public/v1/account' \
-H 'Content-Type: application/json' \
-d '{"email":"<email>","password":"<password>","accept_terms":true}'
| Reply field | What it is |
|---|---|
id_token |
The login token. Send it on every quote as Authorization: Bearer <id_token>. It works for 1 hour. |
refresh_token |
Save it with your secrets, in place of any older one. The refresh step trades it for a new id_token. |
expires_in |
Seconds until the id_token stops working: 3600 (1 hour). |
You can skip this step. It does the same job as sign_in, with the refresh_token in place of the password. This call goes to Google's login service (Firebase), not to us. Its body is a form, not JSON.
curl -s 'https://securetoken.googleapis.com/v1/token?key=AIzaSyDyib9HegXAt3apVxbKlJ_-kYQJ1_dHsQA' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token&refresh_token=<refresh_token>'
| Reply field | What it is |
|---|---|
id_token |
The login token. Send it on every quote as Authorization: Bearer <id_token>. It works for 1 hour. |
refresh_token |
Save it with your secrets, in place of any older one. The refresh step trades it for a new id_token. |
expires_in |
Seconds until the id_token stops working: 3600 (1 hour). |
id_token works for 1 hour. Keep using it until then.id_token, in a web page, a URL, or a log.401 INVALID_TOKEN, sign in again (the sign_up call), then send that call again once with the new id_token.401 INVALID_LOGIN. Do not make a second login: send the right password, or reset it.accept_terms accepts the terms for them.POST https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key=AIzaSyDyib9HegXAt3apVxbKlJ_-kYQJ1_dHsQA with the JSON {"requestType":"PASSWORD_RESET","email":"<email>"}. We email a link to set a new password.id_token at https://www.offermarket.us/developers#try-it. That page also shows how to refresh it: a site login refreshes with a different key. If you have not accepted the terms, send POST https://cms.offermarket.us/public/v1/account with Authorization: Bearer <id_token> and only {"accept_terms": true}.key in the Google URLs is OfferMarket's public web key for logins. It is the same for every caller and it is not a secret. Your secrets are the password and the refresh_token.When you reach a limit, the API answers 429. RATE_LIMITED comes with a Retry-After header in seconds. FREE_CALLS_USED means the free calls are used up: log in to keep quoting. When a call needs a login, the error has error.log_in: the same login steps as above, as JSON. Every answer has an X-Request-Id header; quote it when you contact us.
Rates in these examples are illustrative, not live.
{
"program": "dscr",
"status": "quoted",
"request_id": "3f9a1c2e7b4d8a60",
"rates_as_of": "2026-10-09T13:00:00.000Z",
"options": [
{
"ltv": 75,
"loan_amount": 225000,
"interest_rate": 7.25,
"monthly_principal_and_interest": 1534.9,
"monthly_payment_total": 2022.4,
"dscr": 1.29,
"prepayment_penalty": "5-4-3-2-1",
"covers_requested_amount": null
},
{
"ltv": 80,
"loan_amount": 240000,
"interest_rate": 7.5,
"monthly_principal_and_interest": 1678.11,
"monthly_payment_total": 2165.61,
"dscr": 1.2,
"prepayment_penalty": "5-4-3-2-1",
"covers_requested_amount": null
}
],
"reasons": [],
"assumptions": [
{
"field": "annual_taxes",
"value": 3600,
"note": "Not sent; estimated as 1.20% of purchase_price. Coastal states often cost more."
},
{
"field": "annual_insurance",
"value": 2250,
"note": "Not sent; estimated as 0.75% of purchase_price. Coastal states often cost more."
},
{
"field": "annual_hoa",
"value": 0,
"note": "Not sent; assumed no HOA dues."
},
{
"field": "experience_deals",
"value": 2,
"note": "Not sent; a typical value was used."
},
{
"field": "entity",
"value": "llc",
"note": "Not sent; a typical value was used."
},
{
"field": "citizenship",
"value": "us_citizen",
"note": "Not sent; a typical value was used."
},
{
"field": "leasing_strategy",
"value": "long_term",
"note": "Not sent; a typical value was used."
},
{
"field": "tenant_type",
"value": "market",
"note": "Not sent; a typical value was used."
},
{
"field": "amortization",
"value": "fully_amortizing",
"note": "Not sent; a typical value was used."
}
],
"disclaimer": "Estimate only. This is not a loan offer, a pre-approval, a commitment to lend, or a guarantee of any rate or term. Rates change daily. Final terms depend on underwriting, the appraisal, and approval by OfferMarket Capital LLC and its capital providers. For business-purpose loans on non-owner-occupied investment property only. Terms: https://offermarket.us/terms-of-service (Section 2.2.5). Licenses: https://offermarket.us/licenses.",
"next_step_url": "https://cms.offermarket.us/public/v1/quotes/3f9a1c2e7b4d8a60/loan-file"
}
{
"program": "dscr",
"status": "no_offer",
"request_id": "3f9a1c2e7b4d8a60",
"rates_as_of": "2026-10-09T13:00:00.000Z",
"options": [],
"reasons": [
{
"code": "STATE_NOT_SERVED",
"field": "address",
"hint": "Our capital providers do not fund this loan in IN.",
"value": "IN"
}
],
"assumptions": [
{
"field": "annual_taxes",
"value": 3600,
"note": "Not sent; estimated as 1.20% of purchase_price. Coastal states often cost more."
},
{
"field": "annual_insurance",
"value": 2250,
"note": "Not sent; estimated as 0.75% of purchase_price. Coastal states often cost more."
},
{
"field": "annual_hoa",
"value": 0,
"note": "Not sent; assumed no HOA dues."
},
{
"field": "experience_deals",
"value": 2,
"note": "Not sent; a typical value was used."
},
{
"field": "entity",
"value": "llc",
"note": "Not sent; a typical value was used."
},
{
"field": "citizenship",
"value": "us_citizen",
"note": "Not sent; a typical value was used."
},
{
"field": "leasing_strategy",
"value": "long_term",
"note": "Not sent; a typical value was used."
},
{
"field": "tenant_type",
"value": "market",
"note": "Not sent; a typical value was used."
},
{
"field": "amortization",
"value": "fully_amortizing",
"note": "Not sent; a typical value was used."
}
],
"disclaimer": "Estimate only. This is not a loan offer, a pre-approval, a commitment to lend, or a guarantee of any rate or term. Rates change daily. Final terms depend on underwriting, the appraisal, and approval by OfferMarket Capital LLC and its capital providers. For business-purpose loans on non-owner-occupied investment property only. Terms: https://offermarket.us/terms-of-service (Section 2.2.5). Licenses: https://offermarket.us/licenses.",
"next_step_url": "https://cms.offermarket.us/public/v1/quotes/3f9a1c2e7b4d8a60/loan-file"
}
Send JSON. Unknown fields are rejected, so a typo is reported instead of ignored.
| Field | Type | Required | Description |
|---|---|---|---|
loan_purpose |
one of: purchase, refinance |
Yes | Purchase or refinance. |
address |
string | Full US street address: number and street, city, two-letter state code, and ZIP. Required for one property. Not used for a portfolio. | |
property_type |
one of: single_family, duplex, triplex, quadplex, condominium, 5_plus_units, mixed_use |
Required for one property. Not used for a portfolio. | |
units |
integer | Number of units. Required for 5_plus_units, mixed_use, and a portfolio (the total for all properties). Otherwise read from property_type. | |
unit_size_sqft |
number | Size of the smallest unit in square feet. | |
purchase_price |
number | Yes | What the property costs (a purchase) or cost when it was bought (a refinance). For a portfolio, the total. |
as_is_value |
number | Current value. Required for a refinance. Optional for a purchase. For a portfolio, the total. | |
monthly_rent |
number | Yes | Gross monthly rent, actual or market. For a portfolio, the total. |
leasing_strategy |
one of: long_term, mid_term, short_term |
Default long_term. | |
tenant_type |
one of: market, section_8 |
Default market. | |
annual_taxes |
number | Default 1.2% of as_is_value (or purchase_price when no as_is_value). | |
annual_insurance |
number | Default 0.75% of as_is_value (or purchase_price when no as_is_value). | |
annual_hoa |
number | Default 0. | |
credit_score |
integer | Yes | Self-reported credit score of the lead borrower. |
experience_deals |
integer | Investment properties owned in the last 10 years (rented, flipped, or rehabbed). Default 2. | |
entity |
one of: llc, corporation, trust, personal_name |
Who takes title. Default llc. | |
citizenship |
one of: us_citizen, green_card_holder, foreign_national |
Default us_citizen. | |
refinance_type |
one of: cash_out, rate_term |
Cash out or rate and term. Required for a refinance, and only then. | |
existing_debt |
number | Payoff of current loans. 0 when the property is owned free and clear. Required for a refinance, and only then. | |
purchase_date |
string | YYYY-MM-DD, when the property was bought. A purchase within 180 days limits the loan to what it cost. Required for a refinance, and only then. | |
capital_improvements |
number | Money spent on the property since purchase. 0 when none. Required for a refinance, and only then. | |
property_count |
integer | Number of properties. Default 1. 2 or more makes this a portfolio loan: all in one state. | |
state |
string | State code or name of a portfolio, for example "OH". Required for a portfolio, and only then. | |
counties |
array of string | Counties of a portfolio. Optional, and only for a portfolio. | |
loan_amount |
number | Optional. Options large enough for it get covers_requested_amount = true. | |
amortization |
one of: fully_amortizing, interest_only |
Default fully_amortizing. |
| Field | Type | Description |
|---|---|---|
program |
string | |
status |
one of: quoted, no_offer |
no_offer means no capital provider can quote this loan as entered; see reasons. |
request_id |
string | |
rates_as_of |
string | When the rate sheets behind this quote were loaded. Rates change daily. |
options[] |
array of objects | One option per loan-to-value level, lowest LTV first. |
options[].ltv |
number | Loan-to-value, percent of property value. |
options[].loan_amount |
number | Loan amount in dollars. |
options[].interest_rate |
number | Interest rate, percent per year. |
options[].monthly_principal_and_interest |
number | Monthly principal and interest. |
options[].monthly_payment_total |
number | Principal and interest plus taxes, insurance, and HOA. This is the payment DSCR is based on. |
options[].dscr |
number | Monthly rent divided by monthly_payment_total. |
options[].prepayment_penalty |
string | Penalty by year, in percent. "5-4-3-2-1" is 5% in year 1, 4% in year 2, and so on. |
options[].covers_requested_amount |
boolean | Whether this option's loan_amount is at least the loan_amount you sent. Null when you sent none. |
reasons[] |
array of objects | Why no option was offered. Empty when status is quoted. |
reasons[].code |
string | One of PORTFOLIO_NOT_SUPPORTED, UNIT_SIZE_BELOW_MINIMUM, STATE_NOT_SERVED, CITY_NOT_SERVED, DSCR_TOO_LOW, LOAN_AMOUNT_BELOW_MINIMUM, or a code that names the field: _BELOW_MINIMUM (for example CREDIT_SCORE_BELOW_MINIMUM) or _NOT_ELIGIBLE (for example PROPERTY_TYPE_NOT_ELIGIBLE). |
reasons[].field |
string | The request field the reason is about. |
reasons[].hint |
string | |
reasons[].minimum |
number | The lowest value any capital provider accepts. Sent with *_BELOW_MINIMUM and DSCR_TOO_LOW. |
reasons[].allowed |
array of string | Every value any capital provider accepts for field. Sent with *_NOT_ELIGIBLE. |
reasons[].value |
string | The value that is not served, for example the state code. Sent with STATE_NOT_SERVED and CITY_NOT_SERVED. |
assumptions[] |
array of objects | Every value that was not sent and was filled in, and notes that affect the quote. |
assumptions[].field |
string | |
assumptions[].value |
any | |
assumptions[].note |
string | |
disclaimer |
string | |
next_step_url |
string | Call it (GET or POST) to ask OfferMarket to turn this quote into a loan file. See the loan-file path. |
| HTTP status | Meaning |
|---|---|
| 200 | Quote result. Check status: quoted or no_offer. |
| 400 | The request is not valid. fields lists each problem. |
| 401 | INVALID_TOKEN: the id_token is invalid, expired (they work for 1 hour), or revoked. Run the sign_in step in error.log_in (the sign_up call again), then send the call again once. |
| 403 | PROFILE_REQUIRED: the login has not accepted the terms. Call POST /public/v1/account first. |
| 429 | RATE_LIMITED: wait for Retry-After seconds. FREE_CALLS_USED: an open caller has used all 10 free calls; log in to keep quoting (no Retry-After). Both list the login steps in error.log_in when the call had no login. |
| 503 | Too many quotes are running. Retry after Retry-After seconds. |
Error bodies have error.code (INVALID_REQUEST, INVALID_TOKEN, INVALID_LOGIN, LOGIN_REQUIRED, PROFILE_REQUIRED, FREE_CALLS_USED, RATE_LIMITED, BUSY, INTERNAL), error.message, and for INVALID_REQUEST a fields list and a valid example.
rates_as_of.assumptions. Send real taxes and insurance for a better estimate.next_step_url with the login token. Someone from OfferMarket emails the login's address to set it up.Press "Send it" on the left. The response shows here as it comes back.