The Universal Pole Model (UPM) is the JSON document that flows between every overhead service on the platform. Parsers (SPIDA, O-Calc, PLS, field capture) produce it. Analyzers (clearance, sag, loading, classification, joint-use) consume it. Exporters write it back out into the source format.
If you’re integrating with more than one API, you’ll almost always be moving UPM documents between them — so it’s worth understanding the shape once instead of per-endpoint.
The Universal Vault Model is the underground equivalent. It follows the same conventions (top-level keys, measured-value envelope, additive metadata bucket) and is documented alongside the underground gateway.
Where it shows up
Source file ──► parser API ──► UPM ──► analyzer API ──► UPM + findings ──► exporter API ──► source file
.spida spida-api clearance-api spida-api .spida
.pplx ocalc-api loading-api ocalc-api .pplx
.pol pls-api sag-api pls-api .pol
drone / field-capture-api classification-api esri-api feature layer
ESRI joint-use-api
Every analyzer accepts a UPM document on the request body and returns the same document, optionally enriched with findings, proposals, or computed fields. The gateways do this in a loop for you; if you’re calling services directly, you carry the document across calls.
Top-level keys
A UPM document is a flat JSON object with these top-level keys. Only identity is required — everything else is optional, and analyzers degrade gracefully when fields they care about are missing.
| Key | Purpose |
|---|---|
identity | Stable IDs for the pole and a pointer back to the source file. Required. |
geometry | Pole height, class, species, set depth, lat/lon, plus structure type (single_pole / h_frame / monopole_transmission) and steel geometry for transmission structures. |
crossarms | Crossarms with their insulators and the spans (wires) attached to each insulator. |
pole_mounted_insulators | Insulators attached directly to the pole rather than to a crossarm. |
guys | Down guys to a ground anchor. |
span_guys | Guys that run to a neighboring pole instead of to ground. |
anchors | Ground anchor locations and capacities. |
attachments | Flat envelope of every attachment point — useful when you want one list to iterate. |
equipment | Transformers, regulators, cutouts, riser bracket assemblies, capacitors, switches. |
framing | Detected framing configuration per circuit (horizontal / vertical / triangular). |
clearance | Cached clearance findings, if a clearance pass has already run. |
load_case | Code (GO-95 / NESC / CSA), grade, district, design temperatures, ice/wind state. |
environment | Ambient design conditions for thermal calcs (IEEE 738, C57.91). |
grounding | Grounding rod configuration (IEEE 142). |
vegetation | Tree clearance state. |
avian | Avian protection zone membership and mitigation status. |
service_networks | Secondary / service-drop networks attached to the pole. |
metadata | Free-form extension bucket. Round-trips through every service untouched. |
What each analyzer actually reads
You don’t have to populate every field for every call. The minimum that each service needs:
| Service | Required UPM fields |
|---|---|
| Clearance | identity, geometry.pole_class, crossarms[].insulators[], attachments[], load_case (or it will be inferred). |
| Loading | identity, geometry, plus either load_case or a lat/lon in geometry so it can be derived. |
| Sag | identity, crossarms[].insulators[].spans[], load_case. |
| Classification (framing) | identity, geometry.pole_class, crossarms[], pole_mounted_insulators[]. |
| Guying validation | identity, geometry, crossarms[], guys[], anchors[], load_case. |
| Joint-use | identity, geometry, attachments[] with owner tags. |
Missing data turns into a “could not evaluate” finding rather than an error — the service tells you exactly which field it needed.
Measured-value convention
Every dimensional field on a UPM document is wrapped in a MeasuredValue envelope:
{
"value_si": 13.716,
"unit_si": "m",
"source_value": 45.0,
"source_unit": "ft"
}
value_siis the canonical numeric value. SI units, every time. This is what calculators inside the services use.unit_siis the SI unit label (m,kg,N,m/s,degC).source_valueandsource_unitare the original number and unit as it appeared in the source file or the caller’s input.
When you read values back out of a UPM document, read value_si and convert rather than trusting any _ft or _in keys that may sit alongside for convenience. The SI value is the source of truth.
Identifiers and round-trip stability
The two ID rules that matter:
identity.pole_idis yours — pick anything stable across edits and keep using it. Analyzers key findings by this ID, so changing it mid-workflow loses prior results.- Cross-pole conductor IDs (the
wire.externalIdstyle identifier insidecrossarms[].insulators[].spans[]) are how sag and midspan-clearance match up conductors that span between poles. Preserve these IDs if you mutate a UPM document and rewrite spans.
Minimal example
The smallest UPM document an analyzer will accept:
{
"identity": {
"pole_id": "demo-pole-001",
"source_ref": { "source_system": "spida", "source_key": "loc_3" }
},
"geometry": {
"manufactured_height": {
"value_si": 13.716, "unit_si": "m",
"source_value": 45.0, "source_unit": "ft"
},
"pole_class": "3",
"pole_species": "Douglas Fir",
"location": { "lat": 38.5816, "lon": -121.4944 }
},
"load_case": {
"code": "GO-95",
"grade": "C",
"district": "LIGHT"
},
"crossarms": [
{
"crossarm_id": "xa-1",
"attachment_height": { "value_si": 11.5, "unit_si": "m" },
"direction_deg": 90.0,
"insulators": [
{
"insulator_id": "ins-1a",
"insulator_type": "pin",
"attachment_height": { "value_si": 11.5, "unit_si": "m" },
"spans": [
{
"span_id": "span-1",
"wire_external_id": "primary-phase-a",
"size": "1/0 ACSR",
"length": { "value_si": 39.6, "unit_si": "m" }
}
]
}
]
}
],
"attachments": [
{
"attachment_id": "att-1a",
"attachment_type": "insulator",
"attachment_height": { "value_si": 11.5, "unit_si": "m" },
"voltage_v": 12470.0
}
]
}
Pass this to clearance, loading, sag, or classification and you’ll get a verdict back.
A typical integration
- Parse a source file with the appropriate parser API (
/parse) to get a UPM document. - Enrich, if needed — pull in lat/lon from your GIS, pin a
load_case, tag attachments with owners for joint-use. - Analyze by POSTing the UPM to each analyzer you care about. Each call returns the same UPM plus findings.
- Apply the human-approved fix proposals on your side and update the relevant fields on the UPM document.
- Export back to the source format via the exporter API (
/generate) when you’re ready to round-trip.
The conversational gateway is this same loop with an LLM driving it.
See also
- SPIDA API → — the most mature parser/exporter
- Clearance API → — first analyzer most callers run
- Loading API → — code-aligned operating points everything else depends on
- Overhead gateway → — same loop, LLM-driven