Session endpoint (Rust/Axum)

Implement the session endpoint on your backend with a server-side secret key, server-owned constraints and externalReference.

The session endpoint belongs to your backend. It must authenticate or rate-limit your own users, decide the file policy on the server, and bind the session to your business record with an opaque externalReference. The repository ships a runnable version of this example in examples/customer-backend.

Rust / Axum example

use axum::{extract::State, http::StatusCode, Json};
use serde_json::{json, Value};

#[derive(Clone)]
struct AppState {
    http: reqwest::Client,
    api_base: String,      // e.g. https://api.crossupload.com
    secret_key: String,    // sk_… from your secret manager — never sent to browsers
    site_origin: String,   // https://www.example.com
}

/// POST /api/crossupload/session
async fn create_upload_session(
    State(st): State<AppState>,
    user: AuthenticatedUser,              // your own auth extractor
    Json(prefs): Json<Value>,             // { maxFiles, locale } — preferences only
) -> Result<Json<Value>, StatusCode> {
    // 1. Your own abuse protection.
    if !user.rate_limit_ok("crossupload-session", 20) {
        return Err(StatusCode::TOO_MANY_REQUESTS);
    }
    // 2. Server-owned constraints: clamp the browser's preference.
    let max_files = prefs["maxFiles"].as_i64().unwrap_or(1).clamp(1, 3);
    let application_id = user.current_application_id();
    // 3. Bind to your record with an opaque reference.
    let body = json!({
        "externalReference": format!("user:{}:application:{}", user.id, application_id),
        "maxFiles": max_files,
        "maxFileSize": 10_000_000,
        "allowedTypes": ["application/pdf", "image/jpeg", "image/png", "image/heic"],
        "origin": st.site_origin,
        "locale": if prefs["locale"] == "tr" { "tr" } else { "en" },
    });
    let resp = st.http
        .post(format!("{}/v1/sessions", st.api_base))
        .bearer_auth(&st.secret_key)
        .json(&body)
        .send().await.map_err(|_| StatusCode::BAD_GATEWAY)?;
    if !resp.status().is_success() {
        return Err(StatusCode::BAD_GATEWAY);
    }
    let session: Value = resp.json().await.map_err(|_| StatusCode::BAD_GATEWAY)?;
    // 4. Remember which sessions you issued to this user.
    user.remember_upload_session(session["id"].as_str().unwrap_or_default());
    Ok(Json(session))
}

What the response contains

  • id, status, expiresAt — the session.
  • desktopToken — a short-lived capability scoped to this one session (read, upload, manage, events). Safe to give to the visitor’s browser; it cannot touch other sessions or your account.
  • pairing.url, pairing.qrSvg, pairing.expiresAt — the QR. The single-use secret is in the URL fragment.
  • constraints — the effective, server-enforced limits.
  • apiBase, branding, externalReference.

Rules that matter

  • Client options are preferences. CrossUpload applies the strictest of plan, project and request limits, and you should clamp them on your side too.
  • CORS and Origin allowlists are defense layers, not authentication. The secret key and session capabilities are the authorization.
  • Idempotency: send Idempotency-Key to make retries safe. A replay returns the same session with fresh credentials; a different body with the same key returns 409 idempotency_key_reused.
  • Don’t mint sessions for anonymous traffic without your own rate limits; every session counts against your plan’s initiation (abuse) limit, though not your accepted uploads.

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