Connect Azure DevOps
Connect an Azure DevOps organisation with one read-only personal access token so leancosts can show you the Basic seats nobody has opened in 90 days, priced from what your own Azure bill charges for them. leancosts is read-only here as everywhere: every call it makes to Azure DevOps is a GET. For the full posture see Security and data handling.
This connection brings in a seat roster, not a bill. Your Azure DevOps money is already in leancosts: the Azure connector ingests those meters like any other Azure charge. The token is what lets us say which seats that money is paying for.
Before you start
Section titled “Before you start”- You need to be able to create a personal access token in the Azure DevOps organisation, and the connections-manage permission in leancosts to add a connection.
- If your Azure bill already charges for Azure DevOps, Admin → Connections
says so at the top of the page: “Azure DevOps: … on the Azure bill in
”, with a Connect Azure DevOps button. Dismiss for everyone hides it for the whole workspace, and Undo brings it back. - Azure DevOps support is rolling out. If the Azure DevOps card in the Add-connection picker is disabled and reads “Not enabled in this deployment”, there is nothing to set up yet; it opens when it is ready.
What leancosts reads, and what the token allows
Section titled “What leancosts reads, and what the token allows”Read this before you paste a token. The connect form repeats it.
- What we read. The organisation’s user entitlements: each member’s access level, licence type, creation date, and Azure DevOps’s own last-access date. Identities are hashed before they are stored: no name, no email address and no descriptor is kept, and every finding is a count, never a person.
- What the token can do. A personal access token for this one organisation
with read-only scopes: Member Entitlement Management (read) for the seat
roster, plus Test Management (read), Build (read)
and Project and Team (read) for the Test Plans and parallel job findings.
Without the last three the connection still works and those findings name the
missing scope.
leancosts calls only GET endpoints:
_apis/connectionData,_apis/userentitlementsummaryand_apis/userentitlements. It never creates, edits or deletes anything. - How it is stored. AES-256-GCM encrypted, the same primitive Azure service-principal secrets use, and never logged or echoed back.
1. Create the personal access token
Section titled “1. Create the personal access token”In Azure DevOps, open User settings → Personal access tokens → New Token
(https://dev.azure.com/<your-organisation>/_usersSettings/tokens).
- Organization: pick the organisation you want to connect, not “All accessible organizations”. A token is organisation-scoped and cannot see any other one.
- Scopes: choose Custom defined, then set exactly one: Member Entitlement Management, Read. Nothing else is needed.
- Expiration: tokens expire. Note the date you pick: Azure DevOps does not expose a token’s expiry to the token itself, so leancosts cannot read it back the way it does for Azure and GitHub. Enter it when you add the connection, or add it later in the connection’s Settings, and leancosts counts it down for you. Rotate before the day, or the sync stops with an authentication error.
- Create the token and copy it once; Azure DevOps will not show it again.
2. Add the connection
Section titled “2. Add the connection”- In leancosts go to Admin → Connections and click Add connection.
- Pick Azure DevOps.
- Choose your organisation from the list. The amount beside each one is what your Azure ledger charged it over the last three billing periods, so you can tell two similarly named organisations apart. One that is already connected is greyed out.
- Give the connection a nickname, pick a sync schedule (default every 6 hours), paste the token and click Create connection.
- The row shows Validating… while leancosts proves the token against the organisation, then Connected. Test re-runs that check any time and reports the organisation with its Basic seat split.
The connection tells you when the token expires, under Open → Settings → Credential. Azure DevOps does not report a token’s expiry, so the date shown is the one you entered, always labelled as entered. With none recorded it reads Personal access token expiry not recorded and offers Add date, which stores a date without asking you for the token. Inside 30 days a badge appears on the connection row too, warning at 14 days and urgent at 3. The savings digest carries the same warning to everyone who can manage connections.
If your organisation is not in the list
Section titled “If your organisation is not in the list”The list comes from your Azure bill, so an empty or incomplete list means the bill has not arrived yet, not that something is broken. Work through it in this order:
- Connect the Azure subscription that pays for the organisation, under Admin → Connections, and let it sync. See Connect a cloud account.
- Give the cost sync time to land at least one billing period. Azure DevOps charges appear as the service Azure DevOps.
- Reopen Add connection. The organisation appears on its own.
If you submit an organisation the ledger has not charged, leancosts refuses it and says so:
No Azure DevOps charges for "example-org" in the last 3 billing periods.Connect the Azure account that bills this organization first.3. What to expect after the first sync
Section titled “3. What to expect after the first sync”The sync has two phases and takes seconds, not minutes.
- Entitlement summary: how many Basic seats are assigned, and how many of them your plan includes for free.
- Entitlement roster: every member, with its licence type and last-access date. Members no longer listed are marked as gone rather than deleted, so the history stays honest.
Afterwards:
- The connection row’s meta line reads
<organisation> · <N> Basic assigned · <M> included free, and the connection’s Settings → Billing names the trailing Azure charge and the billing subscription. - The Savings Register gains at most one Azure DevOps finding per connection: paid Basic seats with no activity in the last 90 days. It is filed under Azure, because that is where the money is. If you also pay for Basic + Test Plans seats, a second finding covers them: seats idle for 90 days, or with no test run and no plan edit in 30 days, are priced as a move to Basic, and the rest appear at $0 as “confirm they use Test Plans”, because viewing a plan leaves no record. If you buy Microsoft-hosted parallel jobs, a third finding compares them with the most that ran at once in 30 days of builds. It is review-only, because fewer jobs queue builds above the new count.
- The free seats are never claimed. Only the seats above your included quantity are, so an organisation inside its free tier produces no finding however idle it is.
- The price is your own bill divided by your paid seats, in the currency the bill used. No vendor list price is ever used, and if the period’s charges span two currencies the finding is withheld rather than mixed. A month your Azure sync has only partly ingested is withheld too, because dividing part of a month by your seats would price a seat below what you actually pay.
- Recommended action, when a finding appears: move those users to Stakeholder (free) or remove them. The first included seats stay free either way.
Verify
Section titled “Verify”Check the seat split yourself with the same GET the connection test makes:
ORG=example-orgcurl -sS -u ":$AZDO_PAT" \ "https://vsaex.dev.azure.com/$ORG/_apis/userentitlementsummary?api-version=7.1-preview.1" \ | jq '.licenses[] | select(.accountLicenseType == "express") | {assigned, includedQuantity}'The assigned and includedQuantity it prints are exactly the two numbers on
the connection row. assigned − includedQuantity is what you are actually
paying for, and the only pool leancosts will ever claim from.
Removing a connection
Section titled “Removing a connection”Open → Settings → Remove purges the entitlement roster this connection ingested. Your Azure cost lines are untouched: they belong to the Azure connector. Delete the token in Azure DevOps afterwards.