---
title: Your First Integration
path: getting-started/your-first-integration
status: published
---

A complete, runnable walk-through. We'll build a small script that uploads a local folder to a ScaiDrive share, generates a password-protected external link for the root folder, and prints the URL. Along the way we'll cover error handling, retries, and graceful auth.

## What you'll build

```mermaid
flowchart LR
  L["local folder"] -->|upload| S["ScaiDrive share"] -->|create link| U["shareable URL"]
```

Requirements:

- ScaiDrive URL and a service token.
- A share you have `contributor` or higher role on.
- Python 3.11+ (or Node 20+ for the TypeScript version).

## 1. Set up credentials

```bash
export SCAIDRIVE_URL="https://scaidrive.scailabs.ai"
export SCAIDRIVE_TOKEN="eyJhbGc..."
export SCAIDRIVE_SHARE_ID="shr_01J3KXQ0N"
```

## 2. Create a client wrapper

A thin wrapper that handles the base URL, auth header, token refresh, and retry-on-rate-limit.

```python
# scaidrive_client.py
import os
import time
import httpx


class ScaiDriveError(Exception):
    def __init__(self, code: str, message: str, status: int, request_id: str):
        super().__init__(f"{code}: {message}")
        self.code, self.status, self.request_id = code, status, request_id


class ScaiDrive:
    def __init__(self, base_url: str, token: str, timeout: float = 30.0):
        self._client = httpx.Client(
            base_url=base_url.rstrip("/"),
            headers={"Authorization": f"Bearer {token}"},
            timeout=timeout,
        )

    def request(self, method: str, path: str, **kw):
        for attempt in range(4):
            resp = self._client.request(method, path, **kw)
            if resp.status_code == 429:
                delay = int(resp.headers.get("Retry-After", "2"))
                time.sleep(delay * (2**attempt))
                continue
            if resp.status_code >= 500 and attempt < 3:
                time.sleep(2**attempt)
                continue
            break

        if resp.status_code >= 400:
            body = resp.json() if resp.headers.get("Content-Type", "").startswith("application/json") else {}
            err = (body.get("error") or {})
            raise ScaiDriveError(
                code=err.get("code", "UNKNOWN"),
                message=err.get("message", resp.text),
                status=resp.status_code,
                request_id=(body.get("meta") or {}).get("request_id", ""),
            )
        return resp

    def close(self) -> None:
        self._client.close()


def from_env() -> ScaiDrive:
    return ScaiDrive(os.environ["SCAIDRIVE_URL"], os.environ["SCAIDRIVE_TOKEN"])
```

```typescript
// scaidrive-client.ts
export class ScaiDriveError extends Error {
  constructor(
    public code: string,
    message: string,
    public status: number,
    public requestId: string,
  ) {
    super(`${code}: ${message}`);
  }
}

export class ScaiDrive {
  constructor(
    private baseUrl: string,
    private token: string,
  ) {}

  async request(method: string, path: string, init: RequestInit = {}): Promise<Response> {
    const url = `${this.baseUrl.replace(/\/$/, "")}${path}`;
    const headers = new Headers(init.headers);
    headers.set("Authorization", `Bearer ${this.token}`);

    for (let attempt = 0; attempt < 4; attempt++) {
      const resp = await fetch(url, { ...init, method, headers });
      if (resp.status === 429) {
        const retry = Number(resp.headers.get("Retry-After") ?? 2);
        await new Promise((r) => setTimeout(r, retry * 1000 * 2 ** attempt));
        continue;
      }
      if (resp.status >= 500 && attempt < 3) {
        await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
        continue;
      }
      if (resp.status >= 400) {
        const body = (await resp.json().catch(() => ({}))) as any;
        throw new ScaiDriveError(
          body?.error?.code ?? "UNKNOWN",
          body?.error?.message ?? resp.statusText,
          resp.status,
          body?.meta?.request_id ?? "",
        );
      }
      return resp;
    }
    throw new ScaiDriveError("RETRIES_EXHAUSTED", "rate-limit retries exhausted", 429, "");
  }
}
```

## 3. Walk the local folder

Nothing ScaiDrive-specific here — just walk the tree, collect files.

```python
from pathlib import Path

def walk(root: Path):
    for p in root.rglob("*"):
        if p.is_file():
            rel = p.relative_to(root)
            yield p, rel
```

```typescript
import { readdirSync, statSync } from "node:fs";
import { join, relative } from "node:path";

function* walk(root: string): Generator<{ abs: string; rel: string }> {
  for (const entry of readdirSync(root, { withFileTypes: true })) {
    const abs = join(root, entry.name);
    if (entry.isDirectory()) yield* walk(abs);
    else yield { abs, rel: relative(root, abs) };
  }
}
```

## 4. Create folders as needed

The API requires the target folder to exist before you upload a file into it. Build a cache of `(path → folder_id)` so we don't re-create the same folder per file.

```python
def ensure_folder(client, share_id: str, rel_dir: Path, cache: dict[str, str]) -> str | None:
    """Returns folder_id for rel_dir inside share, creating folders as needed."""
    if str(rel_dir) in (".", ""):
        return None  # share root

    if str(rel_dir) in cache:
        return cache[str(rel_dir)]

    parent_id = ensure_folder(client, share_id, rel_dir.parent, cache)
    resp = client.request(
        "POST", "/api/v1/folders",
        json={"share_id": share_id, "name": rel_dir.name, "parent_id": parent_id},
    )
    folder_id = resp.json()["id"]
    cache[str(rel_dir)] = folder_id
    return folder_id
```

## 5. Upload each file

```python
def upload_file(client, share_id: str, folder_id: str | None, abs_path: Path):
    with abs_path.open("rb") as f:
        data = {"share_id": share_id}
        if folder_id:
            data["folder_id"] = folder_id
        resp = client.request(
            "POST", "/api/v1/files",
            data=data,
            files={"file": (abs_path.name, f)},
        )
    return resp.json()
```

```typescript
async function uploadFile(client: ScaiDrive, shareId: string, folderId: string | null, absPath: string, relName: string) {
  const form = new FormData();
  form.append("share_id", shareId);
  if (folderId) form.append("folder_id", folderId);
  form.append("file", new Blob([readFileSync(absPath)]), relName);
  const resp = await client.request("POST", "/api/v1/files", { body: form });
  return await resp.json();
}
```

## 6. Generate the external link

```python
def create_link(client, share_id: str, folder_id: str, password: str):
    resp = client.request(
        "POST", "/api/v1/external/links",
        json={
            "resource_type": "folder",
            "resource_id": folder_id,
            "share_id": share_id,
            "link_type": "DOWNLOAD",
            "password": password,
            "expires_in_days": 14,
            "custom_name": "Shared batch",
        },
    )
    return resp.json()
```

## 7. Put it all together

```python
# upload_and_share.py
import sys
from pathlib import Path
from scaidrive_client import from_env

def main(local_dir: str, password: str):
    client = from_env()
    share_id = os.environ["SCAIDRIVE_SHARE_ID"]
    root = Path(local_dir)

    cache: dict[str, str] = {}
    root_folder_id = ensure_folder(client, share_id, Path(root.name), cache)

    for abs_path, rel in walk(root):
        folder_id = ensure_folder(client, share_id, Path(root.name) / rel.parent, cache)
        meta = upload_file(client, share_id, folder_id, abs_path)
        print(f"uploaded {rel} -> {meta['id']}")

    link = create_link(client, share_id, root_folder_id, password)
    print(f"\nShare URL: {link['url']}")
    print(f"Password:  {password}")

if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])
```

Run:

```bash
python upload_and_share.py ./my_photos hunter2
```

## What to do when things fail

**`401 AUTH_TOKEN_EXPIRED`** — Your token expired. Refresh with a `refresh_token` grant or obtain a new service token.

**`403 AUTHZ_PERMISSION_DENIED`** — The user doesn't have permission on the target share. Check their role in `/api/v1/users/me/shares`.

**`409 SYNC_CONFLICT`** — A concurrent writer beat you. For uploads the API retries idempotently; for metadata updates, re-read the resource and retry with the latest `base_version_id`.

**`413 PAYLOAD_TOO_LARGE`** — File is over the tenant's per-file size limit. Use [Resumable Uploads](/docs/scaidrive/advanced/resumable-uploads) for large files.

**`429 RATE_LIMITED`** — Respect `Retry-After`. The client above does this automatically.

**`507 QUOTA_EXCEEDED`** — The share, user, or tenant is over quota. Free up space or raise the quota. See [Quotas](/docs/scaidrive/reference/quotas).

See [Errors](/docs/scaidrive/core-concepts/errors) for the full vocabulary.

## What's next

- [Files](/docs/scaidrive/api-guides/files) — full file API, including copy, move, and versioning.
- [Sharing](/docs/scaidrive/api-guides/sharing) — internal shares, roles, and invitations.
- [Sync](/docs/scaidrive/api-guides/sync) — if you're building a client that keeps a local folder in sync.