Appearance
Data API
Push live JSON into a workspace and bind nested fields onto graphics. Designed for low-latency feeds such as esports stats, score tickers, and anything else 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 and 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. Use the Configure Data Sources button in the navbar, or the cog icon next to Refresh Data Sources once you have a spreadsheet-type source.
- 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://app.lumora.live/w/<workspace-id>/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-id> | Your workspace's ID, visible in the address bar right after /w/ whenever you're signed into the control panel. |
<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 workspace ID in the URL. |
| 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://app.lumora.live/w/your-workspace-id/api/v1/data/match-statsPush from Node
js
const url = `https://app.lumora.live/w/${workspaceId}/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://app.lumora.live/w/{workspace_id}/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, and the picker validates the syntax as you type.
- 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, because bindings tolerate temporary shape gaps.
Values a field can't use
Dropdown, colour, team, player, countdown and list fields only accept certain values (see Fields that expect a particular kind of value). JSON fields take any value, and arrays and objects arrive as they are. A resolved value that a field can't use is rejected for that field only: it keeps its last valid value, its data-link icon turns amber, and the Status Panel shows one warning for the source. The push itself still succeeds with 200, every other binding on the same path still updates, and the next valid value clears the warning. Pushing the same rejected value again doesn't repeat the warning.
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
Renaming 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 workspace ID 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.