Build widgets.
Push live data.
Ship to the frame.
The Tomi Frame Developer API lets you build custom widgets, push real-time data via webhook or HTTP, and control your frame programmatically. Open SDK — anyone can build and publish.
How it works
Your data → on the frame in seconds
Deploy a webhook endpoint
Host any URL that returns a JSON object. Tomi Frame calls it every refresh cycle and renders the value on the e-ink display.
Register your widget
Add a webhook widget in the app, paste your URL, choose a label and grid size. No SDK install needed — just an HTTP endpoint.
Or push directly
On the Developer tier, POST values straight into a plugin widget instead of hosting an endpoint at all. Your value is staged for the next wake cycle — as little as 60 seconds.
Webhook format
Your endpoint returns JSON. That's it.
Tomi Frame polls your URL and renders the response. No SDK, no dependencies — any language, any host.
Response schema
{
"value": "22°C", // primary display value (required)
"label": "Kitchen", // footer label (optional, overrides config)
"secondary": "Humid 62%" // supporting text (optional)
}The value field is rendered large. Keep it short — 6–8 chars max for compact sizes.
Example — Python (Flask)
from flask import Flask, jsonify
import requests
app = Flask(__name__)
@app.route("/widget/weather")
def weather():
data = requests.get(
"https://wttr.in/?format=j1"
).json()
temp = data["current_condition"][0]["temp_C"]
return jsonify({
"value": f"{temp}°C",
"label": "Live Temp",
"secondary": "via wttr.in"
})Example — Node.js
export async function GET() {
const res = await fetch(
"https://api.coincap.io/v2/assets/bitcoin"
);
const { data } = await res.json();
const price = Number(data.priceUsd).toFixed(0);
const change = Number(data.changePercent24Hr).toFixed(2);
return Response.json({
value: `$${Number(price).toLocaleString()}`,
label: "Bitcoin",
secondary: `${change > 0 ? "↑" : "↓"} ${Math.abs(change)}%`
});
}Test with curl
# Add a webhook widget via API
curl -X POST https://api.tomiframe.com/user/widgets \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '{
"type": "webhook",
"col": 0, "row": 0,
"col_span": 2, "row_span": 2,
"config": {
"url": "https://your-server.com/widget",
"label": "My Widget"
}
}'Direct push (Developer only)
Send values instead of hosting an endpoint
Push writes straight into an installed plugin widget, so you never have to expose a public URL. A frame spends most of its life asleep with the radio off, so nothing can reach it unprompted — your value waits in place and is drawn on the next wake cycle. On the Developer tier that is 60 seconds.
ttl_sec is how long your value stays before the frame falls back to polling the plugin's endpoint. Push more often than the TTL, or set a TTL longer than your push interval.
# Push values into an installed plugin widget
curl -X POST https://api.tomiframe.com/plugins/YOUR_PLUGIN_ID/push \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"primary": "42",
"secondary": ["passing"],
"ttl_sec": 300
}'
# Response
{ "plugin_id": "ci-builds", "primary": "42",
"secondary": ["passing"], "expires_in_sec": 300 }API Reference
Endpoints
Base URL: https://api.tomiframe.com · All endpoints require Authorization: Bearer <token>
Device
/device/{id}/framePoll current frame — returns checksum, BMP data, next refresh interval, and next alarm
/device/{id}/configSet refresh interval, update firmware version
Plugins
/pluginsBrowse the store plus your own private plugins, with an installed flag on each
/plugins/{id}/installPlace a plugin on the first free slot that fits — send its config values here
/plugins/{id}/installRemove the plugin and free its cells
/plugins/{id}/pushSend values into an installed plugin widget (Developer only)
/plugins/privateRegister a manifest visible only to your account (Developer only)
/plugins/private/{id}Unregister a private plugin and remove its widgets
Widgets
/user/widgetsList all widgets with type, position, config
/user/widgetsCreate a widget — set type, grid position, col_span, row_span, config
/layoutSave full layout (array of widgets) in one call
Display
/user/display-modeGet current mode: dashboard / minimal_clock / art / photo / slideshow
/user/display-modeSwitch mode, set schedule (e.g. art mode 22:00–07:00)
/user/frame-previewRender and return current frame as PNG for web preview
Alarms
/user/alarmsList alarms with time, days mask, enabled state
/user/alarmsCreate alarm — label, time_hhmm (24h), days (Mon–Sun bitmask)
/user/alarms/{id}Update alarm settings
/user/alarms/{id}Delete alarm
Authentication
User endpoints use a JWT bearer token obtained from /auth/login. Device endpoints use the bearer token returned at registration. Developer API keys grant access to both.
# Get a user JWT
curl -X POST https://api.tomiframe.com/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "••••••"}'
# { "access_token": "eyJ...", "refresh_token": "eyJ..." }Grid system
6 × 4 grid, 800 × 480 px
The canvas is 6 columns × 4 rows. Each cell is 122 × 107 px with 8 px gaps and 14 px outer margin. Widgets span 1–6 columns and 1–4 rows. Standard reference size is 3 × 2 cells (382 × 222 px).
| Widget type | Auth required | Refresh | Config fields |
|---|---|---|---|
clock | None | Every frame | timezone |
weather | None (OWM key) | 5 min | city |
rss | None | 15 min | feed_url |
price | None | 5 min | symbol, asset_type |
countdown | None | Every frame | event_name, date |
text | None | Static | text, font_size |
webhook | None (your URL) | 5 min (1 min Developer) | url, label, json_path |
spotify | Spotify OAuth | 30 sec | — |
calendar | Google OAuth | 1 min | — |
github | GitHub OAuth | 5 min | — |
slack | Slack OAuth | 5 min | — |
home_assistant | HA token | 1 min | url, token, entity_ids |
Ready to build?
One-time unlock. No recurring fees. Full API access forever.