Xiber NetOS — API & Integrations Guide
REST API endpoints, MCP tools, and external system integration map.
REST API
| Setting | Value |
|---|---|
| Base URL | http://localhost:8000/api/v1 |
| Production Base URL | https://netos.xiberian.net/api/v1 |
| Docs (Swagger) | https://netos.xiberian.net/docs-api/ |
| OpenAPI Schema | https://netos.xiberian.net/openapi.json |
| Content Type | application/json |
Authentication
The API resolves identity from one of three sources, checked in this order:
- API access tokens (recommended for machines and coding agents)
- Trusted reverse-proxy identity headers (how browser SSO reaches the API)
- Dev-auth header shim (local development only)
There is no native OIDC/JWT validation inside the API today. Browser Microsoft SSO is terminated by oauth2-proxy in front of NetOS; the proxy injects trusted identity headers that the API consumes (see mode 2). The ENTRA_* / JWT_AUDIENCE settings exist but are not yet wired into request validation. Once identity is resolved, NetOS applies its own roles, permissions, scopes, and field-level masking — see Authorization & RBAC for the full model.
1. API access tokens
Use either header format:
Authorization: Bearer ntos_live_...or:
X-NetOS-API-Key: ntos_live_...A Bearer value is only treated as an API token when it starts with ntos_. Tokens are bound to an internal NetOS user; the token authenticates the machine, then NetOS applies that user's normal RBAC roles, scopes, and field-level permissions. Frontend visibility is irrelevant for machine access; backend permissions are still authoritative.
2. Trusted reverse-proxy identity headers
When TRUST_PROXY_AUTH_HEADERS=true and the request's client IP falls inside TRUSTED_AUTH_PROXY_CIDRS (default 127.0.0.1/32,172.16.0.0/12), NetOS trusts identity headers set by the fronting proxy. The first present of these wins:
x-auth-request-email
x-ms-client-principal-name
x-auth-request-user
x-forwarded-email
x-forwarded-user
cf-access-authenticated-user-emailRequests from outside the trusted CIDRs have these headers stripped/ignored. A proxy-resolved user must already exist (or be a bootstrap super-admin) to be authorized.
3. Dev-auth header shim (development only)
When ALLOW_DEV_AUTH=true, unauthenticated requests may pass dev headers:
x-user-email: dev@xiber.com
x-user-role: execIf x-user-role is omitted it defaults to exec (DEV_AUTH_DEFAULT_ROLE); an unknown role returns 403. Do not enable dev auth in production. When neither ALLOW_DEV_AUTH nor a trusted proxy header is present and no API token is supplied, the API returns 401 {"detail": "Authentication required"}.
Identity-header (dev/proxy) example:
curl -sS \
-H "x-user-email: dev@xiber.com" \
-H "x-user-role: exec" \
http://localhost:8000/api/v1/authz/meCreating Machine Access Tokens
Only users with users_edit can create or revoke API tokens.
POST /api/v1/authz/api-tokens{
"user_id": "00000000-0000-0000-0000-000000000000",
"name": "agent01 quote importer",
"expires_at": "2026-09-01T00:00:00Z"
}The response includes the full token once:
{
"api_token_id": "11111111-1111-1111-1111-111111111111",
"user_id": "00000000-0000-0000-0000-000000000000",
"user_email": "agent-netos@xiber.net",
"name": "agent01 quote importer",
"token_prefix": "ntos_live_abcd...",
"expires_at": "2026-09-01T00:00:00+00:00",
"token": "ntos_live_full_secret_value_returned_once"
}Store the token immediately in the calling system's secret manager. NetOS stores only a hash and cannot display the full token again.
List tokens:
GET /api/v1/authz/api-tokensRevoke a token:
DELETE /api/v1/authz/api-tokens/{api_token_id}Rotate a token:
POST /api/v1/authz/api-tokens/{api_token_id}/rotateRotation keeps the same token record but replaces the secret. The previous token stops working immediately, and the new token is returned once.
Machine Access Example
export NETOS_API_TOKEN='ntos_live_...'
curl -sS \
-H "Authorization: Bearer $NETOS_API_TOKEN" \
https://netos.xiberian.net/api/v1/authz/mePrompt For Another Coding Agent
You are integrating with Xiber NetOS.
Base URL:
https://netos.xiberian.net/api/v1
API docs:
https://netos.xiberian.net/docs-api/
OpenAPI schema:
https://netos.xiberian.net/openapi.json
Authenticate every API request with:
Authorization: Bearer <NETOS_API_TOKEN>
The token is bound to an internal NetOS user. Do not try to bypass RBAC. If an endpoint returns 401 or 403, report the missing permission or authorization failure.
Useful endpoints:
- GET /authz/me
- GET /search
- GET /quotes
- POST /quotes/extract
- GET /quotes/drafts
- POST /quotes/drafts
- PATCH /quotes/drafts/{draft_id}
- POST /quotes/drafts/{draft_id}/reject
- POST /quotes/drafts/{draft_id}/commit
- POST /quotes
- POST /quotes/{quote_id}/documents
- POST /quotes/{quote_id}/documents/text
- POST /quotes/{quote_id}/documents/{document_id}/reanalyze
- GET /circuits
- GET /infrastructure
- GET /customers
- GET /rf-links
- GET /electrical-services
Vendor quote model:
- One quote represents one vendor/path/service.
- Term pricing is stored in pricing_options.
- NNI is a valid quote service type and represents an EVPL landing point.
- `target_type` identifies the quote scope. Use `circuit` for transport/path quotes with A and Z endpoints. Use values such as `infrastructure`, `tower`, `rooftop`, `datacenter`, `electrical`, `rf_link`, or `other` for single-site/service quotes that only need a target site/location.
- Quote attachments are stored on the quote record. Uploaded vendor PDFs, images, order forms, and proposals are analyzed in the background when an AI provider is configured; AI summaries and notes are searchable at low search priority.
- Pasted vendor emails or unstructured quote notes can be saved as text quote attachments through `POST /quotes/{quote_id}/documents/text`. NetOS persists the text as a quote document, preserves line breaks for later viewing, queues AI analysis, and includes the text/AI notes in search.
- Claude Desktop and other MCP users should create quote drafts, not committed quotes. NetOS users review drafts in Vendor Quotes -> Draft Inbox, edit the form, then commit or reject them.Endpoints
Health Check
GET /healthz{"status": "ok"}No authentication required.
List Circuits
GET /api/v1/circuitsQuery Parameters:
| Param | Type | Description |
|---|---|---|
carrier | string | Filter by carrier name (case-insensitive substring) |
status | string | Filter by status enum |
service_type | string | Filter by service type enum |
search | string | Full-text search across circuit label, carrier circuit ID, carrier name |
term_end_before | date | Only circuits whose contract term ends before this date |
Response: Array of circuit objects with nested carrier, contract, A endpoint, and Z endpoint data. This is one of the few endpoints backed by a Pydantic response_model (CircuitRead), so its shape is enforced by the schema; most other endpoints return hand-built payload dicts (see List Behavior & Response Shapes). The list is ordered by circuit label and capped at 1000 rows; soft-deleted circuits (deleted_at set) are excluded.
Example:
curl -H "x-user-role: exec" \
"http://localhost:8000/api/v1/circuits?status=active&carrier=Lumen"Get Circuit Detail
GET /api/v1/circuits/{circuit_id}Response includes:
- Circuit attributes (label, carrier circuit ID, service type, bandwidth, status, MRC, NRC)
- Carrier details (name, NOC phone, portal URL)
- Contract details (term dates, renewal type, ETF, escalator)
- A and Z endpoints (name, address, coordinates)
- Exact monitoring URL and generated monitoring search fallback
- Provider portal URL
- Lifecycle events (ordered by date)
Create Circuit
POST /api/v1/circuitsRequires: Write role (exec, finance, network_eng, operations, pm)
Request Body:
{
"xiber_circuit_label": "XIB-LUM-0042",
"carrier_circuit_id": "DHEC.123456",
"carrier_name": "Lumen",
"service_type": "wave",
"bandwidth_mbps": 10000,
"status": "active",
"monitoring_url": "https://librenms.xiber.local/device/123",
"mrc_usd": 2500.00,
"nrc_usd": 5000.00,
"a_endpoint_name": "Xiber HQ",
"z_endpoint_name": "Cologix COL1"
}The manual UI now uses Infrastructure and Customer site selectors for circuit A/Z locations. The REST API still accepts endpoint fields so imports and integrations can create endpoint snapshots. For attribution, creating a circuit is not enough by itself; create a subtended_links relationship to state which infrastructure asset is the upstream donor and which downstream structure or customer endpoint is served over that circuit.
For DIA and broadband services, NetOS treats the circuit as single-site and clears the Z endpoint.
Customer, RF Link, Electrical, and Attribution APIs
Customers
GET /api/v1/customers
POST /api/v1/customers
GET /api/v1/customers/{customer_id}
PATCH /api/v1/customers/{customer_id}
DELETE /api/v1/customers/{customer_id}Customer records represent commercial and MFC customer sites. They can be selected by circuits, RF Links, and subtended links. Customer detail responses include associated circuits, RF Links, serving paths, inherited donor cost, customer revenue, attributed cost, and gross margin.
RF Links
GET /api/v1/rf-links
POST /api/v1/rf-links
PATCH /api/v1/rf-links/{rf_link_id}
DELETE /api/v1/rf-links/{rf_link_id}
POST /api/v1/rf-links/{rf_link_id}/documentsRF Links are built from existing infrastructure and customer sites. Links between two infrastructure assets are backhaul. Links from infrastructure to a customer site are customer endlinks. When a customer endlink is saved, NetOS synchronizes the matching subtended customer endpoint relationship so attribution can show RF transport.
RF System Catalog
GET /api/v1/rf-systems
POST /api/v1/rf-systems
PATCH /api/v1/rf-systems/{rf_system_id}
DELETE /api/v1/rf-systems/{rf_system_id}RF systems define selectable defaults such as name, description, frequency used, capacity, cost, PTP/PtMP type, equipment role, and PtMP compatibility. PtMP source nodes must reference RF systems with role ptmp_base. PtMP customer RF Links must reference a subscriber/client RF system with role ptmp_subscriber; if the subscriber has compatible base system IDs, the selected PtMP source node's base system must be included.
Electrical Services
GET /api/v1/electrical-services
POST /api/v1/electrical-services
PATCH /api/v1/electrical-services/{electrical_service_id}
DELETE /api/v1/electrical-services/{electrical_service_id}
POST /api/v1/electrical-services/{electrical_service_id}/documentsElectrical services attach to exactly one Infrastructure or Customer site. They store provider, service type, account and meter numbers, voltage, amperage, phase, delivery location, service address, average monthly cost, start date, monitoring URL, notes, and supporting documents.
Infrastructure Subtended Links
POST /api/v1/infrastructure/{infrastructure_id}/subtended-links
PATCH /api/v1/infrastructure/{infrastructure_id}/subtended-links/{link_id}
DELETE /api/v1/infrastructure/{infrastructure_id}/subtended-links/{link_id}Subtended links explicitly define the financial hierarchy. Use them to attach customer endpoints, downstream structures, existing circuits, or RF Links to a parent infrastructure asset. This is what drives the attribution tree, dashboard attribution cards, customer inherited-cost drilldowns, and infrastructure financial summaries.
Attribution-conflict 409 and redundant links
Attribution is intentionally single-path by default: a customer endpoint or downstream structure may have only one active (date-overlapping) relationship at a time. If you POST a subtended link whose target is already attributed elsewhere, NetOS returns 409 Conflict with a detail string such as:
{"detail": "Customer 'Acme Tower A' is already attributed to Downtown POP. End the existing relationship before assigning it here. Or mark this link as redundant."}To create a legitimate backup/secondary path instead of ending the existing one, send redundancy_role:
{
"customer_id": "…",
"circuit_id": "…",
"redundancy_role": "secondary"
}- On subtended links,
redundancy_roleaccepts"primary","secondary","standalone", ornull. A"secondary"customer link at a *different* parent facility is allowed through the conflict check; the customer keeps its primary attribution elsewhere. - A link marked
"secondary"contributes no attributed customer revenue (it is a backup path), but transport cost is still counted as real spend. - The
" Or mark this link as redundant."hint is appended only for cross-facility customer conflicts. Downstream-structure and same-facility duplicates are never relaxed this way, and a downstream link that would form a hierarchy loop returns409 "…circular infrastructure hierarchy".
For how backup paths are attributed in financial reporting (cost flows to the backup facility while revenue stays credited at the primary), see Infrastructure Attribution → Redundant / Backup Links.
The same redundancy_role concept applies to circuit creation (POST /circuits accepts redundancy_role) and to customer RF Links (POST /rf-links accepts redundancy_role, limited to "secondary" or null), which is how integrators register a backup RF endlink for a customer.
Bulk Update Circuits
PATCH /api/v1/circuits/bulkRequires: Write role (exec, finance, network_eng, operations, pm)
Request Body:
{
"circuit_ids": ["8d8e4333-8182-4443-8e0d-f2307276db52"],
"updates": {
"status": "active",
"carrier_name": "Lumen",
"service_type": "dia",
"mrc_usd": 1850.00,
"nrc_usd": 0,
"contract_term_end_date": "2027-12-31",
"notes": "Updated through bulk action"
}
}Only fields included in updates are changed. Each affected circuit receives an audit entry.
Response:
{"updated": 1, "requested": 1}Bulk Delete Circuits
POST /api/v1/circuits/bulk-deleteSoft-deletes circuits by setting deleted_at. Records remain available for audit and historical reporting.
Request Body:
{
"circuit_ids": ["8d8e4333-8182-4443-8e0d-f2307276db52"]
}Response:
{"deleted": 1, "requested": 1}Map Data
GET /api/v1/circuits/mapQuery Parameters:
| Param | Type | Description |
|---|---|---|
carrier | string | Filter by carrier |
service_type | string | Filter by service type enum |
status | string | Filter by status enum |
state | string | Filter by endpoint state (US state) |
Response:
{
"circuits": [
{
"id": "uuid",
"label": "XIB-LUM-0042",
"carrier": "Lumen",
"service_type": "wave",
"bandwidth_mbps": 10000,
"status": "active",
"a_endpoint": {"name": "...", "lat": 39.96, "lng": -82.99},
"z_endpoint": {"name": "...", "lat": 39.95, "lng": -83.00},
"geometry": {"type": "LineString", "coordinates": [[-82.99, 39.96], [-83.00, 39.95]]}
}
],
"endpoints": [
{
"id": "uuid",
"name": "Xiber HQ",
"type": "pop",
"geometry": {"type": "Point", "coordinates": [-82.99, 39.96]}
}
],
"infrastructure": [
{
"id": "uuid",
"name": "Cologix COL1",
"type": "data_center",
"geometry": {"type": "Point", "coordinates": [-83.00, 39.95]}
}
],
"filters": {
"carriers": ["Lumen", "Zayo", "Cogent"],
"service_types": ["wave", "dia", "dark_fiber"],
"statuses": ["active", "ordered"],
"states": ["OH", "GA", "IL"]
}
}Dashboard Summary
GET /api/v1/dashboard/summaryResponse includes:
| Section | Fields |
|---|---|
kpis | total_circuits, active_circuits, infrastructure_count, circuit_mrc, facility_mrc, total_mrc, modeled_mrr, modeled_margin, renewal_window_count |
spend_by_carrier | Array of {carrier, mrc, circuit_count} |
service_mix | Array of {service_type, count} |
state_mix | Array of {state, count} |
renewal_pipeline | Array of {state, count} |
margin_histogram | Array of {bucket, count} |
at_risk_circuits | Array of circuit summaries |
top_expensive | Array of circuit summaries |
lowest_margin | Array of circuit summaries |
Import Preview
POST /api/v1/imports/circuits/preview
Content-Type: multipart/form-dataUpload a CSV or XLSX file. Returns import job ID, detected column mapping, headers, sample rows, and validation summary.
Example:
curl -H "x-user-role: exec" \
-F "file=@examples/sample_circuits.csv" \
http://localhost:8000/api/v1/imports/circuits/previewImport Commit
POST /api/v1/imports/circuits/{import_job_id}/commitCommits all valid staged rows. Returns insert/update/invalid counts.
Example:
curl -H "x-user-role: exec" \
-X POST \
http://localhost:8000/api/v1/imports/circuits/{import_job_id}/commitList Service Providers
GET /api/v1/service-providersReturns carrier records with circuit count, infrastructure count, and total spend.
Get Service Provider
GET /api/v1/service-providers/{carrier_id}Returns provider detail with related circuits and infrastructure assets.
List Infrastructure Assets
GET /api/v1/infrastructureReturns infrastructure asset records with facility type, provider, costs, terms, and monitoring_url.
Bulk Update Infrastructure Assets
PATCH /api/v1/infrastructure/bulkRequest Body:
{
"infrastructure_ids": ["ae30e22f-565d-46f3-be94-9d0e001a59de"],
"updates": {
"status": "active",
"provider_name": "American Tower",
"site_type": "tower",
"mrc_usd": 2200.00,
"price_escalator_pct": 3.0,
"term_end_date": "2028-12-31",
"notes": "Updated through bulk action"
}
}Only fields included in updates are changed. Each affected asset receives an audit entry.
Response:
{"updated": 1, "requested": 1}Bulk Delete Infrastructure Assets
POST /api/v1/infrastructure/bulk-deleteSoft-deletes infrastructure assets by setting deleted_at.
Request Body:
{
"infrastructure_ids": ["ae30e22f-565d-46f3-be94-9d0e001a59de"]
}Response:
{"deleted": 1, "requested": 1}Audit Activity
GET /api/v1/audit/activityReturns recent request/write activity including user, role, IP, event type, action, entity, changed fields, and response status.
Feedback Queue
GET /api/v1/feedback
POST /api/v1/feedback
PATCH /api/v1/feedback/{feedback_id}
POST /api/v1/feedback/{feedback_id}/commentsFeedback requests support priority, status, and progress comments. Public comments attempt to notify the requester by email when SMTP is configured.
Error Contract
Errors follow FastAPI's convention: a JSON body with a detail field and an appropriate status code. Integrators should branch on status code and surface detail verbatim.
| Status | Meaning | detail shape |
|---|---|---|
401 | No/invalid identity | String, e.g. "Authentication required", "Invalid API token", "API token expired" |
403 | Authenticated but not permitted | String, e.g. "Missing permission: circuits_edit", "Write access required", "Unknown role" |
404 | Record not found (or soft-deleted) | String, e.g. "Stored document not found" |
409 | State conflict (attribution, in-use, cycle) | String — see Attribution-conflict 409; also raised when deleting an RF system / PtMP node still referenced by RF links |
422 | Validation error | Either FastAPI's structured detail array (Pydantic body validation) or a plain string for hand-rolled checks, e.g. "Label, display name, and site type are required" |
Every route is gated by a route-permission dependency, so a valid identity lacking the mapped permission gets 403 {"detail": "Missing permission: <code>"} before the handler runs.
List Behavior & Response Shapes
- Response conventions. A handful of endpoints (e.g.
GET/POST/PATCH /circuits) declare a Pydanticresponse_model(CircuitRead) and return schema-validated objects. Most other endpoints build response dicts by hand, so field presence tracks the code rather than a published schema — always tolerate extra/missing keys. Financial and other sensitive fields may be returned asnullwhen the caller's RBAC lacks the field-level permission (masking is applied to the payload, not signalled by a separate error). - List caps.
GET /circuitsandGET /customersare capped at 1000 rows (ordered by label/name).GET /infrastructureandGET /rf-linksare not paginated and return all non-deleted rows.GET /searchcaps results at 30 (default 12). There is no cursor/offset pagination yet. - Filtering.
GET /circuitssupportscarrier,status,service_type,search, andterm_end_beforeserver-side.GET /customers,GET /infrastructure, andGET /rf-linkstake no query filters — filter client-side (this is what the MCPlist_*tools do). - Soft delete. Deletes set
deleted_at; every list/detail query filtersdeleted_at IS NULL. Deleted records stay for audit/history but are invisible to reads and return404on direct fetch. Bulk deletes (POST /circuits/bulk-delete,POST /infrastructure/bulk-delete) are soft deletes and return{"deleted": n, "requested": m}. - Bulk update.
PATCH /circuits/bulkandPATCH /infrastructure/bulkapply only the fields present inupdatesand write an audit entry per affected row. - CSV/XLSX imports now cover three entities —
circuits,infrastructure, andcustomers— each withpreview→PATCH .../{import_job_id}/rows(in-app staging-row correction) →commit.
MCP Server
Location: apps/mcp/
The MCP server exposes NetOS data to XOS agents (ExecOS, department agents, Claude Desktop users).
Current Tools
The MCP server is intentionally a thin, permission-aware layer over the NetOS REST API. It does not read the database directly. API-token RBAC controls which rows and fields are returned.
| Tool | Description |
|---|---|
netos_auth_me | Verify the configured NetOS API identity, roles, and permissions |
search_netos | Universal search across inventory, sites, customers, documents, and providers |
list_circuits | List wholesale circuits with optional API filters |
get_circuit | Get a circuit detail record |
list_infrastructure | List infrastructure sites, with simple client-side filters |
get_infrastructure_context | Get full infrastructure context, relationships, economics allowed by RBAC, and compact document/photo metadata |
list_customers | List customer sites, with simple client-side filters |
get_customer_context | Get customer context including contacts, access notes, service paths, and compact document/photo metadata |
get_site_documents | Return metadata, user notes, and AI summaries for site documents/photos |
list_rf_links | List RF links, including PTP/PtMP path metadata |
get_rf_link | Get one RF link by UUID via list filtering |
list_quotes | List vendor quotes using supported API filters |
get_quote | Get quote detail, pricing options, and status history when permitted |
quote_aggregation | Site-level vendor quote aggregation for NNI/port planning signals |
create_quote_draft | Create a vendor quote draft for human review without writing to quote history |
list_quote_drafts | List vendor quote drafts by review status |
get_quote_draft | Get one vendor quote draft by UUID |
update_quote_draft | Correct a draft payload before review/commit |
reject_quote_draft | Reject a duplicate, stale, or invalid draft with a reason |
get_carrier_summary | Provider/carrier summary with circuit count, MRC totals, ETF exposure, contacts, portals, circuits, and infrastructure |
get_renewal_pipeline | Circuit and infrastructure renewal deadlines by state for a lookahead window |
get_field_record | Field-oriented context by kind: circuit, infrastructure, customer, or rf_link |
Planned Tools
| Tool | Description |
|---|---|
get_circuit_pl | Per-circuit P&L with revenue attribution |
get_outage_history | Outage events for a circuit |
| Write tools | Create/update with explicit agent confirmation flow and audit trail |
Authentication
Production MCP deployments should use a NetOS API token generated in Admin. Configure the MCP process with:
NETOS_API_BASE_URL=http://10.10.67.118:8010
NETOS_API_TOKEN=ntos_live_...Use the protected internal API endpoint for machine clients. The public browser hostname https://netos.xiberian.net is fronted by Microsoft SSO through oauth2-proxy and will return an HTML sign-in page before NetOS can validate an API token.
Every MCP request is sent to the REST API with:
Authorization: Bearer <NETOS_API_TOKEN>The token user's RBAC permissions determine what the tools can see. Financial fields, executive summaries, document metadata, and search results follow the same field-level masking rules as the REST API.
For local development only, the MCP server can fall back to trusted dev headers:
NETOS_MCP_AGENT_EMAIL=xos-agent@xiber.com
NETOS_MCP_AGENT_ROLE=agentThis fallback should not be used for production gateway access.
Registration with XOS Gateway
The MCP server should be registered with the Xiber MCP Gateway (gateway.xiberian.net) so it's available to all XOS agents and Claude Desktop users via the SSE bridge. This is not yet configured.
XSurvey Integration (Site Survey Footage)
XSurvey is the system of record for site survey media (terabytes of footage); NetOS stores only survey references — metadata, up to 5 thumbnail URLs, and deep links back into XSurvey's UI. NetOS never ingests media bytes. Surveys are rendered as a "Site Surveys" panel on infrastructure and customer detail pages.
Auth: XSurvey pushes with a NetOS API token bound to the Integration: XSurvey service role (xsurvey_push + read access for its site picker: assets_view, customers_view). Browser deep links and thumbnail loads assume both apps sit behind the shared Entra SSO.
Push Contract (v1)
POST /api/v1/integrations/xsurvey/surveys — permission xsurvey_push. Upsert key: survey_id (XSurvey UUID, stable forever). Envelope:
{
"schema_version": 1,
"survey_id": "8c4c9f6a-…",
"event": "upsert", // or "tombstone"
"netos_entity_type": "infrastructure", // or "customer"
"netos_entity_id": "b1f0d3c2-…",
"captured_at": "2026-07-06",
"technician": "Phil Smith",
"title": "9000 Keystone Crossing — site walk",
"summary": "…from the APPROVED report only…",
"status": "published", // or "archived"
"media_counts": { "photos": 0, "videos": 10, "minutes": 27 },
"thumbnail_urls": ["https://<xsurvey>/api/v1/frames/<uuid>/image"], // max 5
"deeplink_url": "https://<xsurvey>/surveys/<survey_id>", // required on upsert
"report_url": "https://<xsurvey>/reports/<uuid>",
"updated_at": "2026-08-02T14:11:07Z",
"media_retention": "indefinite-tiered"
}Response is a receipt: { "receipt_id": "<uuid>", "survey_id": "…", "action": "created" | "updated" | "tombstoned" | "ignored_stale" | "ignored_stale_tombstone", "received_at": "…" }. Receipts are also written to the NetOS audit log (xsurvey_survey_upsert / xsurvey_survey_tombstone).
Semantics:
- Upserts with an
updated_atolder than the storedsource_updated_atare
ignored (ignored_stale) — retries are always safe.
- Unknown
netos_entity_id→422with a clear message. XSurvey's site picker
should use GET /api/v1/infrastructure and GET /api/v1/customers.
- Tombstones null all content fields and hide the survey from the UI; a
tombstone naming a *different* entity than the stored row (re-link race) is ignored (ignored_stale_tombstone) so the newer linkage survives.
Read-back / Reconciliation
GET /api/v1/integrations/xsurvey/surveys?netos_entity_id=<uuid>&include_tombstoned=false (permission assets_view) returns stored survey references — used by the NetOS UI panel and available for XSurvey-side reconciliation. XSurvey exposes the mirror endpoint (GET /api/v1/surveys?netos_entity_id=&updated_since=) so NetOS can backfill if pushes were missed.
External System Integration Map
| System | Direction | Purpose | Status |
|---|---|---|---|
| XSurvey | Push (inbound) | Site survey references: metadata, thumbnails, deep links (media stays in XSurvey) | Live |
| Sonar | Pull | Customer accounts, MRR, revenue attribution per circuit | Planned |
| Monday.com | Bidirectional | Renewal tasks, variance tasks, circuit order workflow | Planned |
| Wisdm | Pull | Tower and network site geometry for map overlays | Planned |
| LibreNMS / Prometheus / Loki | Pull | Utilization data, outage detection, monitoring deep links | Planned |
| DocuSeal | Push | Generate termination notices and contract execution envelopes | Planned |
| Microsoft 365 / Entra ID | Pull | SSO, user identity, calendar/Teams/SharePoint references | Planned |
| XOS (MCP Gateway) | Expose | Agent-readable circuit tools for automated reporting and decisions | Skeleton |
| Lumen Control Center | Pull | Circuit status sync from carrier portal | Not started |
| Zayo Tranzact | Pull | Circuit status sync from carrier portal | Not started |
| Cogent Portal | Pull | Circuit status sync from carrier portal | Not started |
Planned Webhooks (Outbound)
| Event | Trigger | Destination |
|---|---|---|
circuit.installed | Circuit status → active | Monday.com, Slack |
circuit.decommissioned | Circuit status → decommissioned | Monday.com, Slack |
renewal.state_changed | Renewal state transition | Monday.com, Email, Slack |
invoice.variance_detected | Invoiced MRC ≠ contracted MRC | Monday.com (task for finance) |
*Webhooks are not yet implemented.*
