pixeldrive

API

Host: https://api.pixeldrive.dev. Shared public projects need no auth to list. A Bearer API key lists a public project even when sharing is off, and is required for private projects (tokened URLs). Write keys can POST /v1/upload. IDs in this API are publicIds, not dashboard ids.

EndpointsQueryResponseAuthTypesUploadSDKMCPErrorsLimits

Endpoints

MethodPathReturns
GET/v1/projects/{publicId}Project root: files in the root, plus child folders
GET/v1/folders/{publicId}One folder: files in that folder, plus its child folders
GET/v1/files/{publicId}One file. Requires Authorization: Bearer. Private files return a tokened url + expiresAt.
GET/v1/files?path=One file by path (selects/hero.jpg). Optional folder publicId and ttl. Read key.
DELETE/v1/files/{publicId}Delete a file and its retained versions. Write key. 204.
POST/v1/uploadStart an upload, or replace with replace. Write key. Server picks put vs multipart.
POST/v1/upload/{publicId}/completeVerify the object and return the file JSON.
POST/v1/upload/{publicId}/abortCancel an in-flight upload or replace. 204.
GET/v1/projectThe key’s project listing (same shape as GET /v1/projects/{id}).

Legacy /api/v1/… paths still work. Prefer /v1/….

Query

ParamDefaultEffect
limit100Files to return in this page, 1–500, newest first. Omitted uses the default. Child folders are always included (up to 100) and do not count against limit.
cursoromittedOpaque token from the previous page’s nextCursor. Omit on the first request.
tagomittedTag as shown in the app (slug). Spaces become hyphens: la jolla is la-jolla. On a project listing this is the whole project, not just the root. Unknown slug → empty files (200). Invalid slug → 400.
recursiveomittedSet to true to flatten a small subtree in one response. Nested files include folderPath (names joined with /). Child folders stay the immediate children of the requested container. Cannot be combined with cursor.
ttl900Private files only, on GET /v1/files/{id}. Link lifetime in seconds, 1–86400 (default 15 minutes).
page through a container
let cursor = null;
const files = [];
do {
  const url = new URL(
    "https://api.pixeldrive.dev/v1/projects/YOUR_PUBLIC_ID",
  );
  url.searchParams.set("limit", "200");
  if (cursor) url.searchParams.set("cursor", cursor);
  const page = await fetch(url).then((res) => res.json());
  files.push(...page.files);
  cursor = page.nextCursor;
} while (cursor);
recursive
const res = await fetch(
  "https://api.pixeldrive.dev/v1/projects/YOUR_PUBLIC_ID?recursive=true",
);
const project = await res.json();
// Nested files include folderPath, e.g. "selects/outtakes"

Response

200 application/json
{
  "name": "summer-lookbook",
  "files": [
    {
      "publicId": "6dec2e93a1b2c3d4e5f6a7b8c9d0e1f2",
      "name": "eng-448.jpg",
      "url": "https://cdn.pixeldrive.dev/{projectId}/{publicId}/v1",
      "stableUrl": "https://cdn.pixeldrive.dev/f/{publicId}",
      "optimizable": true,
      "src": "https://cdn.pixeldrive.dev/f/{publicId}?w=1280",
      "srcset": "https://cdn.pixeldrive.dev/f/{publicId}?w=320 320w, …",
      "contentType": "image/jpeg",
      "kind": "image",
      "size": 4821933,
      "width": 5695,
      "height": 3797,
      "blurhash": "LGF5]+Yk^6#M@-5c,1J5",
      "tags": ["favorites"],
      "uploadedAt": 1734567890123
    }
  ],
  "folders": [
    { "publicId": "b86a4c21d0e1f2a3b4c5d6e7f8a9b0c1", "name": "selects" }
  ],
  "nextCursor": null
}
FieldTypeNotes
namestringProject or folder name
files[].publicIdstringUnguessable file id
files[].namestringOriginal filename
files[].urlstringPublic: immutable versioned CDN object (cache ~1 year). Private: tokened /f/ URL.
files[].stableUrlstringPublic: 302 to the current version; follows replaces. Add ?w= for optimization (Pro). Private: same as url (already tokened).
files[].optimizablebooleanSource is within Cloudflare resize caps (100 MB / 100 MP / 12k px)
files[].srcstring?Pro + optimizable: stableUrl?w=1280
files[].srcsetstring?Pro + optimizable: ladder 320–1920 against stableUrl
files[].contentTypestringFrom storage, e.g. image/jpeg
files[].kindimage | video | pdf | doc | textDerived from contentType. See Allowed types.
files[].sizenumberBytes
files[].widthnumber?Client-computed at upload
files[].heightnumber?Client-computed at upload
files[].blurhashstring?Optional placeholder hash
files[].tagsstring[]Slugs on this file, sorted. Same string as ?tag=
files[].folderPathstring?Only when recursive=true and the file is nested
files[].uploadedAtnumberUnix ms (_creationTime)
files[].expiresAtnumber?Private listings only: Unix ms when the tokened url dies
folders[].publicIdstringUse with GET /v1/folders/{id}
folders[].namestringFolder name
nextCursorstring | nullPass as cursor for the next page. null means this was the last page. Always null when recursive=true.

Authorization

Create a key in project settings. Shown once. Send it as Authorization: Bearer pd_live_…. One key is scoped to one project. Write can upload; Read can list and mint URLs. Read keys 404 on upload routes. Keys work on public projects even when the public API toggle is off. Private-project keys mint expiring /f/ URLs. Keys keep working after a plan change, under the current file-size and storage caps.

Allowed types

Same allowlist on the dashboard, POST /v1/upload, the SDK, and MCP. kind is derived from MIME — callers never send it. HTML, SVG, and JavaScript are rejected, including on private projects.

kindWhatAccepts
imagePhotosimage/* except SVG
videoVideovideo/*
pdfPDFapplication/pdf
docWord.doc, .docx
textMarkdowntext/markdown

A project can tighten this list and the max file size in Settings → Uploads. It cannot raise the account plan cap. Keys inherit the project’s rules.

Upload

Write key required. The key’s project is implicit — do not send projectId. size is required so we choose put (≤ 32 MB) vs multipart (16 MB parts). PUT the bytes to the returned URLs; never POST the file body to this API. Caps and rate limits are in Limits. The project’s Settings → Uploads rules apply here too.

POST /v1/upload
{
  "name": "selects/hero.jpg",
  "contentType": "image/jpeg",
  "size": 4821933,
  "folder": "optional-folder-publicId",
  "tags": ["favorites"],
  "replace": "optional-file-publicId",
  "upsert": true
}

Path segments in name find-or-create folders (max 8). Optional folder is a folder publicId; the path is relative to it. Tags are stored on start and applied only after complete. Optional replace is a file publicId in this project — same complete/abort URLs, folder and tags stay, name becomes the new basename, kind may change. upsert: true replaces the ready file at that path instead of minting a second one (do not send it with replace). Paths are unique per folder; a colliding upload without upsert is 409. GET /v1/files?path= looks up by that path. Optional Idempotency-Key header (24 hours). DELETE /v1/files/{publicId} is 204. Cached public URLs may still serve until TTL.

200 put
{
  "id": "6dec2e93…",
  "name": "hero.jpg",
  "folderPath": "selects",
  "contentType": "image/jpeg",
  "size": 4821933,
  "upload": { "mode": "put", "url": "https://…" }
}

Multipart: { "mode": "multipart", "partSize": 16777216, "parts": [{ "partNumber": 1, "url": "…" }] }. Complete with part ETags. Complete returns the same JSON as GET /v1/files/{id}. Abort is 204 while the file is still uploading or replacing.

TypeScript: @pixeldrive-dev/sdk (npm i @pixeldrive-dev/sdk). Agents: @pixeldrive-dev/mcp with PIXELDRIVE_API_KEY.

sdk
import { readFile } from "node:fs/promises";
import { Pixeldrive } from "@pixeldrive-dev/sdk";

const pd = new Pixeldrive({ apiKey: process.env.PIXELDRIVE_API_KEY });

const file = await pd.upload({
  file: await readFile("./brief.pdf"),
  name: "contracts/brief.pdf",
  contentType: "application/pdf",
});

Headers

HeaderValue
Content-Typeapplication/json
Cache-Controlpublic, max-age=60 — or private, no-store when a key is used
Access-Control-Allow-Origin*

Errors

StatusBodyWhen
400{ "error": "Invalid project id" }Empty or nested path
400{ "error": "Invalid limit" }limit is not an integer from 1 to 500
400{ "error": "Invalid tag" }tag is empty after normalize (needs a letter or number)
400{ "error": "recursive cannot be used with cursor" }Both recursive=true and cursor were sent
400{ "error": "Invalid cursor" }cursor is malformed or expired
400{ "error": "Unknown file type", "code": "invalid" }MIME is not on the allowlist, or this project disallows that kind
404{ "error": "Not found", "code": "not_found" }Unknown id, sharing is off, missing/invalid/read key on a write route, or a second DELETE
401{ "error": "Unauthorized", "code": "unauthorized" }Missing or malformed Bearer on upload or DELETE
409{ "error": "…", "code": "conflict" }Complete/abort when nothing is in flight, replace while uploading or already replacing, or Idempotency-Key reused with a different body
413{ "error": "…", "code": "too_large or plan_limit" }Over the plan file-size or storage cap, or this project's size limit
429{ "error": "Too many requests" }API key read limit (300/min) or write starts (60/min)
500{ "error": "Server misconfigured" }Missing CDN config

Private containers look like missing ones. Don’t treat 404 as “this id never existed.”

Limits

Upload uses the same account plan as the dashboard. A project can tighten allowed types and max file size below that plan (Settings → Uploads); keys cannot raise it. Rate limits are per key, not per account.

WhatCap
Write starts60 / minute per key
Reads (list + mint URL)300 / minute per key
Keys per project10
File size100 GB (Pro)
Account storage1 TB (Pro)
Files and projectsUnlimited (Pro)
Single PUT≤ 32 MB; larger files use 16 MB multipart parts
Allowed typesPhotos (image/* except SVG), video, PDF, Word, markdown
Folder path in name8 segments
Idempotency-Key24 hours
Private link TTL15 minutes default, 24 hours max
Files per listing request100 default, 500 max (newest first)
Total files in a containerNo cap — follow nextCursor
Child folders per container100
Files per folder when recursive=true500
Folders walked when recursive=true100
Listing cache60 seconds

A folder with thousands of files is listed by looping nextCursor. For a large tree, paginate the container, then GET /v1/folders/{id} for each child. recursive=true is for small lookbooks, not a full-library dump.