Skip to content

Packdog API Reference

Base URL: https://api.packdog.dev/v1

Auth

There are three identities the API recognises:

  • Customer API key (blk_...) — load access. Used by browser-side code to fetch the current version of a channel. Read-only and scoped to the packages your account has been granted. Lives in browser JS by design.
  • Developer token (dev_...) — upload and publish access. Scoped per package, with separate can_upload and can_publish flags. Used from a server, a CI pipeline, or the packdog CLI.
  • Admin — used by the Packdog operator for service management (creating customers, rotating keys, deleting packages). Not customer-facing and intentionally not documented here. Email post@jetbit.no if you need an admin operation performed against your account.

Public endpoints (no auth): GET /health, GET /.


Core concepts

Versions are immutable upload artifacts. Uploading always creates a new version with a UUID. Versions know nothing about channels.

Channels are named pointers to a version (stable, stage, beta — any name you choose). Each channel has its own independent rollback history.

The typical flow:

upload → auto-published to stage → test → publish to stable

Endpoints

GET /me

Identity for a developer token plus the packages it has access to.

Auth: developer token.

Response:

json
{
  "id": "uuid",
  "name": "Jane Doe",
  "packages": [
    { "id": "uuid", "name": "shooter-game", "created_at": 1234567890 }
  ]
}

GET /packages

List packages you have access to.

Auth: developer token (returns scoped list).

Response:

json
[{ "id": "uuid", "name": "Shooter Game", "created_at": 1234567890 }]

GET /packages/{id}

Get package metadata.

Auth: developer token with access to this package.

Response:

json
{ "id": "uuid", "name": "Shooter Game", "created_at": 1234567890 }

GET /packages/{id}/versions

List all versions for a package, newest first.

Auth: developer token with access to this package.

Response:

json
[
  {
    "id": "uuid",
    "package_id": "uuid",
    "r2_prefix": "packages/uuid/versions/uuid",
    "file_count": 3,
    "total_size": 245678,
    "created_at": 1234567890
  }
]

GET /packages/{id}/versions/{versionId}

Get URLs for a specific version. Use this to test a version before publishing it to a channel.

Auth: developer token with access to this package.

Response:

json
{
  "versionId": "uuid",
  "packageId": "uuid",
  "createdAt": 1234567890,
  "index": "https://pub-xxx.r2.dev/packages/uuid/versions/uuid/index.js",
  "baseUri": "https://pub-xxx.r2.dev/packages/uuid/versions/uuid"
}

POST /packages/{id}/upload

Upload files for a new version. Multipart form-data — each field key is the relative file path, the value is the file. Must include index.js — this is the entry point clients load.

Auth: developer token with can_upload for this package.

Limits enforced server-side:

  • Max 25 MB per file
  • Max 50 MB total
  • Max 200 files per upload
  • File paths must be relative; segments equal to . or .., leading / or \, and empty segments are rejected. Dotfiles like .well-known/security.txt are allowed.

If Content-Length exceeds 50 MB, the upload is rejected with 413 Payload Too Large before the body is read.

bash
curl -X POST https://api.packdog.dev/v1/packages/$PACKAGE_ID/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "index.js=@dist/index.js" \
  -F "style.css=@dist/style.css"

Response:

json
{
  "versionId": "uuid",
  "packageId": "uuid",
  "filesUploaded": 3,
  "totalSize": 245678,
  "message": "Version created. Use POST /packages/{id}/channels/{channel} to publish it."
}

Upload only creates the version — it does not affect any channel.


GET /packages/{id}/channels

List all active channels for a package.

Auth: developer token with access to this package.

Response:

json
[
  { "package_id": "uuid", "channel": "stable", "version_id": "uuid", "updated_at": 1234567890 },
  { "package_id": "uuid", "channel": "stage",  "version_id": "uuid", "updated_at": 1234567890 }
]

GET /packages/{id}/channels/{channel}

Get the current version for a channel. This is what client code calls to load a package.

Auth: customer API key (blk_...).

Response:

json
{
  "packageId": "uuid",
  "channel": "stable",
  "versionId": "uuid",
  "publishedAt": 1234567890,
  "index": "https://pub-xxx.r2.dev/packages/uuid/versions/uuid/index.js",
  "baseUri": "https://pub-xxx.r2.dev/packages/uuid/versions/uuid"
}

index is the URL to load. baseUri is the base for the package to load its own assets.

Cached for 60 seconds at the edge (Cache-Control: public, max-age=60, s-maxage=60).


POST /packages/{id}/channels/{channel}

Publish a version to a channel. Creates the channel if it doesn't exist.

Auth: developer token with can_publish for this package.

Channel name rules:

  • Matches /^[a-z0-9][a-z0-9._-]{0,63}$/i (case-insensitive, 1–64 chars, alphanumeric start, then alphanumeric/./_/-)
  • Reserved names rollback and cleanup are rejected (case-insensitive) — they would shadow URL routing

400 Bad Request is returned when the channel name fails either rule.

Request:

json
{ "versionId": "uuid" }

Response:

json
{ "packageId": "uuid", "channel": "stable", "versionId": "uuid", "message": "Published to channel 'stable'" }

POST /packages/{id}/channels/{channel}/rollback

Roll back a channel to its previous version. Can be called multiple times — each call steps one version back through that channel's history.

Auth: developer token with can_publish for this package.

  • 400 Bad Request if the channel has no previous version to roll back to.
  • 409 Conflict if the channel was modified concurrently between read and write — retry.

Response:

json
{ "packageId": "uuid", "channel": "stable", "versionId": "uuid", "message": "Channel 'stable' rolled back" }

GET /health

Public liveness signal — no other fields are exposed.

json
{ "status": "ok" }

GET /

Public API index. Returns a small JSON document linking to the docs. The marketing landing page is at packdog.dev, separate from the API.

json
{
  "name": "packdog-api",
  "version": "v1",
  "docs": "https://docs.packdog.dev",
  "health": "https://api.packdog.dev/health"
}

Client-side usage

The intended browser-side load:

javascript
const { index, baseUri } = await fetch(
  'https://api.packdog.dev/v1/packages/PACKAGE_ID/channels/stable',
  { headers: { 'Authorization': 'Bearer blk_yourcustomerkey' } }
).then(r => r.json());

await import(index);  // index.js is an ES module / web component

const el = document.createElement('my-component');
el.baseUri = baseUri;  // package uses this internally to load its own assets
document.body.appendChild(el);

The customer API key (blk_...) is semi-public by design — it lives in browser JS and identifies the caller. It is read-only and scoped to packages you have been granted access to. If a key leaks, email post@jetbit.no and we rotate it immediately — the old key stops working as soon as the new one is issued.


Typical developer workflow

bash
# Developer uploads — auto-publishes to stage
packdog upload

# Test against stage, then promote to stable
packdog publish

# If the stable build turns out broken
packdog rollback --channel=stable

See CLI reference and Examples for more.

Packdog runs on Cloudflare Workers, D1, and R2.