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 separatecan_uploadandcan_publishflags. Used from a server, a CI pipeline, or thepackdogCLI. - 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 stableEndpoints
GET /me
Identity for a developer token plus the packages it has access to.
Auth: developer token.
Response:
{
"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:
[{ "id": "uuid", "name": "Shooter Game", "created_at": 1234567890 }]GET /packages/{id}
Get package metadata.
Auth: developer token with access to this package.
Response:
{ "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:
[
{
"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:
{
"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.txtare allowed.
If Content-Length exceeds 50 MB, the upload is rejected with 413 Payload Too Large before the body is read.
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:
{
"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:
[
{ "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:
{
"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
rollbackandcleanupare rejected (case-insensitive) — they would shadow URL routing
400 Bad Request is returned when the channel name fails either rule.
Request:
{ "versionId": "uuid" }Response:
{ "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 Requestif the channel has no previous version to roll back to.409 Conflictif the channel was modified concurrently between read and write — retry.
Response:
{ "packageId": "uuid", "channel": "stable", "versionId": "uuid", "message": "Channel 'stable' rolled back" }GET /health
Public liveness signal — no other fields are exposed.
{ "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.
{
"name": "packdog-api",
"version": "v1",
"docs": "https://docs.packdog.dev",
"health": "https://api.packdog.dev/health"
}Client-side usage
The intended browser-side load:
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
# 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=stableSee CLI reference and Examples for more.