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
-
Initiate upload:
GET /api/packages/versions/newreturns an uploadurland a set of formfields(includingupload_id). -
Upload the tarball:
POSTto the upload URL (multipart or raw tarball), carrying theupload_id. -
Finalize:
GET /api/packages/versions/newUploadFinishvalidates 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/newAuthorization: Bearer club_pat_...Response: 200 OK
{ "url": "https://club.example.com/api/packages/versions/upload", "fields": { "upload_id": "550e8400-e29b-41d4-a716-446655440000" }}| Field | Description |
|---|---|
url | Where the client must POST the tarball |
fields | Form 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/uploadAuthorization: Bearer club_pat_...The upload endpoint accepts either of:
- Multipart form (
multipart/form-data) with afilepart containing the.tar.gzbytes and anupload_idfield (or theupload_idmay be passed as a query parameter). - Raw tarball body (
application/octet-stream), withupload_idpassed as a query parameter.
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
| Check | Failure behavior |
|---|---|
| Archive is a valid gzipped tar | 400 PackageRejected |
Top-level pubspec.yaml is parseable | 400 PackageRejected |
| Package name is lowercase alphanumeric with underscores | 400 PackageRejected |
| Version is canonical semver | 400 PackageRejected |
| Caller is an uploader or publisher admin | 403 InsufficientPermissions |
Nested pubspec.yaml files (e.g. example/pubspec.yaml) are not treated as the package | Rejected, 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 limit | 400 PackageRejected if exceeded |
What Happens on Success
- The
Packagerecord is created if this is the first version. - A
PackageVersionrecord is written with extracted metadata. - The
latestand latest-prerelease pointers are recomputed. - The tarball is moved from temp storage to permanent blob storage.
- The search index is updated.
- An audit log entry is appended.
- The upload session is cleaned up.
Error Responses
| Status | Code | When |
|---|---|---|
| 400 | PackageRejected | Invalid archive, pubspec, name, version, oversized archive, or a duplicate version with different content |
| 400 | InvalidInput | Upload session expired, not found, or in the wrong state |
| 401 | MissingAuthentication | No or invalid credentials |
| 403 | InsufficientPermissions | Caller 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
# Step 1: initiateRESP=$(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: finalizecurl -s -H "Authorization: Bearer $TOKEN" \ "https://club.example.com${FINISH_URL}"