Artifacts

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

  1. Build a shared authentication module. Every script needs the same auth header. Extract it into a reusable module (like auth-helper.js above) instead of duplicating it across scripts.

  2. 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 $top and $skip.

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

  4. Use api-version explicitly in every request. Never omit the API version parameter. The default version may change between API releases, breaking your scripts.

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

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

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

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

  9. Use dry-run modes in cleanup scripts. Every script that modifies or deletes data should have a --dry-run flag that shows what would happen without making changes. Run dry-run first, always.

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

References

Powered by Contentful