Veritris Axiom — Exchange API

Axiom Exchange is the connected layer of the Veritris Axiom energy platform — energy data, transactions, partners, developers, and API-first infrastructure. It sits under Veritris Energy alongside OEPC (the utility-facing operating vehicle), and is one module among Grid, Market/ETRM, Forecast, DER, Retail/CRM, Resilience, eMobility, and Orchestrate.

The API is domain-modeled after the plan/utility/usage/enrollment structure exposed by PowerHQ's public developer docs (powerhq.co, built on EnergyBot's backend), extended with:

This isn't just a design doc. Every endpoint has been run end-to-end against a live PostgreSQL/PostGIS instance, with five real bugs found and fixed along the way. See docs/TESTING.md for the full verification log.

↗ Try the Territory Explorer — an interactive map demo driven by a snapshot of real seeded output: scope to a reseller, drag a box over any area, and see the customers and utility status inside it.

↗ Try the Developer Portal — API catalog, a real username/password login (Twilio-Verify-ready 2FA, inert until keys are configured), a dashboard, and an API Explorer, all wired to the real scope/metering backend. Defaults to a live deployment at axiom-exchange-api.onrender.com — sign in and every number is real, read from GET /usage/me; clear the API base URL field to fall back to a demo mode instead. That default is a free-tier Render service, so it spins down after 15 minutes idle (~30–60s cold start on the next request) and shares its Postgres server with the saved-query/metering database rather than running on the fully separate instance the architecture describes — both noted honestly in docs/TESTING.md and docs/DATA_MODEL.md rather than glossed over. Signing in to the portal also unlocks Swagger, /openapi.json, and the Deployment / Auth Architecture docs — those moved off this public site entirely and are now served only through an authenticated backend endpoint. This is the P0 "developer foundation" slice, not the full commercial console — see docs/TESTING.md for exactly what is and isn't real.

The tenancy model in one paragraph

A reseller is the account holder and the tenant boundary. A customer is deliberately not owned by a single reseller — it attaches to many, one row per relationship in customer_resellers, and the cost key lives on that relationship rather than on either side. The key defaults to the reseller's UUID so it always resolves back to a reseller row, and can be overridden with a reseller's own billing code. A property is an asset carrying reseller_id, customer_id, state, and the derived cost_key. One customer account can therefore hold many properties spread across many states, each attributable to whichever reseller services it. Reseller-scoped callers see only their own data — including through spatial queries, and including seeing only their own side of a customer shared with a competitor.

Authentication & commercial access in one paragraph

Every operation requires a specific OAuth-style scope — visible per-operation in /openapi.json's security blocks — on top of a valid credential; being authenticated is necessary but never sufficient. Privileged capabilities (trading:execute, der:control, settlement:write, grid:control, admin) additionally require a production-environment credential, enforced in code, not left to policy. Every authorized, billable request is metered into a separate usage-events store, billed by scope rather than by raw endpoint path. MIT-licensed SDKs exist for Python, JavaScript, and PHP (sdks/) — they are thin transport wrappers; every call they make still hits the authenticated, metered, proprietary API. Full model, what's implemented vs. target OIDC federation, and the documentation-tier split: Auth Architecture and API Tiers — the former now requires signing in through the Developer Portal (standard username/password, Twilio-Verify-ready 2FA), the latter stays public at docs/API_TIERS.md.

Positioning constraints

Veritris, Veritris Energy, and OEPC are not represented as a regulated utility, retail electric provider, RTO/ISO, telecom carrier, or authorized market participant anywhere in this repository — those statuses require independent verification. Axiom Exchange interoperates with market and utility systems; it is not a market operator. Retail plan and enrollment endpoints existing in the API is not a representation that competitive retail choice is available in any particular jurisdiction. No ® is used on "Veritris" or "Axiom" here pending trademark clearance.

Contact

Veritris Group, Inc.veritris.com

Email energy@veritris.com
Phone +1 580-713-4927
Address 2828 NW 57th Street #207, Oklahoma City, OK 73112
Contact page veritris.com/lawton-managed-msp-contact
Client / support portal veritris.com/clients
Veritris Energy veritris.energy
Axiom platform veritrisaxiom.energy

Also available programmatically at GET /api/v1/contact (unauthenticated).

Documentation

Doc Covers
docs/API_REFERENCE.md Every endpoint, with real captured request/response examples
Auth Architecture (portal login required) Full OAuth scope/role model, what's implemented vs. target OIDC federation, usage metering — sign in at the portal (defaults to the live deployment) to read it
docs/API_TIERS.md Public/customer/partner/trading documentation classification
docs/PRICING.md Subscription tiers and per-call metering reference
docs/LICENSING.md License-type concepts for counsel to formalize (not a binding agreement)
docs/ARCGIS_INTEGRATION.md Wiring AXIOM into the ArcGIS JS API, Esri JSON conversion, publishing to ArcGIS Online
docs/DATA_MODEL.md Why one generic assets table, why Postgres+PostGIS+JSONB instead of a second NoSQL store
Deployment (portal login required) Local, on-prem, AWS, Azure, and Kong deployment, plus the full environment variable reference — sign in at the portal to read it
docs/TESTING.md What was actually verified, and the bugs found + fixed while doing it

Why this stack

Requirement Choice Why
Structured + unstructured data PostgreSQL 16 + PostGIS + JSONB One database instead of two. Relational tables (plans, rates, enrollments, transactions) sit next to a JSONB attributes column on every table for whatever unstructured fields a given record needs (well API numbers, permit conditions, meter specs...) — no schema migration per new field. This is the same pattern Cosmos DB/DynamoDB give you for unstructured data, without giving up joins, transactions, and geospatial indexing for the structured side.
Geospatial polygons for ArcGIS PostGIS geometry columns + GeoAlchemy2 + Shapely Native ST_Intersects/ST_DWithin/ST_Contains spatial queries; output as RFC 7946 GeoJSON (consumed directly by the ArcGIS JS API's GeoJSONLayer) or converted to Esri JSON for older ArcGIS REST/Runtime clients.
Portable to Aurora / Cosmos-class managed DB Amazon Aurora PostgreSQL-Compatible or Azure Database for PostgreSQL Both are wire-compatible with vanilla PostgreSQL and both support PostGIS as a managed extension — AXIOM_DATABASE_URL is the only thing that changes between your laptop and either cloud.
Load balancing nginx Sits in front of N API replicas locally (api1, api2) or inside the cluster/VPC in the cloud, in front of an autoscaled service.
Pluggable into an API Gateway Stateless JWT/API-key auth + auto-generated OpenAPI 3 spec The service never holds session state, so any number of instances can sit behind AWS API Gateway, Azure APIM, or Kong. Import /openapi.json directly into AWS API Gateway or Azure APIM, or see gateway/kong.example.yaml for a Kong config.

Architecture

ArcGIS Online / ArcGIS JS API (GeoJSONLayer) ─┐
Web / mobile clients ─────────────────────────┼──▶ API Gateway (optional)
Service-to-service callers ───────────────────┘        (AWS API GW / Azure APIM / Kong:
                                                          TLS, auth, quota, WAF)
                                                                │
                                                                ▼
                                                        nginx (load balancer)
                                                        least_conn, health-aware
                                                          │              │
                                                          ▼              ▼
                                                     api1 (FastAPI)  api2 (FastAPI)   ← scale out N ways
                                                          │              │
                                                          └──────┬───────┘
                                                                 ▼
                                                PostgreSQL + PostGIS (+ JSONB)
                                                   ── Aurora PostgreSQL / Azure DB for
                                                      PostgreSQL in the cloud
                                                Redis (cache / rate-limit state)

Repository layout

axiom-exchange/
├── api/
│   ├── app/
│   │   ├── core/            settings, DB session, auth
│   │   ├── models/          SQLAlchemy + GeoAlchemy2 models (assets, utilities, plans, usage, exchange)
│   │   ├── schemas/         Pydantic request/response + GeoJSON schemas
│   │   ├── services/        geo_service.py — GeoJSON <-> PostGIS <-> Esri JSON
│   │   ├── api/v1/routers/  assets, geo, tenancy, queries, utilities, plans,
│   │   │                    usage, enrollments, exchange, auth, contact, health
│   │   ├── db/              schema.sql (canonical DDL) + seed_demo.py
│   │   └── main.py
│   ├── Dockerfile
│   └── requirements.txt
├── nginx/nginx.conf          load balancer config
├── gateway/kong.example.yaml example API Gateway config
├── docker-compose.yml        postgres + redis + 2 api replicas + nginx
└── .env.example

Run it locally

cp .env.example .env        # edit AXIOM_JWT_SECRET at minimum
docker compose up --build

Try the ArcGIS-facing endpoints

# List assets, GeoJSON:
curl http://localhost:8080/api/v1/assets?asset_type=solar_farm -H "X-API-Key: axiom-local-dev-key"

# A single asset as a GeoJSON Feature (point this URL at an ArcGIS JS API GeoJSONLayer):
curl http://localhost:8080/api/v1/assets/<asset_id>/geometry -H "X-API-Key: axiom-local-dev-key"

# Same asset as Esri JSON:
curl "http://localhost:8080/api/v1/assets/<asset_id>/geometry?format=esri" -H "X-API-Key: axiom-local-dev-key"

# Draw/paste a polygon and get every intersecting asset back as a FeatureCollection:
curl -X POST http://localhost:8080/api/v1/geo/query \
  -H "X-API-Key: axiom-local-dev-key" -H "Content-Type: application/json" \
  -d '{"geometry": {"type": "Polygon", "coordinates": [[[-97.9,30.15],[-97.6,30.15],[-97.6,30.45],[-97.9,30.45],[-97.9,30.15]]]}}'

# Load whatever's in the current map viewport:
curl "http://localhost:8080/api/v1/geo/bbox?min_lon=-98&min_lat=30&max_lon=-97.4&max_lat=30.6" \
  -H "X-API-Key: axiom-local-dev-key"

# Utility lookup by point (mirrors PowerHQ's "find the utility for this address"):
curl "http://localhost:8080/api/v1/utilities/lookup?lon=-97.74&lat=30.27" -H "X-API-Key: axiom-local-dev-key"

Every geo endpoint accepts ?format=geojson (default, for the ArcGIS JS API's GeoJSONLayer) or ?format=esri (Esri JSON FeatureSet, for ArcGIS REST-based / Runtime SDK clients).

Scaling out

Plugging into an API Gateway

The service exposes a full OpenAPI 3 spec at /openapi.json:

In every case, the gateway can either validate auth itself and forward a trusted identity via the X-Axiom-Gateway-Identity header (paired with a shared secret — see app/core/security.py), or simply pass through a bearer JWT/API key for the service to validate itself.

Notes on the domain model