REST API · v1

MSP License Tracker API

A read-only REST API for your tenant, license, and alert data — for PSA tools and custom integrations. Available on the Pro plan. All responses are JSON; all amounts are in your account currency.

Base URL: https://www.msplicensetracker.com

Authentication

Generate an API key under Settings → API access. The key (prefixed mlt_live_) is shown once — store it securely. Send it as a bearer token on every request:

curl https://www.msplicensetracker.com/api/v1/tenants \
  -H "Authorization: Bearer mlt_live_xxxxxxxxxxxxxxxx"

Requests without a valid, non-revoked key receive 401. Keys can be revoked anytime from the same settings page.

Rate limits

There is no hard rate limit today. Please keep automated polling reasonable — once per minute is ample, since tenant data refreshes on a daily sync cycle. Abusive traffic may be throttled without notice.

Endpoints

GET/api/v1/tenants

All connected tenants with their license, cost, and leakage summary.

Response — data array items

FieldTypeDescription
idstringTenant ID
displayNamestringTenant name
primaryDomainstring | nullPrimary domain
providerstringmicrosoft
connectionStatusstringconnected | error | pending | disconnected
totalLicensesnumberTotal purchased licenses
assignedLicensesnumberAssigned licenses
unassignedLicensesnumberUnassigned (unused) licenses
totalUsersnumberTotal users in the tenant
inactiveUserCountnumberLicensed users flagged inactive
estimatedMonthlyCostnumberEstimated monthly license cost
estimatedMonthlyLeakagenumberEstimated monthly waste
lastSyncAtstring | nullISO timestamp of the last sync

Example response

{
  "data": [
    {
      "id": "tnt_a1b2c3",
      "displayName": "Contoso Ltd",
      "primaryDomain": "contoso.com",
      "provider": "microsoft",
      "connectionStatus": "connected",
      "totalLicenses": 320,
      "assignedLicenses": 298,
      "unassignedLicenses": 22,
      "totalUsers": 305,
      "inactiveUserCount": 14,
      "estimatedMonthlyCost": 8420,
      "estimatedMonthlyLeakage": 1180,
      "lastSyncAt": "2026-05-22T02:14:00.000Z"
    }
  ]
}
GET/api/v1/summary

Account-wide totals across every connected tenant.

Response — data object

FieldTypeDescription
tenantsnumberCount of connected tenants
totalLicensesnumberTotal purchased licenses
assignedLicensesnumberTotal assigned licenses
unassignedLicensesnumberTotal unassigned licenses
inactiveUsersnumberTotal licensed users flagged inactive
estimatedMonthlyCostnumberTotal estimated monthly cost
estimatedMonthlyLeakagenumberTotal estimated monthly waste

Example response

{
  "data": {
    "tenants": 23,
    "totalLicenses": 4180,
    "assignedLicenses": 3790,
    "unassignedLicenses": 390,
    "inactiveUsers": 210,
    "estimatedMonthlyCost": 112400,
    "estimatedMonthlyLeakage": 14820
  }
}
GET/api/v1/alerts

The 100 most recent alerts, newest first.

Response — data array items

FieldTypeDescription
idstringAlert ID
typestringseat_increase | seat_decrease | inactive_users | renewal_due | …
severitystringcritical | warning | info
titlestringShort alert title
messagestringFull alert detail
statusstringunread | read | dismissed | resolved
createdAtstringISO timestamp
tenantobject | null{ id, displayName } of the related tenant

Example response

{
  "data": [
    {
      "id": "alt_x1y2z3",
      "type": "renewal_due",
      "severity": "warning",
      "title": "Microsoft 365 E3 renews in 14 days — 18 unused seats",
      "message": "Microsoft 365 E3 renews on June 5, 2026. 18 of 50 seats …",
      "status": "unread",
      "createdAt": "2026-05-22T09:00:00.000Z",
      "tenant": { "id": "tnt_a1b2c3", "displayName": "Contoso Ltd" }
    }
  ]
}

Errors

Non-200 responses return a JSON body of the form { "error": "…" }.

StatusMeaning
200Success. Payload under the data key.
401Missing, malformed, revoked, or unknown API key.
403Valid key, but the account's plan does not include API access.
5xxUnexpected server error — retry with backoff.

Webhooks

Set a webhook URL under Settings → API access to receive an alert.created event whenever an alert fires. The request body:

{
  "event": "alert.created",
  "alert": {
    "id": "alt_x1y2z3",
    "type": "renewal_due",
    "severity": "warning",
    "title": "Microsoft 365 E3 renews in 14 days — 18 unused seats",
    "message": "Microsoft 365 E3 renews on June 5, 2026 …",
    "tenant": "Contoso Ltd",
    "createdAt": "2026-05-22T09:00:00.000Z"
  },
  "deliveredAt": "2026-05-22T09:00:01.420Z"
}

Verifying the signature

Every request carries an X-MLT-Signature header — sha256=<hmac>. Compute an HMAC-SHA256 of the raw request body using your signing secret and compare before trusting the payload:

import { createHmac, timingSafeEqual } from "crypto";

function verify(rawBody, signatureHeader, signingSecret) {
  const expected =
    "sha256=" + createHmac("sha256", signingSecret).update(rawBody).digest("hex");
  const a = Buffer.from(signatureHeader);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

Need something not covered here? Contact support.