How to Publish a NuGet Package to NuGet.org
There's a satisfying moment when your .NET library is finally ready to share. The code is solid, the public API feels right, and you want other developers to run dotnet add package YourLibrary and get to work. That moment means it's time to publish NuGet package files to NuGet.org -- and this guide walks through every step to make it happen.
NuGet.org is the default public package registry for the .NET ecosystem. It's where Microsoft distributes foundational packages like System.Text.Json and Microsoft.Extensions.DependencyInjection, and it's where thousands of community contributors share their own libraries every day. The process to publish NuGet package releases starts with an account, moves through API key generation and dotnet nuget push, and ends with a short wait for NuGet.org's validation pipeline to clear. There's no magic -- just a handful of concrete steps.
Before you get started, you'll need a .nupkg file built from your project. If you haven't run dotnet pack yet or need to set up your .csproj packaging metadata, The Complete Guide to Creating NuGet Packages in .NET covers all of that. This guide picks up right where package creation leaves off.
Creating a NuGet.org Account
Publishing a NuGet package to NuGet.org starts with an account. Head to https://www.nuget.org and click Sign in. NuGet.org authenticates through Microsoft accounts -- any Outlook, Hotmail, or Microsoft Entra ID (work/school) account works. If you have a Microsoft account already, you don't need to create anything new.
After signing in for the first time, NuGet.org prompts you to register a username. This username becomes your package ownership identity. It shows up on your package pages and in search results, so pick something that clearly represents you or your organization. You cannot change the username after registration, so take an extra second before confirming.
Once the username is set, enable two-factor authentication under Account Settings before moving on. API keys are about to become part of your workflow, and 2FA on the account that owns them is a sensible baseline -- especially before you publish NuGet package versions that others will depend on.
Generating an API Key
To publish NuGet package output from the command line, you need an API key. This credential authenticates push requests without requiring your Microsoft account password in a terminal session.
Navigate to Account Settings → API Keys → Create on NuGet.org.
The most important decision during key creation is the scope. NuGet.org offers two permission levels:
- Push new packages and package versions -- can create brand-new package IDs and push new versions of existing ones
- Push only new versions of existing packages -- restricted to updating packages already associated with your account
For your first push of a new package, you need the broader scope since the package ID doesn't exist yet. Once the package is live, switch to a narrower-scoped key for ongoing version releases. A compromised key with a restricted scope can only do limited damage -- it can't create new package IDs under your account.
Beyond permissions, you can also scope a key to specific package ID glob patterns. Setting MyLibrary.* as the allowed glob means that key can only push packages matching that namespace. Anything outside the pattern gets rejected. It's a small extra step that pays off if you ever need to hand a key to a teammate or rotate it after an incident.
Set an expiration date. Ninety days is a reasonable starting point for manual workflows. Keys don't need to live forever, and NuGet.org makes regeneration easy.
After creation, copy the key immediately. NuGet.org only shows it once. Close the page without copying it and you'll need to delete and create a new key.
Configuring nuget.config for NuGet.org
The nuget.config file tells the NuGet toolchain where to find and push packages. By default, dotnet nuget push already knows about NuGet.org, but understanding explicit configuration becomes important when working in teams or CI environments where multiple feeds might be in play.
You can add NuGet.org as a named source programmatically using:
dotnet nuget add source https://api.nuget.org/v3/index.json --name nuget.org
This updates the NuGet.Config in your user profile (%APPDATA%NuGetNuGet.Config on Windows, ~/.nuget/NuGet/NuGet.Config on macOS/Linux). For repository-level control -- where you want the config to travel with the codebase -- create a nuget.config at the project or solution root:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org"
value="https://api.nuget.org/v3/index.json"
protocolVersion="3" />
</packageSources>
</configuration>
The <clear /> element before the source entry prevents NuGet from merging sources from parent config files. In CI environments, this is important -- build agents often have system-level nuget.config files with stale or conflicting sources, and <clear /> ensures your project only uses what you've declared.
A word on API keys in nuget.config:
You can technically store an API key inside nuget.config using the <apikeys> section. Don't commit this to source control:
<!-- ⚠️ WARNING: Never commit this section to your repository -->
<apikeys>
<add key="https://api.nuget.org/v3/index.json" value="YOUR_API_KEY_HERE" />
</apikeys>
API keys in committed files are a security incident waiting to happen. Instead, pass the key at push time via the --api-key flag using an environment variable -- which keeps the secret out of your shell history and your git log entirely.
Running dotnet nuget push to Publish NuGet Package
With your .nupkg file built and an API key in hand, you're one command away from getting the release out. Here's the full dotnet nuget push command with all relevant flags:
dotnet nuget push ./artifacts/MyLib.1.0.0.nupkg
--api-key $env:NUGET_API_KEY
--source https://api.nuget.org/v3/index.json
--skip-duplicate
Breaking down each argument:
./artifacts/MyLib.1.0.0.nupkg-- path to your package file. You can use a glob like./artifacts/*.nupkgto push multiple packages at once, but be precise -- a glob can accidentally push packages you didn't intend to release.
Note: If your project generates a
.snupkgsymbol package, it does not push automatically alongside*.nupkg-- it requires a separatedotnet nuget push *.snupkgcommand. See NuGet SourceLink and Symbol Packages for the full setup.
--api-key-- your NuGet.org API key, read from an environment variable. Using$env:NUGET_API_KEY(PowerShell) or$NUGET_API_KEY(bash) keeps the actual secret out of your command history and shell logs.--source-- the push endpoint. Always usehttps://api.nuget.org/v3/index.jsonfor NuGet.org. This is the v3 API endpoint required for all current push operations.--skip-duplicate-- if the exact package version already exists on NuGet.org, the command exits with success rather than an error. This flag is essential for idempotent CI pipelines where the same push step might run more than once.
A successful push produces output like this:
Pushing MyLib.1.0.0.nupkg to 'https://www.nuget.org/api/v2/package'...
PUT https://www.nuget.org/api/v2/package/
Created https://www.nuget.org/api/v2/package/ 1843ms
Your package was pushed.
That confirmation means NuGet.org received the package file. But the work isn't done yet -- there's still validation to get through.
What Happens After You Publish NuGet Package
Receiving "Your package was pushed" doesn't mean the package is immediately available to consumers. NuGet.org runs every uploaded package through an automated validation pipeline before making it searchable and installable.
The pipeline includes malware scanning, package structure validation, and search indexing. This typically completes within a few minutes but can take up to 15 minutes during periods of high load. While validation is in progress, the package shows a validating status -- it won't appear in NuGet.org search results, and dotnet add package MyLib won't resolve it.
This is expected behavior. There's nothing to fix and nothing to retry. NuGet.org sends a confirmation email when validation completes and the package transitions to available status.
If validation fails (uncommon for correctly structured packages), the notification email explains why. Common failure reasons are malformed metadata or package structure issues that didn't surface during local dotnet pack. Fix what's flagged, build a new .nupkg, and push again.
Verifying Your Package Is Live
Once the confirmation email arrives, verify the package is truly accessible. Start with the package page:
https://www.nuget.org/packages/YourPackageId/
You should see the package name, version number, description, tags, and download statistics. If the page returns 404, give it a few more minutes -- the search index can lag slightly behind the validation completion timestamp.
For the most definitive verification, actually install the package from a scratch project:
dotnet new console -n TestConsumer
cd TestConsumer
dotnet add package MyLib --version 1.0.0
If the package downloads and the project builds without errors, the package is fully live. This is the real-world test -- it confirms NuGet.org has indexed the package and the toolchain resolves it exactly as a downstream developer would experience.
Unlisting vs Deleting a Package
At some point after you publish NuGet package versions, you may need to pull one back. Maybe there's a critical bug. Maybe the API was designed poorly and you want to steer users away from it. NuGet.org handles this through unlisting rather than permanent deletion.
Unlisting removes a package version from search results and the NuGet.org browsing UI. However, the package file stays on NuGet.org's servers and remains downloadable. Any project that already references MyLib 1.0.0 in a lock file or packages.config can still restore it. This behavior is intentional -- removing package files that downstream projects depend on would break builds at a scale that affects the entire .NET ecosystem.
To unlist a specific version from the command line:
dotnet nuget delete MyLib 1.0.0
--api-key $env:NUGET_API_KEY
--source https://api.nuget.org/v3/index.json
--non-interactive
Despite the command name, dotnet nuget delete on NuGet.org performs an unlist, not a permanent deletion. The --non-interactive flag skips the confirmation prompt -- useful when scripting the step in automation.
For most packages, permanent deletion is disabled on NuGet.org. This is a deliberate ecosystem stability policy from the NuGet team. Genuine permanent removal is handled by NuGet.org staff in exceptional circumstances only -- DMCA takedowns, verified security incidents, and similar cases.
If you discover a serious vulnerability in a published version: unlist it immediately, push a patched version under a new version number, and communicate the change clearly in your release notes and README.
Reserving a Package ID Prefix
NuGet.org offers an optional prefix reservation feature. It displays a verification badge on your package page -- a small shield icon that signals the package owner has claimed the namespace.
Prefix reservation matters most for well-known namespaces. Microsoft., System., Azure., and similar prefixes are reserved by Microsoft. Community maintainers of established packages -- like Serilog., MediatR., or Newtonsoft. -- have reserved their own prefixes to protect against typosquatting and impersonation.
For packages that are part of extensible plugin architectures or shared libraries intended for broad distribution, prefix reservation is worth considering early. The Plugin Contracts and Interfaces in C# guide goes deeper on designing library APIs that lend themselves well to stable, versioned namespaces.
To request a reservation, visit the NuGet prefix reservation page and submit the prefix with a justification. The NuGet team reviews requests and approves them for packages with clear ownership histories and established usage.
For newer or smaller packages, reservation is voluntary. It doesn't affect package functionality -- it's a trust signal. Consider requesting it once your package has an established user base and a recognizable namespace worth protecting.
Next Steps
The first time you publish NuGet package releases manually is the right way to understand the process end to end. Once you've done it a few times, though, doing it by hand for every release gets old quickly. The natural next step is automation.
Wiring up dotnet nuget push in a CI/CD pipeline means every tagged release automatically publishes to NuGet.org without a single manual step. Automating NuGet Publishing with GitHub Actions covers the full setup: storing API keys as repository secrets, configuring the workflow YAML, and using --skip-duplicate so reruns are always safe.
If NuGet.org isn't the right destination -- maybe you're distributing internal libraries across teams and don't want them publicly visible -- Private NuGet Feeds: Azure Artifacts and GitHub Packages covers that workflow in full.
Frequently Asked Questions
Here are the most common questions developers have when they publish NuGet package files to NuGet.org for the first time.
How long does it take for a NuGet package to appear after publishing?
After you publish NuGet package files using dotnet nuget push, NuGet.org queues the package for automated validation. This typically completes within a few minutes but can take up to 15 minutes during high-load periods. When validation finishes, NuGet.org sends a confirmation email and the package becomes searchable and installable via dotnet add package.
Can I permanently delete a published NuGet package?
For most packages, NuGet.org disables permanent deletion to protect dependent projects. What's available is unlisting -- running dotnet nuget delete -- which removes the package from search results but keeps the file downloadable for projects that already reference it. Genuine permanent deletion is handled by the NuGet.org team only in exceptional circumstances like DMCA takedowns or confirmed security incidents.
What is the correct NuGet.org push source URL?
Always use https://api.nuget.org/v3/index.json as the --source value when running dotnet nuget push. This is the v3 NuGet.org API endpoint. It's the only endpoint NuGet.org currently accepts for package pushes, and it's the value to use in both your command-line invocations and nuget.config source entries.
What is the difference between scoped and full API keys on NuGet.org?
A full-scope API key can create new package IDs and push new versions to all packages in your account. A scoped key can be restricted by permission level (new packages only, or new versions of existing packages only) and by package ID glob pattern. Scoped keys reduce the blast radius of a compromised credential -- a key scoped to MyLib.* can't touch anything else in your account.
What does --skip-duplicate do in dotnet nuget push?
The --skip-duplicate flag causes dotnet nuget push to exit successfully when the exact package version already exists on NuGet.org, instead of returning an error. This is particularly useful in CI pipelines where a push step might execute more than once due to retries or workflow re-runs. Without --skip-duplicate, a second push of the same version number fails the pipeline.
How do I verify that my NuGet package was published successfully?
After receiving NuGet.org's confirmation email, check https://www.nuget.org/packages/YourPackageId/ to view the package page. For the most reliable check, run dotnet add package MyLib --version 1.0.0 from a test project -- if the package downloads and the project builds, it's fully live and resolving through NuGet's standard toolchain.
What is NuGet prefix reservation and do I need it?
Prefix reservation lets you claim a package namespace on NuGet.org (like MyCompany.), which adds a verification badge to your packages. It's voluntary for most authors -- it doesn't affect package functionality. It's worth requesting once your package has an established user base and you want to protect the namespace from impersonation or typosquatting by other publishers.
Wrapping Up
The steps to publish NuGet package output to NuGet.org are repeatable and well-understood. Create your account, generate a scoped API key, configure your nuget.config, run dotnet nuget push with the right flags, and wait a few minutes for validation to complete. From there, your library is accessible to any .NET developer worldwide via the standard package toolchain.
Understanding how consumers will actually load and use your package is worth thinking about before you publish. Plugin Architecture in C#: The Complete Guide to Extensible .NET Applications is a solid reference for how distributed assemblies -- exactly what NuGet packages are -- get discovered and loaded at runtime. And if you're thinking about assembly scanning patterns that consumers might apply to your package, Assembly Scanning in Needlr: Filtering and Organizing Type Discovery covers that side of the story.
For the full NuGet workflow from project setup through packaging and publication, The Complete Guide to Creating NuGet Packages in .NET is the place to start.

