Tomi FrameTomi FrameJoin waitlist
</>Developer API

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

1

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.

2

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.

3

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.

Clock
14:32
Thursday, Aug 21
22°
SUNNY
↓18° ↑25°H 62%
Your Widget
↑ 2.4%
BTC / USD
OpenAI releases o4-mini with 95% lower latency...
3 open PRs2 reviews
Blinding LightsThe Weeknd · After Hours
Tomi Frame

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

json
{
  "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)

python
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

javascript
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

bash
# 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.

bash
# 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

GET
/device/{id}/frame

Poll current frame — returns checksum, BMP data, next refresh interval, and next alarm

POST
/device/{id}/config

Set refresh interval, update firmware version

Plugins

GET
/plugins

Browse the store plus your own private plugins, with an installed flag on each

POST
/plugins/{id}/install

Place a plugin on the first free slot that fits — send its config values here

DELETE
/plugins/{id}/install

Remove the plugin and free its cells

POST
/plugins/{id}/push

Send values into an installed plugin widget (Developer only)

POST
/plugins/private

Register a manifest visible only to your account (Developer only)

DELETE
/plugins/private/{id}

Unregister a private plugin and remove its widgets

Widgets

GET
/user/widgets

List all widgets with type, position, config

POST
/user/widgets

Create a widget — set type, grid position, col_span, row_span, config

PUT
/layout

Save full layout (array of widgets) in one call

Display

GET
/user/display-mode

Get current mode: dashboard / minimal_clock / art / photo / slideshow

POST
/user/display-mode

Switch mode, set schedule (e.g. art mode 22:00–07:00)

GET
/user/frame-preview

Render and return current frame as PNG for web preview

Alarms

GET
/user/alarms

List alarms with time, days mask, enabled state

POST
/user/alarms

Create alarm — label, time_hhmm (24h), days (Mon–Sun bitmask)

PUT
/user/alarms/{id}

Update alarm settings

DELETE
/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.

bash
# 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 typeAuth requiredRefreshConfig fields
clockNoneEvery frametimezone
weatherNone (OWM key)5 mincity
rssNone15 minfeed_url
priceNone5 minsymbol, asset_type
countdownNoneEvery frameevent_name, date
textNoneStatictext, font_size
webhookNone (your URL)5 min (1 min Developer)url, label, json_path
spotifySpotify OAuth30 sec
calendarGoogle OAuth1 min
githubGitHub OAuth5 min
slackSlack OAuth5 min
home_assistantHA token1 minurl, token, entity_ids

Ready to build?

One-time unlock. No recurring fees. Full API access forever.