NetOSField accessMenu
Signed in user unavailable
Documentation/API & Integrations
Download PDF

Xiber NetOS — API & Integrations Guide

REST API endpoints, MCP tools, and external system integration map.


REST API

SettingValue
Base URLhttp://localhost:8000/api/v1
Production Base URLhttps://netos.xiberian.net/api/v1
Docs (Swagger)https://netos.xiberian.net/docs-api/
OpenAPI Schemahttps://netos.xiberian.net/openapi.json
Content Typeapplication/json

Authentication

The API resolves identity from one of three sources, checked in this order:

  1. API access tokens (recommended for machines and coding agents)
  2. Trusted reverse-proxy identity headers (how browser SSO reaches the API)
  3. 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-email

Requests 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: exec

If 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/me

Creating 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-tokens

Revoke a token:

DELETE /api/v1/authz/api-tokens/{api_token_id}

Rotate a token:

POST /api/v1/authz/api-tokens/{api_token_id}/rotate

Rotation 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/me

Prompt 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/circuits

Query Parameters:

ParamTypeDescription
carrierstringFilter by carrier name (case-insensitive substring)
statusstringFilter by status enum
service_typestringFilter by service type enum
searchstringFull-text search across circuit label, carrier circuit ID, carrier name
term_end_beforedateOnly 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/circuits

Requires: 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.


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.

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}/documents

RF 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}/documents

Electrical 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.

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 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_role accepts "primary", "secondary", "standalone", or null. 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 returns 409 "…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/bulk

Requires: 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-delete

Soft-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/map

Query Parameters:

ParamTypeDescription
carrierstringFilter by carrier
service_typestringFilter by service type enum
statusstringFilter by status enum
statestringFilter 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/summary

Response includes:

SectionFields
kpistotal_circuits, active_circuits, infrastructure_count, circuit_mrc, facility_mrc, total_mrc, modeled_mrr, modeled_margin, renewal_window_count
spend_by_carrierArray of {carrier, mrc, circuit_count}
service_mixArray of {service_type, count}
state_mixArray of {state, count}
renewal_pipelineArray of {state, count}
margin_histogramArray of {bucket, count}
at_risk_circuitsArray of circuit summaries
top_expensiveArray of circuit summaries
lowest_marginArray of circuit summaries

Import Preview

POST /api/v1/imports/circuits/preview
Content-Type: multipart/form-data

Upload 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/preview

Import Commit

POST /api/v1/imports/circuits/{import_job_id}/commit

Commits 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}/commit

List Service Providers

GET /api/v1/service-providers

Returns 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/infrastructure

Returns infrastructure asset records with facility type, provider, costs, terms, and monitoring_url.


Bulk Update Infrastructure Assets

PATCH /api/v1/infrastructure/bulk

Request 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-delete

Soft-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/activity

Returns 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}/comments

Feedback 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.

StatusMeaningdetail shape
401No/invalid identityString, e.g. "Authentication required", "Invalid API token", "API token expired"
403Authenticated but not permittedString, e.g. "Missing permission: circuits_edit", "Write access required", "Unknown role"
404Record not found (or soft-deleted)String, e.g. "Stored document not found"
409State conflict (attribution, in-use, cycle)String — see Attribution-conflict 409; also raised when deleting an RF system / PtMP node still referenced by RF links
422Validation errorEither 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 Pydantic response_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 as null when the caller's RBAC lacks the field-level permission (masking is applied to the payload, not signalled by a separate error).
  • List caps. GET /circuits and GET /customers are capped at 1000 rows (ordered by label/name). GET /infrastructure and GET /rf-links are not paginated and return all non-deleted rows. GET /search caps results at 30 (default 12). There is no cursor/offset pagination yet.
  • Filtering. GET /circuits supports carrier, status, service_type, search, and term_end_before server-side. GET /customers, GET /infrastructure, and GET /rf-links take no query filters — filter client-side (this is what the MCP list_* tools do).
  • Soft delete. Deletes set deleted_at; every list/detail query filters deleted_at IS NULL. Deleted records stay for audit/history but are invisible to reads and return 404 on 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/bulk and PATCH /infrastructure/bulk apply only the fields present in updates and write an audit entry per affected row.
  • CSV/XLSX imports now cover three entities — circuits, infrastructure, and customers — each with previewPATCH .../{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.

ToolDescription
netos_auth_meVerify the configured NetOS API identity, roles, and permissions
search_netosUniversal search across inventory, sites, customers, documents, and providers
list_circuitsList wholesale circuits with optional API filters
get_circuitGet a circuit detail record
list_infrastructureList infrastructure sites, with simple client-side filters
get_infrastructure_contextGet full infrastructure context, relationships, economics allowed by RBAC, and compact document/photo metadata
list_customersList customer sites, with simple client-side filters
get_customer_contextGet customer context including contacts, access notes, service paths, and compact document/photo metadata
get_site_documentsReturn metadata, user notes, and AI summaries for site documents/photos
list_rf_linksList RF links, including PTP/PtMP path metadata
get_rf_linkGet one RF link by UUID via list filtering
list_quotesList vendor quotes using supported API filters
get_quoteGet quote detail, pricing options, and status history when permitted
quote_aggregationSite-level vendor quote aggregation for NNI/port planning signals
create_quote_draftCreate a vendor quote draft for human review without writing to quote history
list_quote_draftsList vendor quote drafts by review status
get_quote_draftGet one vendor quote draft by UUID
update_quote_draftCorrect a draft payload before review/commit
reject_quote_draftReject a duplicate, stale, or invalid draft with a reason
get_carrier_summaryProvider/carrier summary with circuit count, MRC totals, ETF exposure, contacts, portals, circuits, and infrastructure
get_renewal_pipelineCircuit and infrastructure renewal deadlines by state for a lookahead window
get_field_recordField-oriented context by kind: circuit, infrastructure, customer, or rf_link

Planned Tools

ToolDescription
get_circuit_plPer-circuit P&L with revenue attribution
get_outage_historyOutage events for a circuit
Write toolsCreate/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=agent

This 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_at older than the stored source_updated_at are

ignored (ignored_stale) — retries are always safe.

  • Unknown netos_entity_id422 with 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

SystemDirectionPurposeStatus
XSurveyPush (inbound)Site survey references: metadata, thumbnails, deep links (media stays in XSurvey)Live
SonarPullCustomer accounts, MRR, revenue attribution per circuitPlanned
Monday.comBidirectionalRenewal tasks, variance tasks, circuit order workflowPlanned
WisdmPullTower and network site geometry for map overlaysPlanned
LibreNMS / Prometheus / LokiPullUtilization data, outage detection, monitoring deep linksPlanned
DocuSealPushGenerate termination notices and contract execution envelopesPlanned
Microsoft 365 / Entra IDPullSSO, user identity, calendar/Teams/SharePoint referencesPlanned
XOS (MCP Gateway)ExposeAgent-readable circuit tools for automated reporting and decisionsSkeleton
Lumen Control CenterPullCircuit status sync from carrier portalNot started
Zayo TranzactPullCircuit status sync from carrier portalNot started
Cogent PortalPullCircuit status sync from carrier portalNot started

Planned Webhooks (Outbound)

EventTriggerDestination
circuit.installedCircuit status → activeMonday.com, Slack
circuit.decommissionedCircuit status → decommissionedMonday.com, Slack
renewal.state_changedRenewal state transitionMonday.com, Email, Slack
invoice.variance_detectedInvoiced MRC ≠ contracted MRCMonday.com (task for finance)

*Webhooks are not yet implemented.*