A package URL follows one pattern: pkg, then a colon, the type, an optional namespace, the name, an optional version after an at sign, optional qualifiers after a question mark, and an optional subpath after a hash. Write the pieces left to right from most to least significant.
The structure
From the purl specification README: scheme, type, namespace, name, version, qualifiers, subpath. The scheme is always pkg. Type is the package type, such as npm, maven, pypi, nuget, gem or deb. Namespace is a prefix whose meaning depends on the type. Qualifiers hold type specific extras such as architecture. Subpath points inside the package.
Syntax details sit in ECMA-427, which the README points to.
Worked examples
These come straight from the README examples:
pkg:pypi/django@1.11.1
pkg:nuget/EnterpriseLibrary.Common@6.0.1304
pkg:npm/%40angular/animation@12.3.1
pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1
pkg:gem/jruby-launcher@1.1.2?platform=java
pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie
Common traps
npm scopes. A scoped npm package has the scope as namespace. In the README example the at sign of the scope is percent encoded as %40, giving pkg:npm/%40angular/animation@12.3.1. Do not leave a raw at sign in the namespace, since the at sign separates the version.
Maven. The namespace is the group id and the name is the artifact id, as in org.apache.xmlgraphics and batik-anim above.
Linux packages. The deb and rpm types use the distribution as namespace, and put architecture and distro in qualifiers.
Containers. The docker type can carry a digest as the version, as in the README example with a sha256 value.
How to check yours
- Does it start with pkg:?
- Is the type spelled as the registered type name?
- Is the version the exact resolved version, not a range?
- Are special characters percent encoded?
- Did a parser round trip it? Parse the string and print it back. If it changes, something was wrong.
Libraries exist in most ecosystems for building and parsing purls. Prefer them over string concatenation.
Why it matters
Vulnerability matching by purl is more reliable than matching by free text names, because the type and namespace remove ambiguity between packages that share a name. The 2026 CISA elements require at least one machine processable identifier per component, as reported, and a purl is a common choice. See also what a package URL is.
Quick check
Can I use a version range in a purl?
Use the exact resolved version. A purl names a specific package version.
Where do the type definitions live?
In the purl-spec repository, one JSON Schema file per type, as the README describes.
Sources
Last reviewed: 19 October 2026.