API reference

Base URL: https://cdn.darrellmucheri.com/api/public/v1

Quick start

  1. Ask the site owner for an API key (keys start with nimbus_).
  2. Send files to POST /api/public/v1/files with the key as a Bearer token.
  3. Use the returned url anywhere — it's served from https://files.darrellmucheri.com.

Authentication

Create a key in Admin → API keys. Send it as a Bearer token (or an x-api-key header). Never expose keys in browser code.

Authorization: Bearer nimbus_your_key_here
POST/files

Multipart form-data. Fields: file (required, max 100 MB), folder (optional), key (optional custom file name).

curl -X POST https://cdn.darrellmucheri.com/api/public/v1/files \
  -H "Authorization: Bearer $NIMBUS_KEY" \
  -F "file=@./logo.png" \
  -F "folder=images"
// Node / browser server
const fd = new FormData();
fd.append("file", file);
const res = await fetch("https://cdn.darrellmucheri.com/api/public/v1/files", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.NIMBUS_KEY}` },
  body: fd,
});
const { url } = await res.json();

Response 201

{
  "key": "images/logo.png",
  "size": 18234,
  "contentType": "image/png",
  "url": "https://files.darrellmucheri.com/images/logo.png"
}
GET/files?prefix=&limit=&cursor=

Returns up to limit files (default 100, max 1000). Pass the returned cursor to get the next page.

curl "https://cdn.darrellmucheri.com/api/public/v1/files?prefix=images/&limit=50" \
  -H "Authorization: Bearer $NIMBUS_KEY"
{
  "files": [
    { "key": "images/logo.png", "size": 18234, "lastModified": "2026-10-13T18:00:00.000Z",
      "etag": "9b2c…", "url": "https://files.darrellmucheri.com/images/logo.png" }
  ],
  "cursor": null
}
DELETE/files/:key
curl -X DELETE https://cdn.darrellmucheri.com/api/public/v1/files/images/logo.png \
  -H "Authorization: Bearer $NIMBUS_KEY"

→ { "deleted": "images/logo.png" }
GET/api/public/cdn/:key

Public, no key needed. Responses include Cache-Control: public, max-age=31536000, immutable, ETags and byte-range support for video. If you set a public R2 domain in settings, returned URLs use that domain instead.

<img src="https://files.darrellmucheri.com/images/logo.png" />
POST/api/public/upload

Powers the free uploader on the homepage. No key needed, but each request must carry a solved proof-of-work captcha from GET /api/public/captcha: find a nonce where sha256(salt + nonce) starts with difficulty zeros, then send the challenge plus nonce as JSON in the captcha field. Optional name sets a custom link; if taken, a short prefix is added.

const ch = await (await fetch("https://cdn.darrellmucheri.com/api/public/captcha")).json();
let nonce = 0;
while (!(await sha256(ch.salt + nonce)).startsWith("0".repeat(ch.difficulty))) nonce++;

const fd = new FormData();
fd.append("captcha", JSON.stringify({ ...ch, nonce }));
fd.append("name", "my-photo.jpg");
fd.append("file", file);
const { url } = await (await fetch("https://cdn.darrellmucheri.com/api/public/upload", { method: "POST", body: fd })).json();
// → https://files.darrellmucheri.com/public/my-photo.jpg

Code examples

Python

import requests
r = requests.post("https://cdn.darrellmucheri.com/api/public/v1/files",
    headers={"Authorization": "Bearer " + NIMBUS_KEY},
    files={"file": open("logo.png", "rb")},
    data={"folder": "images"})
print(r.json()["url"])

PHP

$ch = curl_init("https://cdn.darrellmucheri.com/api/public/v1/files");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $key],
  CURLOPT_POSTFIELDS => ["file" => new CURLFile("logo.png")],
  CURLOPT_RETURNTRANSFER => true,
]);
echo json_decode(curl_exec($ch))->url;

List every file (paging)

let cursor = null, all = [];
do {
  const q = new URLSearchParams({ limit: "1000", ...(cursor && { cursor }) });
  const r = await (await fetch(`https://cdn.darrellmucheri.com/api/public/v1/files?${q}`, { headers })).json();
  all.push(...r.files); cursor = r.cursor;
} while (cursor);

Limits

  • Max file size: 100 MB per request.
  • Any file type is accepted.
  • File keys: up to 900 characters; uploading to an existing key via the API overwrites it.
  • Public uploads are stored under public/ and require a fresh captcha each time.
  • CORS is open, but keep API keys on your server.

Errors

400Bad request — missing file or invalid key
401Missing or invalid API key
403Captcha missing, expired or invalid (public upload)
404File not found
413File larger than 100 MB
502 / 503R2 unreachable or not configured
{ "error": "Invalid or missing API key" }