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
licenseKeyfield in the JSON request body. The license key format ispulli-XXXX-XXXX-XXXX-XXXX. - App auth
- Device-scoped app and license endpoints (except activate and
/api/app/update) require anx-machine-uidheader and a request body HMAC-signed with therequestSignTokenfrom activation. Responses are Ed25519-signed.
Release channels
Pulli publishes two release channels:
- stable
- Production releases. The default channel for all endpoints. Available via
/api/downloadand/api/app/update. - nightly
- Pre-release builds for testing. Pass
?channel=nightlyto 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
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/health | Service health check (database, KV, environment) | None |
| GET | /api/stats | Public install/download statistics, cached 1 hour | None |
| POST | /api/event | Track a page_view or download event | None |
Releases
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/releases | List releases, optionally filtered by ?channel=stable|nightly | None |
| GET | /api/get-latest-version | Latest version string — same as GET /api/releases[0].version | None |
| GET | /api/download?arch=x86_64|aarch64 | Download the latest DMG installer (supports range requests) | None |
| GET | /api/download/meta | Latest 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&channel | Check for app updates; 204 if up-to-date, 200 with signature+URL if update available | None |
| GET | /api/app/update?arch&channel&version | Download the update tarball (.tar.gz) for one locator; omit version for latest | None |
App
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/app/telemetry-config | Axiom dataset, ingest token, and edge for opted-in diagnostics | App auth (x-machine-uid + signed body) |
| POST | /api/app/stats | Anonymous daily resource metrics from an activated install | App auth (x-machine-uid + signed body) |
Accounts
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/v1/accounts/generate | Generate a free license key (pulli-XXXX-XXXX-XXXX-XXXX) | None |
| POST | /api/v1/accounts/recover | Recover license key by email | None |
| POST | /api/v1/accounts/status | Check license type, status, devices, email, and renewal notice | License key in body |
| POST | /api/v1/accounts/email | Attach or update the email on a license | License key in body |
| POST | /api/v1/accounts/email/detach | Clear the recovery email; idempotent when none is attached | License key in body |
| POST | /api/v1/accounts/email/send | Mail the stored recovery inbox, or a one-time address in the body; 409 if neither is present | License key in body |
| POST | /api/v1/accounts/devices/deactivate | Free one activation slot on this key | License key in body |
| POST | /api/v1/accounts/delete | Permanently delete account and all associated data | License key in body |
Licenses
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/v1/licenses/activate | Activate a license on a device; returns a requestSignToken and an Ed25519 offlineGrant. Missing signer is 5xx before activation mutation. | None |
| POST | /api/v1/licenses/validate | Validate 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/deactivate | Remove the device activation for the authenticated machine | App auth (x-machine-uid + signed body) |
Billing
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/v1/billing/status | Whether 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 dateGenerate 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
- OpenAPI Specification — machine-readable API spec (JSON)
- llms.txt — AI agent discovery file
- Operator console — licenses, activations, metrics, and release publish (runs locally)
- Download — latest DMG, size, and SHA256
- Pricing — free and Pro plans
- Support — get help
- Discord Community — talk to us and other developers