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. Opening Existing Quotes (RC1 / MuleSoft)
CRITICAL: All testing MUST use RC1 quotes created by MuleSoft. Do NOT create new quotes via the NewQuote flow — the NewQuote workflow is fundamentally different (skips Products through Portfolio, jumps to PointOfSale with a modal acknowledgement). Only the RC1 OpenQuote path produces the full 8-step workflow needed for end-to-end testing.
Why RC1 Only
MuleSoft RC1 creates the quote in Progressive's system with pre-populated data (applicant, property, coverages). The Progressive Q-number (e.g., Q83806794) is generated during this process. Without RC1, Progressive has no property data to rate against.
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
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: Log in to the Progressive portal at 62.qa.foragentsonly.com → Quote Search → filter by Home (HO3), state, Unsold.
Opening an RC1 Quote
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 |
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)
| 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 RC1 dwelling value is too low, 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 RC1 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 data typically doesn't provide. These are nested in ProductSpecificInformation.List[0].Embedded.Questions.
| 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 for RC1 quotes — submit the current HAL state unchanged.
Workflow Node Name Differences (Discovered 2026-03-14)
The RC1 OpenQuote flow uses DIFFERENT workflow node names than previously documented:
| Old Name | Actual RC1 Name | Notes |
|---|---|---|
HouseholdMembers | People | RC1 flow uses People step |
CoveragesBillPlans | CoveragesHO | RC1 flow 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): NamedInsured → Products → People → AdditionalDetails → Coverages → Portfolio → PointOfSale (SSN + OrderPos 200) → FinalSaleHO reached.
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)
RC1 quotes 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)
| 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 RC1 quote by Q-number | {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 RC1 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 RC1 property data | 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. Remaining Work
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).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
- Quote reuse corruption: Re-opening the same RC1 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: RC1 quotes may use ASI code
377(different from .env's440744). The OpenQuote gateway accepts either code for agent43786. - 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.