img-cloud Documentation

img-cloud is a single-tenant, self-hosted media platform: one API for uploading originals, one URL grammar for delivering any size or format, and one set of SDKs to glue it into your apps.

Quickstart

Three steps.

1 · Mint an API key

Sign in to the dashboard at /dashboard/API KeysCreate new key. Save the key_id and key_secret — the secret is shown once.

2 · Install an SDK

# Node.js
npm install cloudimg

# Python
pip install cloudimg

3 · Upload and deliver

const cli = require('cloudimg').createClient({
  baseUrl: 'https://img-cloud.org',
  keyId: process.env.CLOUDIMG_KEY_ID,
  keySecret: process.env.CLOUDIMG_KEY_SECRET,
});

const asset = await cli.upload('./photo.jpg');
console.log(asset.url);
//=> https://img-cloud.org/v1/image/photo-3a7f9c81b2e4

console.log(cli.urlFor(asset.public_id, { w: 400, h: 400, c: 'fill', f: 'webp' }));
//=> https://img-cloud.org/v1/image/w_400,h_400,c_fill,f_webp/photo-3a7f9c81b2e4

Authentication

Two flavors:

Node.js SDK

const { createClient } = require('cloudimg');

const cli = createClient({
  baseUrl: 'https://img-cloud.org',
  keyId: process.env.CLOUDIMG_KEY_ID,
  keySecret: process.env.CLOUDIMG_KEY_SECRET,
});

// Upload from disk
const asset = await cli.upload('./hero.jpg', {
  folder: 'marketing',
  tags: ['hero', 'homepage'],
  visibility: 'public',
});

// Delivery URL with transforms
const thumb = cli.urlFor(asset.public_id, {
  w: 400, h: 400, c: 'fill', f: 'webp', q: 80,
});

// Signed URL for private assets
const signed = await cli.signedUrl(asset.public_id, {
  transform: { w: 200 }, ttl_seconds: 3600,
});

// Update / delete / list
await cli.updateAsset(asset.public_id, { tags: 'archived' });
await cli.deleteAsset(asset.public_id);
const { assets } = await cli.listAssets({ folder: 'marketing', limit: 50 });

Python SDK

from cloudimg import Cloudimg

cli = Cloudimg(
    base_url="https://img-cloud.org",
    key_id=os.environ["CLOUDIMG_KEY_ID"],
    key_secret=os.environ["CLOUDIMG_KEY_SECRET"],
)

asset = cli.upload("photo.jpg", folder="gallery", tags=["hero"])
print(asset["public_id"], asset["url"])

print(cli.url_for(asset["public_id"], w=400, h=400, c="fill", f="webp", q=80))
print(cli.signed_url(asset["public_id"], transform={"w": 200}, ttl_seconds=3600))

cli.update_asset(asset["public_id"], tags="archived,old")
cli.delete_asset(asset["public_id"])

REST API reference

MethodPathPurposeAuth
POST/v1/auth/loginSign in (cookie session)
POST/v1/auth/logoutEnd session
GET/v1/auth/meIdentity checksession
POST/v1/auth/change-passwordUpdate own passwordsession
GET/v1/usersList usersadmin
POST/v1/usersCreate useradmin
PATCH/v1/users/:idUpdate useradmin
DELETE/v1/users/:idDelete useradmin
GET/v1/api-keysList keyssession
POST/v1/api-keysMint a new key (secret returned once)session
POST/v1/api-keys/:id/revokeRevoke a keysession
POST/v1/uploadMultipart upload (field: file)any
GET/v1/assetsList/search assetsany
GET/v1/assets/:public_idAsset metadataowner / admin
PATCH/v1/assets/:public_idUpdate tags / folder / visibilityowner / admin
DELETE/v1/assets/:public_idDelete assetowner / admin
GET/v1/image[/transform]/:public_idDeliver image (with optional transform)public + signed
POST/v1/image/signGenerate signed URL for private assetany
GET/v1/usage/summaryStorage + bandwidth summaryany (admin: ?all=true)

Upload

POST /v1/upload
Authorization: ApiKey ck_xxx:cs_xxx
Content-Type: multipart/form-data

file=<binary>                   # required
public_id=marketing/hero        # optional, will be generated otherwise
folder=marketing                # optional
tags=hero,homepage              # optional, comma-separated
visibility=public|private       # default public
overwrite=true                  # required to replace an existing public_id

Delivery URLs

The delivery URL is just a path. Drop a comma-separated transform string between /v1/image/ and the public_id:

https://img-cloud.org/v1/image/w_400,h_400,c_fill,q_80,f_webp/photo-3a7f9c81b2e4

The first request runs the transform; subsequent requests are served from the on-disk cache.

Signed URLs (private assets)

POST /v1/image/sign
Content-Type: application/json
{
  "public_id": "private/contract-001",
  "transform": "w_1200,q_80",
  "ttl_seconds": 3600
}

Response includes url with ?expires=…&sig=…. Cloudflare won't edge-cache these past their TTL.

Transform grammar

Comma-separated key_value pairs after /v1/image/.

KeyMeaningRangeExample
wWidth1 – 4000 pxw_400
hHeight1 – 4000 pxh_300
cCrop modefit · fill · cover · contain · inside · outside · thumbc_fill
qQuality1 – 100q_80
fFormatauto · jpg · png · webp · avif · giff_webp
dprDPR multiplier1 – 3dpr_2
grGrayscale1gr_1
blBlur sigma0.3 – 25bl_4
rRotate degrees-360 – 360r_90
bgHex background (for c_contain)6-digit hexbg_ffffff

Errors

Errors are JSON with an error field and a meaningful HTTP status:

StatusMeaning
400Malformed request, invalid transform, invalid public_id
401Missing or invalid credentials
403Authenticated but not authorised for this resource
404Asset / route not found
409public_id already exists (use overwrite=true)
413Upload exceeds size cap
415Unsupported media type (e.g. SVG)
429Rate-limited — see Retry-After header

Security model


Questions? Ping the team in your shared channel, or open an issue in the internal tracker.