REST API reference

Endpoints, credential classes, scopes, errors, pagination and idempotency for the CrossUpload REST API.

Base URL: https://app.crossupload.com. JSON in, JSON out. Machine-readable spec: /openapi.yaml.

Credential classes

CredentialPrefixWho holds itCan
Server API keysk_Your backend onlyCreate sessions; read, URL-sign and delete files of its project; read usage.
Desktop capabilitycu_d_Visitor’s browser (one session)session:read, session:upload, session:manage, events:read
Pairing secret(QR fragment)QR codeExchanged once at POST /v1/pair. Not a bearer token.
Phone capabilitycu_p_Paired phone (one session)session:upload only; sees only its own uploads, never URLs.
Project public keypk_AnyoneIdentifies a project. Cannot authorize anything.

Send credentials as Authorization: Bearer ….

Endpoints

EndpointAuthPurpose
POST /v1/sessionsServer keyCreate a constrained session. Supports Idempotency-Key.
GET /v1/sessions/{id}Desktop / phone / server keyCurrent state, files and event cursor.
DELETE /v1/sessions/{id}Desktop (manage)Cancel the session.
POST /v1/sessions/{id}/pairingDesktop (manage)Issue a fresh QR (only before a phone pairs).
POST /v1/pairPairing secret in bodyReturn an upload-only phone capability.
POST /v1/sessions/{id}/uploadsDesktop / phoneReserve slots and get signed POST data.
POST /v1/uploads/{id}/completeMatching capabilityHint that bytes were sent; triggers verification.
POST /v1/sessions/{id}/finishDesktop / phoneFinalize the batch once files are resolved.
GET /v1/sessions/{id}/eventsDesktopAuthenticated SSE stream.
GET /v1/files?sessionId=Server keyFiles of one of your sessions.
GET /v1/files/{id}Server keyMetadata and status.
POST /v1/files/{id}/urlServer keyIssue a temporary delivery URL.
DELETE /v1/files/{id}Server keyPermanent deletion (?invalidateCdn=true optional).
GET /v1/usageServer keyAccount and project usage.
GET /health/live, /health/readyNoneOperational status.

Headless upload flow

# 1. Reserve a slot (desktop capability)
POST /v1/sessions/ses_123/uploads
{ "files": [{ "clientKey": "a1", "name": "id.jpg", "size": 482113, "type": "image/jpeg" }] }

→ { "uploads": [{ "file": { "id": "file_9", "status": "uploading", … },
                   "upload": { "method": "POST", "url": "https://…/quarantine", "fields": [["key","q/…"], …], "expiresAt": "…" } }] }

# 2. multipart POST to upload.url with every field in order, then the file as "file".
# 3. Tell us you are done (a hint — we verify independently)
POST /v1/uploads/file_9/complete   → 202 { "status": "processing", … }
# 4. Wait for "file.ready" on the SSE stream, or poll GET /v1/sessions/ses_123

Re-sending the same clientKey for a file that hasn’t uploaded yet re-issues credentials without consuming another slot. To retry a rejected file, send retryOf with its ID.

Server-sent events

Native EventSource cannot send an Authorization header, so use fetch() and read the stream. Event types: session.created, file.created, file.uploaded, file.ready, file.rejected, file.failed, phone.paired, phone.batch_sent, pairing.refreshed, session.completed, session.expired, session.cancelled. File events include the current file object. Reconnect with Last-Event-ID (or ?cursor=) to resume.

Conventions

  • Errors: { "error": { "code", "message", "requestId" } } with a stable code. Every response has an X-Request-Id header.
  • IDs are opaque strings with prefixes (ses_, file_, …). They identify; they never authorize.
  • Timestamps are ISO-8601 UTC. Sizes are bytes.
  • Dashboard lists use cursor pagination.
  • JSON request bodies are limited to 64 KB.

Something unclear or wrong? Email master@crossupload.com.