Pulli Developer Portal

REST API reference for the Pulli macOS download manager

Pulli exposes a REST API for release information, app updates, download distribution, public statistics, license management, and billing. The API is hosted at https://pulli.app. There is no separate user login — the free license key is the account.

Authentication

Pulli uses three authentication tiers depending on the endpoint:

None
Public endpoints (health, releases, stats, downloads) require no authentication. Rate limited by IP.
License key in body
Account management endpoints require a licenseKey field in the JSON request body. The license key format is pulli-XXXX-XXXX-XXXX-XXXX.
App auth
Device-scoped app and license endpoints (except activate and /api/app/update) require an x-machine-uid header and a request body HMAC-signed with the requestSignToken from activation. Responses are Ed25519-signed.

Release channels

Pulli publishes two release channels:

stable
Production releases. The default channel for all endpoints. Available via /api/download and /api/app/update.
nightly
Pre-release builds for testing. Pass ?channel=nightly to release and update endpoints.

API reference

All endpoints return JSON unless noted otherwise (download endpoints stream binary data). The full machine-readable spec is available at /openapi.json.

Public

MethodPathDescriptionAuth
GET/api/healthService health check (database, KV, environment)None
GET/api/statsPublic install/download statistics, cached 1 hourNone
POST/api/eventTrack a page_view or download eventNone

Releases

MethodPathDescriptionAuth
GET/api/releasesList releases, optionally filtered by ?channel=stable|nightlyNone
GET/api/get-latest-versionLatest version string — same as GET /api/releases[0].versionNone
GET/api/download?arch=x86_64|aarch64Download the latest DMG installer (supports range requests)None
GET/api/download/metaLatest DMG metadata — version, channel, published date, per-arch filename, size, and SHA256. Served from KV snapshot download-meta:v1.None
GET/api/check-update?arch&version&channelCheck for app updates; 204 if up-to-date, 200 with signature+URL if update availableNone
GET/api/app/update?arch&channel&versionDownload the update tarball (.tar.gz) for one locator; omit version for latestNone

App

MethodPathDescriptionAuth
POST/api/app/telemetry-configAxiom dataset, ingest token, and edge for opted-in diagnosticsApp auth (x-machine-uid + signed body)
POST/api/app/statsAnonymous daily resource metrics from an activated installApp auth (x-machine-uid + signed body)

Accounts

MethodPathDescriptionAuth
POST/api/v1/accounts/generateGenerate a free license key (pulli-XXXX-XXXX-XXXX-XXXX)None
POST/api/v1/accounts/recoverRecover license key by emailNone
POST/api/v1/accounts/statusCheck license type, status, devices, email, and renewal noticeLicense key in body
POST/api/v1/accounts/emailAttach or update the email on a licenseLicense key in body
POST/api/v1/accounts/email/detachClear the recovery email; idempotent when none is attachedLicense key in body
POST/api/v1/accounts/email/sendMail the stored recovery inbox, or a one-time address in the body; 409 if neither is presentLicense key in body
POST/api/v1/accounts/devices/deactivateFree one activation slot on this keyLicense key in body
POST/api/v1/accounts/deletePermanently delete account and all associated dataLicense key in body

Licenses

MethodPathDescriptionAuth
POST/api/v1/licenses/activateActivate a license on a device; returns a requestSignToken and an Ed25519 offlineGrant. Missing signer is 5xx before activation mutation.None
POST/api/v1/licenses/validateValidate the authenticated device's license. isValid:true includes offlineGrant; missing signer is 5xx before activation writes.App auth (x-machine-uid + signed body)
POST/api/v1/licenses/deactivateRemove the device activation for the authenticated machineApp auth (x-machine-uid + signed body)

Billing

MethodPathDescriptionAuth
GET/api/v1/billing/statusWhether Pro checkout is available (no secrets)None

Rate limits

License, account, unlock, and app update / event / stats / telemetry routes are rate limited by IP. Each of those routes has a short burst clamp and its own wider bucket. A 429 Too Many Requests includes Retry-After plus X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. Other public endpoints are not on this limiter.

Example requests

Check for updates

GET /api/check-update?arch=aarch64&version=1.1.0&channel=stable

# 200 — update available
{ "version": "1.2.0", "signature": "...", "url": "https://pulli.app/api/app/update?arch=aarch64&channel=stable&version=1.2.0" }

# 204 — up to date

Generate a free license

POST /api/v1/accounts/generate

{ "success": true, "data": { "licenseKey": "pulli-XXXX-XXXX-XXXX-XXXX" } }

Activate a license

POST /api/v1/licenses/activate
Content-Type: application/json

{ "licenseKey": "pulli-XXXX-XXXX-XXXX-XXXX", "machineUid": "abc123", "platform": "darwin", "appVersion": "1.2.0" }

{ "success": true, "data": { "key": "pulli-XXXX-XXXX-XXXX-XXXX", "licenseType": "free", "requestSignToken": "..." } }

List releases

GET /api/releases?channel=stable

[{ "version": "1.2.0", "channel": "stable", "notes": "...", "summary": "...", "createdAt": "2025-07-01T..." }]

Resources