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
Keys → Create 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:
-
API keys (services): mint in the dashboard, send as
Authorization: ApiKey <key_id>:<key_secret>. Revocable individually. -
Browser sessions (humans): standard cookie session via
POST /v1/auth/loginwithemail+password. Used by the dashboard UI.
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
| Method | Path | Purpose | Auth |
|---|---|---|---|
| POST | /v1/auth/login | Sign in (cookie session) | — |
| POST | /v1/auth/logout | End session | — |
| GET | /v1/auth/me | Identity check | session |
| POST | /v1/auth/change-password | Update own password | session |
| GET | /v1/users | List users | admin |
| POST | /v1/users | Create user | admin |
| PATCH | /v1/users/:id | Update user | admin |
| DELETE | /v1/users/:id | Delete user | admin |
| GET | /v1/api-keys | List keys | session |
| POST | /v1/api-keys | Mint a new key (secret returned once) | session |
| POST | /v1/api-keys/:id/revoke | Revoke a key | session |
| POST | /v1/upload | Multipart upload (field: file) | any |
| GET | /v1/assets | List/search assets | any |
| GET | /v1/assets/:public_id | Asset metadata | owner / admin |
| PATCH | /v1/assets/:public_id | Update tags / folder / visibility | owner / admin |
| DELETE | /v1/assets/:public_id | Delete asset | owner / admin |
| GET | /v1/image[/transform]/:public_id | Deliver image (with optional transform) | public + signed |
| POST | /v1/image/sign | Generate signed URL for private asset | any |
| GET | /v1/usage/summary | Storage + bandwidth summary | any (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/.
| Key | Meaning | Range | Example |
|---|---|---|---|
w | Width | 1 – 4000 px | w_400 |
h | Height | 1 – 4000 px | h_300 |
c | Crop mode | fit · fill · cover · contain · inside · outside · thumb | c_fill |
q | Quality | 1 – 100 | q_80 |
f | Format | auto · jpg · png · webp · avif · gif | f_webp |
dpr | DPR multiplier | 1 – 3 | dpr_2 |
gr | Grayscale | 1 | gr_1 |
bl | Blur sigma | 0.3 – 25 | bl_4 |
r | Rotate degrees | -360 – 360 | r_90 |
bg | Hex background (for c_contain) | 6-digit hex | bg_ffffff |
Errors
Errors are JSON with an error field and a meaningful HTTP status:
| Status | Meaning |
|---|---|
| 400 | Malformed request, invalid transform, invalid public_id |
| 401 | Missing or invalid credentials |
| 403 | Authenticated but not authorised for this resource |
| 404 | Asset / route not found |
| 409 | public_id already exists (use overwrite=true) |
| 413 | Upload exceeds size cap |
| 415 | Unsupported media type (e.g. SVG) |
| 429 | Rate-limited — see Retry-After header |
Security model
- All traffic is HTTPS via Cloudflare's edge with a real TLS certificate; HSTS, CSP, and the usual hardening headers are set on every response.
- Passwords are bcrypt-hashed; API key secrets are SHA-256 hashed at rest and compared in constant time.
- Login is rate-limited per IP and per email; uploads are rate-limited per IP.
- Uploads pass magic-byte sniffing; SVGs are rejected; image inputs are capped at 100 megapixels.
- Output transforms are bounded — max 4000 px per side, 16 MP total, blur sigma ≤ 25.
- EXIF, IPTC, and ICC metadata are stripped from every served transform.
- Private assets require time-limited HMAC-signed URLs; Cloudflare won't cache them past expiry.
Questions? Ping the team in your shared channel, or open an issue in the internal tracker.