Carrier API integrations are hard work. Anyone who's built one against a SOAP service over mutual TLS, chased an idempotency rule through three retry layers, or reverse-engineered a date format from production declines knows that. The hard parts engineers expect — auth, transport, error handling, retries, sandbox quirks — are bounded. They have finished states. A team can scope them, build them, and move on.
The translation layer underneath the API doesn't behave that way. Every carrier has a different mental model of the same business — different class-code taxonomies, different appetite logic, different decline shapes — and the translation layer between their model and yours is a permanent piece of software that keeps moving after launch.
It's the part of the integration that doesn't show up in any in-house build estimate. It's also the part that doesn't end.
Class codes carry the rest of the model
When we describe a small-business risk to a carrier — a 12-person restaurant in Pinellas County, Florida, $1.2M revenue, no liquor — the structure that varies most across carriers, drifts most after launch, and drives the most production incidents we triage is the class code.
Three reasons class codes are especially hard.
Multiple, mismatched taxonomies layered on top of each other. Workers' comp programs map onto NCCI. General liability programs map onto SIC or NAICS. Almost every carrier sits a proprietary classification on top of one or more of those, and the proprietary layer is rarely shaped like the public taxonomy underneath. Some carriers ship four-digit numeric codes. Some ship alphanumeric codes with suffix variants — 6006F and the like — that signal state-specific overrides. Some ship multi-layer hierarchies: industry → sub-industry → class description keyword → class description id. Resolving a single agent selection into the right carrier-internal code can require four lookups.
Many-to-many, not one-to-one. The cleanest version of the problem assumes one agent-facing code maps to one internal code. The actual version is many-to-many. The same agent-facing code resolves to different internal codes depending on state, occupancy type, and SIC. The same internal code maps back to multiple agent-facing codes. Carriers handle this internally with rule engines the integrator never sees — only the resulting quote, or the resulting decline.
Chains and loops. Some carriers' class-code structures don't terminate cleanly. Selecting one code pulls in secondary and reclass codes that can chain or loop back. A UWQ answer can replace the primary class with a different one, which brings its own full set of secondary codes, recursively. One selection becomes ten.
The class code is also the load-bearing input for everything downstream. It determines which underwriting questions apply. The UWQ answers determine appetite. Appetite determines whether a quote comes back at all. Drift in the class-code layer cascades through the chain. The other dimensions of the data model — legal entity types, payroll conventions, limits, decline reasons, broker codes — each carry their own variance and each eventually demand their own normalization rule. They're satellites around the class-code problem.
One carrier's "restaurant" is another carrier's "food service — limited" and a third carrier's decline
"Out of appetite" hides a lot of structure. Appetite isn't a flat list of allowed industries. It's a function of class code, state, occupancy type, revenue band, prior losses, and a per-carrier set of guardrails. The class-code mapping is the input to that function. When the mapping drifts, the function silently returns a different answer.
This is the part that doesn't show up in any in-house build estimate. The visible work is the mapping — somebody sits down with a carrier's reference data and builds the lookup table. The hidden work is everything that comes after: detecting that the carrier updated their internal classification last Thursday, regenerating the canonical mapping, replaying in-flight applications, and shipping the change before customers see a wrongly declined quote.
[!note]
Inline diagram (Mermaid, first-pass): how a single agent-facing selection resolves through state lookup, occupancy modifier, NAICS cross-check, carrier-internal code, and appetite — with the silent-decline failure mode branching off when any link drifts.

A typical integration, not an exceptional one
One carrier we integrate with — a large multi-line national writer — maintains a many-to-many mapping between the class codes their agents see and the codes their pricing engine uses internally. The visible code is one field. The internal resolution is three: a lessorsClassCode, a proxyClassCode, and a program assignment, each computed from a combination of state, occupancy type, and SIC. The same agent-facing code resolves to different internal codes depending on the fact pattern. The same internal code maps back to multiple agent-facing codes.
When we built the integration, the carrier shared their internal taxonomy as a spreadsheet. The mapping didn't quite line up with the API's actual behavior — there were resolution rules in the pricing engine that weren't in the documentation. We discovered them in production, one wrongly declined quote at a time, until our error patterns surfaced enough signal that we could reverse-engineer the missing pieces and submit a corrected mapping back to the carrier for review.
That isn't a story about a bad integration. It's a story about a typical one. Across our 60+ live integrations, every one has a version of it. Some carriers ship a single flat code list and the work is just to keep it current. Some carriers ship recursive structures where one class-code selection pulls in secondary and reclass codes that cascade further. Some carriers ship no NAICS↔internal mapping at all and expect the integrator to build it, get it reviewed, and own it forever.
The pattern is the same in every case: the integrator builds and maintains the cross-carrier translation layer that the carriers themselves don't.
The canonical model
There's a trap in-house teams fall into early: treat each carrier integration as its own self-contained system. Pass the carrier's fields through into the application as-is and let the UI render whatever the carrier sent.
It works for the first carrier. It collapses on the second.
By the third carrier, every product surface — quote display, application rehydration, comparison views, broker-facing dashboards — has carrier-specific conditional logic threaded through it. Every change to any carrier ripples into every surface. The translation layer that wasn't built upfront ends up implicitly built everywhere, in pieces, without versioning.
A canonical model is the alternative. One internal representation of an application — class code, entity type, location, payroll, prior losses, UWQ answers — that everything in the platform consumes. Carrier-specific adapters translate the canonical model into and out of each carrier's shape. The rest of the platform stops caring which carrier is on the other end.
The canonical model isn't a schema. It's a contract — between the integration team and the rest of the engineering org — that says: this is the shape of the world; if a carrier disagrees, the disagreement lives in the adapter, not in the product.
The canonical model extends past application data into underwriting question normalization, where the same translation problem repeats with even more shape variance. Part 6 of this series goes deep on UWQ.
Versioning the canonical model
A canonical model that doesn't version is a fossil. Across our live portfolio, something changes on the carrier side roughly every week — a new class code appears in a quote response that wasn't in last Friday's reference data, an underwriting question's applicability shifts, a payload field becomes required. None of it arrives with a changelog.
A versioning strategy has to handle three pressures.
Carrier-initiated changes with no deprecation window. A new class code appears in a Tuesday quote response that wasn't in Friday's reference data. The adapter has to detect, log, and decide: is this a known change to absorb, or a drift to alert on?
Internal canonical schema bumps for new carriers. Every new carrier surfaces edges the model hadn't anticipated. Alphanumeric class codes were one edge. Multi-layer industry hierarchies were another. The model has to evolve without breaking the contracts the rest of the platform depends on.
In-flight application backfills. When a common UWQ definition changes, every in-flight application that referenced the old definition has to be remapped to the new one. That migration is invisible to the carrier and invisible to the customer — and a single missed application is a silent decline.
This is the permanent data-ops function that doesn't show up in any in-house build estimate. It's not engineering work in the traditional sense — it's a discipline that sits between engineering, insurance domain expertise, and production monitoring. Most in-house teams don't build it because they don't yet know it's a category. They learn it the way we learned it: from the silent declines no one noticed until a customer escalated.
What this means for an in-house build
The build estimate for a carrier integration usually covers the API client. It rarely covers the canonical model, the per-carrier adapters, the versioning strategy, the drift detection, the backfill machinery, or the data-ops discipline to keep all of it current. Those aren't optional layers. They're the difference between an integration that works on launch day and one that still works six months later, after the carrier has quietly shipped two class-code updates and an appetite change.
We built our canonical schema because we got tired of losing these bets one regex at a time — if you're early enough in this work that you can still avoid finding out the hard way, that's the call to make now.
For the full lifecycle of a carrier integration — from first legal conversation through ongoing maintenance — see Part 1: Anatomy of a Carrier Integration.

.png)
.png)

.png)
