Code examples

Upload, list, download, copy, move, update metadata, and delete — in curl, JavaScript/TypeScript, Python, and C#.

curl

BASE="https://storage.devd.org/api/v1"
KEY="sk_live_YOUR_KEY"

# Upload
curl -X POST "$BASE/containers/contracts/files" \
  -H "X-API-Key: $KEY" -F "file=@agreement.pdf"

# List
curl "$BASE/containers/contracts/items?limit=50" -H "X-API-Key: $KEY"

# Download
curl -L "$BASE/containers/contracts/files/file_XXXX/content" \
  -H "X-API-Key: $KEY" -o agreement.pdf

# Copy (then poll the job)
curl -X POST "$BASE/containers/contracts/files/file_XXXX/copy" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"targetContainer":"archive"}'
curl "$BASE/jobs/job_YYYY" -H "X-API-Key: $KEY"

# Move into a folder
curl -X POST "$BASE/containers/contracts/files/file_XXXX/move" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"targetFolderId":"folder_ZZZZ"}'

# Update metadata
curl -X PATCH "$BASE/containers/contracts/files/file_XXXX/metadata" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"matterId":"MAT-10021","department":"legal"}'

# Delete
curl -X DELETE "$BASE/containers/contracts/files/file_XXXX" -H "X-API-Key: $KEY"

JavaScript / TypeScript

const BASE = "https://storage.devd.org/api/v1";
const KEY = process.env.SPORAGE_API_KEY!; // never hardcode keys

async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
  const response = await fetch(`${BASE}${path}`, {
    ...init,
    headers: { "X-API-Key": KEY, ...init.headers },
  });
  const body = await response.json();
  if (!body.success) {
    throw new Error(`${body.error.code}: ${body.error.message} (${body.requestId})`);
  }
  return body.data as T;
}

// Upload (multipart)
async function upload(container: string, file: Blob, name: string) {
  const form = new FormData();
  form.append("file", file, name);
  return api<{ file: { id: string; name: string } }>(
    `/containers/${container}/files`,
    { method: "POST", body: form },
  );
}

// List with pagination
async function listAll(container: string) {
  const items = [];
  let cursor: string | null = null;
  do {
    const page = await api<{ items: unknown[]; nextCursor: string | null }>(
      `/containers/${container}/items?limit=100` +
        (cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""),
    );
    items.push(...page.items);
    cursor = page.nextCursor;
  } while (cursor);
  return items;
}

// Download to a Buffer (Node.js)
async function download(container: string, fileId: string) {
  const response = await fetch(
    `${BASE}/containers/${container}/files/${fileId}/content`,
    { headers: { "X-API-Key": KEY } },
  );
  if (!response.ok) throw new Error(`Download failed: ${response.status}`);
  return Buffer.from(await response.arrayBuffer());
}

// Copy and wait for completion
async function copyAndWait(container: string, fileId: string, target: string) {
  const { job } = await api<{ job: { id: string; status: string } }>(
    `/containers/${container}/files/${fileId}/copy`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ targetContainer: target }),
    },
  );
  let status = job;
  while (status.status === "queued" || status.status === "running") {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    status = (await api<{ job: typeof job }>(`/jobs/${job.id}`)).job;
  }
  return status;
}

Python

import os, time, requests

BASE = "https://storage.devd.org/api/v1"
HEADERS = {"X-API-Key": os.environ["SPORAGE_API_KEY"]}

def api(method, path, **kwargs):
    response = requests.request(method, f"{BASE}{path}", headers=HEADERS, **kwargs)
    body = response.json()
    if not body.get("success"):
        error = body["error"]
        raise RuntimeError(f"{error['code']}: {error['message']} ({body['requestId']})")
    return body["data"]

# Upload
with open("agreement.pdf", "rb") as f:
    data = api("POST", "/containers/contracts/files", files={"file": ("agreement.pdf", f)})
file_id = data["file"]["id"]

# List
items = api("GET", "/containers/contracts/items", params={"limit": 100})["items"]

# Download
response = requests.get(
    f"{BASE}/containers/contracts/files/{file_id}/content", headers=HEADERS)
response.raise_for_status()
open("downloaded.pdf", "wb").write(response.content)

# Copy and poll
job = api("POST", f"/containers/contracts/files/{file_id}/copy",
          json={"targetContainer": "archive"})["job"]
while job["status"] in ("queued", "running"):
    time.sleep(1)
    job = api("GET", f"/jobs/{job['id']}")["job"]

# Metadata
api("PATCH", f"/containers/contracts/files/{file_id}/metadata",
    json={"matterId": "MAT-10021", "documentType": "contract"})

# Delete
api("DELETE", f"/containers/contracts/files/{file_id}")

C#

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var baseUrl = "https://storage.devd.org/api/v1";
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key",
    Environment.GetEnvironmentVariable("SPORAGE_API_KEY"));

// Upload
using var form = new MultipartFormDataContent();
var fileContent = new ByteArrayContent(await File.ReadAllBytesAsync("agreement.pdf"));
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
form.Add(fileContent, "file", "agreement.pdf");
var uploadResponse = await client.PostAsync($"{baseUrl}/containers/contracts/files", form);
var uploaded = JsonDocument.Parse(await uploadResponse.Content.ReadAsStringAsync());
var fileId = uploaded.RootElement.GetProperty("data")
    .GetProperty("file").GetProperty("id").GetString();

// List
var listing = await client.GetFromJsonAsync<JsonDocument>(
    $"{baseUrl}/containers/contracts/items?limit=100");

// Download
var bytes = await client.GetByteArrayAsync(
    $"{baseUrl}/containers/contracts/files/{fileId}/content");
await File.WriteAllBytesAsync("downloaded.pdf", bytes);

// Metadata update
var patch = new HttpRequestMessage(HttpMethod.Patch,
    $"{baseUrl}/containers/contracts/files/{fileId}/metadata")
{
    Content = JsonContent.Create(new { matterId = "MAT-10021", department = "legal" })
};
await client.SendAsync(patch);

// Delete
await client.DeleteAsync($"{baseUrl}/containers/contracts/files/{fileId}");

Idempotent uploads

Add an Idempotency-Key header to uploads and other writes. If the request is retried with the same key, the stored response is replayed instead of repeating the operation.

curl -X POST "$BASE/containers/contracts/files" \
  -H "X-API-Key: $KEY" \
  -H "Idempotency-Key: 4f9d2c81-upload-agreement" \
  -F "file=@agreement.pdf"