Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 694 – Upload 2.0 API for Python Package Indexes

PEP 694 – Upload 2.0 API for Python Package Indexes

Author:
Barry Warsaw <barry at python.org>, Donald Stufft <donald at stufft.io>, Ee Durbin <ee at python.org>
PEP-Delegate:
Dustin Ingram <di at python.org>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Topic:
Packaging
Created:
11-Jun-2022
Post-History:
27-Jun-2022, 06-Jan-2025 14-Apr-2025 06-Aug-2025 27-Sep-2025 07-Dec-2025 26-Jun-2026

Table of Contents

Abstract

This PEP proposes an extensible API for uploading files to a Python package index such as PyPI. Along with standardization, the upload API provides additional useful features such as support for:

  • a publishing session, which can be used to simultaneously and atomically publish all artifacts (wheels, sdists) in a package release;
  • “staging” a release, which can be used to test uploads before publicly publishing them, without the need for test.pypi.org;
  • artifacts which can be overwritten and replaced, until a session is published;
  • detailed status on the state of artifact uploads;
  • new project creation without requiring the uploading of an artifact;
  • a protocol to extend the supported upload mechanisms in the future without requiring a full PEP; these can be standardized and recommended for all indexes, or be index-specific.

This PEP does not propose a deprecation schedule for the legacy API.

Rationale

There is currently no standardized API for uploading files to a Python package index such as PyPI. Instead, everyone has been forced to reverse engineer the existing “legacy” API.

The legacy API, while functional, leaks implementation details of the original PyPI code base, which has been faithfully replicated in the new code base and alternative implementations.

In addition, there are a number of major issues with the legacy API:

  • It is fully synchronous, which forces requests to be held open both for the upload itself, and while the index processes the uploaded file to determine success or failure.
  • It does not support any mechanism for parallelizing or resuming an upload. With the largest default file size on PyPI being around 1GiB in size, requiring the entire upload to complete successfully means bandwidth is wasted when such uploads experience a network interruption while the request is in progress.
  • The atomic unit of operation is a single file. This is problematic when a release logically includes an sdist and multiple binary wheels, leading to race conditions where consumers get different versions of the package if they are unlucky enough to require a package before their platform’s wheel has completely uploaded. If the release uploads its sdist first, this may also manifest in some consumers seeing only the sdist, triggering a local build from source.
  • Status reporting is very limited. There’s no support for reporting multiple errors, warnings, deprecations, etc. Status is limited to the HTTP status code and reason phrase, of which the reason phrase has been deprecated since HTTP/2 (RFC 7540).
  • Metadata for a release is submitted alongside the file. However, as this metadata is famously unreliable, most installers instead choose to download the entire file and read the metadata from there.
  • There is no mechanism for allowing an index to do any sort of sanity checks before bandwidth gets expended on an upload. Many error conditions, such as incorrect permissions or quota exhaustion could be checked prior to uploading files.
  • There is no support for “staging” a release prior to publishing it to the index.
  • Creation of new projects requires the uploading of at least one file, leading to “stub” uploads to claim a project name, wasting space.

The new upload API proposed in this PEP provides ways to solve all of these problems, either directly or through an extensible approach, allowing servers to implement features such as resumable and parallel uploads. The upload API this PEP proposes provides better and more standardized error reporting, a more robust release testing experience, and atomic and simultaneous publishing of all release artifacts.

Legacy API

The following is an overview of the legacy API. For the detailed description, consult the PyPI user guide documentation.

Endpoint

The existing upload API lives at a base URL. For PyPI, that URL is currently https://upload.pypi.org/legacy/. Clients performing uploads specify the API they want to call by adding an :action URL parameter with a value of file_upload. [1]

The legacy API also has a protocol_version parameter, in theory allowing new versions of the API to be defined. In practice this has never happened, and the value is always 1.

Thus, the effective upload API on PyPI is: https://upload.pypi.org/legacy/?:action=file_upload&protocol_version=1.

Encoding

The data to be submitted is submitted as a POST request with the content type of multipart/form-data. This reflects the legacy API’s historical nature, which was originally designed not as an API, but rather as a web form on the initial PyPI implementation, with client code written to programmatically submit that form.

Content

Roughly speaking, the metadata contained within the package is submitted as parts where the content disposition is form-data, and the metadata key is the name of the field. The names of these various pieces of metadata are not documented, and they sometimes, but not always match the names used in the METADATA files for package artifacts. The case rarely matches, and the form-data to METADATA conversion is inconsistent.

The upload artifact file itself is sent as a application/octet-stream part with the name of content, and if there is a PGP signature attached, then it will be included as a application/octet-stream part with the name of gpg_signature.

Authentication

Upload authentication is also not standardized.

PyPI uses HTTP Basic Authentication with API tokens as the password and the username __token__. Trusted Publishers authenticate via OpenID Connect and receive short-lived API tokens that are used in the same way.

Upload 2.0 API Specification

This PEP proposes a multi-request workflow, which at a high level involves these steps:

  1. Initiate a publishing session, creating a release stage.
  2. Initiate file upload session(s) to that stage as part of the publishing session.
  3. Negotiate the specific file upload mechanism to use between client and server.
  4. Execute the file upload mechanism for the file upload session(s) using the negotiated mechanism(s).
  5. Complete the file upload session(s), marking them as completed or canceled.
  6. Complete the publishing session, publishing or discarding the stage.
  7. Optionally check the status of a publishing session.

Versioning

This PEP uses the same MAJOR.MINOR versioning system as used in PEP 691, but it is otherwise independently versioned. The legacy API is considered by this PEP to be version 1.0, but this PEP does not modify the legacy API in any way.

The API proposed in this PEP therefore has the version number 2.0.

Both major and minor version numbers of the Upload API MUST only be changed through the PEP process. Index operators and implementers MUST NOT advertise or implement new API versions without an approved PEP. This ensures consistency across all implementations and prevents fragmentation of the ecosystem.

Content Types

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

Root Endpoint

All URLs described here are relative to the “root endpoint”, which may be located anywhere within the url structure of a domain. For example, the root endpoint could be https://upload.example.com/, or https://example.com/upload/.

The choice of the root endpoint is left up to the index operator.

Authentication and Authorization

All endpoints in this specification MUST use standard HTTP authentication mechanisms as defined in RFC 7235.

Authentication follows the standard HTTP pattern:

  • Servers use the WWW-Authenticate response header when authentication is required
  • Clients provide credentials via the Authorization request header
  • 401 Unauthorized indicates missing or invalid authentication
  • 403 Forbidden indicates insufficient permissions

The specific authentication schemes (e.g., Bearer, Basic, Digest) are determined by the index operator.

Authentication establishes the principal making a request. Authorization determines whether that principal may act on a particular session. All session endpoints defined in this specification (i.e. the URLs returned under the links key when a publishing session or file upload session is created) MUST be authorized against the project’s upload permissions. Specifically, a server MUST verify, contemporaneously on each request, that the authenticated principal is currently authorized to upload to the project named by the session, and MUST respond with 403 Forbidden if it is not.

Because this check is performed independently on each request, a session is not tied to the exact credentials that created it:

  • A principal that is granted upload permission after a session is opened may immediately participate in that session.
  • A principal whose upload permission is revoked while a session is open MUST be denied with a 403 Forbidden on any subsequent request, even if that principal created the session.

This denial is evaluated per request and is not “sticky”: if a principal’s permission is later restored, its subsequent requests are authorized again. An index MAY apply a stricter policy, but this specification does not require one.

Servers MUST perform this authorization check on at least every request that creates, modifies, completes, extends, cancels, or publishes a publishing session or file upload session. For upload mechanisms that transfer a file across more than one request (for example, chunked or multipart mechanisms), servers SHOULD authorize each such request.

The unguessable stage preview URL is a separate capability and is deliberately not governed by this authorization check; it grants read-only preview access to any client that holds the token, so that (for example) a CI job can install-test a staged release without project upload credentials.

Errors

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.

Publishing Session

Create a Publishing Session

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

A publishing session is not bound to the specific credentials that created it. Instead, every request against the session MUST be performed by an authenticated principal that is authorized to upload to the project at the time of that request, as described in Authentication and Authorization. A request from a principal that is not, or is no longer, so authorized MUST receive a 403 Forbidden.

For a first-release session on a project that does not yet exist, there are no existing project upload permissions to evaluate; the index instead authorizes the request according to its own name-registration policy, and SHOULD treat the creating principal (and, where applicable, an organization it acts on behalf of) as authorized for the lifetime of the session.

Optional Index-specific Metadata

Indexes can optionally define their own metadata for index-specific behavior. The metadata key MUST begin with an underscore, with the following value easily and uniquely identifying the index. For example, PyPI could allow for projects to be created in an organization account of which the publisher is a member by using the following index-specific metadata section:

{
  "meta": {
    "api-version": "2.0",
    "_pypi.org": {
        "organization": "my-org"
    }
  },
  "name": "foo",
  "version": "1.0",
}

This is only an example. This PEP does not define or reserve any index-specific keys or metadata; that is left up to the index to specify and document. The semantics (e.g. whether invalid keys or values result in an error or are ignored) of the index-specific metadata is also undefined here.

Response Body

The successful response includes the following content:

{
  "meta": {
    "api-version": "2.0"
  },
  "links": {
    "stage": "...",
    "upload": "...",
    "session": "...",
    "publish": "...",
    "extend": "...",
  },
  "mechanisms": ["http-post-bytes"],
  "session-token": "<token-string>",
  "expires-at": "2030-08-01T12:00:00Z",
  "status": "open",
  "files": {},
  "notices": [
    "a notice to display to the user"
  ]
}

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.
mechanisms
A list of file-upload mechanisms supported by the server, sorted in server-preferred order. At least one value is required.
session-token
If the index supports previewing staged releases, this key will contain the unique “session token” that can be provided to installers in order to preview the staged release before it’s published. This token MUST be cryptographically unguessable. If the index does not support stage previewing, this key MUST be omitted.
expires-at
An RFC 3339 formatted timestamp string; this string MUST represent a UTC timestamp using the “Zulu” (i.e. Z) marker, and use only whole seconds (i.e. no fractional seconds). This timestamp represents when the server will expire this session, and thus all of its content, including any uploaded files and the URL links related to the session. The session SHOULD remain active until at least this time unless the client itself has canceled or published the session. Servers MAY choose to extend this expiration time, but should never move it earlier. Clients can query the session status to get the current expiration time of the session, and may request an extension.
status
A string that contains one of open, processing, published, error, or canceled, representing the overall status of the session.
files
A mapping containing the filenames that have been uploaded to this session, to a mapping containing details about each file referenced in this session.
notices
An optional key that points to an array of human-readable informational notices that the server wishes to communicate to the end user. These notices are specific to the overall session, not to any particular file in the session.
Multiple Session Creation Requests

If a second attempt to create a session is received for the same name-version pair while an existing session for that pair is in a non-terminal state – that is, open, processing, or error (see publishing session states) – then a new session is not created. Instead, the server MUST respond with a 409 Conflict and MUST include a Location header that points to the session status URL. Like every other session request, such a request MUST be performed by a principal authorized to upload to the project (see Authentication and Authorization); a request from an unauthorized principal MUST receive a 403 Forbidden instead, which takes precedence over the 409 Conflict so that the existence of the in-progress session is not disclosed. An authorized principal receives the 409 Conflict and Location header and may use the referenced session; this is how multiple authorized publishers (for example, distinct Trusted Publishing workflows) can contribute to the same session.

Otherwise – for example, when the name-version pair has no session in a non-terminal state, either because the previous session for that pair has reached a terminal published or canceled state, or because no session has ever been created for it – a new session is created with the same 201 Created response and payload, except that the publishing session status URL, session-token, and links.stage values MUST be different.

Publishing Session Files

The files key contains a mapping from the names of the files uploaded in this session to a sub-mapping with the following keys:

status
A string with valid values pending, processing, complete, error, and canceled. If there was an error during upload, then clients should not assume the file is in any usable state, error will be returned and it’s best to cancel or delete the file and start over. This action would remove the file name from the files key of the session status response body.
link
The absolute URL that the client should use to reference this specific file. This URL is used to retrieve, replace, or delete the referenced file. If a preview stages are supported, this URL MUST be cryptographically unguessable, and MUST use the same publishing session token to do ensure this constraint. The exact format of the URL is left to the index, but SHOULD be documented.
notices
An optional key with similar format and semantics as the notices session key, except that these notices are specific to the referenced file.

Publishing Session States

A publishing session is always in exactly one of the following states, reported by the status key of the session status response:

State diagram for a publishing session.  From the initial state the session enters ``open``.  From ``open`` a publish request either completes immediately (``201``) to the terminal ``published`` state, is accepted for deferred processing (``202``) into ``processing``, or fails synchronously (``4xx``/``5xx``) and stays ``open``; ``open`` can also be canceled (``DELETE``) to the terminal ``canceled`` state.  ``processing`` resolves to ``published`` on success or to ``error`` on failure. From ``error`` the client can retry publishing -- which behaves like publishing from ``open`` -- or cancel to ``canceled``.  Both ``open`` and ``error`` host file upload sessions.  Canceling during ``processing`` is rejected with ``409``.

The textual description of each state and a complete transition table follow.

open
The session is accepting changes. Files can be uploaded, replaced, and deleted; the session can be previewed and extended; and it can be published or canceled. A newly created session starts in this state.
processing
The client has requested publication and the server accepted the request for deferred processing, returning a 202 Accepted (see Complete a Publishing Session). The session is no longer accepting changes while the server validates and processes it. This is a transitional state; the client polls the session status until it resolves to published or error.
published (terminal)
The session’s files have been published and are publicly available. No further changes are possible.
error
The most recent deferred publish attempt failed. The session is fully editable again – it permits exactly the same operations as open, and differs from open only in that it records that the last publish attempt failed. The human-readable reason MUST be reported in the session’s notices (and, where the failure is attributable to a particular file, in that file’s notices). From this state the client can address the problem and publish again, or cancel the session.
canceled (terminal)
The session was canceled and its staged data discarded. No further changes are possible.

Both open and error are editable states that permit the identical set of operations; a client MUST NOT treat an error session as closed or read-only. The only difference between them is that error additionally records that the previous deferred publish request failed.

Because published and canceled are terminal, reaching either one frees the name-version pair so that a subsequent session may be created for it, for example, to add wheels for additional platforms to an already-published release.

The transitions between these states are:

From Event To
(none) Session created open
open A file is uploaded, replaced, or deleted open
open Publish request completed immediately (201 Created) published
open Publish request accepted for deferred processing (202 Accepted) processing
open or error Publish request fails synchronously unchanged (the error is returned to the caller)
open or error Session canceled (DELETE) canceled
processing Deferred processing succeeds published
processing Deferred processing fails error
processing Cancellation requested rejected with 409 Conflict (see Publishing Session Cancellation)
error A file is uploaded, replaced, or deleted error
error Publish retried processing or published

A synchronous publish failure (i.e. one the server determines within the publish request itself) is returned to the caller as an error response and leaves the session in its current editable state (open stays open; error stays error). The error state is reached only when a publish that was accepted for deferred processing subsequently fails, because in that case the failure cannot be returned to the caller directly and the client discovers it by polling.

Complete a Publishing Session

To complete a session and publish the files that have been included in it, a client issues a POST request to the publish link given in the session creation response body.

The request looks like:

{
  "meta": {
    "api-version": "2.0"
  }
}

If the server is able to immediately complete the publishing session, it may do so and return a 201 Created response, moving the session to the terminal status published. 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 and move the session to the processing state.

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 the session status to change: deferred processing resolves to either published on success or error on failure. When it resolves to error, the session remains editable and the reason is reported in the session’s notices, as described in Publishing Session States.

A publish attempt that fails synchronously (i.e. within the publish request itself) is returned to the client as an error response and leaves the session in its current editable state; it does not move the session to error.

Atomic Publication and Conflicts

Publishing a session is atomic with respect to the release’s filename namespace. Because published artifacts are immutable, the index MUST guarantee that it never publishes two files with the same name for the same release, even in the presence of concurrent uploads arriving through this API or the legacy API.

The point-in-time conflict check performed when a file upload session is created is best-effort: it reflects the published state at the moment of that request and does not guarantee the file will still be conflict-free at publish time, since the published state of the release can change while a session is open. For example, a file with the same name may be published through the legacy API, or through a subsequent session for the same, already-published name and version. The authoritative conflict check is therefore performed atomically at publish time.

Filename reservation. When a client requests publication, the server MUST atomically reserve the filenames of all files in the session within the target release, and hold that reservation for the duration of the publish:

  • While the reservation is held, any other attempt to upload a file with one of those filenames to the same release – whether through this API or the legacy API – MUST be rejected with a 409 Conflict, exactly as if the file were already published. The index MUST enforce this regardless of which upload path the conflicting request arrives through; this is a requirement on the index’s shared published-filename namespace and does not otherwise modify the legacy API.
  • The reservation governs conflict detection only; the reserved files remain staged and MUST NOT become publicly visible until the publish succeeds.
  • If the publish succeeds, the reservation converts to permanent publication. If it fails – synchronously, leaving the session editable, or during deferred processing, moving it to the error state – the server MUST release the reservation, making those filenames available again.

A still-open session does not reserve its filenames; the reservation is acquired only when publication is requested. For an immediate (201 Created) publish the reservation is held only for the duration of the single request and is effectively unobservable. The requirement is meaningful mainly for deferred (202 Accepted then processing) publishes, where there is a real window between the index validating one staged artifact and committing the rest.

This closes that window at a deliberate, conservative cost: a concurrent upload MAY receive a 409 Conflict during a publish that ultimately fails, and then succeed on retry once the reservation is released. The index never publishes two files with the same name, at the cost of occasionally rejecting a concurrent upload that a later retry would allow.

Reporting a publish-time conflict. When the index detects a conflict while publishing – whether a filename collision as above, or another precondition that held when the session was created but no longer holds, such as an exhausted quota or a revoked permission – it reports the failure according to the publishing session state machine:

  • If the server is completing the publish synchronously, it MUST return an error response – a 409 Conflict for a filename collision – identifying the conflicting file(s), and leave the session in its current editable state.
  • If the server accepted the publish for deferred processing and detects the conflict during processing, it MUST move the session to the error state and report the conflicting file(s) in the session’s notices (and, where the failure is attributable to a particular file, in that file’s notices).

In either case the client may delete or replace the offending file, or otherwise resolve the conflict, and publish again, or cancel the session.

Publishing Session Cancellation

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. Once that data is purged, the session’s action and data-bearing URLs – links.upload, links.publish, links.extend, links.stage, and the individual file URLs – SHOULD become unavailable and MAY return 404 Not Found. The session status URL (links.session) is instead retained as described in Session Status Retention: it continues to report the canceled status for an index-specific period before it too MAY return 404 Not Found.

Cancellation is only permitted while the session is open or in the error state. If the session is in the processing state (i.e. because a deferred publishing request is already being processed) the server MUST reject the cancellation with a 409 Conflict, since publication may already be in progress. The client can instead wait for processing to resolve; if it resolves to error, the session can then be canceled.

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.

Publishing Session Status

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.

Session Status Retention

A session status URL is not guaranteed to remain valid indefinitely. Once a session reaches a terminal state – published or canceled – the server SHOULD continue to serve its status URL, reporting the terminal status, for an index-specific retention period, so that clients can reliably observe the final outcome of a session they did not watch to completion. After that retention period elapses, the server MAY return 404 Not Found for the status URL. The length of the retention period is left to the index, but SHOULD be long enough to let a client that initiated or contributed to the session learn its outcome.

This retention applies only to the session status; the data-bearing and action URLs for a terminated session (for example links.stage and the individual file URLs) may become unavailable as soon as their underlying data is no longer needed, as described for cancellation.

Clients MUST be prepared for a session status URL to return 404 Not Found once the session has terminated and its retention period has elapsed. A 404 on a previously valid session status URL is not in itself an error; clients SHOULD treat it as indicating that the session no longer exists. Note that once a terminated session has been purged, a request to create a new session for the same name and version will succeed with a 201 Created rather than returning the 409 Conflict that a still-live session would have produced.

Publishing Session Extension

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.extend URL. If the server does not support session extensions, the links.extend key will not be present in the response.

The request looks like:

{
  "meta": {
    "api-version": "2.0"
  },
  "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.

Publishing Session Token

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. For example, pip could add a --stage $SESSION_TOKEN flag as a convenience for installing from a staged preview. The links.stage key gives the full URL to the stage, which can be used with installers today, 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.

File Upload Session

Create a File Upload Session

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": "..."},
  "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 final total 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 [3]. 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.

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.

A publishing session MAY be created for a name and version that has already been published, for example to add wheels for additional platforms to an existing release. However, because published artifacts are immutable, if the filename in this request matches a file that has already been published for this release, the server MUST reject the request with a 409 Conflict and MUST NOT overwrite the published file. This check is best-effort and reflects the published state at the time of the request; the authoritative, atomic conflict check is performed at publish time, as described in Atomic Publication and Conflicts.

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.

Response Body

The successful response includes the following:

{
  "meta": {
    "api-version": "2.0"
  },
  "links": {
    "file-upload-session": "...",
    "complete": "...",
    "extend": "..."
  },
  "status": "pending",
  "expires-at": "2030-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.

File Upload Session States

A file upload session is always in exactly one of the following states, reported by the status key of the file upload session status response. The same value is reflected for the file in the files mapping of the publishing session status.

State diagram for a file upload session.  From the initial state the session enters ``pending``, during which the negotiated upload mechanism executes.  From ``pending`` completing the upload either succeeds immediately (``201``) to ``complete``, is accepted for deferred processing (``202``) into ``processing``, or fails synchronously (``4xx``/``5xx``) to ``error``; ``pending`` can also be canceled (``DELETE``) to the terminal ``canceled`` state.  ``processing`` resolves to ``complete`` on success or to ``error`` on failure.  Both ``complete`` and ``error`` can be deleted (``DELETE``) to ``canceled``.  Canceling during ``processing`` is rejected with ``409``.

The textual description of each state and a complete transition table follow.

pending
The file upload session has been created and the negotiated upload mechanism is being executed; the file’s bytes are in transit or not yet fully transferred. A newly created session starts in this state and remains in it until the client completes or cancels the upload. A file whose upload is still pending cannot be replaced.
processing
The client has requested completion and the server accepted the request for deferred processing, returning a 202 Accepted (see Complete a File Upload Session). This is a transitional state; the client polls the file upload session status, respecting the Retry-After header, until it resolves to complete or error.
complete
The file has been fully uploaded, validated, and accepted into the publishing session. The file can still be deleted, which removes it from the publishing session and moves this session to canceled.
error
The upload failed and the file is not in a usable state. Unlike a publishing session in the error state, a file upload session cannot be repaired in place: the client MUST cancel or delete the file and, if it still wants to upload it, begin an entirely new file upload session. A file upload session enters error whenever a completion attempt fails – whether the server detects the failure synchronously within the complete request, or asynchronously while the session is processing.
canceled (terminal)
The session was canceled (an in-progress upload) or its completed file was deleted. The session resource and its associated upload mechanisms MUST NOT be assumed reusable; recovering or replacing the file requires a new file upload session.

Only canceled is terminal. Both complete and error still permit a DELETE (which moves the session to canceled); from error, deletion is the only forward action.

The transitions between these states are:

From Event To
(none) File upload session created (202 Accepted) pending
pending Upload mechanism executes (bytes transferred) pending
pending Completion request completed immediately (201 Created) complete
pending Completion request accepted for deferred processing (202 Accepted) processing
pending Completion request fails synchronously error
pending Cancellation requested (DELETE) canceled
processing Deferred processing succeeds complete
processing Deferred processing fails error
processing Cancellation requested rejected with 409 Conflict (see Cancellation and Deletion)
complete File deleted (DELETE) canceled
error File deleted (DELETE) canceled

Unlike a publishing session, where a synchronous publish failure leaves the session editable and only a deferred failure reaches the error state, a file upload session treats any completion failure as unrecoverable for that file, because a partially or incorrectly uploaded file cannot be edited in place. Both a synchronous and a deferred completion failure therefore move the session to error, from which the client deletes the file and starts over.

Complete a File Upload Session

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 complete link in the file upload session creation response body.

The request looks like:

{
  "meta": {
    "api-version": "2.0"
  }
}

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.

If a completion attempt fails – synchronously (in which case the server also returns an error response) or asynchronously while the session is processing – the session moves to the error state, from which the client must cancel or delete the file and start a new file upload session to retry.

Cancellation and Deletion

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.

A DELETE is permitted while the session is pending (canceling an in-progress upload), complete (deleting an uploaded file), or error (discarding a failed upload). If the session is in the processing state – that is, a deferred completion is already underway – the server MUST reject the DELETE with a 409 Conflict, since the outcome is already being decided. The client can instead wait for processing to resolve and then delete the file if needed.

Once canceled or deleted, a client MUST NOT assume that the previous file upload session resource or associated file upload mechanisms can be reused.

Replacing a Partially or Fully Uploaded File

To replace a session file, the file upload MUST have been previously completed, canceled, or deleted. A file whose upload is still in-progress cannot be replaced; if a client attempts to do so, the server MUST return a 409 Conflict.

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.

File Upload Session Status

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.

A file upload session has no existence independent of the publishing session it belongs to, and its status URL is retained accordingly. While the parent publishing session is in a non-terminal state, the server SHOULD keep each of its file upload session status URLs valid, reporting the file upload session’s current status – including a terminal canceled – so that a client can observe the outcome of any file it uploaded. Once the parent publishing session itself terminates, its file upload session URLs are retained no longer than the parent’s own status URL and MAY return 404 Not Found thereafter. As with cancellation, the data-bearing and mechanism portions of these URLs may become unavailable as soon as the underlying file data is purged, independent of the status URL.

File Upload Session Extension

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 extend link from the file upload session creation response. If the server does not support file upload session extensions, the links.extend key will not be present in the response.

The request looks like:

{
  "meta": {
    "api-version": "2.0"
  },
  "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.

Staged Previews

The ability to preview staged releases before they are published is an important feature of this PEP, enabling an additional level of last-mile testing before the release is available to the public. Indexes MAY provide this functionality through the URL provided in the stage sub-key of the links key returned when the publishing session is created. The stage URL can be passed to installers such as pip by setting the –extra-index-url flag to this value. Multiple stages can even be previewed by repeating this flag with multiple values.

If supported, the index will return views that expose the staged releases to the installer tool, making them available to download and install into virtual environments built for that last-mile testing. This option allows existing installers to preview staged releases with no changes to the installer tool required. The details of this user experience are left to installer tool maintainers.

File Upload Mechanisms

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.

Required File Upload Mechanisms

http-post-bytes

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": {...}},...]

Server Specific File Upload Mechanisms

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.

Recommendations for Client Implementers

This section is non-normative and provides guidance for client tool authors implementing the Upload 2.0 protocol. These recommendations are suggestions based on the expected usage patterns of the protocol; client authors are free to implement alternative approaches that best suit their users’ needs.

General Workflow

A typical upload workflow using the Upload 2.0 protocol follows these steps:

  1. Create a publishing session for the project name and version.
  2. For each artifact (sdist, wheels), create a file upload session, execute the negotiated upload mechanism, and complete the file upload session.
  3. Optionally, if the index supports stage previews, use the links.stage URL to test the release before publishing.
  4. Publish the session to make the release public, or cancel it if issues are discovered.

Clients SHOULD handle failures gracefully at each step. If an error occurs during file upload, the client should cancel the file upload session. If an unrecoverable error occurs at any point, the client should cancel the publishing session to clean up server-side resources.

Parallel Uploads

Clients MAY upload multiple files in parallel by creating and executing multiple file upload sessions concurrently within the same publishing session. This can significantly improve upload times for releases with many wheel variants. However, clients should be prepared for servers that do not support parallel uploads and may return 409 Conflict if parallel uploads are attempted.

Multiple Sessions

Clients can decide whether they should create and manage a single session, multiple sessions in series, or multiple sessions in parallel, depending on the mix of artifacts being uploaded. Since publishing sessions are linked to a specific name-version identifier, if a single client command intends to upload several different name-version artifacts, each one must be in a separate publishing session.

For example, twine upload foo-1.1.tar.gz foo-2.0.tar.gz bar-2.0.tar.gz would require three separate publishing sessions, however, if each sdist were also accompanied by wheels matching its name and version, three publishing sessions would still suffice. Clients should be able to manage all of this under-the-hood.

Session Management

Clients should monitor the expires-at timestamp in session responses. For long-running uploads (e.g., large files on slow connections), clients may need to request session extensions if the links.extend endpoint is available. If the server does not support extensions (indicated by the absence of links.extend), clients should warn users when uploads may exceed the session lifetime.

Suggested Command-Line Interfaces

The following examples illustrate how existing tools might expose the Upload 2.0 protocol to users. These are suggestions only; actual implementations may vary.

twine

twine currently provides a simple twine upload dist/* command. The Upload 2.0 protocol could be exposed through additional options:

twine upload dist/*
Maintains backward compatibility. Uses the Upload 2.0 protocol if available, falling back to the legacy protocol if not. Creates a session, uploads all files, and publishes immediately.
twine upload --stage dist/*
Uses the Upload 2.0 protocol to create a session and upload files, but does not publish. This is useful even when the index does not support stage preview URLs, as it still provides the atomic release semantics of Upload 2.0. If the index supports stage previews, prints the links.stage URL for testing. Prints a session identifier that can be used with subsequent commands. This session identifier is local to the client and is mapped internally to the in-progress server session.
twine session publish <session-id>
Publishes a previously staged session.
twine session cancel <session-id>
Cancels a staged session and discards all uploaded files.
twine session status <session-id>
Queries and displays the current status of a session.

uv

uv could provide similar functionality with additional integration:

uv publish dist/*
Creates a session, uploads all files, and publishes. May leverage parallel uploads for faster publishing of multiple wheels.
uv publish --stage dist/*
Uploads without publishing. Like twine, this is valuable even without stage preview support. Prints a session identifier that can be used with the uv session subcommands.
uv publish --test-install dist/*
If the index supports stage previews, uploads files, installs the package from the stage URL into a temporary virtual environment, optionally runs a smoke test command, and only publishes if successful. This provides an integrated “upload, test, publish” workflow.
uv session publish <session-id>
Publishes a previously staged session.
uv session cancel <session-id>
Cancels a staged session and discards all uploaded files.
uv session status <session-id>
Queries and displays the current status of a session.

GitHub Actions

The pypa/gh-action-pypi-publish action could leverage staged releases to enable powerful CI/CD workflows. A multi-job workflow might look like:

jobs:
  upload:
    runs-on: ubuntu-latest
    outputs:
      stage-url: ${{ steps.upload.outputs.stage-url }}
      session-id: ${{ steps.upload.outputs.session-id }}
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - id: upload
        uses: pypa/gh-action-pypi-publish@v2
        with:
          stage: true  # Upload but don't publish

  test:
    needs: upload
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-python@v5
      - name: Test staged release
        run: |
          pip install --extra-index-url "${{ needs.upload.outputs.stage-url }}" my-package
          python -c "import my_package; my_package.smoke_test()"

  publish:
    needs: [upload, test]
    runs-on: ubuntu-latest
    steps:
      - uses: pypa/gh-action-pypi-publish@v2
        with:
          publish-session: ${{ needs.upload.outputs.session-id }}

This pattern allows the actual PyPI artifacts to be tested in a realistic installation scenario before being published. If the test job fails, the workflow can include a cleanup job to cancel the session:

cancel-on-failure:
  needs: [upload, test]
  if: failure()
  runs-on: ubuntu-latest
  steps:
    - uses: pypa/gh-action-pypi-publish@v2
      with:
        cancel-session: ${{ needs.upload.outputs.session-id }}

Even when the index does not support stage preview URLs, the staged upload pattern is still valuable as it ensures atomic releases: either all artifacts are published together, or none are.

Error Handling

Clients should implement robust error handling for the multi-step upload process:

File upload failures: If a file upload fails (network error, validation error, etc.), the client should cancel that file upload session before retrying. The client may then create a new file upload session for the same filename.

Partial upload recovery: If some files have been successfully uploaded but others fail, the client has options:

  • Cancel the entire publishing session and start over.
  • Cancel only the failed file upload sessions and retry those files.
  • If using --stage mode, leave the session open for manual intervention.

Session expiration: If a session expires during upload, the client must create a new publishing session and re-upload all files. Clients should monitor expires-at and warn users proactively.

Publishing failures: If the publish request fails, the session remains in its current state. The client can query the session status to determine the cause and retry the publish operation.

Graceful cancellation: When a user interrupts an upload (e.g., Ctrl+C), clients should attempt to cancel the publishing session to avoid leaving orphaned sessions on the server.

Legacy API Fallback

During the transition period, clients SHOULD support both the Upload 2.0 and legacy protocols. A suggested approach:

  1. Attempt to use Upload 2.0 by checking for the 2.0 endpoint or using content negotiation.
  2. If the server does not support Upload 2.0 (e.g., returns 404 or 406), fall back to the legacy protocol.
  3. Provide a command-line option to force a specific protocol version if needed for debugging or compatibility.

Security Implications

Name squatting potential

Does PEP 694 make it easier to (maliciously) register project names, i.e. to name- or typo-squat? The authors do not believe so. With the legacy API, it’s trivially easy to create and upload a dummy package to register a project name. This PEP does not effectively change that equation either way, nor does it aim to. That said, indexes such as PyPI could impose additional limitations on project registration activities, such as rate limiting either the legacy API or Upload 2.0 API for empty packages or sessions. An index such as PyPI which supports organizations or PEP 752-style implicit namespaces, could implement different rate limiting rules for different actors. Such implementations are left as index-specific policy decisions.

Session authorization

Session access is authorized contemporaneously rather than being bound to the credentials that created the session (see Authentication and Authorization). Indexes MUST re-validate authorization on each session request – including artifact uploads, file upload session completion, session extension requests, and publishing – so that a principal that loses upload permission while a session is open is denied on its subsequent requests, and a principal that gains permission may join an open session.

This model has two consequences worth calling out. First, because every mutating operation is authorized uniformly, any principal currently authorized to upload to the project may add to, cancel, or publish another principal’s open session. The blast radius is limited to the unpublished staging session, since published artifacts are immutable and publishing is atomic. Second, the stage preview URL is a capability that is not gated by upload permission, so a principal whose permission is revoked mid-session – but who has already obtained the stage URL – retains read-only preview access to the staged files until the session is published or canceled. This is a narrow and accepted limitation; an index that considers it a concern can mitigate it by canceling the affected session, or by limiting session lifetimes and extensions.

Malware hosting potential

Staged releases, while useful for testing and embargoes, do provide some potential for larger scale hosting of malware which isn’t detectable by third party external scanning tools, because staged artifacts are only visible to clients which hold the stage token/url. It’s not clear how much proactive malware scanning is actually going on today with indexes such as PyPI, so it’s unclear whether the (optional) staging feature is much of an additional malware vector. Indexes should likely do some amount of proactive malware scanning on all artifacts, regardless of the protocol used to upload them. Because of the multi-step protocol proposed in this PEP, indexes could share session links or uploaded staged files to trusted third party security partners who could assist in scanning.

Indexes can also mitigate the problem by putting limits on session extensions, which might differ between projects depending on the user or (in the case of PyPI) organization which owns the project. Indexes can refuse to extend sessions, and they can use this to limit the availability of packages with unverified contents.

Considering the testing and embargoed use cases may lead to different session expiry choices. Testing a release can have a relatively short session lifespan, e.g. on the order of hours. Embargoed sessions may need to be extended for several days or a few weeks. An index such as PyPI could use any number of criteria to determine the total lifetime of any particular session, such as whether the credentials are a user or an organization. An index could even support Optional Index-specific Metadata to decide whether the testing or embargoed use case is being employed.

FAQ

Does this mean PyPI is planning to drop support for the legacy upload API?

At this time PyPI does not have any specific plans to drop support for the legacy upload API.

Unlike with PEP 691 there are significant benefits to doing so, so it is likely that support for the legacy upload API to be (responsibly) deprecated and removed at some point in the future. Such future deprecation planning is explicitly out of scope for this PEP.

Can I use the upload 2.0 API to reserve a project name?

Yes! If you’re not ready to upload files to make a release, you can still reserve a project name (assuming of course that the name doesn’t already exist).

To do this, create a new publishing session, then publish the session without uploading any files. While the version key is required in the JSON body of the create session request, you can simply use a placeholder version number such as "0.0.0a0". The version is ignored if no artifacts are uploaded.

Generally the user that created the session will become the owner of the new project, however the index could define index-specific metadata to, for example, allow an organization of which the publisher is a member, to own the new project.

Why is the project name required when creating a publishing session?

The project name is required at session creation because index permissions are fundamentally tied to project ownership. Users have roles and permissions on specific projects, and these permissions must be verified before any uploads can proceed.

Requiring the project name upfront provides several benefits:

Immediate permission validation: The server can verify that the authenticated user has upload permission for the project at session creation time, failing fast with a clear error rather than discovering permission issues after files have been uploaded.

Simplified error handling: If a session could span multiple projects, a permission failure on one project mid-upload would leave the session in a complex partial state. With a single project per session, permission errors are unambiguous.

Trusted Publisher compatibility: Indexes like PyPI support Trusted Publishers where OIDC tokens are scoped to specific projects. A single-project session aligns naturally with this authentication model.

Quota enforcement: Projects may have different upload quotas or size limits. Validating these constraints upfront is simpler when the project is known at session creation.

Atomic release semantics: A publishing session represents an atomic release of a single project version. Allowing multiple projects would fundamentally change this model and complicate the definition of what “publish” means for a session.

Even with the single-project restriction, this PEP still improves multi-project releases. A client releasing several projects at once can fully stage every project – creating a publishing session per project and uploading all of its artifacts – before a final step runs through and publishes each one. This gives the whole set a “stage everything, then publish” workflow, even though each publish is still per-project and atomic on its own.

The single-project session also lays a foundation that a future “publish multiple projects” operation could build on as a separate endpoint, coordinating the publication of several already-staged sessions, without changing the per-session model defined here.

Why is the version required when creating a publishing session?

The version is required at session creation to establish a validation contract before any file uploads begin. Since artifact filenames encode the version (per the sdist and wheel filename specifications), the server can validate that all uploaded files match the declared version.

This design enables deterministic behavior with parallel uploads. If the version were optional and inferred from the first uploaded file, a race condition would occur when multiple files are uploaded in parallel: whichever upload the server processes first would “win” and establish the version, causing other uploads with mismatched versions to fail non-deterministically.

By requiring the version upfront, all parallel uploads validate against the same declared version. A file with a mismatched version always fails, regardless of upload timing or order.

For name registration where no artifacts are uploaded, the version can be any valid placeholder (e.g., "0.0.0a0") since it is ignored when no files are included in the session.

Open Questions

Extensions to the Upload 2.0 Protocol

Features such as asynchronous webhook notifications for completion of upload processing were discussed during review of this PEP. The concept of a capabilities extension for the upload protocol was discussed, which would allow implementers to advertise support for optional features such as asynchronous notifications or webhooks.

This idea was left open due to the complexity that would arise in designing such an extension protocol and ensuring that it did not cause excessive fracturing of the ecosystem as Upload 2.0 is rolled out.

Future revisions to the upload protocol should explore such extensions as experience is gained operating Upload 2.0.

Footnotes

Change History

  • TBD
    • Add an Atomic Publication and Conflicts section. Specify that publication is atomic with respect to the release’s filename namespace: the server reserves the session’s filenames for the duration of a publish so that concurrent uploads (including through the legacy API) receive a 409 Conflict, and releases the reservation if the publish fails. Clarify that the conflict check at file upload session creation is best-effort and that the authoritative check happens atomically at publish time, reported synchronously as a 409 Conflict or, for a deferred publish, by moving the session to the error state with the reason in notices.
    • Add a Session Status Retention section. Specify that after a session reaches a terminal (published or canceled) state, the server SHOULD keep serving its status URL reporting the terminal status for an index-specific retention period, after which it MAY return 404 Not Found, and that clients must be prepared for such a 404. Reconcile the cancellation rules accordingly: the data-bearing and action URLs may become unavailable once purged, while the status URL is retained. Tie file upload session status URL retention to the parent publishing session: while the parent is non-terminal the server SHOULD keep its file upload session status URLs valid, and once the parent terminates they are retained no longer than the parent’s status URL.
    • Expand the “Why is the project name required” FAQ to note that single-project sessions still improve multi-project releases (all projects can be fully staged before a final step publishes each one) and lay a foundation for a possible future “publish multiple projects” endpoint.
  • 26-Jun-2026
    • Session actions now use dedicated endpoint links instead of an action key in request bodies. Publishing sessions add links.publish and links.extend; file upload sessions add links.complete and links.extend. The links.session and links.file-upload-session endpoints are now used only for GET (status) and DELETE (cancel) operations.
    • Add non-normative Recommendations for Client Implementers section with suggested UX patterns for tools like twine, uv, and GitHub Actions.
    • Add FAQ entries explaining why project name and version are required at session creation.
    • Add a Security Implications section.
    • Specify that attempting to replace an in-progress file upload returns a 409 Conflict.
    • Specify that uploading a file matching one already published for an existing release returns a 409 Conflict, since published artifacts are immutable.
    • Clarify the wording of the Multiple Sessions client recommendation example.
    • Relax session access from the exact creating credentials to any principal authorized to upload to the project, evaluated contemporaneously on each request. Adds an Authentication and Authorization model, handles permission changes mid-session, supports rotating Trusted Publishing tokens and multiple publishers contributing to one session, and notes the related security implications.
    • Remove the optional metadata key from the file upload session creation request. The uploaded file is the authoritative source of metadata, which the index extracts from the file itself.
    • Define an explicit publishing-session state machine. Rename the session-level pending status to open, add a transitional processing status for deferred (202 Accepted) publishing, and document the error status as a still-editable state that records a failed deferred publish (with the reason reported in notices). Add a Publishing Session States section with state descriptions and a transition table, specify that a synchronous publish failure leaves the session editable rather than entering error, and require the server to reject cancellation with a 409 Conflict while a session is processing. Key the Multiple Session Creation Requests rule off any non-terminal state rather than pending.
    • Document the file upload session state machine with a File Upload Session States section and transition table. Specify that any completion failure – synchronous or deferred – moves the session to error, that an error file cannot be repaired in place (the client cancels or deletes it and starts a new file upload session), and that the server MUST reject a DELETE with a 409 Conflict while a session is processing.
    • Add state transition diagrams to the Publishing Session States and File Upload Session States sections, alongside the existing transition tables.
    • Make the suggested twine and uv command-line interfaces consistent: group the staged-session operations under a session subcommand (session publish/session cancel/session status), give uv the same staged-session follow-ups and session-id output as twine, and align the GitHub Action’s stage input with the --stage flag.
  • 07-Dec-2025
    • Error responses conform to the RFC 9457 format.
  • 23-Sep-2025
    • Remove the nonce and gentoken() algorithm. Indexes are now responsible for generating an cryptographically secure session token and obfuscated stage URL (but only if they support staged previews).
    • Clarify the semantics when multiple session creation requests are received.
    • Clarify publishing session steps such as status polling and session extension.
    • Require that name conform to the normalization rules, and include a link.
    • Require that version conform to the version specs, and include a link.
    • Require filename to conform to either the source or binary distribution file name convention, and include links.
    • Reference RFC 3399 instead of ISO 8601 as the timestamp spec. The RFC is a simpler format that subsets the ISO standard, and is more appropriate to our use case.
    • Other protocol clarifications.
    • Add optional index-specific metadata keys.
  • 06-Aug-2025
    • Add Dustin as the PEP Delegate.
  • 14-Apr-2025
    • Updates based on PyCon US discussions.
    • Added some error return code descriptions where they were underspecified.
    • Combine the canceling and deleting of upload files sections.
    • Simplify the rules for replacing a staged but not yet published file.
    • Add open question about deferring stage previews.
    • Fix some misspellings and poorly worded text.
  • 06-Jan-2025
    • Resurrect and update the PEP.
    • Added Barry as co-author.
    • Standardize terminology on “stage” rather than “draft”.
    • Proposed the root URL for PyPI to be https://upload.pypi.org/2.0
    • Added an optional nonce key for session obfuscation.
    • Standardize JSON keys and made consistent with terminology.
    • Added and modified several APIs, filling gaps and elaborating on details.
    • Align the upload protocol with draft Internet Standard.