API Reference
The Wallie server exposes a REST API over HTTP. All endpoints are available on the port your server is running on (default 9000).
No authentication is required — the API is designed for local network use. Don’t expose it to the internet.
Frame IDs
Section titled “Frame IDs”Each frame gets a short ID assigned automatically by the server the first time it checks in. The desktop app doesn’t display this ID anywhere in the UI — the easiest way to find yours is:
curl -s http://192.168.1.42:9000/admin/api/frames | python3 -m json.toolLook for the "id" field on each frame in the response.
Server status
Section titled “Server status”Quick health check. Returns server version and basic counts.
curl http://192.168.1.42:9000/{ "version": "1.9.1", "running": true, "port": 9000, "sources": 2, "frames": 3, "total_images": 847}GET /health
Section titled “GET /health”Returns 200 OK with {"healthy": true}. Useful for uptime monitors.
GET /admin/api/status
Section titled “GET /admin/api/status”Full server status including image index state and timezone info.
{ "version": "1.9.1", "running": true, "frames_count": 3, "sources_count": 2, "total_images": 847, "server_time": "2026-05-06T14:23:00.000Z", "index": { "is_scanning": false, "initial_scan_complete": true, "image_count": 847 }}Frames
Section titled “Frames”GET /admin/api/frames
Section titled “GET /admin/api/frames”Returns all registered frames with their current status. Use this to find your frame IDs.
curl http://192.168.1.42:9000/admin/api/frames{ "frames": [ { "id": "35506C", "name": "Frame 35506C", "battery_percentage": 72, "usb_power_connected": false, "firmware_version": "2.4.1", "next_check_in": 1746540000, "seconds_until_check_in": 3420, "current_display": { "image_name": "vacation-2024-001.jpg", "served_at": "2026-05-06T10:00:00.000Z" } } ]}| Field | Description |
|---|---|
id | Unique identifier assigned by the server when the frame first checks in. |
battery_percentage | 0–100. Only present after the frame has checked in at least once. |
usb_power_connected | true if the frame is currently charging. |
next_check_in | Unix timestamp of the frame’s next scheduled check-in. |
seconds_until_check_in | Seconds until next check-in (convenience field). |
current_display.image_name | Filename of the photo currently on the display. |
GET /admin/api/frames/{id}
Section titled “GET /admin/api/frames/{id}”Same as above for a single frame.
curl http://192.168.1.42:9000/admin/api/frames/35506CPUT /admin/api/frames/{id}
Section titled “PUT /admin/api/frames/{id}”Update a frame’s settings, including per-source photo order.
curl -X PUT http://192.168.1.42:9000/admin/api/frames/35506C \ -H "Content-Type: application/json" \ -d '{"refresh_minutes": 120}'To change the photo rotation order for one or more sources, include a source_configs array. Each entry must have a sourceId matching an existing source and a sort_mode value:
sort_mode | Behaviour |
|---|---|
lru | Rotate All — shows every photo before repeating. Default. |
random | Shuffle — picks randomly each rotation. |
newest_first | Most recently added photos first. |
oldest_first | Oldest photos first. |
curl -X PUT http://192.168.1.42:9000/admin/api/frames/35506C \ -H "Content-Type: application/json" \ -d '{ "source_configs": [ { "sourceId": "family-photos", "sourceType": "local_folder", "label": "Family Photos", "enabled": true, "weight": 1.0, "timeHint": "anytime", "sort_mode": "random", "config": {} } ] }'To find your sourceId values, check the sources list:
curl -s http://192.168.1.42:9000/admin/api/sources | python3 -m json.toolDELETE /admin/api/frames/{id}
Section titled “DELETE /admin/api/frames/{id}”Remove a frame from the server.
POST /admin/api/frames/{id}/sources
Section titled “POST /admin/api/frames/{id}/sources”Add a photo source to a frame. If the frame already has a source with the same source_id, it is replaced.
curl -X POST http://192.168.1.42:9000/admin/api/frames/35506C/sources \ -H "Content-Type: application/json" \ -d '{"source_id": "family-photos", "sort_mode": "newest_first"}'This endpoint accepts POST only — to see which sources a frame currently has, use GET /admin/api/frames/{id}.
GET /admin/api/frames/{id}/pin
Section titled “GET /admin/api/frames/{id}/pin”POST /admin/api/frames/{id}/pin
Section titled “POST /admin/api/frames/{id}/pin”DELETE /admin/api/frames/{id}/pin
Section titled “DELETE /admin/api/frames/{id}/pin”Get, set, or clear a pinned image for a frame. A pinned image is shown instead of the normal rotation until unpinned.
# pin an image (the path must exist on the server, otherwise 404)curl -X POST http://192.168.1.42:9000/admin/api/frames/35506C/pin \ -H "Content-Type: application/json" \ -d '{"image_path": "/photos/family/vacation-2024-001.jpg"}'
# check the current pincurl http://192.168.1.42:9000/admin/api/frames/35506C/pin# → {"pinned": true, "pinned_image_path": "/photos/family/vacation-2024-001.jpg"}
# unpin — the frame returns to normal rotationcurl -X DELETE http://192.168.1.42:9000/admin/api/frames/35506C/pinThe frame picks up the change at its next scheduled check-in.
Photo sources
Section titled “Photo sources”GET /admin/api/sources
Section titled “GET /admin/api/sources”List all configured photo sources.
{ "sources": [ { "id": "family-photos", "type": "local_folder", "path": "/photos/family", "enabled": true, "image_count": 412 } ]}POST /admin/api/sources
Section titled “POST /admin/api/sources”Add a new source. The server rescans its folders immediately, so the new source’s photos are available as soon as the request returns. Passing an id that already exists replaces that source.
curl -X POST http://192.168.1.42:9000/admin/api/sources \ -H "Content-Type: application/json" \ -d '{"id": "vacation", "type": "local_folder", "path": "/photos/vacation", "enabled": true}'If you omit id, the server generates one — capture it from the response if your script needs to reference the source later.
DELETE /admin/api/sources/{id}
Section titled “DELETE /admin/api/sources/{id}”Remove a source.
Images
Section titled “Images”GET /admin/api/images
Section titled “GET /admin/api/images”List indexed images. Supports filtering:
| Query param | Description |
|---|---|
source_id | Only return images from this source. Omit to list all sources. |
show_alternates | true to include burst-group alternates that are normally hidden behind the best pick. |
The response is not paginated — large libraries return everything in one response.
POST /admin/api/upload
Section titled “POST /admin/api/upload”Upload a photo directly to a source folder.
curl -X POST \ "http://192.168.1.42:9000/admin/api/upload?source_id=family-photos&filename=photo.jpg" \ --data-binary @photo.jpgDELETE /admin/api/delete_image
Section titled “DELETE /admin/api/delete_image”Delete an image file from disk and remove it from the library. Takes the full file path, which must be inside a configured source folder (anything else returns 403).
curl -X DELETE \ "http://192.168.1.42:9000/admin/api/delete_image?path=/photos/family/photo.jpg"GET /admin/api/thumbnail
Section titled “GET /admin/api/thumbnail”Fetch a JPEG thumbnail for an image. Takes the full file path; size is the longest edge in pixels (default 400).
curl "http://192.168.1.42:9000/admin/api/thumbnail?path=/photos/family/photo.jpg&size=200"If the server is busy generating other thumbnails it returns 503 with a Retry-After header — retry after a couple of seconds.
GET /admin/api/logs
Section titled “GET /admin/api/logs”Returns recent server log output. Useful for debugging frame connectivity.
Scripting tips
Section titled “Scripting tips”If you’re generating images with a script (for example an AI pipeline that writes a fresh image for each frame), a few behaviors of the current release are worth knowing:
- Use a new filename for every generation — don’t overwrite a file in place. The server detects “something new to show” by filename, so overwriting
image.pngwith new content won’t trigger a redraw, even if the image is pinned. - Avoid names that differ only by a trailing number (
image_1.png,image_2.png) in the same folder. The duplicate detector treats these as copies of the same photo and hides all but one if two of them land on the same byte size. - Keep images above 100KB. The scanner skips smaller files as probable thumbnails, silently — a folder whose only file is under 100KB reports zero images.
- “Newest first” doesn’t hold the newest image. Rotation always excludes the photo currently on the display, so with two or more files in a folder the frame alternates between the newest and second-newest. For a one-image-per-frame setup, keep exactly one file in the folder (write the new file, then delete the old one) or use the pin endpoint instead.
Examples
Section titled “Examples”Find your frame IDs
Section titled “Find your frame IDs”curl -s http://192.168.1.42:9000/admin/api/frames | \ python3 -c "import json, sysfor f in json.load(sys.stdin)['frames']: print(f\"{f['name']} → id: {f['id']}\")"Check battery on all frames
Section titled “Check battery on all frames”curl -s http://192.168.1.42:9000/admin/api/frames | \ python3 -c "import json, sysfor f in json.load(sys.stdin)['frames']: pct = f.get('battery_percentage', 'unknown') usb = ' (charging)' if f.get('usb_power_connected') else '' print(f\"{f['id']}: {pct}%{usb}\")"Alert when a frame drops below 20%
Section titled “Alert when a frame drops below 20%”#!/bin/bashcurl -s http://192.168.1.42:9000/admin/api/frames | python3 -c "import json, sysfor f in json.load(sys.stdin)['frames']: pct = f.get('battery_percentage') if pct is not None and pct < 20: print(f\"LOW BATTERY: {f['id']} is at {pct}%\")"Run this from a cron job to get notified before a frame goes dark.
Timezone
Section titled “Timezone”GET /admin/api/timezone
Section titled “GET /admin/api/timezone”POST /admin/api/timezone
Section titled “POST /admin/api/timezone”Get or set the timezone used for wake-hour calculations. The desktop app sets this automatically; you only need this if scripting directly against the server.
curl -X POST http://192.168.1.42:9000/admin/api/timezone \ -H "Content-Type: application/json" \ -d '{"offset_seconds": -25200, "name": "America/Los_Angeles"}'