Upload App
Upload a new version of an application, including the binary file and relevant metadata. You can specify an updater type for artifact isolation when uploading files for updaters that generate special files.
Endpoint
POST /upload
Authentication
| Header | Value |
|---|---|
Authorization | Bearer <jwt_token> |
Request Body
The request uses multipart/form-data format with the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
file | file | ✅ | App binary file(s) |
app_name | string | ✅ | Name of the application |
version | string | ✅ | Version of the application |
channel | string | ❌ | Channel (e.g., stable, beta) |
publish | boolean | ❌ | Marks the version available for users |
critical | boolean | ❌ | Marks the version as critical |
platform | string | ❌ | Platform (e.g., linux, windows) |
arch | string | ❌ | Architecture (e.g., amd64, arm64) |
changelog | string | ❌ | Changelog in markdown format |
updater | string | ❌ | Updater type for artifact isolation (manual, velopack, sparkle, squirrel_darwin, squirrel_windows, electron-builder, tauri) |
signature | string | ❌ | Cryptographic signature (required for Tauri updater) |
Every file sent to this endpoint passes through faynoSync and the reverse proxy in front of it. For large builds, use presigned uploads (Upload Init + Upload Complete) to send files directly to object storage.
The updater parameter is useful when uploading files for updaters that generate or ingest special files (like RELEASES for Squirrel Windows, *.yml for electron-builder, releases.{channel}.json for Velopack, or appcast.{channel}.xml for Sparkle). It ensures files are stored in isolated folders specific to that updater.
Example Requests
Single File Upload (Standard)
curl -X POST --location 'http://localhost:9000/upload' \
--header 'Authorization: Bearer <jwt_token>' \
--form 'file=@"/path_to_file/myapp.deb"' \
--form 'data="{\"app_name\":\"myapp\",\"version\":\"0.0.1\",\"channel\":\"stable\",\"publish\":true,\"platform\":\"linux\",\"arch\":\"amd64\",\"changelog\":\"### Changelog\\n\\n- Added new feature X\\n- Fixed bug Y\"}"'
Upload with Updater Parameter (Squirrel Windows)
curl -X POST --location 'http://localhost:9000/upload' \
--header 'Authorization: Bearer <jwt_token>' \
--form 'file=@"/path_to_file/myapp.exe"' \
--form 'file=@"/path_to_file/RELEASES"' \
--form 'data="{\"app_name\":\"myapp\",\"version\":\"0.0.1\",\"channel\":\"stable\",\"publish\":true,\"platform\":\"windows\",\"arch\":\"amd64\",\"updater\":\"squirrel_windows\",\"changelog\":\"### Changelog\\n\\n- Added new feature X\\n- Fixed bug Y\"}"'
Upload with Updater Parameter (Electron Builder)
curl -X POST --location 'http://localhost:9000/upload' \
--header 'Authorization: Bearer <jwt_token>' \
--form 'file=@"/path_to_file/myapp.exe"' \
--form 'file=@"/path_to_file/latest.yml"' \
--form 'data="{\"app_name\":\"myapp\",\"version\":\"0.0.1\",\"channel\":\"stable\",\"publish\":true,\"platform\":\"windows\",\"arch\":\"amd64\",\"updater\":\"electron-builder\",\"changelog\":\"### Changelog\\n\\n- Added new feature X\\n- Fixed bug Y\"}"'
Upload with Updater Parameter (Tauri)
curl -X POST --location 'http://localhost:9000/upload' \
--header 'Authorization: Bearer <jwt_token>' \
--form 'file=@"/path_to_file/myapp.app.tar.gz"' \
--form 'data="{\"app_name\":\"myapp\",\"version\":\"1.0.0\",\"channel\":\"stable\",\"publish\":true,\"platform\":\"darwin\",\"arch\":\"amd64\",\"updater\":\"tauri\",\"signature\":\"dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVUUzIzUGJLcHpQNHdCVEJzTXJjbWFhUUdUVEtaUGRLdGhWdGZJZkF2VmM0TzJGMkdPQUk4V1hzYWJuUUhWbWI2cTFWTkhEZE9lbkVwUERCQmhRPT0K\",\"changelog\":\"### Changelog\\n\\n- Added new feature X\\n- Fixed bug Y\"}"'
Upload with Updater Parameter (Sparkle, macOS)
Sparkle has no signature field — the edSignature already lives inside the uploaded appcast. Upload the generate_appcast output plus every archive/delta it references:
curl -X POST --location 'http://localhost:9000/upload' \
--header 'Authorization: Bearer <jwt_token>' \
--form 'file=@"/path_to_file/appcast.stable.xml"' \
--form 'file=@"/path_to_file/MyApp-1.0.0.zip"' \
--form 'data="{\"app_name\":\"myapp\",\"version\":\"1.0.0\",\"channel\":\"stable\",\"publish\":true,\"platform\":\"darwin\",\"arch\":\"arm64\",\"updater\":\"sparkle\",\"changelog\":\"### Changelog\\n\\n- Added new feature X\\n- Fixed bug Y\"}"'
Multiple File Upload (Standard)
curl -X POST --location 'http://localhost:9000/upload' \
--header 'Authorization: Bearer <jwt_token>' \
--form 'file=@"/path_to_file/myapp.deb"' \
--form 'file=@"/path_to_file/myapp.rpm"' \
--form 'data="{\"app_name\":\"myapp\",\"version\":\"0.0.1\",\"channel\":\"stable\",\"publish\":true,\"platform\":\"linux\",\"arch\":\"amd64\",\"changelog\":\"### Changelog\\n\\n- Added new feature X\\n- Fixed bug Y\"}"'
Response
Success Response (200 OK)
{
"uploadResult.Uploaded": "6411c7c0ec4ff9a9a9bc18fa"
}
Response Fields
| Field | Type | Description |
|---|---|---|
uploadResult.Uploaded | string | The unique identifier (ID) of the uploaded version |
Updater-Specific File Organization
When you specify an updater parameter, files are organized in isolated folders:
File Structure Examples
Standard Upload (No Updater):
/myapp/stable/linux/amd64/
├── myapp-0.0.1.deb
└── myapp-0.0.1.rpm
Squirrel Windows Upload:
/squirrel_windows/myapp/stable/windows/amd64/0.0.1/
├── myapp.exe
└── RELEASES
Electron Builder Upload:
/electron-builder/myapp/stable/windows/amd64/0.0.1/
├── myapp.exe
└── latest.yml
Sparkle Upload (materialized per platform/arch):
/sparkle/admin/myapp/darwin/arm64/
├── MyApp-1.0.0.zip
└── appcast.stable.xml
Supported Updater Types
| Updater | Generated Files | Use Case | Special Requirements |
|---|---|---|---|
manual | None | Standard FaynoSync uploads | None |
velopack | releases.{channel}.json | Velopack cross-platform desktop apps | Requires channel, platform, and arch |
sparkle | appcast.{channel}.xml | Native macOS apps using Sparkle | macOS only; upload the generate_appcast appcast + archives |
squirrel_darwin | None | Squirrel macOS applications | None |
squirrel_windows | RELEASES | Squirrel Windows applications | None |
electron-builder | *.yml | Electron applications | None |
tauri | None | Tauri applications with built-in updater | Requires signature field |
Notes
- You can upload multiple files for different platforms/architectures in a single request
- The
changelogfield supports markdown formatting - Critical versions are typically used for security updates or critical bug fixes
- Published versions are immediately available to end users
- The
channelfield helps organize releases (e.g., stable, beta, nightly) - File uploads support common package formats (.deb, .rpm, .exe, .dmg, etc.)
- Updater Parameter: Use the
updaterparameter when uploading files for updaters that generate special files (RELEASES, *.yml) - Artifact Isolation: Files uploaded with an updater parameter are stored in isolated folders to prevent conflicts
- File Organization: Generated files (RELEASES, *.yml) are properly organized when using updater-specific uploads
- Signature Field: When using Tauri updater, the
signaturefield is required for cryptographic verification of updates. This signature is generated by Tauri's build process and ensures update integrity and authenticity. - Private Apps: a private app accepts only the
manual,tauriandsquirrel_darwinupdaters.velopack,sparkle,electron-builderandsquirrel_windowsneed a publicly served update feed, so they are rejected with400:updater is not supported for private apps: <updater> requires a publicly served update feed; use manual, tauri or squirrel_darwin, or a public app - Uploading to an app that does not exist returns
404 - Conflicts are checked before anything is written. If two files of the request would share a storage key or an extension, the request returns
400(files "<a>" and "<b>" would be stored under the same key/... have the same extension). If the version already has an artifact with the same platform, architecture and extension, it returns409withapp with this name, version, platform, architecture and extension already exists; if the version exists in another channel,409withthis version already exists in another channel; the channel of a version cannot be changed(the same codes as presigned uploads). Before this change both conflicts returned500. In both cases no file is written to storage and no artifact is recorded, so a stored file is never overwritten and a retry never meets a half-recorded version..nupkg,.deltaand.blockmapmay repeat within one platform and architecture (full and delta packages, the.zip.blockmapand.dmg.blockmapof an electron-builder macOS build) as long as each is stored under its own key. Without an updater, files are stored as{app}-{version}{ext}, so a second.nupkg,.deltaor.blockmapfor the same version, platform and architecture also returns409 - Team users: the app, channel, platform and architecture must all be in the team user's allowed lists (see Team Based Authorization). Otherwise the request returns
403withyou don't have access to this <app|channel|platform|architecture>, and no file is written to storage. Before v2.4.0 these lists were not checked on this endpoint.