PayLoad API

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.

Base URL: https://payload.live (HTTP only for local dev)
Auth: session cookie after /auth/login or API key via Authorization: Bearer <key> (or x-api-key). All authenticated routes accept API keys.
Errors: JSON { 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)

invalid_input — A required field is missing or malformed.
invalid_credentials — Email/password didn’t match.
needTotp — TOTP is enabled; call /auth/totp/login-verify.
forbidden / invite_not_allowed — Your invite lacks the needed permission (e.g., spend_credits, view_credits).
expired / invalid_state — The resource (invite/share) is expired or not in a usable state.
attachment_too_large — Email attachment exceeded 25MB.
bad_plan / bad_compression — Unsupported storage plan or compression setting.
captcha_required — Share download is gated; solve CAPTCHA before retrying.

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"