This PEP traces the root cause of most of the issues with the existing API to be roughly two things:
To address these issues, this PEP proposes a multi-request workflow, which at a high level involves
these steps:
Like PEP 691, this PEP proposes that all requests and responses from this upload API will have a
standard content type that describes what the content is, what version of the API it represents,
and what serialization format has been used.
This standard request content type applies to all requests except for requests to execute
a file upload mechanism, which will be specified by the documentation for that mechanism.
The structure of the Content-Type header for all other requests is:
application/vnd.pypi.upload.$version+$format
Since minor API version differences should never be disruptive, only the major version is included
in the content type; the version number is prefixed with a v.
The major API version specified in the .meta.api-version JSON key of client requests
MUST match the Content-Type header for major version.
Unlike PEP 691, this PEP does not change the existing legacy 1.0 upload API in any way,
so servers are required to host the new API described in this PEP at a different endpoint than the
existing upload API.
Since JSON is the only defined request format defined in this PEP, all non-file-upload requests
defined in this PEP MUST include a Content-Type header value of:
application/vnd.pypi.upload.v2+json.
Similar to PEP 691, this PEP also standardizes on using server-driven content negotiation to
allow clients to request different versions or serialization formats,
which includes the format part of the content type.
However, since this PEP expects the existing legacy 1.0 upload API
to exist at a different endpoint,
and this PEP currently only provides for JSON serialization,
this mechanism is not particularly useful.
Clients only have a single version and serialization they can request.
However clients SHOULD be prepared to handle content negotiation gracefully
in the case that additional formats or versions are added in the future.
Servers MUST NOT advertise support for API versions beyond those defined in approved PEPs.
Any new versions or formats require standardization through a new PEP.
Unless otherwise specified, all HTTP requests and responses in this document are assumed to include
the HTTP header:
Content-Type: application/vnd.pypi.upload.v2+json
Unless otherwise specified, all error (4xx and 5xx) responses from the server MUST use the RFC 9457
(Problem Details for HTTP APIs) format. In particular, the server MUST use the “Problem Details JSON
Object” defined in Section 3 and SHOULD use the application/problem+json media
type in its responses.
Clients in general should be prepared to handle HTTP response error status codes which SHOULD contain payloads like
the following, although note that the details are index-specific, as long as they conform to RFC 9457. By way
of example, PyPI could return the following error body:
{
"type": "https://docs.pypi.org/api/errors/error-types#invalid-filename",
"status": 400,
"title": "The artifact used an invalid wheel file name format",
"details": "See https://packaging.python.org/en/latest/specifications/binary-distribution-format/",
"meta": {
"api-version": "2.0"
},
"errors": [
{
"source": "...",
"message": "..."
}
]
}
RFC 9457 defines type, status, title, and details. The meta and errors keys are
“extension members”, defined in Section 3.2. The index SHOULD include these
extension members.
meta- The same request/response metadata structure as defined in the Publishing Session description.
errors- An array of specific errors, each of which contains a
source key, which is a string that
indicates what the source of the error is, and a message key for that specific error.
The message and source strings do not have any specific meaning, and are intended for human
interpretation to aid in diagnosing underlying issue.
Some responses may return more specific HTTP status codes as described in the text below.
A release starts by creating a new publishing session. To create the session, a client submits a
POST request to the root URL like:
{
"meta": {
"api-version": "2.0"
},
"name": "foo",
"version": "1.0",
}
The request includes the following top-level keys:
meta (required)- Describes information about the payload itself. Currently, the only defined sub-key is
api-version the value of which must be the string "2.0". Optional sub-keys can define
index-specific behavior.
name (required)- The name of the project that this session is attempting to release a new version of. The name
MUST conform to the standard package name format
and the server MUST normalize the name.
version (required)- The version of the project that this session is attempting to add files to. The version string
MUST conform to the packaging version specification.
Upon successful session creation, the server returns a 201 Created response. The response MUST also
include a Location header containing the same URL as the links.session
key in the response body.
If a session is created for a project which has no previous release,
then the index MAY reserve the project name before the session is published,
however it MUST NOT be possible to navigate to that project using
the “regular” (i.e. unstaged) access protocols,
until the stage is published.
If this first-release stage gets canceled,
then the index SHOULD delete the project record, as if it were never uploaded.
The session is owned by the user that created it,
and all subsequent requests MUST be performed with the same credentials,
otherwise a 403 Forbidden will be returned on those subsequent requests.
To complete a session and publish the files that have been included in it, a client issues a
POST request to the session link
given in the session creation response body.
The request looks like:
{
"meta": {
"api-version": "2.0"
},
"action": "publish",
}
If the server is able to immediately complete the publishing session, it may do so and return a
201 Created response. If it is unable to immediately complete the publishing session
(for instance, if it needs to do validation that may take longer than reasonable in a single HTTP
request), then it may return a 202 Accepted response.
The server MUST include a Location header in the response pointing back to the Publishing
Session status URL, which can be used to query the current session status. If the server
returned a 202 Accepted, polling that URL can be used to watch for session status changes.
To cancel a publishing session, a client issues a DELETE request to
the session link
given in the session creation response body.
The server then marks the session as canceled, and SHOULD purge any data that was uploaded
as part of that session.
Future attempts to access that session URL or any of the publishing session URLs
MUST return a 404 Not Found.
To prevent dangling sessions, servers may also choose to cancel timed-out sessions on their own
accord. It is recommended that servers expunge their sessions after no less than a week, but each
server may choose their own schedule. Servers MAY support client-directed session
extensions.
At any time, a client can query the status of a session by issuing a GET request to the URL given in the
links.session URL (also provided in the session creation response’s Location header).
The server will respond to this GET request with the same publishing session creation response, that they got when they initially created the publishing session, except with
any changes to status, expires-at, or files reflected.
Servers MAY allow clients to extend sessions, but the overall lifetime and number of extensions
allowed is left to the server. To extend a session, a client issues a POST request to the
links.session URL (same as above, also the Location header).
The request looks like:
{
"meta": {
"api-version": "2.0"
},
"action": "extend",
"extend-for": 3600
}
The number of seconds specified is just a suggestion to the server for the number of additional seconds to
extend the current session. For example, if the client wants to extend the current session for another hour,
extend-for would be 3600. Upon successful extension, the server will respond with the same
publishing session creation response body that they got when they
initially created the publishing session, except with any changes to status, expires-at, or files
reflected.
If the server refuses to extend the session for the requested number of seconds, it MUST still return a
success response, and the expires-at key will simply reflect the current expiration time of the session.
Indexes SHOULD support preview stages so that uploaded files can be live tested
before publishing. E.g. a CI client could perform installation tests using pre-published wheels to ensure
that their new release works as expected before they publish the release publicly.
Indexes advertise their support for staged previews by returning two key pieces of information in their
response to publishing session creation. Indexes which don’t support
staged previews MUST NOT include these in their responses.
The session-token is a short token which could be used as a convenience for installation tool UX, if they
want to support staged previews via a command line switch, e.g. $TOOL install --staging $SESSION_TOKEN.
The links.stage key gives the full URL to the stage, which could be used in the CLI, e.g. pip
install --extra-index-url $STAGE_URL. Both the session token and URL MUST be cryptographically
unguessable, but the algorithm for generating the token is left to the index. The stage URL MUST be
calculable from the session token, using a format documented by the index, but the exact format of the URL is
also left to the index.
After creating a publishing session, the upload endpoint from the response’s session links mapping is used to begin the upload of new files into that session. Clients
MUST use the provided upload URL and MUST NOT assume there is any pattern or commonality to those
URLs from one session to the next.
To initiate a file upload, a client first sends a POST request to the upload URL.
The request looks like:
{
"meta": {
"api-version": "2.0"
},
"filename": "foo-1.0.tar.gz",
"size": 1000,
"hashes": {"sha256": "...", "blake2b": "..."},
"metadata": "...",
"mechanism": "http-post-bytes"
}
Besides the standard meta key, the request JSON has the following additional keys:
filename (required)- The name of the file being uploaded. The filename MUST conform to either the source distribution
file name specification
or the binary distribution file name convention.
Indexes SHOULD validate these file names at the time of the request, returning a
400 Bad Request
error code and an RFC 9457 style error body, as described in the Errors section when the
file names do not conform.
size (required)- The size in bytes of the file being uploaded.
hashes (required)- A mapping of hash names to hex-encoded digests. Each of these digests are the checksums of the
file being uploaded when hashed by the algorithm identified in the name.
By default, any hash algorithm available in hashlib can be used as a key for the hashes
dictionary . At least one secure algorithm from hashlib.algorithms_guaranteed
MUST always be included. This PEP specifically recommends sha256.
Multiple hashes may be passed at a time, but all hashes provided MUST be valid for the file.
mechanism (required)- The file-upload mechanisms the client intends to use for this file.
This mechanism SHOULD be chosen from the list of mechanisms advertised in the
publishing session creation response body.
A client MAY send a mechanism that is not advertised in cases where server operators have
documented a new or upcoming mechanism that is available for use on a “pre-release” basis.
metadata (optional)- If given, this is a string value containing the file’s core metadata.
Servers MAY use the data provided in this request to do some sanity checking prior to allowing
the file to be uploaded. These checks may include, but are not limited to:
- checking if the
filename already exists in a published release;
- checking if the
size would exceed any project or file quota;
- checking if the contents of the
metadata, if provided, are valid.
If the server determines that upload should proceed, it will return a 202 Accepted response, with the
response body below. The status of the publishing session will also
include the filename in the files mapping. If the server cannot proceed with an upload because the
mechanism supplied by the client is not supported it MUST return a 422 Unprocessable Content. The
server MAY allow parallel uploads of files, but is not required to. If the server determines the upload
cannot proceed, it MUST return a 409 Conflict.
The successful response includes the following:
{
"meta": {
"api-version": "2.0"
},
"links": {
"file-upload-session": "..."
},
"status": "pending",
"expires-at": "2025-08-01T13:00:00Z",
"mechanism": {
"identifier": "http-post-bytes",
"file_url": "...",
"attestations_url": "..."
}
}
A Retry-After response header MUST be present to indicate to clients when they should next poll for an
updated status.
Besides the meta key, which has the same format as the request JSON, the success response has
the following keys:
links- A dictionary mapping keys to URLs related to this session,
the details of which are provided below.
status- A string with valid values
pending, processing, complete, error, and canceled
indicating the current state of the file upload session.
expires-at- An RFC 3339 formatted timestamp string representing when the server will expire this file upload
session. This string MUST represent a UTC timestamp using the “Zulu” (i.e.
Z) marker,
and use only whole seconds (i.e. no fractional seconds). The session SHOULD remain active
until at least this time unless the client cancels or completes it. Servers MAY choose to
extend this expiration time, but should never move it earlier.
mechanism- A mapping containing the necessary details for the supported mechanism as negotiated by the client and
server. This mapping MUST contain a key
identifier which maps to the identifier string for the
chosen file upload mechanism.
To complete a file upload session, which indicates that the file upload mechanism has been executed
and did not produce an error, a client issues a POST to the file-upload-session link in the
file upload session creation response body.
The requests looks like:
{
"meta": {
"api-version": "2.0"
},
"action": "complete",
}
If the server is able to immediately complete the file upload session, it may do so and return a 201
Created response and set the status of the file upload session to complete. If it is unable to
immediately complete the file upload session (for instance, if it needs to do validation that may take longer
than reasonable in a single HTTP request), then it may return a 202 Accepted response and set the status
of the file upload session to processing.
In either case, the server should include a Location header pointing back to the file upload session
status URL.
Servers MUST allow clients to poll the file upload session status URL to watch for the status to change.
If the server responds with a 202 Accepted, clients may poll the file upload session status URL to watch
for the status to change. Clients SHOULD respect the Retry-After header value of the file upload
session status response.
A client can cancel an in-progress file upload session, or delete a file that has been completely uploaded.
In both cases, the client performs this by issuing a DELETE request to the links.file-upload-session
URL from the file upload session creation response of the file they want
to delete.
A successful deletion request MUST respond with a 204 No Content.
Once canceled or deleted, a client MUST NOT assume that the previous file upload session resource or
associated file upload mechanisms can be reused.
To replace a session file, the file upload MUST have been previously completed, canceled, or
deleted. It is not possible to replace a file if the upload for that file is in-progress.
To replace a session file, clients should cancel and delete the in-progress upload first. After this, the new file upload can be initiated by beginning the
entire file upload sequence over again. This means providing the metadata
request again to retrieve a new upload resource URL. Clients MUST NOT assume that the previous upload
resource URL can be reused after deletion.
The client can query status of the file upload session by issuing a GET request to the
links.file-upload-session URL from the file upload session creation response. The server responds to this request with the same payload as the file upload
session creation response, except with any changes status and expires-at reflected.
Servers MAY allow clients to extend file upload sessions, but the overall lifetime and number of
extensions allowed is left to the server. To extend a file upload session, a client issues a POST request
to the links.file-upload-session URL from the file upload session creation response.
The request looks like:
{
"meta": {
"api-version": "2.0"
},
"action": "extend",
"extend-for": 3600
}
The number of seconds specified is just a suggestion to the server for the number of additional seconds to
extend the current file upload session. For example, if the client wants to extend session for another hour,
extend-for would be 3600. Upon successful extension, the server will respond with the same file
upload session creation response body that they got when they initially
created the publishing session, except with any changes to status or expires-at reflected.
If the server refuses to extend the session for the requested number of seconds, it MUST still return a
success response, and the expires-at key will simply reflect the current expiration time of the session.
Servers MUST implement required file upload mechanisms.
Such mechanisms serve as a fallback if no server specific implementations exist.
Each major version of the Upload API MUST specify at least one required file upload mechanism.
New required mechanisms MUST NOT be added
and existing required mechanisms MUST NOT be removed
without an update to the major version.
Any server-specific or experimental mechanisms added or removed
MUST NOT change the major or minor version number of this specification.
Upload API version 2.0 compliant servers MUST support the http-post-bytes mechanism.
This mechanism MUST use the same authentication scheme as
the rest of the Upload 2.0 protocol endpoints.
A client executes this mechanism by submitting a POST request to the file_url
returned in the http-post-bytes map of the mechanism map of the
file upload session creation response body like:
Content-Type: application/octet-stream
<binary contents of the file to upload>
Servers MAY support uploading of digital attestations for files (see PEP 740).
This support will be indicated by inclusion of an attestations_url key in the
http-post-bytes map of the mechanism map of the
file upload session creation response body.
Attestations MUST be uploaded to the attestations_url before
file upload session completion.
To upload an attestation, a client submits a POST request to the attestations_url
containing a JSON array of attestation objects like:
Content-Type: application/json
[{"version": 1, "verification_material": {...}, "envelope": {...}},...]
A given server MAY implement an arbitrary number of server specific mechanisms
and is responsible for documenting their usage.
A server specific implementation file upload mechanism identifier has three parts:
<prefix>-<operator identifier>-<implementation identifier>
Server specific implementations MUST use vnd as their prefix.
The operator identifier SHOULD clearly identify the server operator,
be unique from other well known indexes,
and contain only alphanumeric characters [a-z0-9].
The implementation identifier SHOULD concisely describe the underlying implementation
and contain only alphanumeric characters [a-z0-9] and -.
When server operators need to make breaking changes to their upload mechanisms,
they SHOULD create a new mechanism identifier rather than modifying the existing one.
The recommended pattern is to append a version suffix like -v1, -v2, etc.
to the implementation identifier.
This allows clients to explicitly opt into new versions while maintaining
backward compatibility with existing clients.
For example:
| File Upload Mechanism string |
Server Operator |
Mechanism description |
vnd-pypi-s3multipart-presigned |
PyPI |
S3 multipart upload via pre-signed URL |
vnd-pypi-s3multipart-presigned-v2 |
PyPI |
S3 multipart upload via pre-signed URL version 2 |
vnd-pypi-http-fetch |
PyPI |
File delivered by instructing server to fetch from a URL via HTTP request |
vnd-acmecorp-http-fetch |
Acme Corp |
File delivered by instructing server to fetch from a URL via HTTP request |
vnd-acmecorp-postal |
Acme Corp |
File delivered via postal mail |
vnd-widgetinc-stream-v1 |
Widget Inc. |
Streaming upload protocol version 1 |
vnd-widgetinc-stream-v2 |
Widget Inc. |
Streaming upload protocol version 2 |
vnd-madscience-quantumentanglement |
Mad Science Labs |
Upload via quantum entanglement |
If a server intends to precisely match the behavior of another server’s implementation, it MAY respond
with that implementation’s file upload mechanism name.