Progressive Home API — Reverse Engineering
Status: Proven and validated in both QA and Production. The entire Progressive Home agent portal has been reverse-engineered. Login, session creation, and the full quoting REST API are accessible via direct HTTP — no browser, no Playwright. Production validation completed 2026-03-30 (login + MFA + session creation + quoting gateway confirmed).
| Metric | Playwright | Direct HTTP |
|---|---|---|
| Login | ~39s | ~3s |
| Login + session + first API call | ~64s | ~6s |
| Browser required | Yes (Chromium) | No |
| Reliability | Flaky (DOM timing, selectors) | Deterministic |
Architecture
QA Environment
Production Environment
1. Authentication (OAuth2 / PingFederate)
The login uses an OAuth2 Authorization Code flow via PingFederate. Six sequential HTTP requests, no browser rendering required.
| Step | Method | URL | Result |
|---|---|---|---|
| 1 | GET | 62.qa.foragentsonly.com/login/ | 302 to PingFederate, sets ASP.NET_SessionId, FAOUserSessionId, and ~10 domain cookies |
| 2 | GET | qa-login.progressive.com/as/authorization.oauth2?client_id=ClientFao&response_type=code&... | 302 back to FAO with resumePath, sets PF cookie |
| 3 | GET | 62.qa.foragentsonly.com/FederatedLogin/LoginResume/?resumePath=... | Sets ResumePath cookie, 302 to /login/ |
| 4 | GET | 62.qa.foragentsonly.com/login/ | 200 — renders login form. Inline JS contains var postUrl = "https://qa-login.progressive.com/as/.../resume/as/authorization.ping?app=fao" |
| 5 | POST | qa-login.progressive.com/as/{id}/resume/as/authorization.ping?app=fao | Credentials (user1, password1) go to PingFederate. Returns 302 with ?code=... auth code and LOGINOIDFAO_SESSION JWT |
| 6 | GET | 62.qa.foragentsonly.com/federatedlogin/signin/?code={authCode}&state=loginType%3DSTANDARD | Exchanges code for session. Sets WebGuard cookie (Bearer JWT), AGENTCODE, FAOContext. Redirects to /home/ |
JWT Structure (WebGuard cookie)
The WebGuard cookie contains a Bearer JWT issued by PingFederate:
{
"scope": [],
"client_id": "ClientFao",
"iss": "https://qa-login.progressive.com",
"sub": "43786",
"entity_type": "FaoPerson",
"role": [
"AR-1", "TSA-1", "IA-1", "AA-1",
"ddbapipolicysvc-01", "PSAPIReadOnly-01",
"psapisessions-01", "pproapifao-01"
],
"agent_code": "43786",
"exp": 1773305423
}
Expiry is approximately 12 hours from issuance.
Key Discovery: postUrl
The login form does not POST to /login/. JavaScript in the login page dynamically sets form.action = postUrl where postUrl is the PingFederate resume URL embedded inline. This is why a naive POST /login/ fails — credentials must go directly to PingFederate.
1b. Production Authentication (PingFederate Authn REST API — Validated 2026-03-30)
Production uses a completely different authentication mechanism than QA. Instead of the OAuth2 Authorization Code redirect flow, prod uses the PingFederate Authn REST API (/pf-ws/authn/flows/) — a stateful JSON API with mandatory MFA.
Auth Flow (7 sequential HTTP requests)
| Step | Method | URL | Result |
|---|---|---|---|
| 1 | GET | www.foragentsonly.com/login/ | 200 — login page with inline var flowId = "{id}" and var pingBaseUrl = "https://login.progressive.com" |
| 2 | GET | login.progressive.com/pf-ws/authn/flows/{flowId} | 200 — { status: "USERNAME_PASSWORD_REQUIRED" }. Required header: X-XSRF-Header: FAO |
| 3 | POST | login.progressive.com/pf-ws/authn/flows/{flowId}?action=checkUsernamePassword | 200 — { status: "AUTHENTICATION_REQUIRED" } on valid creds. 401 INVALID_CREDENTIALS on bad creds. Body: { "username": "...", "password": "..." } |
| 4 | POST | login.progressive.com/pf-ws/authn/flows/{flowId}?action=authenticate | 200 — { status: "OTP_REQUIRED", devices: [...] }. Triggers OTP delivery to the configured device |
| 5 | POST | login.progressive.com/pf-ws/authn/flows/{flowId}?action=checkOtp | 200 — { status: "MFA_COMPLETED" } on valid OTP. Body: { "otp": "123456" } |
| 6 | POST | login.progressive.com/pf-ws/authn/flows/{flowId}?action=continueAuthentication | 200 — { status: "RESUME", resumeUrl: "https://login.progressive.com/as/{flowId}/resume/as/authorization.ping" } |
| 7 | GET | {resumeUrl} (follow redirects) | 302 → www.foragentsonly.com/federatedlogin/signin/?code={authCode}&state=... → sets WebGuard JWT, AGENTCODE, FAOContext cookies → 302 → /home/ |
Required Headers for All PingFederate Calls
| Header | Value |
|---|---|
X-XSRF-Header | FAO |
Content-Type | application/json (for POST requests) |
Cookies must be tracked across both www.foragentsonly.com and login.progressive.com domains. The PF cookie set by Step 2 is required for subsequent PingFederate calls.
MFA Device Configuration
The authenticate response returns the available MFA devices:
{
"status": "OTP_REQUIRED",
"devices": [{
"id": "b691aa85-1a3d-4df8-8258-10d53c10a1cb",
"type": "Email",
"target": "pr****@goosehead.com",
"usable": true,
"defaultDevice": true
}],
"selectedDeviceRef": { "id": "b691aa85-1a3d-4df8-8258-10d53c10a1cb" },
"otpLifetime": { "duration": 10, "timeUnit": "MINUTES" },
"changeDevicePermitted": true,
"manageDevicesAllowed": false
}
Supported device types: Email, Phone (SMS), Authenticator (TOTP app).
MFA Error Handling
| Status | Code | Meaning |
|---|---|---|
VALIDATION_ERROR | INVALID_OTP | Wrong or expired OTP. attemptsRemaining indicates retries left |
MFA_FAILED | OTP_ATTEMPTS_LIMIT | Too many failed OTP attempts. coolDownExpiresAt (Unix ms) indicates lockout duration |
MFA_FAILED | NO_USABLE_DEVICES | All devices locked out. userMessage contains the lockout duration in minutes |
MFA_FAILED | OTP_RESEND_LIMIT | Too many OTP resend requests |
OTP resend: POST ?action=selectDevice with { "deviceRef": { "id": "{deviceId}" } }.
Production JWT Structure (WebGuard cookie)
{
"scope": [],
"client_id": "ClientFao2",
"sub": "gshd_digitalagent",
"entity_type": "FaoPerson",
"role": [
"UR-1", "ddbapipolicysvc-01", "PSAPIReadOnly-01",
"clmapisumm-01", "olaapireadonly-01",
"psapisessions-01", "pproapiagntupdt-01", "pproapifao-01"
],
"guid": "3743AE34-4D25-48E8-8964-71C1A3DA879A",
"mfa": "true",
"agent_code": "04T42",
"exp": 1774937389
}
Key differences from QA JWT: client_id is ClientFao2 (not ClientFao), mfa field is "true", agent_code is 04T42 (not 43786), roles differ slightly.
QA vs Production Auth Comparison
| Aspect | QA | Production |
|---|---|---|
| Login domain | 62.qa.foragentsonly.com | www.foragentsonly.com |
| PingFederate domain | qa-login.progressive.com | login.progressive.com |
| Auth mechanism | OAuth2 Authorization Code (6-step redirect chain) | PingFederate Authn REST API (7-step JSON API) |
postUrl in login page | PingFederate resume URL (populated) | Empty string (unused) |
flowId in login page | Not used | Required — drives the entire auth flow |
| MFA | Not present | Mandatory (Email OTP confirmed) |
| Bot detection | None observed | None observed (curl with default User-Agent accepted) |
client_id in JWT | ClientFao | ClientFao2 |
MFA Automation Strategies
- Email polling — programmatically read the OTP from the
pr****@goosehead.cominbox (Microsoft Graph API, IMAP, or webhook) - TOTP authenticator — if Progressive allows adding an authenticator device, generate TOTP codes programmatically (blocked:
manageDevicesAllowed: falseon this account) - Manual relay with pause — pause automation, prompt for OTP input, continue. Viable for low-volume or testing scenarios
- Session caching — WebGuard JWT expires in ~12 hours. Cache the authenticated session and reuse across quotes to minimize MFA prompts
2. Session Creation (QuotingGateway)
After login, a quote session is created by hitting the QuotingGateway endpoint. This was the hardest piece to crack — the dashboard uses JavaScript (OpenNewQuote function in the HomeScripts bundle) to build a window.open() URL.
Endpoint
GET /NewBusiness/QuotingGateway/RouteQuote/?from=home&app=NewQuote&st_cd={state}&prod_cd={product}&dflt_st_prod=N&handshakeId={uuid}
| Parameter | Value | Notes |
|---|---|---|
st_cd | TX, OH, etc. | Two-letter state code |
prod_cd | HO, AU, CO, etc. | Product code. HO = Home (HO3) |
dflt_st_prod | N or Y | Whether to remember defaults |
handshakeId | Random UUID v4 | Generated client-side |
Redirect Chain
QA:
GET /NewBusiness/QuotingGateway/RouteQuote/...
→ 302 /NewBusiness/IaqQuoting/RouteQuote/...
→ 302 https://a-vendor.quoting.foragentsonly.com/Slot301/bridgequote/index?quoteAction=NQ&LookupId={token}&SessionIndex=0&SyncId={guid}&environment=Acceptance
→ 302 /Slot301/Quote/Index/?SessionIndex=0&SyncId={guid} (sets UQContext cookie)
→ 200 (SPA HTML with workflowStatesEmbeddedInPage JSON — establishes server-side session)
Production:
GET /NewBusiness/QuotingGateway/RouteQuote/...
→ 302 /NewBusiness/IaqQuoting/RouteQuote/...
→ 302 https://quoting.foragentsonly.com/bridgequote/index?quoteAction=NQ&LookupId={token}&SessionIndex=0&SyncId={guid}&environment=Production-East
→ 302 /Quote/Index/?SessionIndex=0&SyncId={guid} (sets UQContext cookie)
→ 200 (SPA HTML — establishes server-side session)
The LookupId is a server-generated encrypted token. The SyncId is a GUID linking the FAO session to the quoting session. The bridgequote/index hop sets the UQContext cookie containing the quoting session's ContextId, ContextGroupId, and SyncId, then redirects to the actual SPA entry point. In QA, the SPA lives at /Slot301/Quote/Index/; in production, it's at /Quote/Index/ (no slot prefix).
Product Codes
| Code | Product |
|---|---|
AU | Auto |
HO | Home (HO3) |
H5 | Home (HO5) |
HB | Home (HOB) |
CO | Condo (HO6) |
RT | Renters (HO4) |
BT | Boat/PWC |
MC | Motorcycle/ATV |
MT | Motor Home |
TT | Travel Trailer |
UB | Umbrella |
FL | Flood |
CV | Commercial Auto |
BP | Businessowner/Contractor GL |
3. Quoting REST API
Once the SPA page is loaded (establishing the server-side session), all form interactions are REST API calls.
Base URL
QA:
https://a-vendor.quoting.foragentsonly.com/Slot301/api/v1/composite/
Production:
https://quoting.foragentsonly.com/api/v1/composite/
The Slot301 prefix is QA-only. Production uses the root path directly.
Workflow Navigation Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| POST | AppInitWorkflowState?expand=[{"descriptor":"view-model","expand":[]}] | Initialize workflow after SPA load. Requires a JSON body (at minimum {}); omitting the body returns 411 Length Required |
| GET | CurrentWorkflowState?workflowNode={step} | Get current step state and navigation links |
| POST | NextWorkflowState?workflowNode={step} | Submit form data, advance to next step |
| POST | GoToWorkflowState?workflowNode={step} | Jump to a specific step |
| GET | {Step}EditViewModel | Get form field values for a step |
| POST | SaveAndExitWorkflowState | Save and exit the quote |
| POST | DuplicateQuoteWorkflowState | Duplicate a quote |
| GET | PrintWorkflowState | Print view |
Context and Metadata Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| GET | IaqQuoteContext | Session context (state, products, agent, environment) |
| GET | IaqHeader | Customer header (name, phone, email) |
| GET | ProgressBar | Step progress and completion status |
| GET | Help | Contextual help content |
| GET | ProductGuidePdf | Product guide PDF link |
Sub-Workflow Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| varies | SubWorkflowState?subWorkflowName=MaRmvPrefill | Address prefill |
| varies | SubWorkflowState?subWorkflowName=OriginalPolicySummary | Original policy summary |
| varies | SubWorkflowState?subWorkflowName=QuoteComment | Quote comments |
Phone Number Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| POST | AddedPhoneNumbers | Add a phone number |
| DELETE | DeletedPhoneNumbers?id={guid} | Remove a phone number |
| GET | PhoneNumberViewModel | Phone number form model |
Stateless/Cache Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| varies | composite-stateless/PropertyNonSaveRatingCache | Property rating cache |
| varies | composite-stateless/AsiRentersTeaserCache | Renters teaser cache |
| varies | composite-stateless/AsiUmbrellaTeaserCache | Umbrella teaser cache |
Error Logging
| Method | Endpoint | Purpose |
|---|---|---|
| POST | ClientErrorLogger | Client-side error/event logging |
4. HAL Response Format
All API responses follow the HAL (Hypertext Application Language) pattern:
{
"CurrentPageNav": "Workflow/UnsoldQuote/QuoteFlow/PreCoveragesFlow/NamedInsuredGroup/NamedInsured",
"ActiveViewModelHasEdits": false,
"Links": [
{
"Name": "next",
"Descriptors": ["workflow", "next"],
"Uri": "/Slot301/api/v1/composite/NextWorkflowState?workflowNode=NamedInsured"
}
],
"Extenders": {
"ServerTransactionTime": "40"
},
"Embedded": {
"NamedInsured": { "FirstName": "mike", "LastName": "jones", "..." : "..." },
"ProgressBar": { "Items": ["..."] },
"IaqHeader": { "CustomerFirstName": "mike", "..." : "..." },
"IaqQuoteContext": { "QuoteState": "TX", "..." : "..." }
},
"ValidationDetails": {},
"Messages": {},
"ApplicationBuildVersion": "6.0.0.1518"
}
The Links array drives navigation — the SPA follows the next link to advance steps. Embedded contains the view model data for the current page. ValidationDetails contains field-level validation errors.
5. Named Insured — Confirmed Working Example
POST /Slot301/api/v1/composite/NextWorkflowState?workflowNode=NamedInsured&expand=[{"descriptor":"view-model","expand":[]}]
{
"FirstName": "mike",
"LastName": "jones",
"MiddleInitial": "",
"Suffix": "",
"DateOfBirth": "1990-01-01",
"Gender": "M",
"MailingAddress": "123 main st",
"ApartmentUnit": "",
"City": "Burleson",
"State": "TX",
"ZipCode": "76028",
"MailingZipType": "O",
"PriorAddress": "",
"PriorCity": "",
"PriorState": "",
"PriorZipCode": "",
"CurrentResidence": "",
"RecentlyMoved": "N",
"MaRmvPrefillOpen": null,
"AgentCode": "43786",
"AsiAgentCode": "446447",
"MembershipNumber": null,
"SellingCode": null,
"ReferenceNumber": null,
"PrimaryEmailAddress": "na@email.com",
"DisclosureProvided": "Y",
"PhoneNumbers": {
"List": [
{
"PhoneId": "665de64d-c4aa-4442-99ad-fd419ee7eff5",
"PhoneNumber": "321-242-2233",
"PhoneType": "M",
"IsVerified": "N",
"CurrentPageNav": null,
"ApplicationBuildVersion": "6.0.0.1518"
}
],
"Count": 1,
"ApplicationBuildVersion": "6.0.0.1518"
},
"ProductSpecificInformation": {
"List": [
{
"ContentType": "HO",
"Offerings": null,
"PolicyType": null,
"CurrentPageNav": null,
"ApplicationBuildVersion": "6.0.0.1518"
}
],
"Count": 1,
"ApplicationBuildVersion": "6.0.0.1518"
},
"HasInternationalAddress": "N",
"InternationalMailingAddress1": "",
"InternationalMailingAddress2": "",
"InternationalCity": "",
"InternationalState": "",
"InternationalCountry": "",
"InternationalZipCode": "",
"QuoteOrigin": "",
"CurrentPageNav": null,
"ApplicationBuildVersion": "6.0.0.1518"
}
DisclosureProvided must be "Y". Submitting with "N" returns a 400 with an edit message: "The disclosure must be provided to or read to the consumer." The SPA shows this as a disclosure dialog the agent must acknowledge before advancing. In direct HTTP, set it to "Y" upfront.
Response: 201 with ~208KB of JSON. The Embedded.NamedInsured mirrors the submitted data. ValidationDetails is empty (no errors). The Links array provides the next navigation URI for the Products step (NextWorkflowState?workflowNode=ProductsHO).
6. Environment Details
QA Domains
| Domain | Purpose |
|---|---|
62.qa.foragentsonly.com | QA login portal and dashboard (ASP.NET MVC) |
qa-login.progressive.com | PingFederate OAuth2/OIDC provider |
a-vendor.quoting.foragentsonly.com | Quoting SPA and REST API (Angular) |
qa.test.faoservices.foragentsonly.com | Legacy SOAP rating API (not used by quoting SPA) |
q-rtds.progressive.com | Splunk logging |
tdmapi-main-as-tdm-api-qa.np.glb.pgrcloud.app | Deal tracking/logging API |
Production Domains (Validated 2026-03-30)
| Domain | Purpose |
|---|---|
www.foragentsonly.com | Prod login portal and dashboard (ASP.NET MVC) |
login.progressive.com | PingFederate Authn REST API (MFA-enabled) |
quoting.foragentsonly.com | Quoting SPA and REST API (Angular) |
rtds.progressive.com | Splunk logging |
QA vs Production Domain Mapping
| Purpose | QA | Production |
|---|---|---|
| Login portal | 62.qa.foragentsonly.com | www.foragentsonly.com |
| PingFederate | qa-login.progressive.com | login.progressive.com |
| Quoting SPA/API | a-vendor.quoting.foragentsonly.com | quoting.foragentsonly.com |
| Quoting base path | /Slot301/ | / (no slot prefix) |
| Quoting API base | a-vendor.quoting.foragentsonly.com/Slot301/api/v1/composite/ | quoting.foragentsonly.com/api/v1/composite/ |
| Environment label | Acceptance | Production-East |
| Agent code | 43786 | 04T42 |
client_id (JWT) | ClientFao | ClientFao2 |
Key Cookies
| Cookie | Domain | Purpose |
|---|---|---|
WebGuard | .foragentsonly.com | Bearer JWT for authentication |
ASP.NET_SessionId | portal domain | Server-side session for the dashboard |
FAOContext | .foragentsonly.com | Session context (group ID, client ID, server, location) |
AGENTCODE | .foragentsonly.com | Authenticated agent code |
UQContext | .foragentsonly.com | Quoting session context (SyncId, ContextId) |
PF | login.progressive.com | PingFederate session (prod only, required for authn flows) |
BIGipServer~eCommerce~... | quoting domain | F5 load balancer affinity |
7. RC1 Quotes, Agent Visibility, and the OpenQuote Flow
How RC1 Quotes Are Created (DA → MuleSoft → Progressive)
When a customer uses the Digital Agent (DA), MuleSoft submits an RC1 request to Progressive's system. This creates a quote with pre-populated data (applicant, property, coverages) and generates a Progressive Q-number (e.g., Q83806794). The Q-number is stored in our CRN sf_quote_response table. Without this RC1 submission, Progressive has no property data to rate against.
Agent Visibility Constraint (Critical)
MuleSoft uses its own dedicated agent credentials (user ID and password) to submit RC1 quotes to Progressive. These quotes are bound to MuleSoft's agent account in the Progressive portal and are only visible to that agent. Our API automation and config-driven systems authenticate with different credentials — we cannot see or open MuleSoft-created RC1 quotes.
The OpenQuote endpoint (documented below) only works when the requesting agent matches the agent that originally created the quote. Our agent credentials will fail when attempting to open a quote created under MuleSoft's agent.
Previous assumption (INCORRECT): Earlier testing used
OpenQuotewith specific Q-numbers that happened to be accessible in QA. This approach does not generalize — in production, MuleSoft quotes are strictly scoped to MuleSoft's agent.
Future: RC1 Clone/Recreate Strategy
To bridge this gap, the system must:
- Receive RC1 quote data from MuleSoft — applicant info, property details, coverages, and any other pre-populated fields from the RC1 submission
- Create a new quote session in Progressive using our agent credentials (via
NewQuotegateway) - Populate each step with the RC1 data — programmatically submit NamedInsured, ProductsHO, etc. with the data MuleSoft originally provided, effectively cloning the quote under our agent
This clone approach preserves the pre-populated data advantage of RC1 while using credentials we control. The key challenge is that NewQuote produces a different workflow than OpenQuote (see table below) — the clone implementation must account for these differences.
Why RC1 Data Matters
Even though we cannot directly open MuleSoft's quotes, the RC1 data itself is essential. Progressive needs property data (address, construction, roof, etc.) to generate a replacement cost estimate and rate the policy. The RC1 submission provides this data upfront. In the clone approach, we re-submit this same data through Progressive's step workflow under our own session.
NewQuote vs OpenQuote Workflow Differences
| Aspect | NewQuote | OpenQuote (RC1) |
|---|---|---|
| Steps | NamedInsured → AcknowledgeMVR modal → PointOfSale | NamedInsured → Products → HouseholdMembers → ... → FinalSale (8 steps) |
| Property data | None (must be entered manually) | Pre-filled from RC1 submission |
| AcknowledgeMVR modal | Appears after NamedInsured (outlet: "modal") | Does not appear |
GoToWorkflowState | Returns 404 for intermediate steps | Works as expected |
Finding RC1 Quote Numbers (Reference)
Visibility note: These Q-numbers belong to MuleSoft's agent and are not accessible via our agent credentials. This reference is retained for debugging, cross-referencing MuleSoft submissions, and extracting RC1 data for the clone/recreate strategy.
The Progressive Q-number is stored in the CRN sf_quote_response table (public schema), in the company_quote_number__c column.
SELECT company_quote_number__c AS q_number, status__c, premium_total__c, effective_date__c, request_date__c
FROM sf_quote_response
WHERE carrier__c ILIKE '%progressive%'
AND line_of_business__c ILIKE '%home%'
AND company_quote_number__c IS NOT NULL
AND status__c = 'SuccessWithChanges'
ORDER BY request_date__c DESC
LIMIT 10;
| Column | Purpose |
|---|---|
company_quote_number__c | Progressive Q-number (e.g. Q83807337) |
company_client_id__c | ASI agent code used for the RC1 |
company_url__c | Direct URL to ASI quote summary (UAT) |
quote_request_heroku_id | Links to the sf_quote_request table |
clue_status__c / mvr_status__c | Report statuses (if returned by MuleSoft) |
boomi_mulesoft__c | "Mulesoft" confirms RC1 source |
Alternative (MuleSoft agent only): Log in to the Progressive portal at 62.qa.foragentsonly.com using MuleSoft's credentials → Quote Search → filter by Home (HO3), state, Unsold. These quotes will NOT appear when logged in with our automation credentials.
OpenQuote Endpoint (Reference)
Credential constraint: This endpoint requires the requesting agent to match the agent that created the quote. It will not work with our credentials for MuleSoft-created quotes. Retained here for technical reference and for cases where quotes are created under our own agent (e.g., via the future clone strategy or manual portal testing).
GET /NewBusiness/QuotingGateway/RouteQuote/
?app=OpenQuote
&asiQuoteNumber=Q83806794
&asiAgentCode=446447
&st_cd=TX
&prod_cd=HO
&qt_src=ASI
| Parameter | Value | Notes |
|---|---|---|
app | OpenQuote | Not NewQuote |
asiQuoteNumber | Q83806794 | Progressive's Q-number (from portal dashboard, NOT from CRN) |
asiAgentCode | 446447 | ASI agent code (must match the agent that created the quote) |
st_cd | TX | State code |
prod_cd | HO | Product code |
qt_src | ASI | Source = ASI (MuleSoft RC1) |
The redirect chain is identical to new quotes. The opened quote always starts at NamedInsured regardless of how far it previously progressed — each step must be submitted via NextWorkflowState to advance.
8. Products Step — Eligibility Validation (Critical)
The ProductsHO step has a multi-phase submission that differs from all other steps:
Phase 1: Set Fields
The HomeClosingRiskFlag uses standard Y/N values:
| Value | Meaning |
|---|---|
"Y" | Yes (home closing) |
"N" | No (not a closing) |
This is a Question-only field (not in the flat Details object). Required — Progressive returns 400 with edit "Please fill out this field to continue." if empty.
The HomeForeclosureFlag uses radio button values, not Y/N:
| Value | Meaning |
|---|---|
"1" | Yes (foreclosure) |
"2" | No (not a foreclosure) |
The eligibility verification checkboxes use Y/N:
VerifyNoInelCond:"Y"= agent verified no ineligible conditionsVerifyNoUwCond:"Y"= agent verified no underwriting conditions
Phase 2: Validate Eligibility (required before submit)
Before submitting NextWorkflowState?workflowNode=ProductsHO, you MUST call:
PUT /Slot301/api/v1/composite/PropertyAddressEligibility?flow=Validate&expand=[{"descriptor":"view-model","expand":[]}]
Body: the full ProductsHO form data (same as what would go to NextWorkflowState).
This call runs the server-side eligibility check and sets EligibilityVerifiedFlag = "Y". Without this call, NextWorkflowState returns 400 with edit "EligibilityVerifiedFlag: Please select this button to continue.".
Phase 3: Submit
After the eligibility PUT succeeds, submit the step normally:
POST /Slot301/api/v1/composite/NextWorkflowState?workflowNode=ProductsHO&expand=[...]
Common 400 Edits on ProductsHO
| Field | Error | Fix |
|---|---|---|
HomeForeclosureFlag | "Please fill out this field to continue." | Set to "2" (No) — NOT "N" |
VerifyNoInelCond | "Please review the eligibility requirements" | Set to "Y" |
VerifyNoUwCond | "Please review the eligibility requirements" | Set to "Y" |
EligibilityVerifiedFlag | "Please select this button to continue." | Call PUT PropertyAddressEligibility first |
9. Products Step — Required Property Detail Fields (Discovered)
The ProductsHO step has deeply nested required fields inside Properties.List[0].Details.Embedded.Questions. These fields do NOT appear in top-level ValidationDetails — they are embedded HasEdit=true entries.
Correct Field Names (HAL vs. intuitive names)
| HAL Field Name | What It Is | Common Wrong Name |
|---|---|---|
LivingArea | Square footage | TotalLivingArea |
TypeOfConstruction | Construction type code | ConstructionType |
Substructure | Foundation type | FoundationType |
ExteriorWalls | Exterior wall material | ExteriorWallType |
RoofMaterial | Roof material type | RoofType |
YearRoofInstalled | Year roof replaced | YearOfRoof |
RoofDesign | Roof shape/geometry | RoofShape |
Required Fields (Often Missing from RC1 Data)
| Field | Valid Values | Notes |
|---|---|---|
NumberOfStories | "1", "1H", "2", "2H", "3", "BILEVEL", "TRILEVEL" | NOT text like "one"/"two" |
GarageType | "NO", "1A", "2A", "3A", "1B", "2B", "3B", "1C", "2C", "3C", "1D", "2D", "3D" | Format: {count}{type} (A=Attached, B=Built-in, C=Carport, D=Detached) |
CoolingType | "None", "CentralAir", "EvapCooler", "WholeHouseFan", "HeatPump", "Humidifier" | NOT "C" or "N" |
HeatingType | "None", "Radiant", "GeoClosed", "GeoOpen", "Electric", "GasForced", "HeatPump", "OilForced", "PropaneForced", "GasHotWater", "OilHotWater", "PropaneHotWater" | Required |
KitchenGrade | "0" (Builder's), "1" (Custom), "2" (Designer), "3" (Semi-Custom) | Required |
BathroomGrade | "Standard" (Builder's), "Custom", "Designer", "SemiCust" | Required |
DeckType | "None", "Wood", "Redwood", "Composite" | Required |
PropertyAcreage | "1" (0-10ac), "2" (11-20ac), "3" (21-30ac), "4" (31+) | Required |
RoofDesign | "GABLE", "HIP", "FLAT" | UPPERCASE only — "Gable" is silently rejected. This field is Question-only (not in flat Details object). |
Question-Only Fields (Critical Implementation Detail)
Some property detail fields exist ONLY inside Properties.List[0].Details.Embedded.Questions.List — they are NOT present as flat keys on the Details object. When extracting step data and stripping HAL metadata (which removes Embedded), these fields are lost.
Affected fields: RoofDesign (confirmed), potentially others for less common property types.
Fix: Before stripping, collect all Question Property/LastAnswer pairs. After stripping, inject missing fields into the flat Details object so setNestedField can find and update them.
DwellingCoverageValue
Located at Properties.List[0].FlowType.DwellingCoverageValue. The replacement cost estimate is at Properties.List[0].FlowType.ReplacementCostEstimate. Progressive calculates the estimate and dwelling coverage must meet or exceed it.
If the submitted dwelling value is too low (common with RC1-sourced data), the response includes:
HasEdit: trueonDwellingCoverageValue- Edit message: "The value for this home is too low, Replacement cost is $X"
ReplacementCostEstimatefield with the required minimum value
Strategy: Read ReplacementCostEstimate from the HAL state and use it as the minimum for DwellingCoverageValue.
10. Additional Details — Required Fields (Discovered)
Mortgagee Timing (Critical — Discovered 2026-03-14)
Some states (confirmed: OH) require a mortgagee to be added BEFORE AdditionalDetails. If the quote has MortgageeRequired = "Y" and no mortgagee is present, submitting AdditionalDetails triggers PropertyApologyKickout with "Required field 'Mortgagee Name' is missing." — this KILLS the session permanently.
Fix: The SubmitAdditionalDetailsDto accepts an optional mortgagee field. If provided, the mortgagee sub-workflow is called BEFORE submitting AdditionalDetails. The mortgagee can also still be added at PointOfSale for states that don't require it earlier (e.g. TX).
POST SubWorkflowState?subWorkflowName=Mortgagee (before AdditionalDetails, if state requires it)
POST NextWorkflowState?workflowNode=AdditionalDetails
The AdditionalDetails step requires 9 fields that RC1-sourced data typically doesn't include. These are nested in ProductSpecificInformation.List[0].Embedded.Questions and must be supplied by the mapper.
| Field | Valid Values | Notes |
|---|---|---|
PriorInsurer | Long dropdown list of insurers, "First-Time Home Buyer", "No Prior", "Lapse in Coverage" | "No Prior" triggers knockout — use "First-Time Home Buyer" or an actual insurer name |
PriorLiabilityLimitsType | "300K+", "300K", "300K-", "FirstTime", "Lapse", "NoPrior" | Must match PriorInsurer selection |
AnyResidentsSmoke | "N", "Y" | |
OwnADog | "N", "Y" | |
AnimalType | "X" (No), "A" (Akita), ..., "U" (Animal w/ Bite History) | Required even if OwnADog=N, set to "X" |
NumberResidentsInHouse | "1" through "9" | 9 = "9 or More" |
PackagePolicy | "P5" (Auto 250/500), "P3" (Auto 100/300), "P1" (Auto 50/100), "PL" (Auto <50/100), "FL" (Flood), "NO" (None) | Package discount selection |
WeatherReportedClaimsCount | "0" through "5" | 5 = "5 or More" |
YearsClaimFree | "3", "4", "5" | 5 = "5 or More" |
11. Household Members, Coverages, Portfolio
These three steps are pass-through when the upstream data is already populated (e.g., from RC1-sourced data) — submit the current HAL state unchanged.
Workflow Node Name Differences (Discovered 2026-03-14)
The OpenQuote flow uses DIFFERENT workflow node names than previously documented. These same names apply regardless of whether the quote was opened via OpenQuote or created via NewQuote with RC1 data populated through the clone strategy.
| Old Name | Actual Name | Notes |
|---|---|---|
HouseholdMembers | People | Uses People step |
CoveragesBillPlans | CoveragesHO | Uses CoveragesHO step |
Portfolio | Portfolio | Same name |
The step service detects the correct node name from the CurrentPageNav path.
POST NextWorkflowState?workflowNode=People (body = extracted People from CurrentWorkflowState)
POST NextWorkflowState?workflowNode=CoveragesHO (body = extracted CoveragesHO from CurrentWorkflowState)
POST NextWorkflowState?workflowNode=Portfolio (body = extracted Portfolio from CurrentWorkflowState)
These always advance without edits when the upstream steps were completed correctly.
12. Point of Sale — Multi-Phase Submission (Critical)
PointOfSale has a three-phase submission, similar to Products:
Phase 1: Add Mortgagee (Sub-Workflow)
If MortgageeRequired = "Y" (TX requires this), add a mortgagee BEFORE submitting:
POST /Slot301/api/v1/composite/SubWorkflowState?subWorkflowName=Mortgagee
Body:
{
"MortgageeName": "JPMORGAN CHASE BANK NA",
"MortgageeLoanNumber": "999888777",
"MortgageeAddress": "PO Box 4465",
"MortgageeCity": "Springfield",
"MortgageeState": "OH",
"MortgageeZip": "45501"
}
Phase 2: Order CLUE/MVR Reports
Before submitting NextWorkflowState?workflowNode=PointOfSale, you MUST call:
PUT /Slot301/api/v1/composite/OrderPos?validate=OrderPos&expand=[{"descriptor":"view-model","expand":[]}]
Important: This is a PUT, not POST. A POST returns 405 Method Not Allowed.
Body: the full PointOfSale form data (same as what goes to NextWorkflowState). This triggers the CLUE (property) and MVR (driver) report ordering. After this call, the response will have:
ClueOrdered: true/PropertyClueOrdered: trueClueReferenceNumber: "..."ClueOrderDate: "..."InsurViewStatus: "..."
Phase 3: Submit
After reports are ordered:
POST /Slot301/api/v1/composite/NextWorkflowState?workflowNode=PointOfSale
Required PointOfSale Fields
| Field | Location | Notes |
|---|---|---|
SocialSecurityNumber | PointOfSale.Drivers.List[0] | SSN for CLUE ordering. NOT in Persons (no Persons list for HO quotes). |
AutoPolicy | ProductSpecificInformation.List[0].POSPropertyBuyViewModel | Progressive Auto policy # for package discount. Required if PackagePolicy != "NO" |
PropertyPolicy | ProductSpecificInformation.List[0].POSPropertyBuyViewModel | Progressive Property policy # (may be empty) |
AtvOnPremise | ProductSpecificInformation.List[0].POSPropertyBuyViewModel | "1" = Yes, "2" = No |
MortgageeCount | PointOfSale | Set by Mortgagee sub-workflow |
MVR vs CLUE Report Ordering
- MVR reports are ordered automatically during
NextWorkflowState?workflowNode=PointOfSalesubmission.DriverMvrOrderedbecomestrueafter submit. - CLUE reports require the separate
OrderPosPUT call. TheNextWorkflowStatesubmission alone does NOT order CLUE.ClueOrderedandPropertyClueOrderedremainfalsewithout OrderPos.
13. Sub-Workflow Endpoints (New)
| Method | Endpoint | Purpose |
|---|---|---|
| POST | SubWorkflowState?subWorkflowName=Mortgagee | Add/edit mortgagee |
| POST | SubWorkflowState?subWorkflowName=PropertyAdditionalInterest | Add additional interest |
| POST | SubWorkflowState?subWorkflowName=PropertyAdditionalInsured | Add additional insured |
| POST | SubWorkflowState?subWorkflowName=QuoteComment | Add quote comments |
| PUT | OrderPos?validate=OrderPos | Order CLUE/MVR reports (required before PointOfSale advance) |
14. OrderPos — Reverse-Engineered from SPA (2026-03-14)
How the SPA calls OrderPos (from Angular bundle analysis)
The SPA does NOT call OrderPos as a standard REST PUT. It uses a proprietary refreshRelevancy mechanism:
// From main-KP3UWFOP.js (minified Angular bundle)
orderPos(e) {
this.httpBackend.refreshRelevancy(this.orderPosLink, "POS").then(...)
}
// refreshRelevancy builds the request as:
refreshRelevancy(linkUri) {
return this.put()
.jsonContentType() // Content-Type: application/progressive.quoting+json
.embedCurrentViewModel() // Body = trimViewModel(currentState)
.expandViewModel() // expand query param
.relativeUrl(linkUri) // URI from HAL Link
}
Required Headers — ALL Step Submissions (SPA Capture 2026-03-14)
The SPA uses identical headers for ALL workflow steps, not just OrderPos:
| Header | Regular Steps | OrderPos |
|---|---|---|
Content-Type | application/json | application/progressive.quoting+json |
actionName | submit | refreshRelevancy |
SessionIndex | From URL param (e.g. 0, 3) | Same |
X-Request-ID | Random UUID | Same |
Accept | application/hal+json | Same |
CRITICAL: Content-Type determines response key casing:
application/json→ PascalCase responses (CurrentPageNav,Embedded,Links)application/progressive.quoting+json→ camelCase responses (currentPageNav,embedded,links)
Our API uses application/json for regular steps (PascalCase HAL) and application/progressive.quoting+json only for OrderPos (which returns camelCase — handled separately).
SessionIndex must be extracted from the SPA URL (/Slot301/Quote/Index/?SessionIndex=X&SyncId=Y). Hardcoding 0 works for single-session scenarios but fails when Progressive assigns a different index.
Body Format
The SPA's trimViewModel strips: Embedded, Extenders, Links, ValidationDetails, Messages — same as our stripHalMetadata. The body is the stripped PointOfSale data, NOT the raw HAL response.
OrderPos Link Location
The OrderPos URI is in a nested HAL Link inside Embedded.PointOfSale.Links (NOT in the top-level Links array):
Embedded.PointOfSale.Links → { Name: "OrderPos", Uri: "/Slot301/api/v1/composite/OrderPos?validate=OrderPos" }
Current Status: ✅ OrderPos 200 (CLUE Ordered — 2026-03-14)
OrderPos now returns 200 and orders CLUE reports. The fix was two-fold:
- Adding
actionName: submitand dynamicSessionIndexto regular step submission headers - Calling OrderPos post-submit (after NextWorkflowState for PointOfSale), not pre-submit
The working flow:
1. Add mortgagee (SubWorkflowState) — sets mortgagee in server-side context
2. Submit PointOfSale (NextWorkflowState — may return 400 with SSN/mortgagee edits)
3. Re-submit PointOfSale with SSN + mortgagee resolved (advances to AcknowledgeMVR or stays at POS)
4. OrderPos post-submit (PUT — fresh GET of PointOfSale state, then PUT OrderPos) → 200 (10s)
5. Re-submit PointOfSale → advances to FinalSaleHO
E2E test (Q83806608, AZ — via MuleSoft agent): NamedInsured → Products → People → AdditionalDetails → Coverages → Portfolio → PointOfSale (SSN + OrderPos 200) → FinalSaleHO reached. (This test used MuleSoft's agent credentials to open an RC1 quote; see Section 7 for agent visibility constraints.)
FinalSale (Step 8) — Terminal Step (Discovered 2026-03-14)
FinalSale has NO next link — it is the terminal workflow step. The HAL links at FinalSale:
| Name | URI | Purpose |
|---|---|---|
FinalSaleHO | FinalSalePropertyEditViewModel?tabId=HO | GET the quote summary / bind form |
FinalSalePropertySellQuote | FinalSalePropertySellQuoteWorkflowState | PUT to bind/issue the policy |
SellQuote | SellQuoteWorkflowState | Alternative sell link (auto quotes) |
goTo | GoToWorkflowState?workflowNode=FinalSaleHO | Jump to FinalSaleHO |
current | CurrentWorkflowState?workflowNode=FinalSaleHO | Get current FinalSale state |
back | PreviousWorkflowState?workflowNode=FinalSaleHO | Go back to PointOfSale |
The workflow node is FinalSaleHO (not FinalSale). NextWorkflowState?workflowNode=FinalSale returns 500.
FinalSale Bind Flow — Reverse-Engineered from SPA (2026-03-14)
The SPA's "Issue Policy" button calls sellQuote(), which uses refreshRelevancy (same mechanism as OrderPos):
// From main-KP3UWFOP.js (minified Angular bundle)
sellQuote() {
if (!this.shouldDisableSellButton) {
this.shouldDisplaySellPropertyButton
? this.httpBackend.refreshRelevancy(this.finalSalePropertySellQuoteLink).then(n => {
let demoLink = n.resource.Links.find(d => d.Name === "DemoModeAsiSale");
demoLink != null && this.httpBackend.refreshRelevancy(demoLink);
})
: this.httpBackend.refreshRelevancy(this.sellLink);
}
}
Bind = PUT FinalSalePropertySellQuote link with refreshRelevancy headers:
| Header | Value |
|---|---|
Content-Type | application/progressive.quoting+json |
actionName | refreshRelevancy |
SessionIndex | From URL param |
X-Request-ID | Random UUID |
Accept | application/hal+json |
Body: stripped current FinalSaleHO view model (same stripping as OrderPos — remove Embedded, Links, ValidationDetails, Messages).
QA Environment: DemoModeAsiSale (Auto-Bind)
In QA environments, the FinalSalePropertySellQuote response includes a DemoModeAsiSale HAL link. Calling refreshRelevancy on this link auto-completes the sale without payment processing. This is the QA automation equivalent of the payment flow.
Bind Success Indicators
After successful bind:
Extenders.SoldPropertyPaymentSuccess="True"QuotedProducts[].PolicyNumberpopulated with the issued policy number- Button text changes to "View {HO} Policy"
AsiPostSaleLinkbecomes available for SSO redirect to ASI post-sale page
Payment Retry Flow
If the initial bind fails (payment issue), the SPA shows a PropertyPaymentRetry modal:
continue()callsputViewModelTo(finalSalePropertySellQuoteLink.Uri)(standard PUT, not refreshRelevancy)- Button text: "Submit Payment"
- On success: transitions to sold state
15. AdditionalDetails — Required Defaults (Discovered 2026-03-14)
The AdditionalDetails step has additional Question-only fields beyond the 9 documented in section 10. These must have non-empty values or the step returns 201 but does NOT advance (no HasEdit, no errors — just stays at AdditionalDetails).
| Field | Default | Notes |
|---|---|---|
OccupancyType | "Primary" | Empty in AZ quotes; TX pre-fills "Primary" |
IntendEsign | "N" | E-sign intent flag |
OpenFoundation | "N" | Foundation type flag |
HomeUpdate | "N" | Home improvement flag; triggers re-render |
SecuredSubdivision | "N" | Gated community |
NonWeatherReportedClaimsCount | "0" | Separate from WeatherReportedClaimsCount |
WoodburningStoveFlag | "N" | Woodburning stove present |
AccreditedBuilder | "N" | Accredited builder |
Root cause: The mapper must inject Question answers into ProductSpecificInformation.List[0] after stripping HAL metadata. Without this, the Question fields are lost during stripping and Progressive's server doesn't receive the values.
Fix: injectMissingQuestionFields now also handles ProductSpecificInformation (not just Properties and Drivers). Empty defaults are set via fillEmptyNestedField.
16. People Step — All Drivers Need Required Fields (Discovered 2026-03-14)
Quotes with RC1-sourced data can have additional household members (drivers) with empty Gender, MaritalStatus, and Relationship. The PNI (driver 0) is typically pre-filled, but additional drivers (index 1+) may have empty Question answers.
Fix: The mapper now iterates ALL drivers and sets defaults:
- PNI:
Genderfrom existing,MaritalStatusfrom existing or"S",Relationship: "I" - Non-PNI:
Genderfrom existing or"M",MaritalStatusfrom existing or"S",Relationshipfrom existing or"O"
17. E2E Timing (Observed 2026-03-14 AZ Quote Q83788438 — via MuleSoft Agent)
| Step | Time | Notes |
|---|---|---|
| Auth (OAuth2 login) | ~3-5s | Cached session reuse: ~0s |
| Session creation (OpenQuote redirect chain + AppInit) | ~10-15s | IaqQuoting redirect is 5-7s alone |
| AcknowledgeMVR modal dismiss | ~0.3s | Auto-detected and dismissed |
| NamedInsured (GET + POST) | ~2-3s | |
| ProductsHO (eligibility + submit) | ~10-45s | Dwelling coverage auto-adjusted, varies by property |
| People submit | ~1-2s | |
| AdditionalDetails submit | ~10-27s | Longest step; includes mortgagee sub-workflow if needed |
| CoveragesHO submit | ~1-2s | |
| Portfolio submit | ~1s | |
| PointOfSale (mortgagee + submit + OrderPos attempt) | ~3-5s | SSN must be set; OrderPos still returns 400 (mortgagee state persistence) |
| Total end-to-end (auth through PointOfSale) | ~40-90s | Fresh login adds ~5s |
18. API Endpoint Reference
Base: POST /api/v1/progressive/home/
| Endpoint | Description | 200 | 400 | 500 |
|---|---|---|---|---|
new-quote | Create new quote session | {syncId, currentPage, quoteData} | Missing state/productCode | Auth failure, portal unreachable |
open-quote | Open existing quote by Q-number (requires matching agent credentials — see Section 7 agent visibility constraint) | {syncId, currentPage, quoteData, links} | Missing quoteNumber/state/productCode | Quote not found, PropertyApologyKickout |
get-step | Read step data without submitting | {syncId, currentPage, embedded, links, validationDetails} | Invalid workflowNode | Session expired, quote corrupted |
go-to-step | Jump to a completed step | {syncId, currentPage, quoteData} | Step not yet completed | Session expired |
view-model | Fetch edit view model (FinalSale, etc.) | {syncId, currentPage, quoteData} | Invalid viewModelName | Session error |
named-insured | Step 1: Applicant info | {syncId, currentPage, quoteData} | DisclosureProvided=N, invalid email domain | Session error |
products | Step 2: Property details | {syncId, currentPage, quoteData} | Eligibility failed, DwellingCoverage below replacement cost, invalid field codes (TypeOfConstruction, ExteriorWalls must use Progressive codes) | PropertyApologyKickout (NullReferenceException on bad property data) |
household-members | Step 3: Additional members | {syncId, currentPage, quoteData} | Married PNI without spouse listed, missing Gender/MaritalStatus | Session error |
additional-details | Step 4: Insurance history | {syncId, currentPage, quoteData} | Missing PriorInsurer, mortgagee required (TX/AZ/OH) | PropertyApologyKickout (missing mortgagee — session permanently killed) |
coverages | Step 5: Coverages + billing | {syncId, currentPage, quoteData} | Invalid coverage values | Session error |
portfolio | Step 6: Portfolio verification | {syncId, currentPage, quoteData} | Verification failed | Session error |
point-of-sale | Step 7: SSN + reports + mortgagee | {syncId, currentPage, quoteData, reportData?} | Missing SSN, PropertyPolicy required, CLUE ordering failed | PropertyApologyKickout |
final-sale | Step 8: Bind policy, return documents | {syncId, currentPage, quoteData, bindResult} | E-sign not completed, reports not ordered, payment failed | Bind failed, Progressive API error |
Standard Step Response (200)
{
"success": true,
"data": {
"syncId": "8fed2734-b02b-438c-b0e5-30a79d2a32f7",
"currentPage": "Workflow/.../ProductsHO",
"quoteData": { "ProductsHO": { "...form fields..." } },
"validationDetails": {},
"messages": {},
"links": [{ "Name": "next", "Uri": "/Slot301/api/v1/composite/NextWorkflowState?workflowNode=ProductsHO" }]
}
}
Point of Sale Response with Report Data (200)
The point-of-sale endpoint returns reportData when runReports: true. The OrderPos call is synchronous (~10s) — Progressive processes the CLUE report inline, no polling needed.
{
"success": true,
"data": {
"syncId": "43e06a71-a1c9-4b64-a59c-87d92f9f0e2a",
"currentPage": "Workflow/.../PointOfSale",
"quoteData": { "PointOfSale": { "...SSN, mortgagee, drivers..." } },
"validationDetails": {},
"messages": {},
"links": [],
"reportData": {
"propertyClueOrdered": true,
"clueOrdered": false,
"driverMvrOrdered": true,
"eligibleDriversMvrOrdered": true,
"insurViewStatus": null,
"clueReferenceNumber": null,
"clueOrderDate": null,
"mvrOrderDate": null,
"shouldDisplayPropertyClueClaimsTable": "N",
"orderPosElapsedMs": 10398,
"rawOrderPosResponse": {
"pointOfSale": {
"propertyClueOrdered": true,
"propertyClueSelectIndicator": "Y",
"clueNotNeeded": false,
"clueOrdered": false,
"driverMvrOrdered": true,
"eligibleDriversMvrOrdered": true,
"insurViewStatus": null,
"insurViewReferenceNumber": null,
"mvrReorder": false,
"mvrNotNeeded": false,
"shouldDisplayPropertyClueClaimsTable": "N",
"claimDatesAffectRate": "Y",
"drivers": {
"list": [{
"driverMvrOrdered": true,
"mvrOrderDate": null,
"mvrStatMocked": "N",
"mvrDrvrFrst": null,
"mvrDrvrLnam": null,
"licenseStatus": null
}]
}
}
}
}
}
}
Key fields for Fastlane Portal:
reportData.propertyClueOrdered—true= Property CLUE report ordered (HO quotes)reportData.clueOrdered—true= Auto CLUE ordered (auto quotes only,falsefor HO)reportData.driverMvrOrdered—true= MVR report ordered for PNI driverreportData.shouldDisplayPropertyClueClaimsTable—"Y"= property has CLUE claims (show claims table)reportData.orderPosElapsedMs— time taken (typically 8-12s for bureau processing)reportData.rawOrderPosResponse— full Progressive response (camelCase keys from OrderPos)
FinalSale Bind Response (200 — policy issued)
The final-sale endpoint returns bindResult after calling FinalSalePropertySellQuote. In QA, the DemoModeAsiSale link auto-completes the sale.
{
"success": true,
"data": {
"syncId": "8fed2734-b02b-438c-b0e5-30a79d2a32f7",
"currentPage": "Workflow/.../FinalSaleHO",
"quoteData": {
"FinalSaleHO": {
"PniFullName": "john doe",
"PolicyEffectiveDate": "03/28/2026",
"EsignAuthorization": "Y",
"EsignDeliveryType": "Email"
}
},
"validationDetails": {},
"messages": {},
"links": [],
"bindResult": {
"sold": true,
"policyNumber": "P123456789",
"premiumTotal": 1645.78,
"effectiveDate": "03/28/2026",
"paymentStatus": null
}
}
}
Key bindResult fields:
sold—truewhenExtenders.SoldPropertyPaymentSuccess= "True"policyNumber— Progressive policy number (fromQuotedProducts[].PolicyNumber)premiumTotal— Total premium fromSelectedBillPlan.TotalAmount.AmounteffectiveDate— Policy effective datepaymentStatus— Payment processing status (null in QA demo mode)
FinalSale Bind Failure (200 — validation edits, not sold)
{
"success": true,
"data": {
"currentPage": "Workflow/.../FinalSaleHO",
"quoteData": { "FinalSaleHO": { "...HasEdit fields..." } },
"validationDetails": {},
"bindResult": {
"sold": false,
"policyNumber": null,
"premiumTotal": 1645.78,
"effectiveDate": "03/28/2026",
"paymentStatus": null
}
}
}
FinalSale Premium Summary (from get-step at FinalSale)
{
"success": true,
"data": {
"currentPage": "Workflow/.../FinalSaleHO",
"quoteData": {
"FinalSaleHO": {
"PniFullName": "john doe",
"PolicyEffectiveDate": "03/28/2026",
"PrimaryEmailAddress": "john.doe@email.com",
"EsignAuthorization": "N",
"SelectedBillPlan": {
"TotalAmount": { "Amount": 1645.78 },
"PaymentOptionDescription": "Mortgage Billed",
"NumberOfPayments": "1",
"PaymentSchedule": [
{ "DueDate": "Today", "Amount": { "Amount": 1645.78 } }
]
}
}
}
}
}
400 — Validation Error (step did not advance)
When Progressive returns 400 with ActiveViewModelHasEdits: true, the step stays on the same page. Errors are in two places:
1. Top-level validationDetails (rare — most edits are embedded):
{
"success": true,
"data": {
"currentPage": "Workflow/.../NamedInsured",
"quoteData": { "NamedInsured": { "...fields..." } },
"validationDetails": { "PrimaryEmailAddress": "Not a valid email address." },
"messages": {}
}
}
2. Embedded edits (common — in Embedded.{Step}.*.Questions.List[n].HasEdit):
{
"success": true,
"data": {
"currentPage": "Workflow/.../ProductsHO",
"quoteData": {
"ProductsHO": {
"Properties": {
"List": [{
"Details": {
"...nested Questions with HasEdit: true...": {
"Property": "TypeOfConstruction",
"HasEdit": true,
"Edits": [{ "Description": "Entered value is not in the valid value list." }]
}
}
}]
}
}
},
"validationDetails": {}
}
}
Common 400 errors by step:
| Step | Field | Error | Fix |
|---|---|---|---|
| NamedInsured | PrimaryEmailAddress | "Not a valid email address." | Use real domain (@gmail.com, not @email.com) |
| ProductsHO | TypeOfConstruction | "Entered value is not in the valid value list." | Use Progressive codes: F, MY, SNC, V |
| ProductsHO | ExteriorWalls | "Entered value is not in the valid value list." | Use: Stucco, Brick, Vinyl, Wood, etc. |
| ProductsHO | DwellingCoverageValue | "The value for this home is too low" | Read ReplacementCostEstimate and use it as minimum |
| People | MaritalStatus | "Married primary named insured requires a spouse" | Add spouse or change MaritalStatus to "S" |
| PointOfSale | PropertyPolicy | "Please fill out this field to continue." | Set to "NONE" even when PackagePolicy=NO |
500 — Fatal Error (session killed)
PropertyApologyKickout — the session is permanently destroyed. No recovery possible.
{
"success": false,
"error": "Step AdditionalDetails failed. Please try again."
}
Server-side response:
{
"Links": [],
"Extenders": {
"PropertyApologyKickout": "True",
"PropertyApologyUrl": "/Slot301/Apology/ApologyPropertyUnexpectedError?SessionIndex=0&edits=Required%20field%20%27Mortgagee%20Name%27%20is%20missing."
},
"Embedded": {},
"ValidationDetails": {},
"Messages": {}
}
Common 500 triggers:
| Cause | Step | Prevention |
|---|---|---|
| Missing mortgagee | AdditionalDetails | Always add mortgagee via sub-workflow BEFORE this step |
| Bad property data (from RC1 or clone) | Products (eligibility) | Server returns NullReferenceException — skip this quote |
| Expired/corrupted session | Any step | Re-open with a fresh session |
NextWorkflowState?workflowNode=FinalSale | FinalSale | Use FinalSaleHO (not FinalSale) |
| OrderPos while at AcknowledgeMVR modal | PointOfSale | Dismiss modal first, then call OrderPos |
19. Config-Driven Frontend Architecture (Discovered 2026-04-02)
Progressive's quoting portal is fully config-driven. The Angular SPA does not hardcode any form fields — every field's type, validation, visibility, options, and labels are delivered by the REST API in each step's HAL response. This means the portal can be white-labeled by consuming Progressive's API directly and rendering our own UI from their field configuration.
Discovery Method
Authenticated against QA (62.qa.foragentsonly.com) via curl, created a quote session through the QuotingGateway redirect chain, then called CurrentWorkflowState and NextWorkflowState to capture raw HAL responses.
Question Schema
Every form field in every step is represented as a Question object inside Embedded.{StepName}.Embedded.Questions.List[]. Each Question contains:
| Property | Type | Purpose |
|---|---|---|
Property | string | Field name/key (e.g. FirstName, TypeOfConstruction) |
FieldType | string | Data type: MVP_TEXT, MVP_INTEGER, date |
ControlType | string | UI widget hint (empty in practice — SPA uses FieldType) |
Label | string | Display label |
ShortLabel | string | Compact label variant |
SubLabel | string | Secondary label text |
Placeholder | string | Input placeholder |
LabelType | string | Label display mode |
AdditionalHeaderText | string | Section header text |
Required | boolean | Whether field is mandatory |
Disabled | boolean | Whether field is read-only |
ShouldDisplay | boolean | Field visibility |
ShouldDisplayControl | boolean | UI control visibility |
ShouldDisableControl | boolean | UI control enabled state |
ValidValues | array | Dropdown options: [{ Value, Display }] |
MaxLength | integer | Character limit |
Maskable | boolean | Whether to mask input (e.g. SSN) |
IsValueAutoTrimmed | boolean | Auto-trim whitespace |
LastAnswer | string | Current/default value |
HasEdit | boolean | Has validation error |
Edits | array | Validation error messages: [{ Description }] |
ForceEditToBeDisplayed | boolean | Force show validation error |
ForceRefreshRelevancy | boolean | Trigger re-render on value change |
ValidationId | string | Validation rule ID (e.g. alphaNumSpcl1b, numeric, alphaSpcl1) |
AdditionalCharacterValidation | string | Extra character validation |
ClientValidationIdOverride | string | Client-side validation override |
HoverHelpId | string | Tooltip/help content ID |
HoverHelpDisplayName | string | Tooltip display name |
ValuesToTranslate | array | i18n translation mappings |
ShouldTranslateLastAnswer | boolean | Whether to translate the answer |
ProgressBar Configuration
The ProgressBar is also config-driven. Each step is an item with navigation state:
{
"Title": "NAMED INSURED",
"Id": "NamedInsured",
"Enabled": true,
"Complete": false,
"HasEdits": false,
"Disabled": false,
"Visited": true
}
The 8-step flow: NamedInsured → Products → HouseholdMembers → AdditionalDetails → Coverages → Portfolio → PointOfSale → FinalSale.
Step Response Sizes
| Step | Response Size | Question Fields | Fields with ValidValues |
|---|---|---|---|
| NamedInsured | ~164 KB | 29 | 12 (Gender, State, Suffix, etc.) |
| ProductsHO | ~213 KB | 64 | 45 (construction, roof, exterior, etc.) |
NamedInsured Field Configuration (29 fields)
All fields delivered by CurrentWorkflowState?workflowNode=NamedInsured in Embedded.NamedInsured.Embedded.Questions.List:
| Property | FieldType | Required | ValidValues | ValidationId |
|---|---|---|---|---|
FirstName | MVP_TEXT | Yes | — | alphaNumSpcl1b |
LastName | MVP_TEXT | Yes | — | alphaSpcl1 |
MiddleInitial | MVP_TEXT | No | — | alpha |
Suffix | MVP_TEXT | No | 6 options (JR, SR, I–IV) | — |
DateOfBirth | date | Yes | — | — |
Gender | MVP_TEXT | Yes | F, M | — |
MailingAddress | MVP_TEXT | Yes | — | alphaNumSpcl3 |
City | MVP_TEXT | Yes | — | alphaSpcl6 |
State | MVP_TEXT | Yes | 57 options (all US states + territories) | — |
ZipCode | MVP_INTEGER | Yes | — | numeric |
RecentlyMoved | MVP_TEXT | Yes | Y, N | — |
PriorAddress | MVP_TEXT | Yes | — | alphaNumSpcl3 |
PriorState | MVP_TEXT | Yes | 51 options | — |
AgentCode | MVP_TEXT | Yes | 6 options (agent's available codes) | — |
AsiAgentCode | MVP_TEXT | Yes | 3 options (440744, 446447, 453719) | — |
DisclosureProvided | MVP_TEXT | Yes | Y, N | — |
HasInternationalAddress | MVP_TEXT | No | Y, N | — |
InternationalCountry | MVP_TEXT | Yes | 246 options (ISO country codes) | — |
PrimaryEmailAddress | MVP_TEXT | No | — | alphaNumSpcl4 |
Hidden fields (ShouldDisplay: false) include PriorAddress, PriorCity, PriorState, PriorZipCode (shown only when RecentlyMoved=Y) and all International* fields (shown only when HasInternationalAddress=Y).
ProductsHO Field Configuration (64 fields across 5 sections)
The ProductsHO step nests Questions into sub-objects matching the property structure:
Address Section (Properties.List[0].Address.Embedded.Questions) — 30 fields:
Eligibility flags (all MVP_TEXT, Required, ValidValues Y/N or 1/2):
HomeClosingRiskFlag, HomeForeclosureFlag, ResidenceType (11 options), NewConstructionFlag, ExcludeWindFlag (hidden), PriorLiabilityFlag, SinkholeFlag, TrampolineFlag, UndergroundTankFlag, IneligibleDogFlag, RoofOverlayFlag, ConstructionFlag, UnrepairedFlag, BusinessAtLocationFlag, PoolFenceFlag, ResidentEmployeeFlag, HomeDaycareFlag, TwoPlusClaimsFlag, CanceledNonrenewedDeclinedFlag, IneligibleElectricalSystemFlag, IneligiblePlumbingSystemFlag, PropertyEndorseAsLocationAddressFlag, VerifyNoInelCond, VerifyNoUwCond.
Two of these merit a clarifying note because their FAO presentation differs from how Fastlane surfaces (or hides) them:
HomeClosingRiskFlagis the FAO "Is this home a new purchase?" Y/N radio. It is captured upstream in DA/GaQ (CRNdisclosures.homeClosing) and prefilled into HAL byCrnToProductsHoMapper.applyDisclosures; Fastlane does NOT re-ask it on the Property Details step. The value round-trips through HALLastAnsweron every/step-config/submit, so itsY/Nanswer remains authoritative for downstream gating (e.g.PurchasePrice.ShouldDisplay).NewConstructionFlagis the FAO "New Construction" inline-checkbox attribute (a brand-new build that has not yet been finished/occupied). HAL reportsRequired: falsebecause on FAO the unchecked checkbox impliesN. Fastlane renders it as a Yes/No tile viaControlledHalRadioCardand adds it toFORCE_REQUIRED_FIELDSso the user must make an explicit selection — Yes/No tiles have no implicit "no answer = N" the way the FAO checkbox does. The CRN mapper does NOT prefill this field; the user's Fastlane choice is the sole source.
RCE Section (Properties.List[0].Rce.Embedded.Questions) — 3 fields:
| Field | ValidValues |
|---|---|
YearBuilt | (integer, no options) |
NumberOfStories | 7 options: 1, 1H, 2, 2H, 3, BILEVEL, TRILEVEL |
LivingArea | (integer, no options) |
Details Section (Properties.List[0].Details.Embedded.Questions) — 23 fields:
| Field | ValidValues |
|---|---|
TypeOfConstruction | 4: F, MY, SNC, V |
ExteriorWalls | 17 options |
RoofMaterial | 17 options |
RoofDesign | 3: GABLE, HIP, FLAT |
Substructure | 9 options |
GarageType | 13 options |
CoolingType | 6 options |
HeatingType | 12 options |
SwimmingPool | 4 options |
KitchenGrade | 4: 0, 1, 2, 3 |
BathroomGrade | 4: Standard, Custom, Designer, SemiCust |
DeckType | 4: None, Wood, Redwood, Composite |
PropertyAcreage | 4: 1 (0-10ac), 2 (11-20), 3 (21-30), 4 (31+) |
ProtectionClass | 10: 01–10 |
NumberOfFullBaths | 8: 1–8 |
NumberOfHalfBaths | 9: 0–8 |
Conditionally hidden: PurchasePrice (revealed when HomeClosingRiskFlag === 'Y', i.e. the GaQ/RC1 flow captured the quote as a new purchase). Hidden fields: FireHydrant1000Feet, PctBasement, PctFinishedBasement, DeckArea.
Two Details Section fields are intentionally NOT re-asked on Fastlane and are instead prefilled into HAL from CRN by CrnToProductsHoMapper.applyConstruction (same upstream-capture pattern as HomeClosingRiskFlag):
RoofMaterial— captured upstream in DA/GaQ as "Roofing Materials" and stored incrn.dwelling.roofMaterialCd. The CRN code is translated to Progressive's enum via theROOF_MATERIAL_CD_TO_PROGRESSIVEtable (e.g.CompShingle→CompositeShingle) and round-trips through HALLastAnsweron every/step-config/submit.YearRoofInstalled— captured upstream in DA/GaQ as "Year Roof Constructed/Replaced" and stored incrn.dwelling.roofingImprovementYear. The integer year is passed through unchanged.
FlowType Section (Properties.List[0].FlowType.Embedded.Questions) — 2 fields:
DwellingCoverageValue(Required, editable)ReplacementCostEstimate(Hidden, Disabled — read-only calculated value)
HATEOAS Navigation Links
Each step response includes a Links array (21 links for NamedInsured) providing the full navigation graph:
| Link Name | Descriptors | Purpose |
|---|---|---|
{StepName} | view-model, active | GET the edit view model |
next | workflow, next | POST to advance to next step |
goTo | workflow, goTo | POST to jump to a specific step |
current | workflow, current | GET current step state |
PrintLink | — | Print view |
ProgressBar | auto-expand | Step progress config |
IaqHeader | auto-expand | Customer header |
IaqQuoteContext | auto-expand | Session context |
help | get | Contextual help |
ApologyIndex | — | Generic error page |
ApologySessionExpired | — | Session timeout page |
ApologySessionAccess | — | Session recovery |
ApologyProperty | — | Property-specific error |
ScriptInfoLibrary
The AppInitWorkflowState response includes a ScriptInfoLibrary array for external script injection:
[
{ "Name": "Quantum", "EnvironmentAttributes": { "src": "https://perf-www.qa.progressive.com/js/cdn/qm/quantum-progressivenonprod.js" } },
{ "Name": "ForMotiv", "EnvironmentAttributes": { "endPoint": "https://testapi.formotiv.com/v1", "apiHost": "https://testapi.formotiv.com", "userId": "..." } }
]
White-Label Implications
Since Progressive's API delivers the complete form configuration for every step:
- Field rendering — Our portal can read
Questions.Listand render each field usingFieldType,ValidValues,Required,ShouldDisplay,MaxLength, andValidationId - Validation — Client-side validation rules are provided via
ValidationId(e.g.alphaNumSpcl1b,numeric). Server-side validation is returned asHasEdit/Editson re-submission - Conditional visibility —
ShouldDisplayandShouldDisplayControlchange per response (e.g.PriorAddressfields appear whenRecentlyMoved=Y). TheForceRefreshRelevancyflag on a field means changing its value should trigger a re-fetch for updated visibility - Dropdown options — All select/radio options come from
ValidValuesarrays — no hardcoding needed - Navigation — The
Linksarray andProgressBaritems drive step sequencing, enabling/disabling, and completion tracking - Form state —
LastAnswerprovides the current value for every field, enabling prefill from RC1-sourced data (via clone strategy) or previously submitted values
The key constraint: Progressive's server maintains the session state. All form data must be submitted via their API (NextWorkflowState) — we cannot skip steps or submit out of order. But we can fully control the UI/UX while Progressive's backend handles quoting logic, eligibility, rating, and policy issuance.
20. Remaining Work
Critical: RC1 Clone/Recreate Strategy (Blocking Production Use)
MuleSoft RC1 quotes are created under MuleSoft's agent credentials and are not visible to our automation agent (see Section 7). Before the direct HTTP API can be used in production, we must implement the clone/recreate flow:
- Extract RC1 data from MuleSoft — Pull the applicant, property, coverage, and other pre-populated fields from the MuleSoft RC1 submission (available in CRN
sf_quote_request/sf_quote_responsetables or via a MuleSoft webhook/API) - Create a new session under our agent — Call
NewQuotevia the QuotingGateway with our credentials - Map RC1 data into each Progressive step — Submit NamedInsured, ProductsHO, People, AdditionalDetails, CoveragesHO, Portfolio, and PointOfSale with the RC1-sourced data
- Handle NewQuote vs OpenQuote workflow differences —
NewQuoteproduces a different step flow (see Section 7 table). The clone implementation must account for step ordering and any modal acknowledgements that differ from theOpenQuotepath
Open questions:
- Does
NewQuoteproduce the full 8-step workflow when property data is submitted at NamedInsured/Products, or does it always truncate to the abbreviated flow? - Can
DuplicateQuoteWorkflowStatebe used to clone a quote across agents? - Should the clone happen at RC1 time (proactively) or at quote-open time (on demand)?
Step Implementation Status
Named Insured— ✅ Working.Products— ✅ Working. Field codes must use Progressive values (e.g.FnotFrame,StucconotBrickVeneer).People— ✅ Working. Married PNI requires a spouse to be listed.Additional Details— ⚠️ Partially working. Mortgagee required for ALL states (TX, AZ, OH confirmed). Non-advancing 201 issue persists for some AZ quotes — missing Question-only fields.CoveragesHO— ✅ Working.Portfolio— ✅ Working.Point of Sale— ✅ Working. OrderPos 200 (CLUE ordered), MVR ordered via NextWorkflowState, SSN accepted, mortgagee sub-workflow works.PropertyPolicyfield required even when PackagePolicy=NO. Confirmed on Q83806608 (AZ, via MuleSoft agent).Final Sale / Bind— ✅ Endpoint identified and implemented.PUT FinalSalePropertySellQuotewithrefreshRelevancyheaders binds the policy.DemoModeAsiSaleauto-completes in QA. Success indicated bySoldPropertyPaymentSuccessextender andPolicyNumberin QuotedProducts. Awaiting full E2E validation — blocked by AdditionalDetails non-advancing issue.
Known Issues
- RC1 agent visibility (BLOCKING): MuleSoft-created RC1 quotes are bound to MuleSoft's agent credentials and invisible to our automation agent. The
OpenQuoteendpoint returns "quote not found" when our agent tries to open them. See Section 7 for the full explanation and the clone/recreate strategy required to resolve this. - Quote reuse corruption: Re-opening the same quote corrupts Progressive's server-side state. Each quote should go through the flow once per session.
- Mortgagee timing: All tested states (TX, AZ, OH) require mortgagee before AdditionalDetails. If missing,
PropertyApologyKickoutkills the session permanently. - AdditionalDetails non-advancing (AZ): The step returns 201 with no errors but doesn't advance. Root cause: Question-only fields in
ProductSpecificInformationare empty after HAL stripping. TheinjectMissingQuestionFieldsfix works for known fields but AZ quotes may have additional state-specific fields. Needs further investigation. - AcknowledgeMVR modal dismiss: The modal at POS→FinalSale transition requires the full stripped view model as body (not
{}). The SPA useshttpBackend.forward()which sendssubmitViewModelTo(nextLink.Uri)with the current view model. Our dismiss now sends the stripped modal state. - SSN format: Test SSNs
123-45-6789and078-05-1120rejected. Use987-65-4320(masked asXXXXX4320). - Email validation: Progressive rejects
@email.comdomain. Use real domains like@gmail.com. - ASI Agent Code: MuleSoft RC1 quotes may use ASI code
377(different from .env's440744). When usingOpenQuoteunder the matching agent, the gateway accepts either code for agent43786. With the clone strategy, our agent's ASI codes must be used instead. - FinalSale workflow node: Use
FinalSaleHO(notFinalSale). The step service and mapper now resolve the correct embedded key dynamically. - PointOfSale multi-submit pattern: The first submit may return 400 (SSN/mortgagee edits). Required: submit → dismiss AcknowledgeMVR modal (if present) → OrderPos → re-submit.
- PropertyPolicy field: Required at PointOfSale even when
PackagePolicy=NO. Set to"NONE"or any non-empty value. - Products field codes:
TypeOfConstructionmust beF/MY/SNC/V(notFrame/Masonry).ExteriorWallsmust match Progressive valid values exactly (e.g.Stucco,Brick,Vinyl— notBrickVeneer). Check valid values from the HAL response'sValidValuesarrays. - OrderPos timing: Must be called AFTER POS submit advances (and AcknowledgeMVR modal is dismissed). Calling OrderPos while at the modal returns 500.
- SPA header requirements (FIXED 2026-03-14): All step submissions require
actionName: submit, dynamicSessionIndex, andX-Request-IDheaders.