Azure Artifacts REST API for Automation
A comprehensive guide to automating Azure Artifacts operations using the REST API, covering feed management, package operations, version control, permissions, retention automation, and building custom tooling for package lifecycle management.
Azure Artifacts REST API for Automation
Overview
The Azure Artifacts REST API gives you programmatic control over everything you can do in the Azure DevOps portal -- creating feeds, publishing packages, managing permissions, querying package metadata, and configuring retention policies. If you are managing more than a handful of feeds across an organization, or if you need to integrate package operations into custom workflows, the REST API is how you scale beyond the portal UI.
I use the REST API for everything from automated cleanup scripts that run weekly to custom dashboards that track package usage across the organization. The API is well-structured and consistent across package types (NuGet, npm, Maven, PyPI, Universal Packages), which means once you learn the patterns for one ecosystem, the others follow the same conventions. This article covers the complete API surface with working examples for every common operation.
Prerequisites
- An Azure DevOps organization with Azure Artifacts enabled
- A Personal Access Token (PAT) with Packaging (Read & Write) scope
- Node.js 18+ for the example scripts
- Basic understanding of REST APIs and HTTP methods
- Familiarity with Azure Artifacts concepts (feeds, views, upstream sources)
API Authentication
All Azure Artifacts API calls require authentication. The two methods are Personal Access Tokens (PATs) and OAuth tokens.
PAT Authentication
PATs use HTTP Basic authentication with an empty username:
// auth-helper.js -- Shared authentication module
var https = require("https");
var config = {
org: process.env.AZURE_DEVOPS_ORG || "my-organization",
project: process.env.AZURE_DEVOPS_PROJECT || "my-project",
pat: process.env.AZURE_DEVOPS_PAT
};
if (!config.pat) {
console.error("Error: AZURE_DEVOPS_PAT environment variable is required");
process.exit(1);
}
var auth = Buffer.from(":" + config.pat).toString("base64");
function apiRequest(method, hostname, path, body, callback) {
var bodyStr = body ? JSON.stringify(body) : null;
var options = {
hostname: hostname,
path: path,
method: method,
headers: {
"Content-Type": "application/json",
"Authorization": "Basic " + auth,
"Accept": "application/json"
}
};
if (bodyStr) {
options.headers["Content-Length"] = Buffer.byteLength(bodyStr);
}
var req = https.request(options, function(res) {
var data = "";
res.on("data", function(chunk) { data += chunk; });
res.on("end", function() {
var parsed = null;
try { parsed = JSON.parse(data); } catch (e) { parsed = data; }
callback(null, res.statusCode, parsed);
});
});
req.on("error", function(err) { callback(err); });
if (bodyStr) req.write(bodyStr);
req.end();
}
module.exports = {
config: config,
request: apiRequest
};
OAuth Token Authentication (Pipelines)
In Azure Pipelines, use $(System.AccessToken):
steps:
- script: |
curl -H "Authorization: Bearer $(System.AccessToken)" \
"https://feeds.dev.azure.com/my-org/_apis/packaging/feeds?api-version=7.1"
displayName: Query feeds with OAuth
API Base URLs
Azure Artifacts uses two base hostnames:
| Hostname | Purpose |
|---|---|
feeds.dev.azure.com |
Feed management, package listing, permissions, retention |
pkgs.dev.azure.com |
Package operations (publish, download, promote, delete) |
The path structure is consistent:
# Organization-scoped
https://feeds.dev.azure.com/{org}/_apis/packaging/feeds
# Project-scoped
https://feeds.dev.azure.com/{org}/{project}/_apis/packaging/feeds
Feed Management API
List All Feeds
// list-feeds.js
var api = require("./auth-helper");
var path = "/" + api.config.org + "/_apis/packaging/feeds?api-version=7.1";
api.request("GET", "feeds.dev.azure.com", path, null, function(err, status, data) {
if (err) return console.error("Error:", err.message);
console.log("Feeds (" + data.count + " total):");
data.value.forEach(function(feed) {
var scope = feed.project ? feed.project.name : "Organization";
console.log(" " + feed.name + " [" + scope + "] -- " + (feed.packageCount || 0) + " packages");
});
});
Create a Feed
// create-feed.js
var api = require("./auth-helper");
var feedName = process.argv[2];
var feedDescription = process.argv[3] || "";
if (!feedName) {
console.error("Usage: node create-feed.js <name> [description]");
process.exit(1);
}
var feedDefinition = {
name: feedName,
description: feedDescription,
hideDeletedPackageVersions: true,
upstreamEnabled: true,
upstreamSources: [
{ name: "npmjs", protocol: "npm", location: "https://registry.npmjs.org/", upstreamSourceType: "public" },
{ name: "NuGet Gallery", protocol: "nuget", location: "https://api.nuget.org/v3/index.json", upstreamSourceType: "public" },
{ name: "PyPI", protocol: "pypi", location: "https://pypi.org/", upstreamSourceType: "public" },
{ name: "Maven Central", protocol: "maven", location: "https://repo.maven.apache.org/maven2/", upstreamSourceType: "public" }
]
};
var path = "/" + api.config.org + "/" + api.config.project + "/_apis/packaging/feeds?api-version=7.1";
api.request("POST", "feeds.dev.azure.com", path, feedDefinition, function(err, status, data) {
if (err) return console.error("Error:", err.message);
if (status === 201) {
console.log("Feed created: " + data.name);
console.log("ID: " + data.id);
} else if (status === 409) {
console.log("Feed already exists: " + feedName);
} else {
console.error("Failed (" + status + "):", JSON.stringify(data));
}
});
Update Feed Settings
// update-feed.js
var api = require("./auth-helper");
var feedId = process.argv[2];
if (!feedId) {
console.error("Usage: node update-feed.js <feedName>");
process.exit(1);
}
// Get current feed state
var getPath = "/" + api.config.org + "/" + api.config.project +
"/_apis/packaging/feeds/" + feedId + "?api-version=7.1";
api.request("GET", "feeds.dev.azure.com", getPath, null, function(err, status, feed) {
if (err) return console.error("Error:", err.message);
if (status !== 200) return console.error("Feed not found:", feedId);
// Modify settings
feed.description = "Updated: " + new Date().toISOString().split("T")[0];
feed.hideDeletedPackageVersions = true;
var updatePath = "/" + api.config.org + "/" + api.config.project +
"/_apis/packaging/feeds/" + feedId + "?api-version=7.1";
api.request("PATCH", "feeds.dev.azure.com", updatePath, feed, function(err, status, result) {
if (err) return console.error("Error:", err.message);
if (status === 200) {
console.log("Feed updated: " + feedId);
} else {
console.error("Update failed (" + status + "):", JSON.stringify(result));
}
});
});
Delete a Feed
// delete-feed.js
var api = require("./auth-helper");
var feedId = process.argv[2];
if (!feedId) {
console.error("Usage: node delete-feed.js <feedName>");
process.exit(1);
}
var path = "/" + api.config.org + "/" + api.config.project +
"/_apis/packaging/feeds/" + feedId + "?api-version=7.1";
api.request("DELETE", "feeds.dev.azure.com", path, null, function(err, status) {
if (err) return console.error("Error:", err.message);
if (status === 204) {
console.log("Feed deleted: " + feedId);
} else {
console.error("Delete failed (" + status + ")");
}
});
Package Operations API
List Packages in a Feed
// list-packages.js
var api = require("./auth-helper");
var feedId = process.argv[2] || "my-packages";
var protocolType = process.argv[3]; // optional: NuGet, npm, PyPi, Maven, UPack
var path = "/" + api.config.org + "/" + api.config.project +
"/_apis/packaging/feeds/" + feedId + "/packages?api-version=7.1&$top=100";
if (protocolType) {
path += "&protocolType=" + protocolType;
}
api.request("GET", "feeds.dev.azure.com", path, null, function(err, status, data) {
if (err) return console.error("Error:", err.message);
console.log("Packages in " + feedId + " (" + data.count + " total):");
console.log("");
(data.value || []).forEach(function(pkg) {
var latest = pkg.versions[0];
console.log(" " + pkg.name);
console.log(" Protocol: " + pkg.protocolType);
console.log(" Latest: " + latest.version);
console.log(" Published: " + new Date(latest.publishDate).toLocaleDateString());
console.log(" Versions: " + pkg.versions.length);
console.log("");
});
});
Get Package Versions
// get-versions.js
var api = require("./auth-helper");
var feedId = process.argv[2];
var packageId = process.argv[3];
if (!feedId || !packageId) {
console.error("Usage: node get-versions.js <feedName> <packageId>");
process.exit(1);
}
var path = "/" + api.config.org + "/" + api.config.project +
"/_apis/packaging/feeds/" + feedId + "/packages/" + packageId +
"/versions?api-version=7.1&$top=50&isDeleted=false";
api.request("GET", "feeds.dev.azure.com", path, null, function(err, status, data) {
if (err) return console.error("Error:", err.message);
console.log("Versions (" + (data.value || []).length + "):");
(data.value || []).forEach(function(v) {
var views = (v.views || []).map(function(view) { return view.name; }).join(", ");
var viewStr = views ? " [" + views + "]" : "";
console.log(" " + v.version + viewStr +
" -- " + new Date(v.publishDate).toLocaleDateString());
});
});
Promote a Package to a Feed View
// promote-package.js
var api = require("./auth-helper");
var feedId = process.argv[2];
var protocol = process.argv[3]; // nuget, npm, pypi, maven, upack
var packageName = process.argv[4];
var version = process.argv[5];
var targetView = process.argv[6] || "Release";
if (!feedId || !protocol || !packageName || !version) {
console.error("Usage: node promote-package.js <feed> <protocol> <package> <version> [view]");
console.error("Example: node promote-package.js my-feed nuget MyLib 1.0.0 Release");
process.exit(1);
}
var body = {
views: {
op: "add",
path: "/views/-",
value: targetView
}
};
var path = "/" + api.config.org + "/" + api.config.project +
"/_apis/packaging/feeds/" + feedId + "/" + protocol +
"/packages/" + packageName + "/versions/" + version + "?api-version=7.1";
api.request("PATCH", "pkgs.dev.azure.com", path, body, function(err, status, data) {
if (err) return console.error("Error:", err.message);
if (status === 200) {
console.log("Promoted " + packageName + "@" + version + " to @" + targetView);
} else {
console.error("Failed (" + status + "):", JSON.stringify(data));
}
});
Unlist/Delete a Package Version
// unlist-package.js
var api = require("./auth-helper");
var feedId = process.argv[2];
var protocol = process.argv[3];
var packageName = process.argv[4];
var version = process.argv[5];
if (!feedId || !protocol || !packageName || !version) {
console.error("Usage: node unlist-package.js <feed> <protocol> <package> <version>");
process.exit(1);
}
var body = { listed: false };
var path = "/" + api.config.org + "/" + api.config.project +
"/_apis/packaging/feeds/" + feedId + "/" + protocol +
"/packages/" + packageName + "/versions/" + version + "?api-version=7.1";
api.request("PATCH", "pkgs.dev.azure.com", path, body, function(err, status) {
if (err) return console.error("Error:", err.message);
if (status === 200) {
console.log("Unlisted " + packageName + "@" + version);
} else {
console.error("Failed (" + status + ")");
}
});
Permissions API
Get Feed Permissions
// get-permissions.js
var api = require("./auth-helper");
var feedId = process.argv[2] || "my-packages";
var path = "/" + api.config.org + "/" + api.config.project +
"/_apis/packaging/feeds/" + feedId + "/permissions?api-version=7.1";
api.request("GET", "feeds.dev.azure.com", path, null, function(err, status, data) {
if (err) return console.error("Error:", err.message);
var roleMap = {
reader: "Reader",
collaborator: "Collaborator",
contributor: "Contributor",
administrator: "Owner"
};
console.log("Permissions for " + feedId + ":");
(data.value || []).forEach(function(perm) {
var name = perm.displayName || "Unknown";
var role = roleMap[perm.role] || perm.role;
console.log(" " + name + " --> " + role);
});
});
Set Feed Permissions
// set-permissions.js
var api = require("./auth-helper");
var feedId = process.argv[2];
var identityDescriptor = process.argv[3];
var role = process.argv[4]; // reader, collaborator, contributor, administrator
if (!feedId || !identityDescriptor || !role) {
console.error("Usage: node set-permissions.js <feed> <identity> <role>");
process.exit(1);
}
var body = [{
identityDescriptor: identityDescriptor,
role: role
}];
var path = "/" + api.config.org + "/" + api.config.project +
"/_apis/packaging/feeds/" + feedId + "/permissions?api-version=7.1";
api.request("PATCH", "feeds.dev.azure.com", path, body, function(err, status) {
if (err) return console.error("Error:", err.message);
if (status === 200) {
console.log("Permission set: " + role + " for " + identityDescriptor);
} else {
console.error("Failed (" + status + ")");
}
});
Retention Policy API
Get Retention Policy
// get-retention.js
var api = require("./auth-helper");
var feedId = process.argv[2] || "my-packages";
var path = "/" + api.config.org + "/" + api.config.project +
"/_apis/packaging/feeds/" + feedId + "/retentionpolicies?api-version=7.1";
api.request("GET", "feeds.dev.azure.com", path, null, function(err, status, data) {
if (err) return console.error("Error:", err.message);
if (status === 200) {
console.log("Retention policy for " + feedId + ":");
console.log(" Max versions per package: " + (data.countLimit || "unlimited"));
console.log(" Days to keep downloaded: " + (data.daysToKeepRecentlyDownloadedPackages || "unlimited"));
} else {
console.log("No retention policy set for " + feedId);
}
});
Set Retention Policy
// set-retention.js
var api = require("./auth-helper");
var feedId = process.argv[2];
var maxVersions = parseInt(process.argv[3]) || 30;
var daysToKeep = parseInt(process.argv[4]) || 30;
if (!feedId) {
console.error("Usage: node set-retention.js <feed> [maxVersions] [daysToKeep]");
process.exit(1);
}
var policy = {
countLimit: maxVersions,
daysToKeepRecentlyDownloadedPackages: daysToKeep
};
var path = "/" + api.config.org + "/" + api.config.project +
"/_apis/packaging/feeds/" + feedId + "/retentionpolicies?api-version=7.1";
api.request("PUT", "feeds.dev.azure.com", path, policy, function(err, status) {
if (err) return console.error("Error:", err.message);
if (status === 200) {
console.log("Retention policy set for " + feedId + ":");
console.log(" Max versions: " + maxVersions);
console.log(" Days to keep downloaded: " + daysToKeep);
} else {
console.error("Failed (" + status + ")");
}
});
Complete Working Example
This is a comprehensive CLI tool that wraps the most common Azure Artifacts API operations into a single utility:
// az-artifacts-cli.js -- Complete Azure Artifacts management CLI
var https = require("https");
var config = {
org: process.env.AZURE_DEVOPS_ORG || "my-organization",
project: process.env.AZURE_DEVOPS_PROJECT || "my-project",
pat: process.env.AZURE_DEVOPS_PAT
};
if (!config.pat) {
console.error("Error: AZURE_DEVOPS_PAT environment variable is required");
console.error("Optional: AZURE_DEVOPS_ORG, AZURE_DEVOPS_PROJECT");
process.exit(1);
}
var auth = Buffer.from(":" + config.pat).toString("base64");
function api(method, hostname, path, body, callback) {
var bodyStr = body ? JSON.stringify(body) : null;
var options = {
hostname: hostname,
path: path,
method: method,
headers: {
"Content-Type": "application/json",
"Authorization": "Basic " + auth,
"Accept": "application/json"
}
};
if (bodyStr) options.headers["Content-Length"] = Buffer.byteLength(bodyStr);
var req = https.request(options, function(res) {
var data = "";
res.on("data", function(chunk) { data += chunk; });
res.on("end", function() {
var parsed;
try { parsed = JSON.parse(data); } catch (e) { parsed = data; }
callback(null, res.statusCode, parsed);
});
});
req.on("error", function(err) { callback(err); });
if (bodyStr) req.write(bodyStr);
req.end();
}
function feedsPath(suffix) {
return "/" + config.org + "/" + config.project + "/_apis/packaging/feeds" + (suffix || "") + "?api-version=7.1";
}
function pkgsPath(feedId, suffix) {
return "/" + config.org + "/" + config.project + "/_apis/packaging/feeds/" + feedId + suffix + "?api-version=7.1";
}
// Commands
var commands = {
"feeds": function() {
api("GET", "feeds.dev.azure.com", feedsPath(), null, function(err, status, data) {
if (err) return console.error("Error:", err.message);
console.log("Feeds (" + data.count + "):");
(data.value || []).forEach(function(f) {
var scope = f.project ? f.project.name : "Org";
var upstream = f.upstreamEnabled ? "upstream" : "no-upstream";
console.log(" " + f.name + " [" + scope + "] " + (f.packageCount || 0) + " pkgs (" + upstream + ")");
});
});
},
"packages": function(args) {
var feedId = args[0];
if (!feedId) return console.error("Usage: packages <feed> [protocolType]");
var proto = args[1];
var suffix = "/packages?api-version=7.1&$top=100";
if (proto) suffix += "&protocolType=" + proto;
api("GET", "feeds.dev.azure.com",
"/" + config.org + "/" + config.project + "/_apis/packaging/feeds/" + feedId + suffix,
null, function(err, status, data) {
if (err) return console.error("Error:", err.message);
(data.value || []).forEach(function(pkg) {
var v = pkg.versions[0];
console.log(" " + pkg.name + " " + v.version + " (" + pkg.protocolType + ")");
});
console.log("\nTotal: " + data.count);
});
},
"versions": function(args) {
var feedId = args[0];
var pkgName = args[1];
if (!feedId || !pkgName) return console.error("Usage: versions <feed> <packageName>");
// First find the package ID
var searchPath = "/" + config.org + "/" + config.project +
"/_apis/packaging/feeds/" + feedId + "/packages?api-version=7.1&packageNameQuery=" + pkgName;
api("GET", "feeds.dev.azure.com", searchPath, null, function(err, status, data) {
if (err) return console.error("Error:", err.message);
if (!data.value || data.value.length === 0) return console.log("Package not found: " + pkgName);
var pkg = data.value[0];
var vPath = "/" + config.org + "/" + config.project +
"/_apis/packaging/feeds/" + feedId + "/packages/" + pkg.id +
"/versions?api-version=7.1&$top=50";
api("GET", "feeds.dev.azure.com", vPath, null, function(err, status, vData) {
if (err) return console.error("Error:", err.message);
console.log("Versions of " + pkg.name + ":");
(vData.value || []).forEach(function(v) {
var views = (v.views || []).map(function(vw) { return "@" + vw.name; }).join(" ");
console.log(" " + v.version + " " + views +
" (" + new Date(v.publishDate).toLocaleDateString() + ")");
});
});
});
},
"promote": function(args) {
var feedId = args[0];
var protocol = args[1];
var pkgName = args[2];
var version = args[3];
var view = args[4] || "Release";
if (!feedId || !protocol || !pkgName || !version) {
return console.error("Usage: promote <feed> <protocol> <package> <version> [view]");
}
var body = { views: { op: "add", path: "/views/-", value: view } };
var path = "/" + config.org + "/" + config.project +
"/_apis/packaging/feeds/" + feedId + "/" + protocol +
"/packages/" + pkgName + "/versions/" + version + "?api-version=7.1";
api("PATCH", "pkgs.dev.azure.com", path, body, function(err, status) {
if (err) return console.error("Error:", err.message);
if (status === 200) console.log("Promoted " + pkgName + "@" + version + " to @" + view);
else console.error("Failed (" + status + ")");
});
},
"unlist": function(args) {
var feedId = args[0];
var protocol = args[1];
var pkgName = args[2];
var version = args[3];
if (!feedId || !protocol || !pkgName || !version) {
return console.error("Usage: unlist <feed> <protocol> <package> <version>");
}
var body = { listed: false };
var path = "/" + config.org + "/" + config.project +
"/_apis/packaging/feeds/" + feedId + "/" + protocol +
"/packages/" + pkgName + "/versions/" + version + "?api-version=7.1";
api("PATCH", "pkgs.dev.azure.com", path, body, function(err, status) {
if (err) return console.error("Error:", err.message);
if (status === 200) console.log("Unlisted " + pkgName + "@" + version);
else console.error("Failed (" + status + ")");
});
},
"retention": function(args) {
var feedId = args[0];
var action = args[1]; // "get" or "set"
if (!feedId) return console.error("Usage: retention <feed> [get|set] [maxVersions] [daysToKeep]");
var path = "/" + config.org + "/" + config.project +
"/_apis/packaging/feeds/" + feedId + "/retentionpolicies?api-version=7.1";
if (action === "set") {
var maxVersions = parseInt(args[2]) || 30;
var daysToKeep = parseInt(args[3]) || 30;
var policy = { countLimit: maxVersions, daysToKeepRecentlyDownloadedPackages: daysToKeep };
api("PUT", "feeds.dev.azure.com", path, policy, function(err, status) {
if (err) return console.error("Error:", err.message);
if (status === 200) console.log("Retention set: " + maxVersions + " versions, " + daysToKeep + " days");
else console.error("Failed (" + status + ")");
});
} else {
api("GET", "feeds.dev.azure.com", path, null, function(err, status, data) {
if (err) return console.error("Error:", err.message);
if (status === 200) {
console.log("Retention for " + feedId + ":");
console.log(" Max versions: " + (data.countLimit || "unlimited"));
console.log(" Days to keep: " + (data.daysToKeepRecentlyDownloadedPackages || "unlimited"));
} else {
console.log("No retention policy set");
}
});
}
},
"permissions": function(args) {
var feedId = args[0];
if (!feedId) return console.error("Usage: permissions <feed>");
var path = "/" + config.org + "/" + config.project +
"/_apis/packaging/feeds/" + feedId + "/permissions?api-version=7.1";
api("GET", "feeds.dev.azure.com", path, null, function(err, status, data) {
if (err) return console.error("Error:", err.message);
var roles = { reader: "Reader", collaborator: "Collaborator", contributor: "Contributor", administrator: "Owner" };
console.log("Permissions for " + feedId + ":");
(data.value || []).forEach(function(p) {
console.log(" " + (p.displayName || "Unknown") + " --> " + (roles[p.role] || p.role));
});
});
},
"report": function() {
api("GET", "feeds.dev.azure.com", feedsPath(), null, function(err, status, data) {
if (err) return console.error("Error:", err.message);
console.log("Azure Artifacts Report");
console.log("Organization: " + config.org);
console.log("Date: " + new Date().toISOString().split("T")[0]);
console.log("======================");
console.log("");
var totalPackages = 0;
(data.value || []).forEach(function(feed) {
var count = feed.packageCount || 0;
totalPackages += count;
var scope = feed.project ? feed.project.name : "Organization";
console.log(feed.name + " [" + scope + "]");
console.log(" Packages: " + count);
console.log(" Upstream: " + (feed.upstreamEnabled ? "enabled" : "disabled"));
console.log("");
});
console.log("======================");
console.log("Total feeds: " + data.count);
console.log("Total packages: " + totalPackages);
});
}
};
// Route command
var command = process.argv[2];
var args = process.argv.slice(3);
if (commands[command]) {
commands[command](args);
} else {
console.log("Azure Artifacts CLI");
console.log("");
console.log("Commands:");
console.log(" feeds List all feeds");
console.log(" packages <feed> [protocol] List packages in a feed");
console.log(" versions <feed> <package> List package versions");
console.log(" promote <feed> <proto> <pkg> <ver> [view] Promote to feed view");
console.log(" unlist <feed> <proto> <pkg> <ver> Unlist a version");
console.log(" retention <feed> get Show retention policy");
console.log(" retention <feed> set <max> <days> Set retention policy");
console.log(" permissions <feed> Show feed permissions");
console.log(" report Full organization report");
console.log("");
console.log("Protocols: nuget, npm, pypi, maven, upack");
console.log("");
console.log("Environment:");
console.log(" AZURE_DEVOPS_PAT Required");
console.log(" AZURE_DEVOPS_ORG Organization name");
console.log(" AZURE_DEVOPS_PROJECT Project name");
}
# List feeds
node az-artifacts-cli.js feeds
# List npm packages in a feed
node az-artifacts-cli.js packages my-feed npm
# Show versions of a package
node az-artifacts-cli.js versions my-feed MyCompany.SDK
# Promote a package to Release
node az-artifacts-cli.js promote my-feed nuget MyCompany.SDK 1.0.0 Release
# Set retention policy
node az-artifacts-cli.js retention my-feed set 20 14
# Generate report
node az-artifacts-cli.js report
Output from report:
Azure Artifacts Report
Organization: my-organization
Date: 2026-02-09
======================
shared-packages [Organization]
Packages: 47
Upstream: enabled
npm-packages [platform]
Packages: 132
Upstream: enabled
python-packages [data-engineering]
Packages: 23
Upstream: enabled
======================
Total feeds: 3
Total packages: 202
Common Issues and Troubleshooting
1. API Returns 401 Unauthorized
Error:
{"$id":"1","message":"TF400813: The user '' is not authorized to access this resource."}
The PAT is invalid, expired, or missing the Packaging scope. Verify your PAT at User Settings > Personal Access Tokens. For pipeline scripts using $(System.AccessToken), ensure the build service has permissions on the feed.
2. API Returns 404 Not Found for a Feed
Error:
{"$id":"1","message":"Feed 'my-feed' not found."}
Check whether the feed is organization-scoped or project-scoped. Organization-scoped feeds use the path without a project: /{org}/_apis/packaging/feeds/{feedId}. Project-scoped feeds require the project in the path: /{org}/{project}/_apis/packaging/feeds/{feedId}.
3. Rate Limiting (429 Too Many Requests)
Error:
HTTP 429: Rate limit exceeded. Retry after 30 seconds.
Azure DevOps enforces rate limits on API calls. Add retry logic with exponential backoff:
function apiWithRetry(method, hostname, path, body, retries, callback) {
api(method, hostname, path, body, function(err, status, data) {
if (status === 429 && retries > 0) {
var retryAfter = 30;
console.log("Rate limited. Retrying in " + retryAfter + "s...");
setTimeout(function() {
apiWithRetry(method, hostname, path, body, retries - 1, callback);
}, retryAfter * 1000);
} else {
callback(err, status, data);
}
});
}
4. Package Protocol Name Mismatch
Error:
{"$id":"1","message":"The protocol 'NuGet' is not supported by this endpoint."}
Protocol names in the API are case-sensitive and lowercase: use nuget, npm, pypi, maven, upack. Not NuGet, NPM, or PyPI.
5. Pagination Missing Results
Error: Listing packages returns only 100 results when you have 500.
The API defaults to returning 100 items. Use $top and $skip for pagination:
function getAllPackages(feedId, skip, allPackages, callback) {
var path = "/" + config.org + "/" + config.project +
"/_apis/packaging/feeds/" + feedId + "/packages?api-version=7.1&$top=100&$skip=" + skip;
api("GET", "feeds.dev.azure.com", path, null, function(err, status, data) {
if (err) return callback(err);
allPackages = allPackages.concat(data.value || []);
if (data.value && data.value.length === 100) {
getAllPackages(feedId, skip + 100, allPackages, callback);
} else {
callback(null, allPackages);
}
});
}
6. PATCH Operations Return 400 Bad Request
Error:
{"$id":"1","message":"The request body is invalid."}
The PATCH body format varies by operation. For promoting packages, the body is { views: { op: "add", path: "/views/-", value: "Release" } }. For unlisting, the body is { listed: false }. Check the API documentation for the exact format required by each endpoint.
Best Practices
Build a shared authentication module. Every script needs the same auth header. Extract it into a reusable module (like
auth-helper.jsabove) instead of duplicating it across scripts.Always handle pagination. The API returns at most 1,000 items per request (default 100). If you have more packages than that, you must paginate with
$topand$skip.Implement retry logic for production scripts. Rate limiting, transient errors, and network issues are common. Retry with exponential backoff for 429 and 5xx status codes.
Use
api-versionexplicitly in every request. Never omit the API version parameter. The default version may change between API releases, breaking your scripts.Prefer
$(System.AccessToken)over PATs in pipelines. System tokens are ephemeral and scoped. PATs are long-lived and over-scoped. Use the system token for all pipeline-based API calls.Cache feed metadata when making bulk operations. If you are processing hundreds of packages, fetch the feed and package list once and work from the cached data. Do not re-fetch the feed details for each package operation.
Log all destructive operations. Delete, unlist, and permission changes should be logged with timestamps and the identity that performed them. Your future self will thank you during incident investigations.
Test scripts against a non-production feed first. Create a test feed, publish some dummy packages, and run your scripts against it before pointing them at production feeds.
Use dry-run modes in cleanup scripts. Every script that modifies or deletes data should have a
--dry-runflag that shows what would happen without making changes. Run dry-run first, always.Version your automation scripts. Treat your Azure Artifacts management scripts as code. Put them in a repository, review changes, and version them. A broken cleanup script can delete production packages.