Reference for the JSON/REST API exposed by 640by480.com.
All endpoints are mounted under /api/. Base URL in production:
https://640by480.com/api/
The API accepts two authentication schemes on every endpoint:
Authorization: Token <key> header. Obtain a key via POST /api/auth/login/.There is no HTTP Basic, JWT, or OAuth support.
IsAuthenticatedOrReadOnly — anonymous clients can GET, authenticated clients can write.IsAuthorOrReadOnly — only the original author of a post or comment can PUT, PATCH, or DELETE it.POST /api/auth/login/Obtain (or retrieve an existing) auth token. No authentication required.
Request body (JSON or form-encoded):
{ "username": "alice", "password": "hunter2" }
Response 200:
{
"token": "<your token here>",
"user_id": 7,
"username": "alice"
}
If the user already has a token, the same key is returned (get_or_create semantics).
Response 400: validation errors from DRF.
POST /api/auth/logout/Invalidate the calling user's token. Authentication required.
Request body: empty.
Response 200:
{ "message": "Successfully logged out." }
Response 500 (no token to delete, or other error):
{ "error": "<message>" }
Wired via DRF DefaultRouter against PostViewSet. Standard CRUD plus one custom action.
GET /api/posts/ is paginated with PageNumberPagination and PAGE_SIZE = 20. Use ?page=N to navigate.
{
"count": 137,
"next": "https://640by480.com/api/posts/?page=2",
"previous": null,
"results": [ /* post objects */ ]
}
List results are ordered by -created (newest first).
Posts can be filtered to a single camera by id:
GET /api/posts/?camera=9
Pagination still applies. Invalid or non-integer camera values return an empty list.
{
"id": 42,
"image": "https://640by480.com/media/foo.jpg",
"thumbnail": "https://640by480.com/media/foo_thumbnail.jpg",
"detail": "https://640by480.com/media/foo_detail.jpg",
"description": "string or null",
"author": { "id": 7, "username": "alice" },
"created": "2025-04-01T12:34:56Z",
"modified": "2025-04-01T12:34:56Z",
"comment_count": 3,
"comments": [ /* comment objects */ ],
"camera": { "id": 9, "manufacturer": "Apple", "name": "QuickTake 150" }
}
thumbnail is generated at max 250×250.detail is generated at max 640×640.author, created, modified, thumbnail, detail.description may be null.camera is a nested object on read, or null if the post is unattributed.
To set it on write, use the camera_id field instead (see "Create" / "Update" below).Every post stores three versions of the uploaded image, generated server-side at upload time:
| Field | Max dimensions | Typical use |
|---|---|---|
image |
original (no resize) | full-resolution download |
detail |
640 × 640 | single-post / detail view |
thumbnail |
250 × 250 | feed / grid view |
The thumbnail and detail versions are produced with PIL's Image.thumbnail(), which preserves aspect ratio and constrains the longer side to the limit. A landscape photo therefore becomes something like 640×427 for detail, not a forced 640×640 square. EXIF orientation is normalized so portrait photos are not sideways.
Filename convention: if the original upload is foo.jpg, the resized files in the same media directory are:
foo.jpg — originalfoo_detail.jpg — detailfoo_thumbnail.jpg — thumbnailThe post object exposes all three as absolute URLs. Pick the field that matches the size you want and GET it directly — image bytes are served as static media (no Authorization header required for the image fetch itself):
# Feed view: list posts and use the thumbnail URL
curl https://640by480.com/api/posts/ | jq '.results[].thumbnail'
# Detail view: fetch one post and use the detail URL
curl https://640by480.com/api/posts/42/ | jq -r '.detail'
# Original: same response, use the image URL
curl https://640by480.com/api/posts/42/ | jq -r '.image'
There is no API parameter to request a custom size — only these three fixed variants exist.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/posts/ |
none | Paginated list, newest first. |
POST |
/api/posts/ |
required | Create a post. |
GET |
/api/posts/{id}/ |
none | Retrieve a single post. |
PUT |
/api/posts/{id}/ |
author | Full update. |
PATCH |
/api/posts/{id}/ |
author | Partial update. |
DELETE |
/api/posts/{id}/ |
author | Delete the post. |
POST |
/api/posts/{id}/add_comment/ |
required | Add a comment to this post. |
POST /api/posts/ — Create a postmultipart/form-data (image is a file upload).image (required, file) — JPEG, PNG, GIF, HEIC, MPO, or QTK (Apple QuickTake 100/150 native raw). HEIC, MPO, and QTK uploads are transcoded to JPEG server-side. QTK lets vintage Mac/Apple II clients ship the camera's native file straight to the server without converting locally.description (optional, text).camera_id (optional, integer) — id of an existing Camera (see GET /api/cameras/). Omit to leave the post unattributed.author is set automatically from the authenticated user; do not send it.Response 201: the full post object.
Image files larger than 50 megapixels are rejected as a decompression-bomb guard.
PATCH /api/posts/{id}/ — Update fields on an existing postAuthor-only. Accepts the same writable fields as create:
descriptioncamera_idimage (will re-trigger thumbnail/detail generation)Send only the fields you want to change. Other fields are left as-is.
POST /api/posts/{id}/add_comment/Convenience for posting a comment without specifying post_id.
Request body:
{ "text": "Nice shot!" }
Response 201: the new comment object.
Response 400: { /* validation errors */ }
Wired via the same router against CommentViewSet. Same CRUD shape as posts.
{
"id": 9,
"text": "Nice shot!",
"author": { "id": 7, "username": "alice" },
"created": "2025-04-01T12:34:56Z",
"modified": "2025-04-01T12:34:56Z"
}
The serialized comment does not include the parent post id. The post relationship is only visible by reading the parent post's
commentsarray.
GET /api/comments/ uses the global PAGE_SIZE = 20. Same response envelope as posts.
No explicit ordering — falls back to the model's default (typically insertion order / primary key).
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/comments/ |
none | Paginated list. |
POST |
/api/comments/ |
required | Create a comment. Requires post_id in body. |
GET |
/api/comments/{id}/ |
none | Retrieve a single comment. |
PUT |
/api/comments/{id}/ |
author | Full update. |
PATCH |
/api/comments/{id}/ |
author | Partial update. |
DELETE |
/api/comments/{id}/ |
author | Delete the comment. |
POST /api/comments/ — Create a commentRequest body:
{ "text": "Nice shot!", "post_id": 42 }
post_id is consumed by the view but does not appear in the response.post_id does not match an existing post.Response 201: the comment object.
Read-only browse of the canonical Camera list (no pagination). Cameras are managed server-side; new ones are created automatically when a user uploads with a camera name we haven't seen before.
{ "id": 9, "manufacturer": "Apple", "name": "QuickTake 150" }
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/cameras/ |
none | List every Camera. |
GET |
/api/cameras/{id}/ |
none | Retrieve a single Camera. |
Combine with /api/posts/?camera={id}/ to fetch every post taken with a given camera.
GET /api/ returns DRF's auto-generated index:
{
"posts": "https://640by480.com/api/posts/",
"comments": "https://640by480.com/api/comments/",
"cameras": "https://640by480.com/api/cameras/"
}
# 1. Get a token
TOKEN=$(curl -s -X POST https://640by480.com/api/auth/login/ \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"hunter2"}' \
| python -c 'import sys,json; print(json.load(sys.stdin)["token"])')
# 2. Upload a photo
curl -X POST https://640by480.com/api/posts/ \
-H "Authorization: Token $TOKEN" \
-F "image=@cat.jpg" \
-F "description=Mittens at sunset"
curl https://640by480.com/api/posts/?page=1
curl -X POST https://640by480.com/api/posts/42/add_comment/ \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-d '{"text":"Beautiful framing."}'
The API uses DRF's built-in throttle system. When a limit is exceeded, the server returns 429 Too Many Requests with a Retry-After header telling the client when to try again.
| Endpoint | Limit | Scope |
|---|---|---|
POST /api/auth/login/ |
5 / minute | per IP |
POST /api/posts/ (create a post) |
6 / minute | per user |
POST /api/posts/{id}/add_comment/ |
30 / minute | per user |
POST /api/comments/ (create a comment) |
30 / minute | per user |
| Any anonymous endpoint (general) | 60 / minute | per IP |
| Any authenticated endpoint (general) | 120 / minute | per user |
Read endpoints (GET /api/posts/, /api/comments/, /api/cameras/, etc.) only fall under the general anon/user buckets — they're cheap and there's no benefit to rate-limiting them harder.
Z suffix (USE_TZ = True).X-CSRFToken on unsafe methods. Token-auth clients are exempt.digicam/urls.py, the re_path(r'^api/', include('api.urls')) line is registered before the catch-all profile route path('<str:username>/', ...), so every /api/* request resolves to the API. Do not reorder these — putting the catch-all first would cause GET /api/ (the API root) to render the Profile view for a user literally named "api" instead.