Sword IT
Documentation
← Dashboard

Sword IT — Documentation

Everything about the Device Inventory & ITAM app: what it does, how to do the everyday work, the IT and admin procedures, and the full API reference at the end. Written for Management, IT and Administration alike — each section is badged with who it is for.

What this app is everyone

One screen that answers three questions the company otherwise answers by opening five consoles:

  • Do we know where every machine is? Devices are pulled from JumpCloud, Kandji, Google Workspace, SentinelOne and Snipe-IT, matched by serial number, and every disagreement between them is surfaced as an issue.
  • Is every machine protected? Missing MDM enrolment, missing SentinelOne agent, missing disk encryption — each is a filter and a KPI.
  • What are we paying for, and is it used? Software seats arrive from the in-app vendor connectors (Okta, Slack, Google Workspace, Zoom), get priced, and each seat is classified as genuinely used or waste.

Snipe-IT is the source of truth for what we own; the other sources are evidence of what is actually out there. The app reconciles the two views — it never writes back to any source.

Roles & access everyone

Your Okta groups map to one of three roles at login. What you can see is identical for all roles — what you can change differs:

  • Management — read everything, change nothing. Buttons for mutations are hidden, and the server refuses them anyway (a 403, even by direct request).
  • IT — everything Management has, plus operational actions: run/cancel syncs, annotate devices, tick offboarding items, flag service accounts, create vendors, manage API tokens.
  • Administration — everything IT has, plus the Django admin site (prices, tools, users, audit log browsing).

Every mutation is attributed to your user in the audit trail. There are no shared logins.

Signing in everyone

  1. Open the app URL — you are redirected to Okta.
  2. Authenticate with your normal Okta account (MFA included).
  3. You land on the Overview. Your name and role appear in the settings menu (☰, top right).

No Okta assignment, or wrong role? That is an Okta group membership question for IT — the app itself has no user management.

If Okta is down entirely, Administration can use the break-glass procedure.

Reading the Overview everyone

The Overview is a briefing, top to bottom in priority order:

  • Briefing sentence — the fleet in one line: how many laptops need attention and why.
  • Needs attention — clickable cards per problem class (missing MDM, not in SentinelOne, Okta risk, discrepancies, unmanaged). Clicking one opens the matching Sync state list.
  • Software costs — monthly spend, estimated waste, and savings already banked. Click through for the full Costs tab.
  • Coverage KPIs — MDM / SentinelOne / Google Workspace / encryption coverage with trend sparklines.
  • Details — the underlying tables, collapsed by default.

Make it yours

The Customize button (top right of the Overview) lets you hide any section — the choice is saved to your account and follows you across browsers.

Investigating devices everyone

Investigate is the working table: every reconciled device, filterable and searchable. It carries two base lists — Main (one row per serial, all sources merged) and All Devices (every entry from every source) — plus the Custom filter builder for lists you shape yourself.

The other device lists

Every other list has its own sidebar entry, built like the Security tab: a row of stat cards on top and one device table underneath. The cards are the selector — click one and the table shows those devices (sortable, exportable, a row opens the drawer).

  • Sync state (Fleet) — In sync, Sources disagree, Not in MDM, Unmanaged, Outdated record, Not in use. The badge in the sidebar is the number of laptops not in sync.
  • MDMs (Insights) — In Kandji, Not in Kandji, In JumpCloud, Not in JumpCloud, and the MDM coverage percentage.
  • Google Workspace (Insights) — Not in GW, In Snipe not in GW, In MDM not in GW, and the GW coverage percentage.
  • Security also carries Not encrypted; Multi-device users lives under Compliance → Machine swaps.

Drill-throughs from the Overview, Security and the palette land on the right entry automatically; a filter outside every family opens in Investigate as an ad-hoc list.

The device drawer

Click any row. The drawer shows one tab per source that saw the device — the full vendor record, not a summary — plus:

  • Issues: every disagreement, in words.
  • History: the device's recorded timeline — status changes, renames, reassignments, first sighting. An entry is only logged when something actually changed since the previous sync; a run of identical entries folds into one line with a ×N badge (hover it for the date range).
  • Open in [source] ↗ links: jump straight to the device in Snipe-IT, Kandji or JumpCloud, and to the user's Okta profile.
  • Copy: a Slack-ready summary of the device.

Selection & bulk actions

Checkboxes select rows; the toolbar then offers CSV export of the selection and (IT+) bulk annotation.

Costs & waste everyone

Every software seat is classified into exactly one state:

  • Active — used within the dormancy window (default 30 days). Costed, not waste.
  • Dormant — assigned to a real person who has not used it in the window. Waste.
  • Orphaned — assigned to someone no longer in the directory. Waste, and alerted to Slack.
  • Pending — invited, never accepted, but the licence is already assigned and billed (Zoom). Waste once the invite is older than the dormancy window.
  • Unknown — the vendor reports no activity signal, so we cannot call it waste. Counted and priced, never claimed as waste. Honesty beats a bigger waste number.
  • Unprovisioned — in the directory, holds a device, but no seat. Not a cost; a provisioning gap.

The tabs

  • Waste — every dormant/orphaned seat with its monthly cost.
  • People / Divisions — spend rolled up per person and per division. Click a person for their full profile.
  • Trend — monthly snapshots of spend and waste.
  • Savings — waste that disappeared after being flagged, annualised. The number the project justifies itself with.

Trust rules

If a currency has no exchange rate, its amounts are reported separately, never silently converted. If a tier or price is missing, the base price applies. The headline always equals the sum of the detail tables.

Renewals & negotiation packs everyone

The Renewals tab lists every tool with a renewal date inside the horizon, most urgent first, with a recommended seat count.

Before a vendor call

  1. Click the renewal row — the negotiation pack opens.
  2. Read the six numbers: paying now, seats assigned vs purchased, genuinely used, wasted, renew-at recommendation, annual saving.
  3. "The argument" lists the talking points the data supports — dormant seats, unassigned purchased seats, price rises vs usage.
  4. Copy brief puts a Slack-ready summary on your clipboard for the deal channel.

The pack reads the same engine as the dashboard, so its numbers can never disagree with what is on screen.

Shortcuts & personalisation everyone

  • Cmd/Ctrl+K — the command palette: jump to any tab or filter, find a device by serial/hostname/user, find a person, or run actions (Sync Now, Add Vendor, Evidence Pack).
  • Offboarding — the country tables start collapsed; open one and it stays open for you next time.
  • Sidebar — every section header (Fleet, Insights, Operate, Money, System) folds on click and stays folded for you; the section holding the current page reopens itself. The chevron at the top shrinks the whole sidebar to icons. Settings (administrators) sits alone at the foot of the list, above Sync Now.
  • Saved views (☆) — name the screen you keep coming back to (tab + filter + search) and reapply it in one click. Saved to your account, max 20.
  • Alerts — the same alerts that go to Slack (orphans, renewals, degraded feeds, backup problems), in-app, with an unread badge.
  • Dark Mode / Light Mode — the sidebar row names the mode you are in; click it to switch. Density lives under Options, with your account, sessions, the report generator, documentation and API tokens.
  • Phone — the app works below 760px: the sidebar becomes a top strip and tables scroll sideways. Made for "check one thing", not daily driving.

Running a sync IT

Syncs also run automatically on a schedule; run one manually after fixing something at a source.

  1. Click ⟳ Sync Now (bottom of the sidebar). The progress pill shows per-source status live.
  2. A source failing does not abort the sync — its last good data is carried forward and the source is marked degraded.
  3. A persistent banner appears when a source has failed repeatedly; it also alerts to Slack. Fix the source (credentials, API status), then sync again.
  4. Cancel from the same pill if you started one by mistake — cancelling resets the cooldown so you can immediately re-run.

Source health, last webhook, backup age and alert queue live in Pulse (sidebar) — the first place to look when numbers seem stale.

Tags, exclusions & snoozes IT

Annotations are how you teach the dashboard fleet context it cannot know. There are exactly four kinds:

  • Tag — a label (kiosk, loaner, event-pool). Filterable; never changes counts.
  • Exclusion — removes a device from KPIs and attention counts. Requires a reason. For devices that are correctly "wrong" (a test bench, a spare in a drawer).
  • Snooze — silences a device's issues temporarily (max 90 days). Requires a reason. For known, in-progress situations.
  • Note — free text on the device. No effect on anything.

Adding

  1. In Investigate, select one or more rows.
  2. Click Annotate, pick the kind, fill the reason (mandatory for exclusions and snoozes).
  3. Chips appear next to the serial; excluded/snoozed devices leave the headline counts on the next refresh.

Reviewing and undoing

Operate → Annotations lists every active annotation across the fleet — searchable, filterable by kind. This is where you un-exclude a device (exclusions hide devices from the very filters you would look for them in, so this tab is deliberately outside the filters), end a snooze early, or clean up tags. Snoozes also expire on their own.

Every add and remove is attributed and audited.

Offboarding & onboarding IT

Offboarding someone

  1. Open the person (click their email anywhere, or Cmd+K → their name).
  2. The Offboarding checklist in their profile is generated from what they actually hold: one item per license, one per device, plus the fixed steps (Okta deprovisioned, devices recovered, Snipe-IT updated).
  3. Tick items as you complete them. Each tick is saved immediately and records who ticked it and when — this is the audit evidence that the offboarding happened.
  4. If a new seat for the person appears later, re-opening the profile adds it to the checklist without touching your existing ticks. Completed items are never deleted, even when the seat disappears.

The Operate → Offboarding tab lists everyone deprovisioned in Okta who still holds seats or devices — your worklist. Devices are split into one table per country, read from the Snipe-IT status label: United States, Portugal, and an Other table that only appears when a device has no US/PT marker in its status or is not in Snipe-IT at all — those need a decision on where they belong.

Onboarding gaps

The same profile shows what a joiner is missing compared to their department: tools that the majority of their peers hold. Use it as the checklist for a new starter's first week.

Service accounts IT

Shared and automation accounts (svc-ci@…, design-shared@…) would otherwise pollute onboarding baselines and read as waste. Flagging one:

  1. Open the account like a person and click Service acct, or use the admin action on the Person list.
  2. Add a note saying what it is for — your future self will ask.
  3. The account moves to Operate → Service accounts and drops out of onboarding baselines.
Policy: flags are manual, always. The app never auto-detects service accounts — there are few of them and misclassifying a human as a bot (or vice versa) silently corrupts several numbers. Each flag/unflag is an audited decision.

Vendors, tiers & prices IT

Adding a vendor

  1. Costs tab → + Vendor.
  2. Fill name, source_key (the connector/feed identity), price, currency, billing period. Tick "reports last-activity" only if the vendor really does — without it seats classify as unknown, never waste.
  3. Optionally add tiers, one per line: plus 15.
  4. On save you get the source_key the connector registers under (plus the webhook endpoint for external feeds).
  5. Seats appear after the first completed connector run; costs after the next snapshot.

Tiers

When a vendor has plans (Slack Pro vs Business+), seats carry the plan name and are priced at their tier. Waste is costed per tier — which is why downgrading dormant premium seats shows up as a real saving. A seat with an unconfigured tier falls back to the base price; nothing breaks.

Changing a price

Edit the tool or tier in the admin (Administration) — that is it. Every real price movement is recorded automatically in the price history with who changed it, and feeds the negotiation pack's "price moved X% while usage moved Y%" argument. No manual logging.

Compliance & machine swaps IT

Insights → Compliance has two sub-tabs:

Coverage by division

Fleet-wide cards first (encrypted, SentinelOne, in an MDM, attributed to a division), then encryption / SentinelOne / MDM percentages per division, worst first. Click a division and it unfolds right under itself: its departments with their own percentages, then the devices, least compliant first; click a department to narrow the devices to it, click the division again to close. Two Okta fields drive this: division is the top level (Technology, Clinical Ops, Commercial…) and Okta's department is the sub-level inside it (Research & Dev, Algorithms, Treatment…). Read the banner: devices whose owner has no department on record are excluded and counted separately — the page tells you how much of the fleet the numbers actually describe rather than flattering itself.

Machine swaps

Cards for swap candidates, people involved, the longest quiet spell and multi-device users, then the swap table (a row opens the drawer) and the list of people holding 2+ deployed laptops.

Likely machine swaps

People holding a newer laptop while an older one has been quiet for 45+ days. Each row carries the replacement machine, so the recovery message writes itself. Two active machines are never flagged — a desktop + laptop pair is legitimate — and laptops in In Project Use status are left out on both sides: a project machine is neither the one to recover nor a replacement.

Audit evidence pack IT

When an auditor asks "show me that access was controlled": Compliance → Download evidence pack. One ZIP, as of one moment:

  • access-review.csv — who holds which seat, with directory status and classification.
  • fleet-inventory.csv — every device with owner, encryption, agent coverage, sources.
  • model-changes.csv + audit-events.csv — every audited change and event in the period (default 90 days).
  • README.txt + manifest.json — period, generator, row counts: what makes it defensible.

Generating a pack is itself an audited event, so packs appear in each other's trails.

Admin site & audit log administration

/admin/ is for data that changes rarely: tools and tiers (prices, renewal dates), cost configuration (display currency, dormancy window, FX rates), users and groups.

Finding "who changed X"

  • Model changes: admin → Log entries — every audited save with the before/after diff and the actor.
  • Events: admin → Audit events — exports, break-glass logins, token lifecycle, service-account decisions.

Guardrails you will hit (on purpose)

  • People rows cannot be deleted — a leaver's stale directory row is how their seats read as orphaned.
  • Machine-written tables (seats, snapshots, alerts) are read-only in admin; they are evidence, not settings.
  • Bulk actions that skip the audit trail are refused by CI — if an action is missing, that is why.

API tokens IT

For scripts and dashboards that need the data without a browser: settings menu (☰) → API tokens.

  1. Name the token after the consumer (grafana, joao-report-script) and click Create.
  2. Copy the token immediately — it is shown exactly once and stored only as a hash.
  3. Use it as Authorization: Bearer itam_… against the v1 endpoints.
  4. Revoke from the same list the moment a consumer is retired. Creation and revocation are audited.
  5. Tokens expire one year after creation. The list shows how long each has left (amber inside the last 30 days); an expired token is refused like a revoked one, so re-create it before then and swap it into the consumer. A shorter lifetime can be requested at creation (expires_in_days), never a longer one.

Tokens are read-only by construction — a leaked token can read state, never change it. Still treat one like a password.

Break-glass access administration

For exactly one scenario: Okta is down or misconfigured and the app must be reached anyway. Not a convenience login.

  1. Go to /admin/login/ and enter the break-glass superuser credentials (1Password).
  2. A one-time code is emailed to the operations address. Enter it — the password alone is never enough.
  3. Every use fires an audit event and a Slack alert. Expect a "was that you?".
  4. When Okta is restored, sign out and rotate the break-glass password.

Backups, alerts & status administration

Where to look first

Pulse (sidebar) → Operations: app version, backup age, undelivered-alert queue, last webhook receipt. Anything red there tells you which runbook to open.

Backups

Nightly pg_dump with a row-count manifest, pruned on a retention window. Two alerts cover the two failure modes: backup failed (a dump ran and errored) and backup stale (no dump has run at all — the CronJob is dead). Restores are drilled with restore_db --verify; the procedure is in the repo runbook (itam/README.md).

Alerts

All alerts (orphans, renewals, degraded feeds, backups, break-glass) go to one queue, surfaced by the in-app bell (pull endpoints stay available for a future external deliverer). Alert history is never pruned.

Scheduled jobs

  • Waste scan — daily: classifies seats, emits alerts, writes the trend snapshot, checks backup age.
  • Backup — nightly dump.
  • Session cleanup — daily clearsessions.
Authentication — All API endpoints (except GET /lb/health) require the X-Dashboard-Secret header whose value must match the DASHBOARD_SECRET environment variable. Requests without the header or with a wrong value return 401 Unauthorized. After 5 failed attempts within 60 seconds, the IP is blocked for 10 minutes and subsequent requests return 429 with a Retry-After header. If DASHBOARD_SECRET is not set, all endpoints return 503.
Common Error Codes
CodeMeaning
401Missing or incorrect X-Dashboard-Secret header.
409Conflict — a sync is already in progress.
429Too many requests (rate limited). Check Retry-After header.
500Unexpected server error.
503DASHBOARD_SECRET not configured, or a feature is disabled.
Rate limiting note: POST /api/sync enforces a 10-second cooldown between triggers regardless of auth. Use GET /api/sync-status to poll progress instead of re-triggering.
GET /lb/health No auth Liveness check for load balancers and uptime monitors.
Always returns 200 OK with a static JSON payload as long as the process is running. No authentication required. Suitable for health probes in container orchestrators or reverse proxies.
200 OK
{ "status": "ok" }
curl
curl http://localhost:5050/lb/health
POST /api/sync Auth required Trigger a full inventory sync in the background.
Fetches devices from all configured sources (JumpCloud, Kandji, Google Workspace, SentinelOne, Snipe-IT) concurrently and runs reconciliation in a background thread. Returns immediately with 202 Accepted and a sync_id to track progress via GET /api/sync-status.

Note: Tenable is intentionally out of current product scope and is not included in live dashboard sync outputs.

Enforces a 10-second cooldown between triggers. Returns 409 if a sync is already running.
No request body. Only the X-Dashboard-Secret header is required.
202 Accepted
{
  "started": true,
  "sync_id": "a3f8b2c1d4e5f607",
  "message": "Sync started"
}
409 Conflict
{
  "error": "Sync already in progress"
}
CodeCondition
202Sync started successfully.
401Missing or invalid secret.
409A sync is already running.
429Cooldown period has not elapsed or too many auth failures.
500Failed to start background worker thread.
bash
curl -X POST http://localhost:5050/api/sync \
  -H "X-Dashboard-Secret: YOUR_SECRET"
GET /api/sync-status Auth required Poll live progress of the current or most recent sync.
Returns the live progress of a sync. Poll this endpoint after POST /api/sync to track completion. The phase field cycles through: queuedfetching_sourcesreconcilingcompleted (or failed). Each source entry in sources has a status of queued, running, ok, error, or not_configured.
200 OK (sync in progress)
{
  "available": true,
  "sync_id": "a3f8b2c1d4e5f607",
  "running": true,
  "phase": "fetching_sources",
  "started_at": "2026-04-17T10:00:00Z",
  "updated_at": "2026-04-17T10:00:03Z",
  "sources": {
    "jumpcloud":  { "status": "ok",      "count": 312, "elapsed_ms": 2840 },
    "kandji":     { "status": "running" },
        "sentinelone":{ "status": "queued"  }
  }
}
200 OK (no sync yet)
{
  "available": false,
  "running": false
}
bash
curl http://localhost:5050/api/sync-status \
  -H "X-Dashboard-Secret: YOUR_SECRET"
GET /api/last Auth required Return the full result of the most recent completed sync.
Returns the cached result of the last successfully completed sync without triggering a new one. The dashboard uses this to pick up results from auto-scheduled syncs. Returns { "available": false } if no sync has completed since the server started.
200 OK (data available)
{
  "timestamp": "2026-04-17T10:00:45Z",
  "sync_id": "a3f8b2c1d4e5f607",
  "devices": [ ... ],
  "gw_devices": [ ... ],
  "sources": {
    "jumpcloud": { "count": 312, "status": "ok" },
    "kandji":    { "count": 287, "status": "ok" }
  },
  "errors": [],
  "summary": {
    "total": 412,
    "ok": 380,
    "discrepancy": 18,
    "unmanaged": 14
  },
  "timings_ms": {
    "jumpcloud": 2840,
    "kandji":    1920
  }
}
200 OK (no data yet)
{
  "available": false
}
FieldTypeDescription
serialstringNormalised serial number (uppercase, stripped).
hostnamestringDevice hostname from source.
usernamestringAssigned user email.
platformstringOS family: macOS, Windows, Linux, ChromeOS.
os_versionstringOS version string from MDM.
sourcestringPrimary source: jumpcloud, kandji, google_workspace, sentinelone.
statusstringok | discrepancy | unmanaged | outdated_info | unused | phantom.
issuesstring[]List of detected issues for this device.
confidenceintRecord confidence score 0-100.
is_staleboolTrue if last seen more than 30 days ago.
encryption_statusstringEnabled | Disabled | Unknown (macOS/Windows only).
snipe_asset_tagstringSnipe-IT asset tag, empty if not in Snipe-IT.
snipe_statusstringSnipe-IT deployment status label.
last_seenstringISO 8601 timestamp from source.
bash
curl http://localhost:5050/api/last \
  -H "X-Dashboard-Secret: YOUR_SECRET"
GET /api/history Auth required List historical sync run summaries from the database.
Returns lightweight summaries of past sync runs stored in the SQLite database. One record is kept per day (latest sync of the day). Returns 503 if the database is unavailable.
200 OK
{
  "available": true,
  "count": 14,
  "history": [
    {
      "id": 42,
      "sync_id": "a3f8b2c1d4e5f607",
      "timestamp": "2026-04-17T10:00:45Z",
      "total": 412,
      "ok": 380,
      "discrepancy": 18,
      "unmanaged": 14,
      "error_count": 0
    }
  ]
}
bash
curl http://localhost:5050/api/history \
  -H "X-Dashboard-Secret: YOUR_SECRET"
GET /api/config Auth required Return auto-sync configuration.
Returns server-side configuration exposed to the frontend. Currently exposes the auto-sync interval set via the AUTO_SYNC_INTERVAL_MINUTES environment variable. Returns null if auto-sync is not configured.
200 OK (configured)
{
  "auto_sync_interval_minutes": 30
}
200 OK (not configured)
{
  "auto_sync_interval_minutes": null
}
bash
curl http://localhost:5050/api/config \
  -H "X-Dashboard-Secret: YOUR_SECRET"
POST /api/writeback Auth required Disabled Create unmanaged devices in Snipe-IT. Currently returns 503.
Intended to create new asset records in Snipe-IT for devices that are active in MDM but absent from Snipe-IT (status: unmanaged). This endpoint is currently disabled while the implementation is under review. All requests return 503 Service Unavailable regardless of input.
503 Service Unavailable
{
  "error": "Writeback to Snipe-IT is currently disabled"
}
application/json
{
  "serials": ["ABC123", "XYZ789"]
}
bash
curl -X POST http://localhost:5050/api/writeback \
  -H "X-Dashboard-Secret: YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"serials": ["ABC123"]}'

Feature endpoints

Session-authenticated JSON endpoints behind the dashboard features. R = any role, Op = IT/Administration. All mutations require the CSRF header the frontend sends automatically.

Annotations

  • GET /api/annotations — all active annotations grouped by serial, plus tag vocabulary. R
  • POST /api/annotations/create — bulk create: {serials, type, content?, expires_days?}. Op
  • POST /api/annotations/<id>/delete — remove one (the un-exclude path). Op

People

  • GET /api/people/profile?email= — directory record + seats + devices + flags. R
  • GET /api/people/offboarding?email= / POST /api/people/offboarding/<id> — persistent checklist; ticks are audited. R / Op
  • GET /api/people/onboarding?email= — tools a department majority holds that this person lacks. R
  • GET /api/people/service-accounts / POST /api/people/service-accounts/set — flag list / manual flagging (audited). R / Op

Costs

  • GET /api/costs/summary|per-user|per-department|waste|renewals|trend|savings — the Costs tabs. R
  • GET /api/costs/tools / POST /api/costs/tools/create — vendor list with tiers / the wizard. R / Op
  • GET /api/costs/negotiation-pack?source_key= — the renewal one-pager. R
  • GET /api/costs/price-history?source_key= — automatic price-change log. R

Fleet & audit

  • GET /api/device-timeline?serial= — recorded transitions for one device. R
  • GET /api/machine-swaps?stale_days= — recovery candidates. R
  • GET /api/department-compliance — coverage by division (keys say department), honest about attribution; ?department= adds the device rows and the per-department teams breakdown. R
  • GET|POST /api/costs/directory-guard — the one-shot pass through the Okta directory roster guard (Settings → Operations). A
  • GET|POST /api/costs/directory-run — run the Okta directory feed now / see what the last run did. A
  • POST /api/audit/evidence-pack{period_days} → ZIP download; audited. R

Platform

  • GET /api/status — version, sync, backup age, alert queue, last webhook. R
  • GET /api/notifications — latest 50 alerts for the bell. R
  • GET /api/preferences?key= / POST /api/preferences/set — per-user UI state (saved-views, overview-layout, notification-reads, custom-presets, pinned-filters, hidden-cols). R
  • POST /api/export/devices.xlsx — server-built workbook of the caller's filtered serial set; audited. R
  • GET /api/audit/log — merged human-activity feed (model changes + audit events). Admin
  • GET/POST /api/sync-config[/set] — DB-backed sync schedule (interval, pause); the hourly CronJob tick obeys it. Audited. Admin
  • POST /api/sync with {"source": "kandji"} — single-source retry; other sources carry forward. Op
  • GET /api/sync-timings — per-source fetch durations, last 30 runs. Admin
  • GET /api/hygiene — data-quality worklist (missing serials, no department, hostname violations, dupes). Op
  • GET /api/webhook-receipts — feed delivery log (connectors + webhooks). Admin
  • GET /styleguide — living design styleguide rendered off the production CSS tokens. Admin
  • GET /docs/api — interactive Swagger reference for the v1 bearer-token API (spec: /static/openapi.yaml). R
  • GET /api/sessions / POST /api/sessions/revoke — active-session list and revocation (own; all with ?all=1 for admins). Revocations audited. R
  • GET /api/devices/<serial>/annotation-history — audit trail of one device's annotations. R
  • GET /api/tokens / POST /api/tokens/create / POST /api/tokens/<id>/revoke — API token lifecycle; audited. Op

Integration API (v1) — bearer tokens

For scripts and external dashboards. Authenticate with Authorization: Bearer itam_… (create tokens in the settings menu). Strictly read-only: token auth never creates a session and only GET endpoints accept it. Versioned under /api/v1/ so it stays stable while the dashboard's internal endpoints evolve.

  • GET /api/v1/summary — device counts by status + the cost headline. The monitoring endpoint.
  • GET /api/v1/devices?status=&serial=&offset=&limit= — reconciled devices (max 500/page).
  • GET /api/v1/seats?email=&tool=&offset=&limit= — classified software seats.

401 means missing/invalid/revoked token. There is no v1 write surface, by design.

Costs & Waste — how to read the numbers
The Costs tab tracks what every software seat costs and flags the ones that look like wasted money. Every assigned seat gets exactly one label:

active — the person exists in Okta and used the tool recently. All good, not waste.

dormant — the person exists, but hasn't used the tool in over 60 days (configurable). We're paying for a seat nobody uses. Counted as waste.

orphaned — the seat belongs to someone who left the company (deprovisioned in Okta, or not in the directory at all). Counted as waste.

pending — the invite was never accepted, but the vendor already assigned (and bills) the licence. Counted as waste once the invite is older than the dormancy window.

unknown — the vendor doesn't tell us usage data, so we can't say whether the seat is used. Shown separately, never counted as waste (we don't guess).

unprovisioned — only appears for tools explicitly configured to allow non-directory accounts (e.g. service accounts). Informational.

Estimated Waste = dormant + orphaned + aged pending only. Unverifiable seats never inflate it, so the number shown to management is defensible. Costs in foreign currencies convert via FX rates that refresh automatically (ECB); anything that can't be converted is excluded from the total and flagged, never silently mis-summed.

Waste — every wasted seat, who, why and what it costs. Click a person for everything they hold (licenses + laptops) and a copy-paste offboarding checklist.

Renewals — contracts renewing in the next 60 days, with a suggested seat count to renegotiate at. Downloadable as a calendar (.ics).

People / Departments — spend per person and per team, with an audit-logged access-review CSV export for compliance.

Trend — monthly spend and waste over time.

Savings — every wasted seat that got removed and how much it saves per month, accumulated by quarter. This is the tool's proof of value.