Developer guide

File Upload API

Upload one large file or many files over plain HTTP, then create a project. Nothing here is language-specific — any HTTP client (libcurl, WinHTTP, .NET, Java, Go, Python …) can implement it.

Base URLhttps://360-for-you.com/api/v1
AuthAuthorization: Bearer sk_…

Every request needs the header Authorization: Bearer sk_… — create a key on your profile page. The key needs the projects:write scope (uploads + project creation) and projects:read (to poll project status); the default key has both. Responses are JSON; errors look like {"error": "<code>"} with the HTTP status carrying the meaning.

The flow

  1. 1
    POST /uploads — create a session, get an upload_id.
  2. 2
    PUT /uploads/{upload_id}/{filename} — upload each file (whole, or in chunks). Repeat for every file; all files share one session.
  3. 3
    GET /uploads/{upload_id}(optional) verify all files are complete.
  4. 4
    POST /projects with {"upload_id": "…"} — create one project from the session.
  5. 5
    GET /projects/{name} — poll until status == 100 (ready).

A single file is just step 2 done once. Many files = step 2 repeated into the same upload_id. You may also upload one archive (.zip, .7z, .rar) — the server unpacks it.

Endpoints

POST/uploads

Create an upload session. No body.

201 {"upload_id": "Bx0xyTjt", "expires_in_seconds": 259200}

upload_id is opaque (treat it as a token). Abandoned sessions are deleted after expires_in_seconds (~72 h).

402 storage_quota_exceeded 403 tariff_forbids_upload

PUT/uploads/{upload_id}/{filename}

Upload a whole file, or one chunk of it. Body = the raw bytes.

HeaderRequiredValue
AuthorizationyesBearer sk_…
Content-Typeyesapplication/octet-stream
Content-Rangechunks onlybytes <start>-<end>/<total>
  • Whole file (one request): omit Content-Range; send the full body with a correct Content-Length. Best for small files.
  • Chunked: send Content-Range: bytes <start>-<end>/<total> where end is inclusive and <total> is the full file size. The body length must equal end - start + 1.

200 {"received": <int>, "total": <int>, "complete": <bool>}

received = bytes the server now holds (the resume high-water mark). complete turns true when received == total; the file is finalized at that moment.

400 invalid_upload_id · invalid_filename · invalid_content_range · chunk_length_mismatch
404 upload_session_not_found 409 non_contiguous_chunk (includes received — resume from there)
411 length_required (single-shot PUT without Content-Length) 413 file_too_large · session_too_large (includes limit)
415 extension_not_allowed (includes extension and the allowed list)

GET/uploads/{upload_id}/{filename}

One file's progress — use it to resume.

200 {"received": <int>, "total": <int>, "complete": <bool>}   404 file_not_found

GET/uploads/{upload_id}

List every file in the session and its progress.

200 {"upload_id": "…", "files": [{"name": "…", "received": …, "total": …, "complete": …}]}

DELETE/uploads/{upload_id}

Discard a session and everything in it (idempotent).

200 {"deleted": true}

POST/projects

Create one project from the session. JSON body:

{ "upload_id": "Bx0xyTjt", "title": "My project", "privacy_mode": 2 }
  • Provide either upload_id or download_link (a remote URL) — not both, not neither.
  • title is optional (auto-generated if omitted). Many other optional processing fields exist (privacy, coordinate system, anonymization, point-cloud and video options) — see the interactive reference.
  • All files in the session must be complete first, otherwise creation fails.

201 {"name": "Bx0xyTjt", "status": 0, "message": "processing_started"}

name is the project's URL slug — authoritative; it may differ from upload_id on a rare collision.

400 upload_id_or_download_link_required · provide_either_upload_id_or_download_link · invalid_upload_id · project_creation_failed (a file isn't fully uploaded yet)
404 upload_session_not_found 402 storage_quota_exceeded 403 tariff

GET/projects/{name}

Poll project status. status: 0 = queued, 1…99 = processing, 100 = ready. While below 100 only {name, status} is returned; at 100 the full record is returned.

Chunking & resume rules

  • Chunks must be contiguous and in order — each chunk's start equals the previous received. A gap is rejected with 409 and the current received.
  • Re-sending a chunk is safe (idempotent): overlapping bytes overwrite in place. Re-PUTting an already-complete file returns complete: true unchanged.
  • One in-flight PUT per file. To go faster, upload different files in parallel — never parallel chunks of the same file.
  • Resume after any failure: GET the file status, then continue PUTing from received. This is the recommended pattern for desktop apps on unstable networks.

Resume-safe upload (pseudocode)

CHUNK = 8 * 1024 * 1024                       # 8 MiB; any size is fine

function upload_file(upload_id, path):
    size = filesize(path); name = basename(path)

    resp = GET  /uploads/{upload_id}/{name}             # how much does the server already have?
    received = (resp.status == 200) ? resp.body.received : 0

    while received < size:
        chunk = read(path, offset=received, length=CHUNK)
        end   = received + length(chunk) - 1
        resp  = PUT /uploads/{upload_id}/{name}
                    headers: Content-Type:  application/octet-stream
                             Content-Range: bytes {received}-{end}/{size}
                    body: chunk
        switch resp.status:
            200: received = resp.body.received          # advance to server high-water
            409: received = resp.body.received; continue # resync, then retry
            5xx / network error: sleep(backoff); continue # retry is safe (idempotent)
            else: abort(resp)                            # 4xx (413/415/…) -> fix and stop
    # now resp.body.complete == true

Filenames, limits, pacing

  • Filenames are flattened. Path separators and unsafe characters are stripped server-side; nested folders are not supported — send a flat name, or upload an archive for a tree. Don't send two files with the same name into one session (the second is ignored).
  • Allowed extensions are server-configured (images, point clouds, CAD/BIM incl. .ifc, gaussian splats, 360 video, archives, …). A disallowed file returns 415 with the full allowed list.
  • Size caps (per file and per session) are server-configured; exceeding returns 413 with limit.

Rate limits — HTTP 429 when exceeded; back off and retry:

EndpointLimit
POST /uploads30 / hour
PUT /uploads/…/{filename}1200 / minute
GET /uploads/…/{filename}600 / minute
GET /uploads/{upload_id}120 / minute
DELETE /uploads/{upload_id}60 / minute
POST /projects10 / hour
GET /projects/{name}60 / minute

Examples

Python — resumable, one or many files (requests)

import os, requests

BASE  = "https://360-for-you.com/api/v1"
KEY   = os.environ["API_KEY"]                  # never hard-code the key
CHUNK = 8 * 1024 * 1024                         # 8 MiB; any size is fine

s = requests.Session()
s.headers["Authorization"] = f"Bearer {KEY}"

def upload_file(upload_id, path):
    size = os.path.getsize(path); name = os.path.basename(path)
    r = s.get(f"{BASE}/uploads/{upload_id}/{name}")            # how much is already there? (resume)
    received = r.json()["received"] if r.status_code == 200 else 0
    with open(path, "rb") as f:
        while received < size:
            f.seek(received)
            chunk = f.read(CHUNK)
            end = received + len(chunk) - 1
            r = s.put(f"{BASE}/uploads/{upload_id}/{name}", data=chunk,
                      headers={"Content-Type": "application/octet-stream",
                               "Content-Range": f"bytes {received}-{end}/{size}"})
            if r.status_code == 409:                           # gap: resync to server offset, retry
                received = r.json()["received"]; continue
            r.raise_for_status()
            received = r.json()["received"]                    # advance to the server high-water mark

def create_project(paths, title):
    upload_id = s.post(f"{BASE}/uploads").json()["upload_id"]
    for path in paths:                                         # one file or many -> one session
        upload_file(upload_id, path)
    r = s.post(f"{BASE}/projects", json={"title": title, "upload_id": upload_id})
    r.raise_for_status()
    return r.json()["name"]                                    # project URL slug

slug = create_project(["floor-1.e57", "floor-2.e57"], "Building scan")
print("https://360-for-you.com/projects/" + slug)

Download the full example — same flow with retries, resume across runs (API_UPLOAD_ID) and one-or-many files; reads the key from API_KEY.

One big file, chunked & resumable (curl)

KEY=sk_...; BASE=https://360-for-you.com/api/v1
ID=$(curl -s -X POST $BASE/uploads -H "Authorization: Bearer $KEY" | jq -r .upload_id)

# one chunk (repeat per slice, advancing the range):
curl -X PUT "$BASE/uploads/$ID/scan.e57" -H "Authorization: Bearer $KEY" \
     -H "Content-Type: application/octet-stream" \
     -H "Content-Range: bytes 0-104857599/524288000" --data-binary @scan.e57.part0
# resume after a drop: GET .../scan.e57 -> {"received": N}, continue from N

curl -X POST $BASE/projects -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
     -d "{\"title\":\"Big scan\",\"upload_id\":\"$ID\"}"

Many small files, one project (curl)

ID=$(curl -s -X POST $BASE/uploads -H "Authorization: Bearer $KEY" | jq -r .upload_id)
for f in *.jpg; do
  curl -X PUT "$BASE/uploads/$ID/$f" -H "Authorization: Bearer $KEY" \
       -H "Content-Type: application/octet-stream" --data-binary @"$f"   # no Content-Range = whole file
done
curl -X POST $BASE/projects -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
     -d "{\"title\":\"Photoset\",\"upload_id\":\"$ID\"}"

Resources

Asistente de IA
¡Hola! No dudes en hacer cualquier pregunta sobre tours virtuales, nubes de puntos, 3D Gaussian Splatting, modelos 3D, uso del sitio web, pagos y más. Encontraré la respuesta o reenviaré tu pregunta a nuestro equipo de soporte.
Nuestro IA no pudo responder a su pregunta. Nuestro equipo de soporte estará encantado de responder a su pregunta. Por favor, proporcione su dirección de correo electrónico. No usamos el correo electrónico para boletines. Solo lo usamos para responder a su pregunta.