Skip to content

Publishing Endpoints

Publishing a package to club follows the 3-step flow mandated by the Dart Pub Repository Specification v2. The dart pub publish command and the club CLI handle this automatically; this page documents the underlying HTTP calls.

Flow Overview

  1. Initiate upload: GET /api/packages/versions/new returns an upload url and a set of form fields (including upload_id).

  2. Upload the tarball: POST to the upload URL (multipart or raw tarball), carrying the upload_id.

  3. Finalize: GET /api/packages/versions/newUploadFinish validates the tarball, creates the records, and returns a success message.

All three endpoints require authentication. The caller must be an uploader of the package (or an admin of its publisher); the finalize step enforces this.


Step 1: Initiate Upload

GET /api/packages/versions/new
Authorization: Bearer club_pat_...

Response: 200 OK

{
"url": "https://club.example.com/api/packages/versions/upload",
"fields": {
"upload_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
FieldDescription
urlWhere the client must POST the tarball
fieldsForm fields to include in the multipart upload. Contains upload_id, the identifier for this upload session (also needed in Step 3)

Step 2: Upload the Tarball

POST /api/packages/versions/upload
Authorization: Bearer club_pat_...

The upload endpoint accepts either of:

  • Multipart form (multipart/form-data) with a file part containing the .tar.gz bytes and an upload_id field (or the upload_id may be passed as a query parameter).
  • Raw tarball body (application/octet-stream), with upload_id passed as a query parameter.
Terminal window
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-F "upload_id=$UPLOAD_ID" \
-F "file=@package.tar.gz;type=application/gzip" \
"$UPLOAD_URL"

On success the server responds with a 302 Found redirect whose Location header points at Step 3:

Location: /api/packages/versions/newUploadFinish?upload_id=<uploadId>

Step 3: Finalize

GET /api/packages/versions/newUploadFinish?upload_id=<uploadId>
Authorization: Bearer club_pat_...

The upload_id can also be passed as a path segment: /api/packages/versions/newUploadFinish/<uploadId>.

The server validates the archive, extracts pubspec.yaml, README, CHANGELOG, and example/, enforces authorization, and persists the version.

Response: 200 OK

{
"success": {
"message": "Successfully uploaded my_package version 2.1.0."
}
}

Validation

CheckFailure behavior
Archive is a valid gzipped tar400 PackageRejected
Top-level pubspec.yaml is parseable400 PackageRejected
Package name is lowercase alphanumeric with underscores400 PackageRejected
Version is canonical semver400 PackageRejected
Caller is an uploader or publisher admin403 InsufficientPermissions
Nested pubspec.yaml files (e.g. example/pubspec.yaml) are not treated as the packageRejected, 400 PackageRejected
Duplicate version with matching SHA-256 (no force)Treated as an idempotent success
Duplicate version with different SHA-256 (no force)400 PackageRejected
Archive within the configured size limit400 PackageRejected if exceeded

What Happens on Success

  1. The Package record is created if this is the first version.
  2. A PackageVersion record is written with extracted metadata.
  3. The latest and latest-prerelease pointers are recomputed.
  4. The tarball is moved from temp storage to permanent blob storage.
  5. The search index is updated.
  6. An audit log entry is appended.
  7. The upload session is cleaned up.

Error Responses

StatusCodeWhen
400PackageRejectedInvalid archive, pubspec, name, version, oversized archive, or a duplicate version with different content
400InvalidInputUpload session expired, not found, or in the wrong state
401MissingAuthenticationNo or invalid credentials
403InsufficientPermissionsCaller is not authorized to publish this package

Errors use the standard envelope:

{
"error": {
"code": "PackageRejected",
"message": "Version '2.1.0' of package 'my_package' already exists."
}
}

Manual Publish with curl

Terminal window
# Step 1: initiate
RESP=$(curl -s -H "Authorization: Bearer $TOKEN" \
https://club.example.com/api/packages/versions/new)
UPLOAD_URL=$(echo "$RESP" | jq -r '.url')
UPLOAD_ID=$(echo "$RESP" | jq -r '.fields.upload_id')
# Step 2: upload (capture the redirect target)
FINISH_URL=$(curl -s -D - -o /dev/null \
-H "Authorization: Bearer $TOKEN" \
-F "upload_id=$UPLOAD_ID" \
-F "file=@package.tar.gz;type=application/gzip" \
"$UPLOAD_URL" | awk 'BEGIN{IGNORECASE=1}/^location:/{print $2}' | tr -d '\r')
# Step 3: finalize
curl -s -H "Authorization: Bearer $TOKEN" \
"https://club.example.com${FINISH_URL}"