{"openapi":"3.1.0","info":{"title":"Pulli API","description":"Public REST API for the Pulli macOS download manager. Provides release information, app updates, download endpoints, public statistics, license management, and billing checkout. The free license key is the account — there is no separate user login. Authenticated device endpoints require an x-machine-uid header and a signed request body obtained during license activation.","version":"1.0.0","contact":{"name":"Pulli","url":"https://pulli.app","email":"hello@pulli.app"},"license":{"name":"Proprietary","url":"https://pulli.app/terms"}},"servers":[{"url":"https://pulli.app","description":"Production"}],"tags":[{"name":"Public","description":"Unauthenticated public endpoints"},{"name":"Releases","description":"App release information and downloads"},{"name":"Stats","description":"Public statistics"},{"name":"Accounts","description":"License key generation and account management"},{"name":"Licenses","description":"License activation, validation, and deactivation"},{"name":"Billing","description":"Checkout and payment webhook endpoints"},{"name":"Events","description":"Analytics event tracking"}],"paths":{"/api/health":{"get":{"tags":["Public"],"operationId":"getHealth","summary":"Service health check","description":"Returns the health status of the service including database, KV, and environment checks.","responses":{"200":{"description":"Service is healthy or degraded","content":{"application/json":{"schema":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["healthy","degraded"]},"checks":{"type":"object","properties":{"database":{"type":"string","enum":["healthy","unhealthy","unknown"]},"kv":{"type":"string","enum":["healthy","unhealthy","unknown"]},"environment":{"type":"string","enum":["healthy","unhealthy","unknown"]}}}}}}}},"503":{"description":"Service is degraded — one or more subsystems are unhealthy"}}}},"/api/releases":{"get":{"tags":["Releases"],"operationId":"listReleases","summary":"List releases","description":"Returns a list of published releases, optionally filtered by channel. Each release includes version, channel, release notes, and summary.","parameters":[{"name":"channel","in":"query","required":false,"schema":{"type":"string","enum":["stable","nightly"]},"description":"Filter releases by channel. Omit to list all channels."}],"responses":{"200":{"description":"List of releases","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"version":{"type":"string","example":"1.2.0"},"channel":{"type":"string","enum":["stable","nightly"]},"notes":{"type":"string","nullable":true},"summary":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}}}}}}}}},"/api/get-latest-version":{"get":{"tags":["Releases"],"operationId":"getLatestVersion","summary":"Get latest version","description":"Returns the latest released version number across all channels.","responses":{"200":{"description":"Latest version","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string","example":"1.2.0"}}}}}},"500":{"description":"Internal error retrieving version"}}}},"/api/download":{"get":{"tags":["Releases"],"operationId":"downloadLatestDmg","summary":"Download latest DMG","description":"Streams the latest DMG installer for the requested architecture. Supports HTTP range requests for resumable downloads.","parameters":[{"name":"arch","in":"query","required":true,"schema":{"type":"string","enum":["x86_64","aarch64"]},"description":"CPU architecture for the download."}],"responses":{"200":{"description":"DMG file stream","headers":{"content-type":{"schema":{"type":"string","example":"application/x-apple-diskimage"}},"content-disposition":{"schema":{"type":"string"}},"accept-ranges":{"schema":{"type":"string","example":"bytes"}},"content-length":{"schema":{"type":"integer"}}}},"206":{"description":"Partial content (range request)","headers":{"content-range":{"schema":{"type":"string","example":"bytes 0-1023/2048"}}}},"404":{"description":"Download file not found for the requested version and architecture"},"416":{"description":"Requested range not satisfiable"}}}},"/api/check-update":{"get":{"tags":["Releases"],"operationId":"checkForUpdate","summary":"Check for app updates","description":"Compares the caller's current version against the latest release. Returns 204 No Content if up to date, or update information (signature, URL, summary) if a newer version exists. Optionally tracks the check against a license key and machine UID if provided in headers.","parameters":[{"name":"arch","in":"query","required":true,"schema":{"type":"string","enum":["x86_64","aarch64"]}},{"name":"version","in":"query","required":true,"schema":{"type":"string"},"description":"Current app version (e.g. 1.1.0)."},{"name":"channel","in":"query","required":false,"schema":{"type":"string","enum":["stable","nightly"]},"default":"stable"}],"headers":{"license":{"description":"Optional license key for tracking","schema":{"type":"string"}},"x-machine-uid":{"description":"Optional machine UID for activation tracking","schema":{"type":"string"}}},"responses":{"200":{"description":"Update available","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string","example":"1.2.0"},"signature":{"type":"string","description":"Ed25519 signature of the update tarball"},"url":{"type":"string","description":"URL to download the update tarball"},"releaseNotesSummary":{"type":"string","nullable":true}}}}}},"204":{"description":"No update available — caller is on the latest version"}}}},"/api/app/update":{"get":{"tags":["Releases"],"operationId":"downloadUpdate","summary":"Download update tarball","description":"Streams the update tarball (.tar.gz) for the latest release on the requested channel and architecture. Used by the Pulli desktop app's auto-updater.","parameters":[{"name":"arch","in":"query","required":true,"schema":{"type":"string","enum":["x86_64","aarch64"]}},{"name":"channel","in":"query","required":false,"schema":{"type":"string","enum":["stable","nightly"]},"default":"stable"}],"responses":{"200":{"description":"Update tarball stream","headers":{"content-type":{"schema":{"type":"string","example":"application/gzip"}},"content-disposition":{"schema":{"type":"string"}},"content-length":{"schema":{"type":"integer"}}}},"404":{"description":"Update file not found"},"500":{"description":"Internal error"}}}},"/api/stats":{"get":{"tags":["Stats"],"operationId":"getPublicStats","summary":"Get public statistics","description":"Returns public install and download statistics, including daily download counts for the past 365 days and aggregate totals. Cached for 1 hour.","responses":{"200":{"description":"Public statistics","content":{"application/json":{"schema":{"type":"object","properties":{"daily":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","example":"2025-07-01"},"count":{"type":"integer"}}}},"totals":{"type":"object","properties":{"installs":{"type":"integer"},"downloads":{"type":"integer"},"activeThisWeek":{"type":"integer"},"countries":{"type":"integer"}}}}}}}},"429":{"description":"Rate limited"},"500":{"description":"Internal error"}}}},"/api/event":{"post":{"tags":["Events"],"operationId":"trackEvent","summary":"Track an analytics event","description":"Records a page view or download event from the website. Rate limited to 30 requests per minute.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["page_view","download"]},"arch":{"type":"string","enum":["x86_64","aarch64"]},"channel":{"type":"string","enum":["stable","nightly"]},"path":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"Event recorded","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"429":{"description":"Rate limited"}}}},"/api/v1/accounts/generate":{"post":{"tags":["Accounts"],"operationId":"generateFreeLicense","summary":"Generate a free license key","description":"Creates a new free Pulli license key. No authentication required. Rate limited to 5 per hour.","responses":{"200":{"description":"License key generated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"licenseKey":{"type":"string","example":"pulli-XXXX-XXXX-XXXX"}}}}}}}},"429":{"description":"Rate limited"},"500":{"description":"Generation failed"}}}},"/api/v1/accounts/recover":{"post":{"tags":["Accounts"],"operationId":"recoverLicense","summary":"Recover license key by email","description":"Sends the license key to the associated email address. Always returns success (even if no license exists) to prevent email enumeration. Rate limited to 5 per hour.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Recovery email sent (or no-op if email not found)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"null"},"error":{"type":"null"}}}}}},"429":{"description":"Rate limited"}}}},"/api/v1/accounts/status":{"post":{"tags":["Accounts"],"operationId":"getAccountStatus","summary":"Check account status","description":"Returns the license type, status, feature flags, device activations, and associated email for a given license key.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["licenseKey"],"properties":{"licenseKey":{"type":"string"}}}}}},"responses":{"200":{"description":"Account status","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"license":{"type":"object","properties":{"type":{"type":"string","enum":["free","pro"]},"status":{"type":"string","enum":["active","expired"]},"productId":{"type":"string","nullable":true},"featureFlags":{"type":"object","nullable":true},"maxDevices":{"type":"integer","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"devices":{"type":"array","items":{"type":"object","properties":{"machineUid":{"type":"string"},"machineName":{"type":"string"},"platform":{"type":"string"},"activatedAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":"string","format":"date-time","nullable":true}}}},"email":{"type":"string","nullable":true}}},"error":{"type":"null"}}}}}},"429":{"description":"Rate limited"}}}},"/api/v1/accounts/email":{"post":{"tags":["Accounts"],"operationId":"updateAccountEmail","summary":"Update account email","description":"Attaches or updates the email address associated with a license key. Sends a confirmation email when an email is first attached.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["licenseKey","email"],"properties":{"licenseKey":{"type":"string"},"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Email updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"email":{"type":"string"}}},"error":{"type":"null"}}}}}},"409":{"description":"Email already in use by another account"},"429":{"description":"Rate limited"}}}},"/api/v1/accounts/delete":{"post":{"tags":["Accounts"],"operationId":"deleteAccount","summary":"Delete account","description":"Permanently deletes a license and all associated data (activations, events). Anonymizes the Polar customer record if the account had a Pro subscription. Requires confirmation field set to \"DELETE\". Rate limited to 3 per hour.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["licenseKey","confirm"],"properties":{"licenseKey":{"type":"string"},"confirm":{"type":"string","enum":["DELETE"]}}}}}},"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"polarAnonymized":{"type":"boolean"},"needsManualPolarCancellation":{"type":"boolean"}}},"error":{"type":"null"}}}}}},"429":{"description":"Rate limited"}}}},"/api/v1/licenses/activate":{"post":{"tags":["Licenses"],"operationId":"activateLicense","summary":"Activate a license on a device","description":"Activates a license key on a specific machine. Returns the license type, max devices, devices used, and a request sign token used for subsequent authenticated requests. No app-auth middleware required — this is the enrollment endpoint.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["licenseKey","machineUid","platform","appVersion"],"properties":{"licenseKey":{"type":"string"},"machineUid":{"type":"string"},"deviceName":{"type":"string"},"platform":{"type":"string","example":"darwin"},"appVersion":{"type":"string","example":"1.2.0"}}}}}},"responses":{"200":{"description":"License activated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"key":{"type":"string"},"licenseType":{"type":"string","enum":["free","pro"]},"email":{"type":"string"},"validUntil":{"type":"string","format":"date-time","nullable":true},"maxDevices":{"type":"integer","nullable":true},"devicesUsed":{"type":"integer"},"metadata":{"type":"object","nullable":true},"requestSignToken":{"type":"string","description":"Token for signing subsequent authenticated requests"}}},"error":{"type":"null"}}}}}},"400":{"description":"Invalid license key format or request body"},"404":{"description":"License not found"},"409":{"description":"Max devices exceeded for this license"},"429":{"description":"Rate limited"}}}},"/api/v1/licenses/validate":{"post":{"tags":["Licenses"],"operationId":"validateLicense","summary":"Validate a license","description":"Checks whether the authenticated device's license is still valid. Requires app-auth (x-machine-uid header + signed request body).","headers":{"x-machine-uid":{"description":"Machine UID obtained during activation","schema":{"type":"string"},"required":true}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["license","machineUid","platform","appVersion"],"properties":{"license":{"type":"string","description":"License key"},"machineUid":{"type":"string"},"platform":{"type":"string"},"appVersion":{"type":"string"}}}}}},"responses":{"200":{"description":"License validation result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"isValid":{"type":"boolean"},"serverTimestamp":{"type":"string","nullable":true},"daysRemaining":{"type":"integer","nullable":true},"devicesUsed":{"type":"integer"}}},"error":{"type":"null"}}}}}},"401":{"description":"Unauthorized — machine not activated or signature invalid"},"403":{"description":"License expired"},"429":{"description":"Rate limited"}}}},"/api/v1/licenses/deactivate":{"post":{"tags":["Licenses"],"operationId":"deactivateLicense","summary":"Deactivate a license on a device","description":"Removes the device activation for the authenticated machine. Requires app-auth (x-machine-uid header + signed request body). The caller can only deactivate the machine it authenticated as.","headers":{"x-machine-uid":{"description":"Machine UID obtained during activation","schema":{"type":"string"},"required":true}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["licenseKey","machineUid"],"properties":{"licenseKey":{"type":"string"},"machineUid":{"type":"string"}}}}}},"responses":{"200":{"description":"License deactivated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"null"},"error":{"type":"null"}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Machine mismatch — cannot deactivate another machine"},"429":{"description":"Rate limited"}}}},"/api/v1/billing/checkout":{"post":{"tags":["Billing"],"operationId":"createCheckoutSession","summary":"Create a checkout session","description":"Creates a Polar.sh checkout URL for upgrading a free license to Pro. Requires an existing valid license key. Billing period can be monthly, yearly, or lifetime.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["billing","licenseKey"],"properties":{"billing":{"type":"string","enum":["monthly","yearly","lifetime"]},"licenseKey":{"type":"string"}}}}}},"responses":{"200":{"description":"Checkout session created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"url":{"type":"string","description":"Polar checkout URL"}}},"error":{"type":"null"}}}}}},"409":{"description":"Already on Pulli Pro"},"429":{"description":"Rate limited"},"502":{"description":"Checkout creation failed"}}}}},"components":{"securitySchemes":{"AppAuth":{"type":"apiKey","in":"header","name":"x-machine-uid","description":"Machine UID obtained during license activation. Paired with a signed request body using the requestSignToken returned from activation."}}}}