API Documentation
Upload images programmatically using the CDN API.
Machine-readable spec
The full API surface is published as an OpenAPI 3.2 document:
| Format | URL |
|---|---|
| JSON | https://cdn.hackclub.com/openapi.json |
| YAML | https://cdn.hackclub.com/openapi.yaml |
It is also served at /api/openapi.json and /api/openapi.yaml, needs no
authentication, and is linked from every page as
<link rel="service-desc" href="/openapi.json">. Point your client generator,
agent, or API console at it rather than scraping this page.
Agents should start at https://cdn.hackclub.com/llms.txt, which
says when this service is the right tool, when it is not, and how to
authenticate and call it. Every indexable page is listed in
https://cdn.hackclub.com/sitemap.xml.
Authentication
Create an API key at API Keys. Keys are shown once, so copy it immediately.
Include the key in the Authorization header:
Authorization: Bearer sk_cdn_your_key_here
POST /api/v4/upload
Upload a file via multipart form data.
curl -X POST \
-H "Authorization: Bearer sk_cdn_your_key_here" \
-F "[email protected]" \
https://cdn.hackclub.com/api/v4/upload
const formData = new FormData();
formData.append('file', fileInput.files[0]);
const response = await fetch('https://cdn.hackclub.com/api/v4/upload', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_cdn_your_key_here' },
body: formData
});
const { url } = await response.json();
Response:
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"filename": "photo.jpg",
"size": 12345,
"content_type": "image/jpeg",
"url": "https://cdn.hackclub.com/01234567-89ab-cdef-0123-456789abcdef/photo.jpg",
"created_at": "2026-01-29T12:00:00Z"
}
POST /api/v4/upload_from_url
Upload an image from a URL.
Optional header: X-Download-Authorization — passed as Authorization when fetching the source URL (useful for protected resources).
curl -X POST \
-H "Authorization: Bearer sk_cdn_your_key_here" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/image.jpg"}' \
https://cdn.hackclub.com/api/v4/upload_from_url
# With authentication for the source URL:
curl -X POST \
-H "Authorization: Bearer sk_cdn_your_key_here" \
-H "X-Download-Authorization: Bearer source_token_here" \
-H "Content-Type: application/json" \
-d '{"url":"https://protected.example.com/image.jpg"}' \
https://cdn.hackclub.com/api/v4/upload_from_url
const response = await fetch('https://cdn.hackclub.com/api/v4/upload_from_url', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_cdn_your_key_here',
'Content-Type': 'application/json',
// Optional: auth for the source URL
'X-Download-Authorization': 'Bearer source_token_here'
},
body: JSON.stringify({ url: 'https://example.com/image.jpg' })
});
const { url } = await response.json();
DELETE /api/v4/upload/:id
Delete an uploaded file by its ID.
curl -X DELETE \
-H "Authorization: Bearer sk_cdn_your_key_here" \
https://cdn.hackclub.com/api/v4/upload/01234567-89ab-cdef-0123-456789abcdef
const response = await fetch('https://cdn.hackclub.com/api/v4/upload/01234567-89ab-cdef-0123-456789abcdef', {
method: 'DELETE',
headers: { 'Authorization': 'Bearer sk_cdn_your_key_here' }
});
const result = await response.json();
Response:
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"deleted": true
}
Returns 404 if the upload doesn't exist or doesn't belong to you.
POST /api/v4/uploads
Upload up to 40 files in one request. Partial success is normal: stored files come back in uploads, rejected ones in failed.
curl -X POST \
-H "Authorization: Bearer sk_cdn_your_key_here" \
-F "files[][email protected]" \
-F "files[][email protected]" \
https://cdn.hackclub.com/api/v4/uploads
Response (201 when at least one file was stored, otherwise 422):
{
"uploads": [
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"filename": "one.png",
"size": 12345,
"content_type": "image/png",
"url": "https://cdn.hackclub.com/01234567-89ab-cdef-0123-456789abcdef/one.png",
"created_at": "2026-01-29T12:00:00Z"
}
],
"failed": [
{ "filename": "two.png", "reason": "File exceeds your per-file limit" }
]
}
PATCH /api/v4/uploads/:id/rename
Rename an upload. The filename is part of the CDN URL, so the old URL stops resolving — update any references.
curl -X PATCH \
-H "Authorization: Bearer sk_cdn_your_key_here" \
-H "Content-Type: application/json" \
-d '{"filename":"better-name.png"}' \
https://cdn.hackclub.com/api/v4/uploads/01234567-89ab-cdef-0123-456789abcdef/rename
Responds with the updated upload object.
DELETE /api/v4/uploads/batch
Delete several uploads at once. IDs that aren't yours are reported in not_found instead of failing the request.
curl -X DELETE \
-H "Authorization: Bearer sk_cdn_your_key_here" \
-H "Content-Type: application/json" \
-d '{"ids":["01234567-89ab-cdef-0123-456789abcdef"]}' \
https://cdn.hackclub.com/api/v4/uploads/batch
Response:
{
"deleted": [
{ "id": "01234567-89ab-cdef-0123-456789abcdef", "filename": "photo.jpg" }
],
"not_found": ["ffffffff-ffff-ffff-ffff-ffffffffffff"]
}
POST /api/v4/revoke
Revoke the API key making the request. Immediate and irreversible; uploads are untouched.
curl -X POST \
-H "Authorization: Bearer sk_cdn_your_key_here" \
https://cdn.hackclub.com/api/v4/revoke
GET /api/v4/me
Get the authenticated user and quota information.
curl -H "Authorization: Bearer sk_cdn_your_key_here" \
https://cdn.hackclub.com/api/v4/me
{
"id": "usr_abc123",
"email": "[email protected]",
"name": "Your Name",
"storage_used": 1048576000,
"storage_limit": 53687091200,
"quota_tier": "verified"
}
Quota fields:
storage_used— bytes usedstorage_limit— bytes allowedquota_tier—"unverified","verified", or"functionally_unlimited"
Errors
Every error is JSON and follows a predictable shape. For example, a missing file parameter returns 400:
{
"error": "Missing file parameter",
"code": "missing_parameter",
"message": "Required parameter `file` is missing or blank.",
"hint": "Send the file as multipart/form-data under the `file` field, e.g. `curl -F \"[email protected]\"`.",
"parameter": "file",
"status": 400,
"documentation_url": "https://cdn.hackclub.com/docs/api"
}
| Field | Meaning |
|---|---|
code |
Stable machine-readable identifier. Branch on this. |
message |
Human-readable description of what went wrong. |
hint |
How to resolve it. |
status |
HTTP status, repeated for clients that only read the body. |
documentation_url |
Where to read more. |
error |
Short legacy string, kept for backwards compatibility. Prefer code. |
details |
Field-level validation messages, when applicable. |
parameter |
The offending parameter, when applicable. |
error_id |
Support identifier, on server errors. |
| Status | Codes |
|---|---|
| 400 | missing_parameter, too_many_files |
| 401 | invalid_auth |
| 402 | quota_exceeded, file_too_large |
| 404 | not_found, upload_not_found, route_not_found, original_url_not_found |
| 422 | validation_failed, upload_failed, rename_failed |
| 500 | internal_error |
Quota error (402) adds a quota object:
{
"error": "Storage quota exceeded",
"code": "quota_exceeded",
"message": "This upload would exceed the storage quota for your account (unverified tier).",
"hint": "Delete files you no longer need, or ask for a higher tier — see https://cdn.hackclub.com/docs/quotas.",
"quota": {
"storage_used": 52428800,
"storage_limit": 52428800,
"quota_tier": "unverified",
"percentage_used": 100.0
},
"status": 402,
"documentation_url": "https://cdn.hackclub.com/docs/api"
}
Requests to a path that doesn't exist under /api, and any request that asks for
JSON with Accept: application/json, return route_not_found rather than an
HTML 404 page.
See Storage Quotas for details on getting more space.