---
title: "Initialize a two-step upload"
method: POST
path: "/v1/upload/init"
---

# Initialize a two-step upload

`POST /v1/upload/init`

## Request

- Base URL: `https://api.payaion.com`
- URL: `https://api.payaion.com/v1/upload/init`
- Auth: one of:
  - none
  - API key in header `X-Aion-Key`
  - HTTP bearer

Creates an upload record and returns a short-lived upload token.
The client uploads the file to `uploadUrl` with `X-Upload-Token`.
Used by the web UI and desktop apps.

## Request body

- object
  - `fileName` string, required
  - `sizeBytes` integer, required
  - `pricePerDownload` number, nullable: Price per download in USD (e.g. 0.50). Omit or set to 0 for free downloads. Must be at least 0.000001 — anything smaller rounds to zero atomic USDC and is rejected as `invalid_price`. A price needs somewhere to pay: either the account's payout address or, for a keyless caller, `payoutAddress` below.
  - `payoutAddress` string: Where a keyless (guest) seller's earnings go — this is what lets someone sell without an account. Honoured for guest callers only: for an account the payout address is a dashboard setting, and a value sent here by an API key is ignored so a key can never redirect that account's earnings.
  - `retentionHours` 12 | 24 | 168 | 336: How long the share link lives: 12h, 24h, 7 days or 14 days. Capped by the caller's plan (guests max 14 days). Cannot be combined with a price — a buyer's access is guaranteed for 30 days, which a short link cannot honour, so the pair is rejected with `retention_conflicts_with_price`. Omit for the plan default.
  - `listingIntent` object: Marketplace listing published once the file is READY. Requires an API key with the `marketplace:list` scope — guests are refused.
    - `title` string: 3–120 characters
    - `description` string: 40–500 characters
    - `category` 'reports' | 'datasets' | 'code' | 'media' | 'models' | 'prompts' | 'other'
    - `tags` string[]: Max 8, alphanumeric and hyphens, 30 chars each
  - `passwordHash` string: bcrypt hash of a file password (free files only — a paid file is gated by payment). Computed by the web BFF; plaintext never reaches the gateway.
  - `passwordHint` string: Shown to a visitor who needs the password. Max 120 characters.
  - `encryption` 'e2ee': Private Link. The file is encrypted client-side into a PAY1 container and the key never reaches Payaion; `sizeBytes` is then the ciphertext size and `fileName` is ignored (stored as "encrypted"). Cannot be combined with a price or a listing (`e2ee_not_sellable`, `e2ee_not_listable`). A `passwordHash` is allowed and recommended for sensitive files. The client MUST then also derive the file key from the link key and the password (PAY1 header flag 0x01; see `deriveFileKey` in the web client), so the ciphertext itself cannot be opened without the password — the server-side check is only a first gate. Ceiling: 100 MB for guests, 500 MB for accounts (`e2ee_too_large`). Refused with `private_link_disabled` where the server has it off.
  - `encMeta` string: Required with `encryption: e2ee`. base64 of `nonceBase || AES-GCM(K, {n, t, s})` — the file name, MIME type and plaintext size under the file key — so the owner's dashboard can show a name without the key ever leaving the browser.
  - `wrappedKey` string: Accounts only, required with `encryption: e2ee`: base64 of the file key wrapped for the account's wallet vault (AES-GCM under a key derived from a wallet signature). Guests must omit it (`e2ee_vault_forbidden`); an account that omits it is refused (`e2ee_vault_required`).

## Response `201`

Upload initialized

- UploadInitResult
  - `uploadId` string, required
  - `shareHash` string, required
  - `uploadUrl` string, uri, required
  - `uploadToken` string, required: Bearer token for the direct upload POST, valid 5 minutes from init and checked only when the upload starts — a slow transfer that began in time is never cut off. If more than 5 minutes pass before you start (or before a retry), the worker answers 401 expired_upload_token: call init again.
  - `expiresAt` string, date-time, required: When the stored file expires, not the upload token.

## Other responses

- `400`: Bad request
- `401`: Invalid or missing credentials
- `403`: Guest sending `listingIntent`, a key without `marketplace:list`, or the account's storage / live-file cap is full.
- `413`: File exceeds size limit
- `429`: Rate limit exceeded
- `503`: Too many concurrent uploads for this caller — retry after `retryAfterSec`.

---

[API](https://skmtc.dev/payaion/apis/payaion-api.md) · [All operations](https://skmtc.dev/payaion/apis/payaion-api/llms.txt) · [OpenAPI document](https://skmtc.dev/payaion/apis/payaion-api/revisions/29927240cfc2?raw)
