Sections · 6/11
HTTP API
Transport, authorization, and complete list of /v1 routes — request, body, response, error codes.
TIP
A machine-readable description of all routes is at /openapi.json (OpenAPI 3.1).
sparku openapi <address> prints the schema with one integration's fields.
Transport#
SparkU listens to HTTP/1.1 on a UNIX socket ~/.sparku/run/api.sock. The socket exists
only while "Custom Integrations" is enabled in Settings and SparkU is running — if the file
doesn't exist, the feature is disabled or the app is not running (see If It Doesn't Work).
Socket permissions are 0700 for the directory and 0600 for the file: access only to the same
user. There is no TCP port, so requests can only be sent from the same Mac.
curl --unix-socket ~/.sparku/run/api.sock http://sparku/v1/health
The host in the URL (http://sparku/...) does not resolve — the connection still goes through the
socket; the name is only needed for curl to assemble a correct request.
Request and response bodies are JSON (Content-Type: application/json; charset=utf-8).
Body encoding is Content-Length; Transfer-Encoding: chunked is not supported.
Authorization#
All integration routes (/v1/integrations/{slug}/...) require a header:
Authorization: Bearer <token>
The token is issued when an integration is created (Settings → Custom Integrations →
slug → Connection) or when importing .sparku (.sparku Files and Import). Without
the header — 401 unauthorized; with a wrong or revoked token — 401 invalid_token.
GET /v1/health and POST /v1/imports don't require a token: the former has nothing to protect, and
the latter doesn't yet know which integration the request belongs to — the integration doesn't exist yet.
Routes#
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/health |
protocol and app version |
GET |
/v1/integrations/{slug} |
schema of fields and current values |
POST |
/v1/integrations/{slug}/fields |
partial write values (upsert) |
DELETE |
/v1/integrations/{slug}/fields |
clear all values |
POST |
/v1/integrations/{slug}/notify |
flash and text for a few seconds |
POST |
/v1/integrations/{slug}/glow |
light spots at given places of the outline |
DELETE |
/v1/integrations/{slug}/glow |
turn the spots off |
POST |
/v1/integrations/{slug}/dialogs |
question to user — Questions to User |
GET |
/v1/integrations/{slug}/events |
stream of taps (SSE) — Buttons and Events |
POST |
/v1/imports |
import .sparku — .sparku Files and Import |
A method that the route doesn't support responds 405 method_not_allowed with
an Allow header. Unknown path under /v1 — 404 not_found.
GET /v1/health#
No token required. Doesn't check that the integration exists — just "SparkU is here".
curl --unix-socket ~/.sparku/run/api.sock http://sparku/v1/health
{"status": "ok", "api_version": 1, "app_version": "2.4.0"}
GET /v1/integrations/{slug}#
Schema of the integration's fields and current values (if they haven't expired by TTL).
curl --unix-socket ~/.sparku/run/api.sock \
-H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
http://sparku/v1/integrations/deploy
{
"slug": "deploy",
"name": "Deploy",
"enabled": true,
"fields": [
{"key": "progress", "type": "progress", "label": "Progress"},
{"key": "stage", "type": "state", "label": "Stage", "states": ["running", "done", "failed"]},
{"key": "title", "type": "text", "label": "Service", "max_length": 60}
],
"values": {"progress": 0.4, "stage": "running", "title": "api · prod"},
"updated_at": "2026-09-28T10:15:00Z",
"expires_at": "2026-09-28T11:15:00Z"
}
expires_at is absent from the response if values have no TTL. If values don't exist yet or have expired,
values is an empty object, and updated_at/expires_at are absent.
Codes: 200, 404 unknown_integration, 401.
POST /v1/integrations/{slug}/fields#
Partial write values — upsert: listed fields change, others are left alone. null for a field clears it.
Write is all or nothing: if any field fails validation, none are applied.
curl --unix-socket ~/.sparku/run/api.sock \
-H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
-H "Content-Type: application/json" \
-d '{"progress": 0.72, "stage": "running", "title": "api · prod"}' \
http://sparku/v1/integrations/deploy/fields
{"ok": true, "updated": ["progress", "stage", "title"], "cleared": []}
Value types in the body — see Field types. Body {"progress": null}
goes into cleared, not updated.
?ttl= — how long to remember values from this write: number of seconds (?ttl=90)
or relative time (?ttl=5m, ?ttl=1h30m), no more than 7 days. Without the parameter,
the TTL from the integration settings applies (default — "until SparkU restart").
The countdown resets for each write. If TTL is set, the response also contains expires_at:
{"ok": true, "updated": ["progress"], "cleared": [], "expires_at": "2026-09-28T11:20:00Z"}
?bind= — bind values to a process: process (the one that sent the request),
a PID (?bind=$ from a shell) or none (remove the binding). When the process exits,
the values are cleared. A bound response contains bound_pid. See
binding to a process.
Codes: 200, 400 empty_body, 400 invalid_json, 400 invalid_ttl, 400 invalid_bind,
409 integration_disabled, 422 invalid_fields, 401, 404.
422 invalid_fields lists each invalid field:
{"error": {"code": "invalid_fields", "message": "progress: expected 0…1, got 70",
"fields": [{"field": "progress", "message": "progress: expected 0…1, got 70"}]}}
DELETE /v1/integrations/{slug}/fields#
Clears all values for the integration — equivalent to sparku end. Response is 204 No Content, no body.
curl --unix-socket ~/.sparku/run/api.sock -X DELETE \
-H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
http://sparku/v1/integrations/deploy/fields
Codes: 204, 401, 404. Works even for disabled integrations.
POST /v1/integrations/{slug}/notify#
Flash on the bezel and/or switch the text for a few seconds. At least one of text or state is required.
curl --unix-socket ~/.sparku/run/api.sock \
-H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
-d '{"text": "Build failed", "state": "failed", "color": "red", "duration": 4}' \
http://sparku/v1/integrations/deploy/notify
| Field | Type | Rule |
|---|---|---|
text |
string | truncated to 120 characters (with …) |
state |
string | value of any field with type state in the integration |
color |
string | one of semantic names (blue, red, success, …) |
duration |
number | 1…10 seconds, default 4 |
{"ok": true}
No more than once per 2 seconds per integration: a call sooner than that returns 429 too_many_notices with a Retry-After header (seconds until the next call is allowed).
Codes: 200, 400 invalid_json, 409 integration_disabled, 409 notify_disabled
(notify is disabled in the integration settings), 422 invalid_notice, 429 too_many_notices, 401, 404.
POST /v1/integrations/{slug}/glow#
Coordinate glow: light appears at given places of the island outline and stays there until the next frame arrives. Useful for audio visualization, several parallel tasks “by section”, and meters.
curl --unix-socket ~/.sparku/run/api.sock \
-H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
-d '{"spots": [{"position": 0.2, "color": "green"},
{"position": 0.8, "color": "#FF3B30", "intensity": 0.6, "width": 0.15}]}' \
"http://sparku/v1/integrations/deploy/glow?ttl=10"
| Field | Type | Rule |
|---|---|---|
spots |
array | required, at most 64 spots; an empty array means dark |
spots[].position |
number | 0…1 left to right across the island, 0.5 is the middle under the notch |
spots[].color |
string | a semantic name (blue, red, success, …) or #RRGGBB |
spots[].intensity |
number | 0…1, default 1 |
spots[].width |
number | half-width of the spot as a fraction of the island width, 0.005…1, default 0.08 |
intensity |
number | outline thickness and glow strength, 0.2…3, default 1 |
{"ok": true}
- Each request replaces the whole frame. Overlapping spots blend their colors.
- No rate limit: send frames even 60 times a second, the island shows at most ~30.
?ttl=— how long a frame stays lit without the next one: seconds or5m/1h30m, default 5 seconds. If the script dies, the glow fades by itself.- While a flash or status of another widget is on, it takes the shape of your spots: its colors, brightness along the outline from the spots (at least 30%). Between integrations with spots, the integration priority decides.
DELETE …/glowandsparku endturn the spots off immediately.
Codes: 200, 400 invalid_ttl, 409 integration_disabled, 422 invalid_glow, 401,
404.
POST /v1/integrations/{slug}/dialogs#
Question to the user on the island. Details — Questions to User.
GET /v1/integrations/{slug}/events#
Stream of taps in the card (SSE). Details — Buttons and Events.
POST /v1/imports#
Import .sparku. Details — .sparku Files and Import.
Unified error format#
Any error is JSON of the form:
{"error": {"code": "invalid_fields", "message": "human-readable text"}}
message is written for humans and assumes the client is usually a shell script: the text
can be printed to the terminal as-is. Some errors add fields to the error object (for example fields for invalid_fields).
Error codes#
| Code | HTTP | Where |
|---|---|---|
not_found |
404 | path not under /v1 or doesn't exist |
unknown_integration |
404 | no integration with that slug |
unauthorized |
401 | no Authorization: Bearer … header |
invalid_token |
401 | token doesn't belong to this integration |
method_not_allowed |
405 | method not supported on this path |
empty_body |
400 | request body is empty (fields) |
invalid_json |
400 | body is not a JSON object |
invalid_ttl |
400 | can't parse ?ttl= |
invalid_bind |
400 | ?bind= is not process, none or the PID of a running process |
invalid_fields |
422 | value(s) failed type validation |
invalid_notice |
422 | notify: wrong text/state/color/duration |
invalid_glow |
422 | glow: wrong spots, color or intensity |
invalid_dialog |
422 | dialogs: can't parse question body |
invalid_package |
422 | imports: can't parse .sparku file |
integration_disabled |
409 | integration is disabled in Settings |
notify_disabled |
409 | notify is disabled in the integration settings |
dialog_pending |
409 | integration already has an unanswered question |
import_pending |
409 | SparkU is already offering another import to the user |
import_declined |
403 | user declined the offered integration |
too_many_notices |
429 | notify more than once per 2 seconds |
too_many_connections |
503 | more than 64 concurrent connections |
unavailable |
503 | SparkU is not yet ready to receive requests |
withdrawn |
503 | SparkU is closing or API was disabled while waiting |
bad_request |
400 / 411 / 413 / 431 / 505 | HTTP violation: bad headers, no Content-Length, body over 1 MB, headers too large, unsupported version |
NOTE
bad_request is a unified code for HTTP parsing errors; see the message and HTTP status
for the specific reason.
Coalescing and limits#
fieldsaccepts requests without rate limiting, but updates to the island appear no more than once per frame — a fastpushcycle doesn't flicker intermediate values.notify— no more than once per 2 seconds per integration (429if exceeded).glow— no rate limit, the island shows at most ~30 frames per second.- Dialog — no more than one unanswered question per integration (
409 dialog_pending). - Import — no more than one offer per entire app at a time (
409 import_pending). - Connections — no more than 64 concurrent per entire server (
503 too_many_connections). - Request body — no more than 1 MB (limit accounts for the
imagefield in base64). - Number history for chart and trend — no more than one point per second, 120 points per field, memory-only in the SparkU process.
Compatibility of /v1#
Inside /v1, changes are only additive: new routes and new response fields, but
no changes to existing behavior. A script written today will continue to work after
a SparkU update without modification.