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 upload URL and anuploadId. -
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 (write scope).
Step 1: Initiate Upload
GET /api/packages/versions/newAuthorization: Bearer club_pat_...Response: 200 OK
{ "success": { "uploadUrl": "https://club.example.com/api/packages/versions/upload", "uploadId": "550e8400-e29b-41d4-a716-446655440000" }}| Field | Description |
|---|---|
success.uploadUrl | Where the client must POST the tarball |
success.uploadId | Identifier for this upload session; include it in Step 2 and 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 BadRequest |
Top-level pubspec.yaml is parseable | 400 BadRequest |
| Package name is lowercase alphanumeric with underscores | 400 BadRequest |
| Version is canonical semver | 400 BadRequest |
| Caller is an uploader or publisher admin | 403 Forbidden |
Nested pubspec.yaml files (e.g. example/pubspec.yaml) are not treated as the package | Rejected, 400 BadRequest |
| Duplicate version with matching SHA-256 | Treated as an idempotent success |
| Duplicate version with different SHA-256 | 409 Conflict |
| Archive within the configured size limit | 400 BadRequest 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 | BadRequest | Invalid archive, pubspec, name, or version |
| 401 | MissingAuthentication | No or invalid credentials |
| 403 | Forbidden | Caller is not authorized to publish this package |
| 404 | NotFound | Upload session expired or not found |
| 409 | Conflict | Version already exists with different content |
Errors use the standard envelope:
{ "error": { "code": "Conflict", "message": "Version 2.1.0 already exists with a different archive hash." }}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 '.success.uploadUrl')UPLOAD_ID=$(echo "$RESP" | jq -r '.success.uploadId')
# 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}"