Appearance
Data API
Push live JSON into a workspace and bind nested fields onto graphics. Designed for low-latency feeds — esports stats, score tickers, anything your own system can shape and emit. Sub-1-second from POST to graphic re-render on CG-SDK and OGraf graphics.
Polling-based sources
If your data lives in a Google Sheet, a CSV/JSON URL, or an Excel file, the poll-based data sources are the simpler fit. Use the Data API when your system pushes the data and update latency matters.
When to use
| Scenario | Use |
|---|---|
| Operator updates a Google Sheet manually during the show | Google Sheets |
| Stats provider exposes a CSV / JSON URL we can poll on a schedule | CSV / JSON URL |
| Operator updates an Excel file on their machine | Excel |
| Your system has live JSON it can push to us | Data API |
| Your data updates several times per second | Data API |
The Data API inverts the usual flow — Lumora is a sink, your external system drives writes. No polling cadence to tune; updates land as fast as you push them.
Authentication
Push requests use a workspace Server API key on the standard Authorization header:
http
Authorization: Bearer lumora_live_sk_<43 chars>The Data API doesn't introduce a new key type. Mint a key once from Settings → API Keys and reuse it across server-side triggers, refreshes, and pushes. The same scope rules apply: a key only works against the workspace it was minted in.
JWTs are rejected
This endpoint is API-key-only. A signed-in browser session (Supabase JWT) gets a 401. The control panel uses JWTs to manage data sources, but pushes must come from a server with a long-lived API key.
Setting up a data source
- In the control panel, open the Data Sources modal (cog icon next to Refresh Data Sources).
- Click + Data API.
- Fill in:
- Name — display label, free-form.
- Slug — appears in the push URL. Lowercase letters, numbers, dashes; 1–50 characters; no leading or trailing dash.
- Sample payload (optional) — paste an example of the JSON your system will send. The binding picker uses it to render a clickable tree, so you can build bindings before the first real push lands. Persists with the source.
- Save. The modal shows the push URL and a copy button.
Renaming the slug later is safe — bindings reference the source by id internally.
Endpoint
http
POST https://<workspace>.app.lumora.live/api/v1/data/<slug>| Method | POST only. No GET, PUT, or DELETE. |
| Auth | Authorization: Bearer lumora_live_sk_<43 chars> |
| Content-Type | Must include application/json |
| Body | Any JSON value (object, array, primitive). 1 MB max. |
Parameters
| Name | Where to find it |
|---|---|
<workspace> | Your workspace's subdomain — the part before .app.lumora.live in the URL when you're signed in. |
<slug> | The slug you chose when creating the data source. Visible in the Data Sources modal. |
Responses
Success
json
{ "ok": true }Error
json
{ "error": "Invalid JSON" }Status codes
| Code | Meaning |
|---|---|
| 200 | Push accepted. The cached payload is updated and bound graphics re-render. |
| 400 | Invalid JSON, payload over 1 MB, or non-application/json Content-Type. |
| 401 | Missing/invalid API key, or this endpoint was called with a session JWT instead of an API key. |
| 403 | The API key's workspace doesn't match the URL's workspace. |
| 404 | No data source with this slug in this workspace. |
| 429 | Burst or daily rate limit exceeded. The previous cached value continues to serve graphics. |
A 404 after a slug rename or deletion is expected — bindings still resolve internally by id. Update your push target to the new slug.
Examples
Push a simple payload
bash
curl -X POST \
-H "Authorization: Bearer lumora_live_sk_<your key>" \
-H "Content-Type: application/json" \
-d '{"match": {"score": {"home": 1, "away": 0}, "minute": 32}}' \
https://your-workspace.app.lumora.live/api/v1/data/match-statsPush from Node
js
const url = `https://${workspace}.app.lumora.live/api/v1/data/${slug}`
await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LUMORA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ match: { score: { home: 1, away: 0 }, minute: 32 } }),
})Push from Python
python
import os, requests
url = f"https://{workspace}.app.lumora.live/api/v1/data/{slug}"
requests.post(
url,
headers={
"Authorization": f"Bearer {os.environ['LUMORA_API_KEY']}",
"Content-Type": "application/json",
},
json={"match": {"score": {"home": 1, "away": 0}, "minute": 32}},
)Binding graphics to the payload
Once a push has landed (or you've pasted a sample payload), you can bind graphic fields to nested values.
- In the graphic editor, click Data Bindings.
- For a bindable element, click Link.
- In the Data Source dropdown, pick your Data API source.
- The latest payload renders as a collapsible JSON tree. Click any leaf to insert its path. The path also accepts free-text editing — the picker live-validates the syntax.
- Link to this Path to confirm.
The bound graphic field updates whenever a new payload arrives.
Path syntax
Dot-separated keys with bracket array indexing:
text
match.score
teams[0].players[2].name
events[5].timestamp
[0].name # payload root is itself an array
grid[3][7] # consecutive brackets
event-type # hyphens permitted in keys
player_name.first_name # underscores permittedAllowed in keys: [A-Za-z_][A-Za-z0-9_-]*. Indices: non-negative integers. No wildcards, no filters, no recursive descent — preprocess on your side if you need aggregates.
A path that doesn't resolve in the current payload renders as the graphic's SDK default (or blank if no default was declared). There's no error — bindings tolerate temporary shape gaps.
Behaviour
- Latest value only. Each push overwrites the previous payload. There's no history or queue.
- Persistence. The latest payload is persisted to the database every 10 seconds when it has changed. Quiet sources don't write. The cache survives a server restart.
- No diff suppression. Every push triggers a graphic re-render and a socket fan-out, even if the payload is identical to the last one.
- Lottie graphics. Live updates apply on the next take-on (lockstep mode), the same as other data sources. Sub-1-second latency holds for CG-SDK and OGraf only.
- Over-quota behaviour. A
429rejects the write — the previous cached payload keeps serving graphics. Pushes resume cleanly when the window resets.
Rate limits
The Data API shares the workspace's existing API rate limit budget — see Server API rate limits. Both the per-minute burst and the rolling-24h daily ceiling apply on every push. Whichever runs out first triggers a 429 Too Many Requests with a Retry-After header.
For high-frequency feeds (e.g. live esports stats arriving multiple times per second), pick a plan that comfortably covers your push rate.
Deleting a source
Deleting a data source removes it permanently. The confirmation dialog tells you how many graphic fields and graphics will be unlinked first. On confirm:
- the binding entry is removed from each affected graphic
- the resolved value is cleared from the graphic's data so the SDK default re-asserts
- on-air graphics revert immediately
- the cached payload and database row are deleted
A renaming flow doesn't unlink anything — only deletion does.
Troubleshooting
401 Unauthorized. The Authorization header is missing, malformed, or you're sending a Supabase session JWT instead of a server API key. Mint an API key from Settings → API Keys and check for stray whitespace before/after the key.
403 Forbidden. The API key's workspace doesn't match the subdomain in the URL. Check both. Don't reuse a key from one workspace against another's URL.
404 Not Found. Slug typo, the source has been deleted, or the source belongs to a different workspace. Verify in the Data Sources modal.
400 Invalid JSON. The body didn't parse. If you're shell-quoting in Bash, watch for nested-quote escaping; use a heredoc or a payload file with --data-binary @file.json.
400 Payload too large. Bodies over 1 MB are rejected. Trim the payload, or split into multiple sources if your shape needs more.
400 Content-Type must be application/json. Set the header explicitly. Some HTTP libraries default to text/plain or application/x-www-form-urlencoded; override.
Graphic field stays blank after a push. The path probably doesn't resolve in the current payload. Open the binding in the picker — the tree shows the live payload, so you can verify the shape matches what you expected.
Field flickers on Lottie graphics. Expected. Lottie graphics apply data updates on next take-on, not live, to avoid mid-animation reloads. Use CG-SDK or OGraf if you need live updates on-air.
429 Too Many Requests. You've exhausted the workspace's per-minute or per-day API quota. The cached payload continues to serve; the next-allowed retry time is in the Retry-After header. Slow your push rate or upgrade your plan.