Overview
Dashboard first, API optional
The dashboard is primary. The REST API mirrors the same flows for automation. Use session cookies (browser or cURL) or API keys for server-to-server work. Plain English: sign in, grab a cookie or API key, then call the endpoints below to upload files, issue shares, invite teammates, send emails, and top up credits.
/auth/login or API key via Authorization: Bearer <key> (or x-api-key). All authenticated routes accept API keys.{ error: { code, message, details?, requestId, retryable? } }. All endpoints are HTTPS-only in production.Typical flow: (1) Create an account and verify email. (2) If server-to-server, create an API key in the dashboard. (3) Pick a storage plan (main/temp) and upload. (4) Create a share token or vanity link. (5) Invite teammates with permissions. (6) Set credit caps and top up if needed.
Common error codes (plain English)
/auth/totp/login-verify.Identity & TOTP (session auth)
POST /auth/signup { email, password, rememberMe?, acceptTerms, confirmOver13 }
201 Created → { user, verifyToken }
4xx → error codes: invalid_input, consent_required, weak_password, email_taken
POST /auth/login { email, password, rememberMe? }
200 → { user } or { needTotp: true }
401 invalid_credentials
POST /auth/totp/login-verify { token }
200 → { user }
400 invalid_totp | no_pending_totp | totp_not_enabled
POST /auth/logout → 200 { ok: true }
GET /auth/me → 200 { user }
POST /auth/verify-email { token } → 200 { ok: true } | 400 invalid_token | 403 forbidden
POST /auth/totp/setup → 200 { secret, otpauthUrl }
POST /auth/totp/verify { token } → 200 { ok: true } | 400 invalid_totp
POST /auth/resend-verification → 200 { ok: true, verifyToken }
Credits & Billing (session or API key)
GET /credits/pricing → 200 { tiers[] }
GET /credits/config → 200 { publishableKey }
POST /credits/payment-intent { amountGBP }
200 → { clientSecret, publishableKey, credits }
400 invalid_amount | invalid_tier | invite_not_allowed (when on invite)
POST /credits/payment-intent/confirm { paymentIntentId }
200 → { status, credits }
GET /credits/balance → 200 { balance, creditCapMonthly, creditCapMode, monthUsed } | 403 forbidden (invite lacks view_credits)
POST /credits/cap { creditCapMonthly, creditCapMode } → 200 { ok: true } | 403 invite_not_allowed
GET /credits/history → 200 { history[] } | 403 forbidden (invite)
Plain English: create a Payment Intent with amountGBP, confirm it client-side via Stripe, then call /credits/payment-intent/confirm to lock in credits. Invited users cannot buy unless their invite allows spending.
Storage (session or API key)
POST /files/upload (multipart: file, storagePlan=main|temp, retentionDays?, compression?)
201 → { file: { id, name, size, storagePlan, expiresAt, compression } }
4xx → error codes: no_file, bad_plan, bad_compression
POST /files/init { filename, size, mime, storagePlan, retentionDays?, checksum }
201 → { uploadId, chunkSize, totalChunks, expiresAt, compression }
4xx invalid_input | bad_plan | bad_compression
PUT /files/:id/chunk/:index (raw bytes) → 200 { uploadedChunks, complete }
POST /files/:id/complete → 200 { ok: true }
GET /files → 200 { files[] }
GET /files/:id/download → binary stream (403 forbidden without download perm)
DELETE /files/:id → 200 { ok: true }
Pricing: main ~1000 cr/GB/month, temp ~500 cr/GB/month (billed upfront), download 1 cr/GB (min 1).
Plain English: use /files/upload for simple uploads or the init/chunk/complete trio for large files. Pick temp for cheaper, short-lived storage with automatic expiry.
Shares (session or API key)
POST /shares (multipart upload: file, storagePlan=main|temp, retentionDays?, password?, expiresInDays?, downloadLimit?, convertToMain?)
201 → { token, expiresAt, downloadLimit, requiresPassword, convertToMain, file }
4xx invalid_input | bad_plan | bad_compression | weak_password
GET /shares → 200 { shares[] }
GET /shares/:token → 200 { file, requiresPassword, expiresAt, remainingDownloads } | 410 expired | 404 file_not_found
POST /shares/:token/download { password? } → file stream; 401 invalid_password; 429 captcha_required
POST /shares/:id/convert-to-main → 200 { ok: true } | 403 forbidden (invite spend)
Plain English: create a share to hand someone a download link. Tokens can expire, have download limits, and optional passwords. Vanity links stay public; do not place secrets there.
Connected Access (session or API key)
GET /access → 200 { owned, received, activeInviteId, activePermissions }
POST /access/invite { email, permissions[], creditCapMonthly?, expiresInDays? }
201 → { token, expiresAt, permissions, creditCapMonthly }
400 invalid_input | invalid_permissions
POST /access/accept { token } → 200 { ok: true } | 400 invalid_state | 410 expired | 403 email_mismatch
POST /access/use { inviteId } → 200 { ok: true } | 404 not_found | 410 expired
POST /access/clear → 200 { ok: true }
PATCH /access/:id → 200 { ok: true, invite }
DELETE /access/:id → 200 { ok: true }
Plain English: invites let teammates act in your workspace with scoped permissions and optional monthly credit caps. Switch invites on the dashboard “Access” page to act-as. Clear to return to self.
Auto Email (session or API key)
GET /email → 200 { jobs[] }
POST /email/schedule (multipart optional attachment <=25MB)
fields: to, subject, text, sendAt (ISO), timezone, attachment?
201 → { job }
400 invalid_input | invalid_date | attachment_too_large
POST /email/:id/cancel → 200 { ok: true } | 404 not_found | 400 invalid_state
Pricing: 1 credit/email; attachment stored until send (counts toward storage)
Plain English: schedule future-dated emails. Each send costs 1 credit. Attachments are kept until the job fires; failed sends retry up to 3 times with backoff.
Payload examples
// Create a share with upload (multipart)
POST /shares
file=@path/to/file.bin
storagePlan=main
expiresInDays=7
// Invite a collaborator with caps
POST /access/invite
{ "email": "teammate@x.com", "permissions": ["view_files", "download"], "creditCapMonthly": 500 }
// Schedule an email (local time) with attachment
POST /email/schedule multipart
to=user@x.com
subject=Hello
text=Body
sendAt=2024-04-30T09:00
timezone=Europe/London
attachment=@/path/to/file.pdf
Need programmatic? See below.
Code snippets
Node.js using fetch with session cookie:
import fetch from 'node-fetch';
const jar = [];
async function login() {
const res = await fetch('https://payload.live/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'you@example.com', password: 'pass' }),
redirect: 'manual'
});
jar.push(res.headers.get('set-cookie'));
}
async function listFiles() {
const res = await fetch('https://payload.live/files', {
headers: { cookie: jar.join('; ') },
});
console.log(await res.json());
}
Node.js (API key upload small file):
import fs from 'fs';
import FormData from 'form-data';
async function uploadWithKey(pathname) {
const form = new FormData();
form.append('file', fs.createReadStream(pathname));
form.append('storagePlan', 'main');
const res = await fetch('https://payload.live/files/upload', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.PAYLOAD_API_KEY}` },
body: form
});
if (!res.ok) throw new Error(await res.text());
console.log(await res.json());
}
uploadWithKey('logo.png');
Next.js App Router route handler (API key):
export async function GET() {
const res = await fetch(process.env.PAYLOAD_URL + '/files', {
headers: { Authorization: `Bearer ${process.env.PAYLOAD_API_KEY}` },
cache: 'no-store'
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data?.error?.message || res.statusText);
}
const data = await res.json();
return Response.json(data.files);
}
Python (requests) with API key:
import requests
BASE = "https://payload.live"
API_KEY = "YOUR_KEY"
def list_files():
r = requests.get(f"{BASE}/files", headers={"Authorization": f"Bearer {API_KEY}"})
r.raise_for_status()
print(r.json())
def create_share(file_path):
with open(file_path, "rb") as f:
files = {"file": f}
data = {"storagePlan": "main", "expiresInDays": 3}
r = requests.post(f"{BASE}/shares", headers={"Authorization": f"Bearer {API_KEY}"}, files=files, data=data)
r.raise_for_status()
print(r.json())
list_files()
create_share("logo.png")
cURL quickstart (session cookie):
curl -X POST https://payload.live/auth/login \
-H "Content-Type: application/json" \
-c cookies.txt -b cookies.txt \
-d '{"email":"you@example.com","password":"pass"}'
curl -X GET https://payload.live/files \
-c cookies.txt -b cookies.txt
curl -X POST https://payload.live/credits/payment-intent \
-H "Content-Type: application/json" \
-c cookies.txt -b cookies.txt \
-d '{"amountGBP":10}'
cURL (API key) minimal:
curl -H "Authorization: Bearer $PAYLOAD_API_KEY" https://payload.live/files
curl -X POST https://payload.live/shares \
-H "Authorization: Bearer $PAYLOAD_API_KEY" \
-F "file=@logo.png" \
-F "storagePlan=main" \
-F "expiresInDays=7"