Skip to content

Home Services — Browser-to-server

Download Markdown · Download PDF

Home Service Leads · API v1 · Browser-to-server

Section titled “Home Service Leads · API v1 · Browser-to-server”
Specification version 1.0
Last updated 2026-09-15
Audience Publishers posting home-service leads from the consumer’s browser
Support info@freshwaymedia.com

If you post from your backend, use the server-to-server specification instead. Personal-loan posting from the browser uses POST /v1/personal-loans/leads.


Freshway Media accepts home-service leads over a JSON REST API. The consumer’s browser submits a single application per request from your form page; we validate it, run compliance and fraud checks, offer it to our buyer network, and return a decision synchronously in the HTTP response.

A request that returns 200 OK has been processed, not necessarily purchased. Read the status field to determine the outcome.

We do not support a ping/post flow. There is no separate price-quote call: one request submits the lead and returns the decision.

A publishable key is visible to anyone who views your page source. It is not a secret. Its security rests on origin restriction and rate limiting rather than confidentiality.

  1. Request sandbox credentials from info@freshwaymedia.com. You will receive a publishable key and your campaign names.
  2. Send us the exact origins you will post from so we can register them. Register https://www.example.com and https://example.com separately if you use both.
  3. Build and post a request against the sandbox base URL using the example in section 5.
  4. Confirm your handling of accepted, declined, and duplicate outcomes, and of validation errors. Sandbox outcomes are not deterministic; handle all three status values. See section 10.
  5. Request production credentials. Sandbox keys do not work in production.

Environment Base URL
Sandbox https://sandbox-api.freshwayleads.com
Production https://api.freshwayleads.com
Operation Method Path
Submit a home-service lead POST /v1/home-services/leads
CORS preflight OPTIONS /v1/home-services/leads

TLS 1.2 or higher is required. Plain HTTP requests are rejected; they are not redirected.


Present your publishable key as a bearer token:

Authorization: Bearer <your-publishable-key>
Key type Prefix Where it may be used Secret?
Publishable, production fw_live_pk_ Browser JavaScript, from a registered origin No
Publishable, sandbox fw_test_pk_ Browser JavaScript, from a registered origin No

Your account and entitled trades are identified by the key. Do not send an affiliate identifier in the request body; it is ignored.

A publishable key can only submit leads. It cannot read leads, reporting, or account data.

Publishable keys are restricted to the origins you register. A request from an unregistered origin, or with no Origin header, is rejected with 403. Rotate a publishable key if you see traffic you do not recognize.

A missing, malformed, unknown, inactive, or revoked key, or a key belonging to the other environment, is rejected with 401. Inactive and revoked keys are indistinguishable from an unknown key in the response.

We answer the preflight OPTIONS /v1/home-services/leads request with 204 No Content. A registered origin receives the headers below. An unregistered origin, or a request with no Origin header, is denied. Preflight is not counted against your rate limit.

The browser sends Origin. You do not set that header in fetch.

Response header Value
Access-Control-Allow-Origin The single registered origin that sent the request
Access-Control-Allow-Methods POST, OPTIONS
Access-Control-Allow-Headers Authorization, Content-Type, Accept, X-Request-Id
Access-Control-Expose-Headers X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After
Access-Control-Max-Age 86400

We do not send Access-Control-Allow-Credentials, and we never reflect an arbitrary origin.


Header Required Value
Authorization Required Bearer <your-publishable-key> — see section 3
Content-Type Required application/json; charset=utf-8
Accept Recommended application/json
X-Request-Id Optional Your own correlation id. Echoed back in the response and in our logs.
Idempotency-Key Reserved Accepted and ignored. Do not rely on it. See section 9.

The browser also sends Origin. We use it to enforce your registered origins.


The payload is a flat JSON object using camelCase field names.

Do not send ipAddress or userAgent. We read them from the connection and the request headers.

Concern Rule
Character encoding UTF-8
Field naming camelCase, case-sensitive
Enumerated values Lower camelCase string values, case-sensitive. Never numeric codes.
Booleans JSON true / false. Not "1", "Y", or "true".
Numbers JSON numbers. Not quoted strings. No currency symbols, thousands separators, or units.
Money US dollars and cents throughout. Never cents-only, and never with a currency symbol or thousands separator. Amounts you send are plain JSON numbers and may include cents: 210 and 210.47 are both valid. More than two decimal places is rejected rather than rounded.
Timestamps RFC 3339 with a timezone. Prefer UTC with a Z suffix and millisecond precision: 2026-09-01T14:32:07.512Z. A numeric offset (+00:00) is also accepted, and fractional seconds are optional. Locale formats such as 01/09/2026 are rejected.
Empty values Omit optional fields you have no value for, or send null. After trimming, an empty string on an optional field is treated as omitted. An empty string on a required field fails as required.
Unknown fields Ignored, so we can add fields without breaking you. Do not rely on them being stored.
Trimming Leading and trailing whitespace is trimmed from all string fields before validation.

Required values: Required always; Conditional required only for certain trades (see the Required cell for the condition); Recommended optional but materially improves acceptance rate and price; Optional accepted and passed through.

Field Type Required Constraints / allowed values Description
trade string (enum) Required windows
roofing
plumbing
electrical
bathroom
kitchen
gutters
heating
siding
flooring
homeSecurity
lawnCare
mold
painting
pestControl
waterDamage
airConditioning
The home-service trade the consumer is requesting. Determines which conditional fields apply and which buyers the lead is offered to.
campaignName string Required Max 200 chars. Must match one of your active campaign names, which we give you with your keys. Matched ignoring case; surrounding whitespace is trimmed. Do not reuse a campaign name on /v1/personal-loans/leads. Which of your campaigns this lead belongs to. Determines the commission terms the lead is paid on, so it is required on every request and there is no default, even if you have only one campaign. A name we do not recognize as yours is rejected with 400; the lead is not processed.
subId string Optional Max 50 chars Your own traffic source or sub-affiliate identifier, free-form and not validated. This is for your own reporting breakdowns and does not affect attribution or payment: that is campaignName.
submittedAt string (date-time) Recommended RFC 3339 with a timezone. Must not be more than 24 hours in the past or more than 5 minutes in the future. When the consumer submitted the form on your property. Defaults to our receipt time if omitted.
firstName string Required 1–50 chars. Letters, spaces, hyphens, and apostrophes. Consumer’s first name.
lastName string Required 1–50 chars. Letters, spaces, hyphens, and apostrophes. Consumer’s last name.
email string (email) Required Max 254 chars. Valid addressable mailbox. Consumer’s email address.
phone string Required 10-digit NANP (2051234567) or E.164 (+12051234567). Both forms are accepted. Consumer’s primary contact number. Mobile numbers are strongly preferred.
address1 string Required 1–100 chars Street address of the property to be serviced.
address2 string Optional Max 100 chars Unit, suite, or apartment.
city string Required 1–50 chars City of the service address.
state string Required Exactly 2 chars. Uppercase USPS state or territory code, e.g. TX. State of the service address.
postalCode string Required ^[0-9]{5}$ 5-digit ZIP code of the service address. ZIP+4 is not accepted; send the first five digits only.
isHomeOwner boolean Required true / false Whether the consumer owns the property. Non-owners are accepted but decline at a high rate; see notHomeOwner in section 6.
propertyType string (enum) Recommended residential
commercial
multiUnit
mobileHome
Type of property to be serviced.
utilityProvider string Optional Max 50 chars Consumer’s utility provider, e.g. Reliant Energy.
averageMonthlyUtilityBill number Optional 0–99999 USD per month. Cents allowed, max 2 decimal places. Consumer’s typical monthly utility bill.
annualIncome number Optional 0–99999999 USD per year. Cents allowed, max 2 decimal places. Consumer’s self-reported annual household income.
projectType string (enum) Recommended replacement
repair
newInstall
Nature of the work requested.
projectStatus string (enum) Recommended planningAndBudgeting
readyToHire
How far along the consumer is in their decision.
projectTimeline string (enum) Recommended flexible
withinOneWeek
oneToTwoWeeks
moreThanTwoWeeks
When the consumer wants work to begin.
projectDescription string Optional Max 1000 chars Consumer’s own description of the project. Passed to the buyer verbatim. Do not include phone numbers, email addresses, or other contact details.
wallRemovalRequired boolean Optional true / false Whether the project requires removing walls.
windowCount string (enum) Conditional — required when trade is windows one
two
threeToFive
sixToNine
tenPlus
other
Number of windows in scope.
roofingType string (enum) Conditional — required when trade is roofing cedar
metal
shingles
slate
tile
flat
other
Existing or desired roof material.
sidingType string (enum) Conditional — required when trade is siding vinyl
stucco
fiber
cement
wood
metal
other
Existing or desired siding material.
heatingType string (enum) Conditional — required when trade is heating boiler
radiator
electric
furnace
gasHeat
heatPump
oilHeat
other
Heating system in scope.
airConditioningType string (enum) Conditional — required when trade is airConditioning centralAir
airDucts
commercialCooling
ductlessAc
thermostats
other
Cooling system in scope.
consent boolean Required Must be true. A lead the consumer did not consent to is rejected with 400. The consumer gave express written consent to be contacted about this request, including by phone and SMS.
consentLanguage string Required 1–1000 chars The verbatim disclosure text displayed to the consumer at the point of submission. Retained as your compliance record.
consentAt string (date-time) Recommended RFC 3339 with a timezone. Must not be later than submittedAt. When the consumer accepted the disclosure.
trustedFormCertificateUrl string (url) Optional Max 255 chars. When present: absolute https URL on cert.trustedform.com, with a non-empty path. TrustedForm certificate for this submission. Omit if you do not have one. When present, must be a well-formed https://cert.trustedform.com/... URL. consentNotVerified may be returned in a later revision; see section 6.
landingPageUrl string (url) Required Max 2048 chars. Absolute https URL. A page that collects consumer PII must be served over TLS. The page on which the consumer submitted the form. Must be the actual form page, not your domain root.

Beyond the constraints above we apply additional screening to email, phone, and trustedFormCertificateUrl — for example rejecting undeliverable mailboxes, disposable and role-based email domains, and non-dialable number ranges. This screening tightens over time within v1 and is not exhaustively enumerated here. Do not build logic that depends on a malformed value being accepted today.

POST /v1/home-services/leads HTTP/1.1
Host: sandbox-api.freshwayleads.com
Origin: https://yourlandingpage.com
Authorization: Bearer fw_test_pk_1a2b3c4d5e6f7890
Content-Type: application/json; charset=utf-8
Accept: application/json
{
"trade": "windows",
"campaignName": "HS-ACME-01",
"subId": "spring-promo-42",
"submittedAt": "2026-09-01T14:32:07.512Z",
"firstName": "Test",
"lastName": "McTester",
"email": "test@notarealemailaddress.com",
"phone": "+12051234567",
"address1": "123 Main St",
"address2": "Apt 4B",
"city": "Dallas",
"state": "TX",
"postalCode": "75201",
"isHomeOwner": true,
"propertyType": "residential",
"utilityProvider": "Reliant Energy",
"averageMonthlyUtilityBill": 210.47,
"annualIncome": 80000,
"projectType": "replacement",
"projectStatus": "readyToHire",
"projectTimeline": "withinOneWeek",
"projectDescription": "Six drafty single-pane windows on the north side of the house.",
"wallRemovalRequired": false,
"windowCount": "sixToNine",
"consent": true,
"consentLanguage": "By clicking Submit I agree to be contacted by up to four home improvement professionals at the phone number provided, including by automated technology and SMS, about my project. Consent is not a condition of purchase.",
"consentAt": "2026-09-01T14:32:05.980Z",
"trustedFormCertificateUrl": "https://cert.trustedform.com/0f7a5c1e9b3d4a628f10c2e5b7d9a3f18c604e2b",
"landingPageUrl": "https://yourlandingpage.com/windows/quote"
}

A processed lead returns 200 OK with Content-Type: application/json.

This response is delivered into the consumer’s browser and is visible in developer tools. It does not include a payout figure.

Header Description
X-Request-Id Correlation id for this request. Echoes your value if you supplied one. Quote it in support requests.
X-RateLimit-Limit Your request ceiling for the current window.
X-RateLimit-Remaining Requests left in the current window.
X-RateLimit-Reset Unix epoch seconds at which the window resets.
Field Type Always present Description
leadId string Yes Freshway’s identifier for this lead. An opaque string, at most 32 characters. Store it and quote it in support requests. Do not parse it or infer anything from its format, which may change.
status string (enum) Yes accepted — a buyer purchased the lead.
declined — processed but not purchased.
duplicate — reserved; not currently returned. See section 9.
receivedAt string (date-time) Yes When we received the request. RFC 3339 UTC.
completedAt string (date-time) Yes When we finished processing. RFC 3339 UTC.
redirectUrl string (url) Only when status is accepted Buyer-supplied URL for the consumer. Absent, not empty, when there is nothing to redirect to. See below.
declineReason string (enum) Only when status is declined Machine-readable reason. See table below.
declineMessage string Only when status is declined Always The lead was not purchased. For your logs. Do not branch on this text.
originalLeadId string Only when status is duplicate The leadId of the earlier submission.

Successful responses carry no errors field. Errors appear only on 4xx and 5xx responses, in the format described in section 7.

Navigate the consumer to this URL in the page. This is the buyer’s own confirmation or offer page and is the intended consumer experience.

When the field is present, always send the consumer. Buyers treat redirect rate as a quality signal; a low rate costs you coverage even though the lead already sold.

declineReason Meaning Should you retry?
noBuyerMatch No buyer in the network purchased the lead. This is the general case. No.
notHomeOwner The consumer does not own the property, and non-owner leads are declined at a high rate. No.
failedFraudChecks The lead did not pass our fraud screening. No.
outOfCoverageArea No buyer services that postal code for that trade. No.
consentNotVerified The TrustedForm certificate could not be claimed or did not match the submission. No. Fix your certificate capture.
belowMinimumPrice No buyer bid at or above the floor price for this lead. No.

A trade your account is not entitled to post is not a decline. It returns 403 with type: forbidden, because the lead was never processed and the remedy is a conversation with us rather than a change to your payload. See section 7.

outOfCoverageArea, consentNotVerified, and belowMinimumPrice are not currently returned. We return noBuyerMatch instead. Handle all six so you do not need to change anything when they start appearing.

{
"leadId": "01K4A7XJ2P8Q3R5S6T7V8W9XYZ",
"status": "accepted",
"redirectUrl": "https://buyer.example.com/offers/12345",
"receivedAt": "2026-09-01T14:32:08.104Z",
"completedAt": "2026-09-01T14:32:09.016Z"
}
{
"leadId": "01K4A7XJ7C2D4E6F8G0H1J2K3M",
"status": "declined",
"declineReason": "noBuyerMatch",
"declineMessage": "The lead was not purchased.",
"receivedAt": "2026-09-01T14:32:08.104Z",
"completedAt": "2026-09-01T14:32:08.755Z"
}
{
"leadId": "01K4A7XKB5N6P7Q8R9S0T1U2V3",
"status": "duplicate",
"originalLeadId": "01K4A7XJ2P8Q3R5S6T7V8W9XYZ",
"receivedAt": "2026-09-01T14:35:11.220Z",
"completedAt": "2026-09-01T14:35:11.298Z"
}

Errors return Content-Type: application/problem+json and follow RFC 9457 Problem Details.

Field Type Description
type string (uri) Stable identifier for the error class. Branch on this, not on status, title, or detail.
title string Short human-readable summary of the error class.
status integer HTTP status code, repeated for convenience.
detail string Human-readable explanation specific to this occurrence.
instance string Path of the request that failed.
requestId string Correlation id. Matches the X-Request-Id response header.
errors array Field-level failures. Present on validation errors only.
errors[].field string Name of the offending request field.
errors[].code string Machine-readable failure code: required, invalidFormat, tooLong, notAllowedValue, outOfRange.
errors[].message string Human-readable description of the field failure.

Six codes, each with distinct handling. Where two conditions call for the same action from you, they share a status code and are distinguished by type.

Status type When Retry?
200 OK Lead processed. Read status for the outcome. n/a
400 Bad Request .../malformed-request Body is not valid JSON, or Content-Type is wrong. No — fix the request.
400 Bad Request .../validation-failed Body is valid JSON but one or more fields fail validation. errors is populated. No — fix the fields.
401 Unauthorized .../unauthenticated Missing, malformed, unknown, inactive, or revoked key, or a key that belongs to the other environment. No — fix credentials.
403 Forbidden .../forbidden Key is valid but not entitled to post this trade; or the request came from an unregistered origin or with no Origin header. No — contact us.
429 Too Many Requests .../rate-limited Rate limit exceeded. Honor Retry-After. Yes, after the delay.
500 Internal Server Error .../internal-error Unexpected failure on our side. Yes, with backoff.

Any 4xx means the lead was not processed and never will be. Do not resubmit the same payload unchanged. A 429 is the exception: the payload was never looked at, so resubmit it after the delay.

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
X-Request-Id: 9c1f4b7a-5d2e-4a83-b6c9-70e1f2a3d4b5
{
"type": "https://docs.freshwayleads.com/errors/validation-failed",
"title": "One or more fields failed validation.",
"status": 400,
"detail": "The lead was not processed. Correct the fields listed in 'errors' and submit a new request.",
"instance": "/v1/home-services/leads",
"requestId": "9c1f4b7a-5d2e-4a83-b6c9-70e1f2a3d4b5",
"errors": [
{
"field": "windowCount",
"code": "required",
"message": "windowCount is required when trade is 'windows'."
},
{
"field": "phone",
"code": "invalidFormat",
"message": "Expected a 10-digit NANP number or an E.164 number."
},
{
"field": "consent",
"code": "notAllowedValue",
"message": "consent must be true. Leads without express written consent are not accepted."
}
]
}
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 24
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1788192000
{
"type": "https://docs.freshwayleads.com/errors/rate-limited",
"title": "Rate limit exceeded.",
"status": 429,
"detail": "Your account is limited to 100 requests per minute. Retry after 24 seconds.",
"instance": "/v1/home-services/leads",
"requestId": "1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7081"
}

The default limit is 100 requests per minute per key, measured in a fixed one-minute window.

Browser traffic is additionally limited to 5 requests per minute per consumer IP address, to absorb a single misbehaving client without consuming your whole key allowance. A real consumer submits once, so this ceiling is not reachable by legitimate use. A consumer who trips the per-IP limit receives a 429; your key allowance is unaffected.

Every POST /v1/home-services/leads response, successful or not, carries the current state. CORS preflight (OPTIONS) is not counted and does not carry these headers.

Header Meaning
X-RateLimit-Limit Your ceiling for the window.
X-RateLimit-Remaining Requests left in the window.
X-RateLimit-Reset Unix epoch seconds at which the window resets and Remaining returns to Limit.

Exceeding the limit returns 429 with a Retry-After header in seconds. Honor it; it overrides your own backoff schedule.

Contact us before a campaign launch if you expect to exceed 100 requests per minute.


Repeated submissions are still processed and can still be sold. A consumer double-clicking submit, or a retry after a timeout where the first request succeeded, can produce a second purchased lead.

status: "duplicate" and originalLeadId are reserved and not currently returned. Handle them if they appear.

The Idempotency-Key header is accepted and ignored. Do not rely on it.

Processing involves live calls to buyers, so latency is normal.

Setting Recommendation
Connect timeout 5 seconds
Read timeout 30 seconds — do not set this lower, or you will abandon leads we are actively selling
Retry on 429, 500, connection failures, read timeouts
Do not retry on 400, 401, 403
Backoff Exponential with jitter, e.g. 1s, 2s, 4s
Max attempts 3 total

A read timeout does not mean the lead was not processed. Retry it, knowing that a successful first attempt plus a retry can sell the consumer twice.


Sandbox credentials (fw_test_pk_) are refused by production, and production credentials (fw_live_pk_) are refused by sandbox. Validation, rate limits, CORS, authentication, error shapes, and response fields are the same in both environments.

Sandbox outcomes are not deterministic. A given payload is not guaranteed to return accepted, declined, or duplicate. Handle all three status values, treat an unrecognised status as not accepted, and treat an unrecognised declineReason as a generic decline.

Do not post real consumer data to sandbox.

If trustedFormCertificateUrl is sent, it is checked for URL shape. A well-formed https://cert.trustedform.com/... URL passes. Omitting the field is accepted. consentNotVerified may be returned in a later revision.

Before requesting production credentials, confirm you parse leadId and X-Request-Id, treat 4xx other than 429 as terminal, honor Retry-After on 429, and tolerate the three status values and the six declineReason codes.


  • Consent. Only post leads where the consumer gave express written consent covering contact by phone and SMS. Send the exact disclosure text in consentLanguage. Falsified consent is grounds for immediate termination and chargeback of all affected leads.
  • TrustedForm. Send a well-formed certificate URL on cert.trustedform.com when you have one. Omitting the field is accepted. When present, the URL is checked for shape. consentNotVerified may be returned in a later revision.
  • Transport. TLS 1.2 or higher, HTTPS only.
  • Data minimization. Post only the fields in this specification. Do not send SSNs, bank account numbers, card numbers, or government identifiers. Unknown fields are ignored rather than rejected, so including them is not a validation error — it is still a compliance failure on your side.
  • Logging. Do not log full request bodies containing consumer PII in systems that are not access-controlled. Log leadId, X-Request-Id, status, and declineReason instead.
  • Credentials. One publishable key per environment, covering your whole account rather than one per campaign: a key says who you are, and campaignName on each request says which of your campaigns the lead belongs to. The same publishable keys also authenticate /v1/personal-loans/leads. Use a distinct campaignName for each product. Publishable keys may appear in browser JavaScript but only work from the origins you register with us.
  • Rotation. Ask us for a replacement key before retiring the one you hold. Both work at the same time, so you can cut over at your own pace and tell us to revoke the old key afterwards. There is no window where posting fails.

The major version is in the URL path. Within v1 we make only backward-compatible changes:

  • New optional request fields.
  • New response fields.
  • New enum values on existing fields, including new trade values and new declineReason codes.
  • New type values on existing status codes.
  • Tightened validation on fields already documented as constrained.

Build your integration to tolerate these. Ignore response fields you do not recognize, treat an unrecognized declineReason as a generic decline rather than a parse failure, treat an unrecognized status as “not accepted”, and branch on type rather than on the numeric status code alone.

Breaking changes ship as a new path version with a written deprecation window for the previous one.