Открытый API SparkU — способ показать на островке (вырезе экрана) данные, которые
собирает ваш собственный скрипт или программа: прогресс сборки, метрики сервера,
таймер, очередь задач. Скрипт не рисует интерфейс — он только присылает значения в
готовые слоты, которые вы сами настроили заранее.
Типичные сценарии:
CI/деплой — этап пайплайна, ветка, ссылка на сборку, кнопка «Открыть логи».
Таймеры — обратный отсчёт до конца встречи, чайника, бэкапа.
ИИ-агенты спрашивают человека — агент присылает вопрос с вариантами ответа и
ждёт, пока вы нажмёте на островке.
Модель: человек описывает форму, скрипт заполняет её#
Настройки → Свои интеграции скрипт / программа
┌───────────────────────────────┐ │ sparku push deploy progress=0.7
│ «Деплой» адрес: deploy │ │ curl --unix-socket … /v1/…
│ поля: title, progress, stage │ ▼
│ индикатор, строка, карточка │ ~/.sparku/run/api.sock
│ кнопки, приоритет, срок жизни │ ▼
└──────────────┬────────────────┘ значения полей
└──────────────────► элементы островка
Интеграцию — название, адрес (slug), поля и то, как они выглядят на островке, —
создаёт только человек: руками в редакторе, мастером по заготовке или импортом файла
.sparku с явным согласием. Скрипт получает адрес и токен и с этого момента может
только записывать значения в уже существующие поля. Добавить новый элемент,
кнопку или карточку скрипт не может — состав островка неизменен, меняется только
содержимое.
WARNING
Свои интеграции — экспериментальная функция. Она может вести себя непредсказуемо
(карточки и островок иногда отображаются не так, как задумано), а частые обновления
из скриптов повышают расход процессора и батареи. Функция выключена по умолчанию —
включить её нужно осознанно в Настройках, прочитав предупреждение.
API работает через UNIX-сокет ~/.sparku/run/api.sock и существует, только пока
функция включена в настройках. Сетевого порта нет: писать в интеграцию может любая
программа на этом же Mac, запущенная от вашего имени, — то есть то же самое, что
может делать любой ваш скрипт и так. Токен нужен для адресации и защиты от опечаток и
чужих скриптов, а не как граница доступа между процессами одного пользователя.
The SparkU public API is a way to display on the Island (notch/Dynamic Island) data that your own script or program collects: build progress, server metrics, a timer, a task queue. The script doesn't draw the interface — it only sends values to ready-made slots that you configure in advance.
Integration — name, address (slug), fields, and how they look on the Island — is created only by you: by hand in the editor, by wizard, or by importing a .sparku file with explicit consent. The script gets an address and token and from that point can only write values to fields that already exist. It cannot add a new element, button, or card — the Island layout is fixed, only the content changes.
WARNING
Custom Integrations is an experimental feature. It may behave unexpectedly
(cards and the Island sometimes display differently than intended), and frequent updates
from scripts increase CPU and battery usage. The feature is disabled by default —
you must consciously enable it in Settings after reading the warning.
The API works through a UNIX socket at ~/.sparku/run/api.sock and exists only while the feature is enabled in Settings. There is no network port: any program on this same Mac running under your user can write to the integration — that is, the same level of access as any of your scripts already have. The token is for addressing, protection from typos and other scripts, not as an access boundary between processes owned by one user.
Settings > Integrations section > "Custom Integrations" block at the bottom (badge "EXPERIMENTAL") > "Turn On…". A warning appears: the feature is experimental, unexpected behavior and increased CPU and battery usage are possible. The "Turn On" button doesn't respond to Return — you must click it explicitly.
After consent, a "Custom Integrations" section appears in the Settings sidebar and a new-integration wizard opens.
In the same Settings section — "sparku" line with "Install…" button. It creates a symbolic link /usr/local/bin/sparku pointing to the utility inside SparkU.app. If /usr/local/bin is not writable, macOS will ask for admin password once.
The wizard offers five templates: "Task with progress", "Server metrics", "Timer", "Build status", "List". Each already contains fields, an indicator, a line, and a card — just give a name and address (slug) or accept the suggested one. "Custom" creates an empty integration without fields.
Let's use the "Build status" template with address build. It already has fields stage (status), branch (text), started (time), link (link), and an "Open" button on the card.
In the integration editor — "Connection" section: the line export SPARKU_TOKEN_BUILD=… and ready examples for curl, Python, Node, Swift. Copy the export and run it in the terminal where you'll test:
export SPARKU_TOKEN_BUILD=spk_your_token
sparku will find this environment variable by the integration address — you don't need to pass the token every time manually.
The command prints step-by-step: socket exists, SparkU responds, token is found, integration exists, and what fields it has. If something is wrong — doctor tells you at which step.
An integration is what you assemble in Settings: a name, an address (slug), a set of typed fields, and how they appear on the Island (indicator, line, card). The integration structure is static: scripts send values but cannot add a field, button, or element beyond what you configured. One integration gives exactly one indicator and one line on the Island — if you need two lines, they're two separate integrations.
A disabled integration responds with 409 integration_disabled to writes and displays nothing; its configuration and token are preserved.
Each integration has its own token, issued when it's created. It's stored in Keychain and doesn't protect against other processes under your user — they have access to the socket anyway. Its purpose is addressing, protection from typos and other scripts, and selective access revocation by reissuing the token.
Where to get it. Settings > Custom Integrations > your integration > "Connection" section: the line export SPARKU_TOKEN_<ADDRESS>=… and ready code examples.
How to pass it. Header Authorization: Bearer <token> in every request.
Search order in CLI (sparku tries in order until it finds a non-empty value):
--token <token>
--token-file <path>
environment variable SPARKU_TOKEN_<ADDRESS> (address in uppercase, hyphens → _; for example home-server → SPARKU_TOKEN_HOME_SERVER)
variable SPARKU_TOKEN
variable SPARKU_TOKEN_FILE (path to file with token)
SPARKU_TOKEN without an address suffix is convenient when a script writes to one integration; SPARKU_TOKEN_<ADDRESS> — when writing to several at once.
A field is a named, typed slot: a key (progress, cpu), a type (field types), and a label. A field key is lowercase Latin letters, digits, and _, starts with a letter, no longer than 32 characters (it's also an environment variable name SPARKU_FIELD_<KEY> for button shell actions, so hyphens are not allowed). An integration has at most 32 fields.
Writing fields is all or nothing: if any value fails validation, POST …/fields rejects the entire request, and the Island doesn't end up half-updated.
Line template ({key}): substitutes the value in the default display for the field type. {{ and }} are literal braces, no format specifiers. Default display: number — up to two decimal places plus unit (21°C, 120 ms), progress — 43%, state — its label, time — countdown to moment or time since moment (4:59, 1:02:03), list — number of lines, boolean — "yes"/"no". No value — em dash (—).
Built from a template with slots — there's no free grid to keep the visual row unified with built-in integrations.
Template
Model
Slots / fields
Value
Currency
main (number/progress/text), label
Status
agents
main (state), label, "since" (time)
Progress
Pomodoro
main (progress/number), label, countdown (time)
Timer
Pomodoro
main (time), label, ring (progress)
Scales
Resource Monitor
2–4 progress/number fields (with min/max) in a row
Limit
Claude Code
fast window (progress), slow window (progress), label
Metrics
agents
2–4 text/number/progress/state/bool/time fields in columns
List
Calendar, Todo
one list field
Now
Media
title (text, required), label, cover (image), scale (progress)
Graph
—
one number field — drawn from accumulated history
The card has a common header and a row of 0–3 controls. A card can request normal or wide width (like built-in integrations) and a suitable compact variant.
A control is a button, capsule button, toggle (controls a bool field), or menu (picks a state field value). Up to three per card. What the tap does is decided by you in the editor, not the script:
Action
What happens
event
sends an event to listeners GET …/events / sparku listen; without a listener the control is inactive
url
opens a link, file, or app (address — static or {field})
shell
runs a command; field values are passed only through environment variables SPARKU_FIELD_<KEY>, never substituted into the command text
shortcut
launches an app command from the Shortcuts app by name
A listener gets an event for any tap regardless of what action is set above.
The Island is a shared resource: the notch border, attention beacon, and dialog queue work as a stack of requests, not "first wins, rest wait". Your custom integration has three priority levels:
Low
Normal — equal to SparkU's built-in integrations (default)
High — above built-in
With equal priority, the request submitted last wins. Updating an already-submitted request doesn't bump it forward: a module reporting progress once a second doesn't steal the slot from one that requested after it.
Field values live in memory only and don't survive a SparkU restart — integration configuration is always saved. By default, values live until a SparkU restart or explicit sparku end; an integration can set its own TTL, and a specific request can override it with the ?ttl= parameter:
POST /v1/integrations/deploy/fields?ttl=5m
POST /v1/integrations/deploy/fields?ttl=90
ttl is a number of seconds or 5m / 1h30m (same units as time fields), at most 7 days. Empty or missing parameter — TTL from integration settings. Each new write restarts the TTL count.
sparku push supports the same parameter as the --ttl flag (see CLI).
A TTL doesn't help when a script crashes: it wrote "Deploy 40%" and died, and the island keeps showing it until the TTL runs out. Binding solves this differently — values live as long as a process does:
POST /v1/integrations/deploy/fields?bind=process # the process that sent the request
POST /v1/integrations/deploy/fields?bind=4211 # the process with this PID
POST /v1/integrations/deploy/fields?bind=none # remove the binding
When the process exits, the values are cleared at once, as with sparku end. The binding stays for later writes without bind; a new bind replaces it. The TTL still applies too — whichever comes first.
bind=process is for programs that keep running themselves (a Python script, a Node service). curl and sparku exit right after the request, so they need the script's PID: in a shell that's ?bind=$, and sparku push --bind fills it in for you.
POST …/notify (flash + line for a few seconds) can be called at most once per 2 seconds per integration. More frequent calls get 429 too_many_notices with a Retry-After header. The limit is separate from writing values through fields — that's accepted without limits, just displayed no more than once per frame.
For the graph (template "Graph") and trend, SparkU accumulates history for each numeric field: at most one point per second (a write within the same second replaces the last point), 120 points per field, in memory only — history is lost with values when SparkU restarts.
Field types
How fields are coerced from CLI strings and JSON, push examples, and 422 error texts.
POST /v1/integrations/{slug}/fields accepts a JSON object {"key": value}. Each value is coerced to the type you declared in the editor. If any value fails validation — the entire request is rejected (422 invalid_fields) with an error text for each field.
sparku push supports two assignment syntaxes:
field=value — sends a string, the server coerces it to the field type;
field:=json — sends the value as is, parsed as JSON
(items:='[{"title":"a"}]', done:=true, state:=null).
null (via field:=null) clears a field without removing it from the schema.
Relative offset from now: +5m, -1h30m, +90s, +2d. Units are d, h, m, s, can be combined but only in decreasing order (1h30m, not 30m1h). A single time field works as both a timer (+5m — countdown) and a stopwatch (-10m — elapsed time).
ISO 8601 with time and timezone (2026-10-01T12:00:00Z, with fractions of a second too).
UNIX time — only as a number in field:=1699999999, not as a string in field=….
Each element is an object {"title", "subtitle"?, "value"?, "state"?, "icon"?} or just a string (becomes title). Only title is required. At most 50 elements per write.
The 422 invalid_fields response contains fields: [{field, message}], and the combined message is field: message; field2: message2. Examples of real texts:
progress: expected 0…1, got 70
progress: expected number 0…1, got "many"
cpu: expected number, got "abc"
ready: expected true or false, got "maybe"
stage: expected one of: queued, running, passed, failed; got "cancelled"
ends: expected time (ISO 8601, +5m, -1h30m, or UNIX time), got "tomorrow"
cover: file not found: /Users/me/cover.png
cover: only PNG and JPEG are supported
cover: base64 not parsed or image larger than 256 KB
link: expected URL with scheme (https://…), got "example.com"
link: scheme "javascript" is not supported
items: expected array, got object
items: at most 50 rows, got 63
items[0]: no title
title: expected string, got array
Error for an undeclared field:
field not declared in integration "deploy"; available: title, progress, stage
field not declared: integration "deploy" has no fields yet
Both texts appear in the API response and in the integration log in Settings.
sparku CLI
All sparku commands, their flags, token search order, and exit codes.
Settings > Custom Integrations > "sparku" line > "Install…". Creates a symbolic link /usr/local/bin/sparku pointing to the utility inside SparkU.app/Contents/Helpers/sparku; the link updates with the app.
The socket address can be overridden by the SPARKU_SOCKET environment variable or the --socket flag (default ~/.sparku/run/api.sock).
NOTE
SparkU unavailability is checked before token check: sparku push … || [ $? -eq 69 ] in a script reliably catches "SparkU not running", even if the token is also not configured.
without --exec — each event as NDJSON line by line to stdout (good for jq, while read);
with --exec — runs a command on each event; data arrives only through the environment (SPARKU_SLUG, SPARKU_CONTROL, SPARKU_VALUE, SPARKU_EVENT), nothing is substituted into the command text;
--reconnect — reconnect if SparkU closes the connection (e.g., restarts), instead of exiting with error.
The API description in OpenAPI 3.1 (JSON) — for client generators and AI agents.
sparku openapi [address]
Without an address — the general schema of all /v1 routes; it doesn't need SparkU and is also on the site: /openapi.json. With an address — the schema of that integration: the push body lists its fields with types, bounds and state values, so a generated client catches a typo in a field name.
Separate scheme — doesn't overlap with <sysexits.h>, so shell if sparku ask deploy "Deploy?"; then … works:
Code
Meaning
0
answer received (choice or custom text — to stdout)
1
declined
2
timeout, question removed from Island ("Hide"), or SparkU closed
409 dialog_pending (integration already has an unanswered question) is returned as code 69, as are all other unavailability errors.
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.
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.
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.
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.
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.
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.
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:
?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.
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.
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.
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 or 5m /
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 …/glow and sparku end turn the spots off immediately.
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).
fields accepts requests without rate limiting, but updates to the island appear
no more than once per frame — a fast push cycle doesn't flicker intermediate values.
notify — no more than once per 2 seconds per integration (429 if 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 image field 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.
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.
Buttons and Events
Event stream /events, at-most-once delivery, sparku listen in NDJSON and with --exec, other button actions.
A button, capsule, toggle, or menu in a card can send an "event to a listener"
(Field types, control elements). A listener is a client that keeps
GET /v1/integrations/{slug}/events open. As long as the stream is open by at least one
client, elements with "send event" actions are active; without a listener — inactive, nothing to tap.
Delivery is at-most-once, with no queue: an event that happens while no one is listening
is lost forever. Reconnecting the listener does not return missed taps.
event: control
data: {"integration":"deploy","control":"pause","value":true,"at":"2026-09-28T10:15:03Z"}
value only appears for a toggle (bool) or menu (state) — what the element was switched to.
Button and capsule don't have value in the event.
Every 15 seconds the server writes an SSE comment:
: ping
Clients ignore it; the server detects a lost client (sleep, network break) by a write error
for that ping and closes the connection — there's no point keeping a dead listener.
While the stream is open, there is nothing to send into it and no need to: anything the client
sends after the request is ignored by the server.
With --exec, a command runs on each control event; hello is not passed to it.
Event data goes to the handler only through environment variables, not as arguments or in the command text:
Variable
Value
SPARKU_SLUG
integration address
SPARKU_CONTROL
id of the control element
SPARKU_VALUE
toggle/menu value (true/false or state id); empty for button
--reconnect reconnects while SparkU is unreachable or API is disabled, instead of
exiting. Without it, listen exits with code 69 when the connection breaks.
Sending an event to a listener is not the only thing a control element can do.
The exact action is set by the user in the integration editor (not a script):
Open a link, file, or app — {key} in the address is replaced
with the value of that field, percent-encoded; a template from a single URL field ({url})
is taken as-is.
Execute a shell command — the user types the command text in the editor. Field values
are never substituted into the command text, only into the environment —
SPARKU_FIELD_<KEY> (key in uppercase), plus SPARKU_SLUG,
SPARKU_CONTROL, and SPARKU_VALUE for toggle/menu.
Run a Shortcuts command — by name, no shell: shortcuts run <name>.
Listeners get a tap event regardless of which of these actions is configured — it
executes alongside sending the event to the stream.
Questions to User
POST …/dialogs and sparku ask — options, confirmation, timeout, attachment; how an agent asks for permission.
POST /v1/integrations/{slug}/dialogs shows a question on the island and keeps the
connection open until the user answers (or the question is withdrawn). The answer comes in the
body of the same HTTP response — curl or an HTTP client that doesn't close the
connection early needs to wait for timeout from the request body.
An integration can have at most one unanswered question at a time: a second dialogs
before an answer to the first gets 409 dialog_pending. Dialogs must be explicitly
enabled in the integration settings.
file the user can open from the question; must exist
With options or custom — a question with options; otherwise — a confirmation with two
buttons. You can't specify both options/custom and accept/decline — accept/decline apply only to confirmation.
Always 200 OK; the response body depends on the outcome:
Situation
Body
selected option(s)
{"answer": "chose", "options": ["yes"]}
custom answer
{"answer": "custom", "text": "…"}
accepted
{"answer": "accepted"}
declined
{"answer": "declined"} or {"answer": "declined", "comment": "…"}
timeout expired
{"answer": "expired"}
question withdrawn
{"answer": "withdrawn"}
withdrawn is returned if: the client disconnects before answering (though then there's
no one to answer — the question is just removed from the island), SparkU closes, the API is disabled,
or the integration is disabled/deleted while a question is waiting for an answer.
To hide a question with the "Hide" button only works for an integration with a card — you can
bring it back with the "Reply" button in the card; until then it remains unanswered on the server.
An agent like Claude Code or Codex that runs commands in a shell can ask for permission the same
way a CI script does: through sparku ask to an integration the user set up beforehand (for example,
"Agent" with the slug agent; if it has a card, the question can be hidden and answered later). This is not a separate protocol — an agent is just another API client,
like any other process.
if sparku ask agent "Delete branch feature/old and close its PR on GitHub?" \ --accept Delete --decline "Don't touch" --timeout 90s; then git push origin --delete feature/old gh pr close 42else echo "cancelled by user" >&2 exit 1fi
While a question is on the island, the agent blocks on the sparku ask call — this is expected:
the user makes the decision, and the dangerous step can't run until they answer.
If the user doesn't answer before --timeout, default 10 minutes, ask returns 2, and the
agent should treat it as a refusal, not as permission to proceed.
TIP
The tool an agent runs commands with usually has its own timeout (2 minutes by default in
Claude Code). Keep --timeout below it, otherwise the agent stops waiting before the user
can answer, and the question is withdrawn from the island.
An instruction for the agent (in CLAUDE.md, AGENTS.md, etc.) can read:
Before irreversible actions (deleting, force-pushing, deploying, migrations), askthe user: sparku ask agent "<what and why>" --accept Yes --decline No --timeout 90sExit 0 — go ahead, 1 — declined, 2 — no answer: treat it as declined.
.sparku Files and Import
.sparku format, what it doesn't include, POST /v1/imports and sparku import, user consent, risks.
Not included in the file:id, createdAt, isEnabled, and of course the token — an
imported integration always gets a new one. Only the user creates the schema: the file carries
what they've already configured, not a script's right to build the interface itself (see
Concepts).
File size limit is 512 KB: an integration is dozens of fields and templates, not megabytes.
A file saved by a newer SparkU version (version higher than supported) is rejected with a
request to update the app.
A button in the integration editor's header (Settings → Custom Integrations → slug) saves
the file <address>.sparku (name is the integration name, transliterated to address).
"+" → "Import from file…" opens a .sparku from disk. SparkU shows a consent screen: who is
offering the integration, what will appear on the island and in the card, and separately — which
control elements open URLs, run commands, or launch Shortcuts. A taken address (slug) is renamed to an available one —
deploy → deploy-2.
Double-clicking a .sparku file in Finder opens the same screen — SparkU is registered as
the owner of the file type. If Custom Integrations are turned off, SparkU opens the
Integrations section and asks you to turn them on first.
Same endpoint, but initiated by a script or installer: the request body is the contents of
the .sparku file as-is (not a JSON wrapper, the file itself). No token needed — the
integration doesn't exist yet.
Status 201 Created. Refusal — 403 import_declined. While another offer is already waiting
for an answer (from settings or another request), a new POST /v1/imports gets 409 import_pending immediately, without waiting.
If SparkU closes or the API is disabled while a request is waiting — 503 withdrawn.
A file that fails schema validation (as in the editor) — 422 invalid_package.
Importing .sparku is trust in someone else's configuration, not their code: the file itself
is not executed, but control elements in it can be set to run commands on this Mac when
tapped in the card (action "Run command" or "Launch Shortcut"). The import screen shows such
elements explicitly — with the command text or Shortcuts name — before the user agrees.
Read them as carefully as you would a command line from an email.
Control elements with "send event to listener" action are safe in themselves: they only
report a tap to whoever ran sparku listen — they can't execute anything without a separate
listener.
WARNING
Don't run sparku import and don't open .sparku files from sources you don't trust,
even if the import screen showed no risks: URLs in "Open" elements are also worth checking
before you tap the button on the island.
Each recipe shows what integration you need (fields and card template, see
Field types) and working code. Tokens everywhere are read from
SPARKU_TOKEN_<ADDRESS> — see sparku CLI.
sparku doctorsparku doctor deploy # plus check token and schema for a specific integration
Steps through and prints ✓/✗ on each: socket, /v1/health, CLI version against
app version, and with an integration address — the token source and its fields.
Stops on the first problem and returns the corresponding exit code
(sparku CLI).
sparku 2.4.0
✓ socket: /Users/you/.sparku/run/api.sock
✓ SparkU 2.4.0, API v1
✓ token for "deploy": $SPARKU_TOKEN_DEPLOY
✓ integration "Deploy": progress (progress), stage (state), title (text)
"Custom Integrations" is disabled. Settings → Integrations → Custom Integrations →
"Enable…". The feature is experimental and disabled by default (see
Overview).
Any request to a missing socket — code 69 (sparku) or a connection error
(curl: (7) Failed to connect, ConnectionRefusedError in Python). A script can reliably tell
"SparkU not running" from other errors: unavailability is checked first, before token and address.
{"error": {"code": "unauthorized", "message": "token required: Authorization: Bearer <token> header (SparkU Settings → Custom Integrations → Connection)"}}{"error": {"code": "invalid_token", "message": "token doesn't belong to integration \"deploy\" — it may have been reissued"}}
unauthorized — no Authorization header at all. In sparku this means the token was not found in
--token, --token-file, SPARKU_TOKEN_<ADDRESS>, or SPARKU_TOKEN (lookup order —
sparku CLI).
invalid_token — token exists but doesn't match: typo, wrong token, or it was reissued in Settings
(for example after a suspected leak). Get the current token there: slug → Connection.
The error text is readable in the terminal as-is — which field and what's wrong with it. Write is
all or nothing: if any field in the request fails validation, none are applied, even if others
were correct. Check the field's type and range against the schema — GET /v1/integrations/{slug} or
the "Fields" section in the editor. sparku — code 65.
{"error": {"code": "too_many_notices", "message": "notify no more than once per 2s"}}
The response header Retry-After says how long to wait. This is not a data error — the limit
protects against a "strobe" if notify runs in a loop. sparku — code 75; a script can catch
just 75 to tell frequent notify apart from other issues.
SparkU is already offering another import to the user
wait for its answer
All these codes are temporary state, not a request error: sparku returns 69
("SparkU unavailable") for the first two and the same for the rest — see the code table in
HTTP API.
Settings → Custom Integrations → slug → "Log" section — the last 50 requests to
this integration: action (push/end/notify/ask/listen/describe), response code,
error text or user answer, and the client process (python3, curl, etc. by PID on the
socket). This is the first place to look when a script "seems to be sending something" but
nothing changes on the island: the log shows whether a request arrived and what the server
responded, without needing to debug the script.
In the integration editor (the "Test" section), you can send values the same way push does,
clear them, call notify, and ask a test question — all without leaving the terminal. Useful
to separate "card is misconfigured" from "script is sending wrong data": if values from the
console display on the island correctly but values from the script don't, the problem is in
the script (address, token, field names, JSON format), not the integration itself.
Expired values. If GET /v1/integrations/{slug} shows an empty
values but you definitely wrote values — check expires_at from the previous response:
the TTL (integration's valueLifetime or ?ttl=) expired. push is upsert; the next
write will restore them.
CLI and app version mismatch.sparku doctor prints a warning
if sparku in /usr/local/bin links to a different copy of SparkU than the one running.
Refresh the symlink: Settings → Custom Integrations → "Command line".
sparku listen gets no events. While the stream is not open, elements with "send event
to listener" action are inactive on the island itself — a tap doesn't go anywhere, no event
fires at all. Open listen first, then tap the button; see Buttons and Events.