SparkUAPIExperimentalllms.txtРусский

Sections · 1/11

Overview

What is the SparkU public API and why scripts output data to the Island.

Markdown ↗

What it is#

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.

Typical scenarios:

  • CI/deploy — pipeline stage, branch, build link, "Open logs" button.
  • Builds — percentage, stage, time remaining.
  • Server monitoring — CPU, memory, latency, status "working / slow / down".
  • Timers — countdown to end of meeting, kettle, backup.
  • AI agents ask humans — agent sends a question with answer options and waits for you to tap on the Island.

Model: You describe the form, scripts fill it in#

 Settings → Custom Integrations             script / program
 ┌───────────────────────────────┐           │ sparku push deploy progress=0.7
 │ "Deploy"  address: deploy     │           │ curl --unix-socket … /v1/…
 │ fields:  title, progress, stage           ▼
 │ indicator, line, card          │      ~/.sparku/run/api.sock
 │ buttons, priority, ttl         │           ▼
 └──────────────┬────────────────┘     field values
                └──────────────────►  Island elements

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.

Local only, no network#

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.

Next steps#

  • Quick start — enable the feature, install the CLI, send your first value.
  • Concepts — integration, slug, token, fields, Island elements and cards, priority, value TTL.
  • Field types — what and how to send in push, error texts.
  • sparku CLI — all commands, flags, exit codes.

Machine-readable versions: /llms.txt (index), /llms-full.txt (all pages in one Markdown file), /docs/api/<page>.md.

Overview

What is the SparkU public API and why scripts output data to the Island.

What it is#

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.

Typical scenarios:

Model: You describe the form, scripts fill it in#

 Settings → Custom Integrations             script / program
 ┌───────────────────────────────┐           │ sparku push deploy progress=0.7
 │ "Deploy"  address: deploy     │           │ curl --unix-socket … /v1/…
 │ fields:  title, progress, stage           ▼
 │ indicator, line, card          │      ~/.sparku/run/api.sock
 │ buttons, priority, ttl         │           ▼
 └──────────────┬────────────────┘     field values
                └──────────────────►  Island elements

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.

Local only, no network#

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.

Next steps#

Quick start

Enable the feature, install sparku, and send your first value to the Island.

1. Enable the feature#

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.

2. Install the CLI#

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.

Check that the command is available:

sparku version

3. Create an integration with the wizard#

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.

4. Copy the token#

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.

5. First push#

sparku push build stage=running branch=main started=+0s

The indicator and line of the card on the Island should update. Expand the Island — you'll see the "Build status" card with an "Open" button.

6. Check that everything works#

sparku doctor build

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.

7. Clear values#

sparku end build

Clears all values for the integration — the Island will have no data for it (elements with visibility "While there is data" disappear).

The same thing via curl#

Without the CLI, directly through the UNIX socket:

curl --unix-socket ~/.sparku/run/api.sock \
  -H "Authorization: Bearer $SPARKU_TOKEN_BUILD" \
  -d '{"stage": "running", "branch": "main", "started": "+0s"}' \
  http://sparku/v1/integrations/build/fields

Clear values:

curl --unix-socket ~/.sparku/run/api.sock -X DELETE \
  -H "Authorization: Bearer $SPARKU_TOKEN_BUILD" \
  http://sparku/v1/integrations/build/fields

Next steps#

Concepts

Integration, slug, token, fields, Island, card, priority, and value TTL.

Integration#

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.

Slug (address)#

A slug is how a script finds an integration: /v1/integrations/{slug}/…, sparku push {slug} …. Rules:

Valid slug examples: deploy, build, server, home-server. On .sparku import, if the address matches, SparkU picks a free one: deploy → deploy-2.

Token#

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):

  1. --token <token>
  2. --token-file <path>
  3. environment variable SPARKU_TOKEN_<ADDRESS> (address in uppercase, hyphens → _; for example home-server → SPARKU_TOKEN_HOME_SERVER)
  4. variable SPARKU_TOKEN
  5. 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.

Fields#

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.

Island: indicator, line, card#

Indicator (collapsed Island, 28 pt slot)#

One per integration. Styles:

Style Shows Source
Icon static SF Symbol —
Ring progress ring with icon or number inside field progress or number with min/max
Value short string up to 4 chars text, number, progress, state, bool
Equalizer animation like "Media" field bool enables motion
Spinner rotating indicator field bool enables rotation

Field state can be assigned separately (stateField) — its value replaces the icon, color, and animation of the indicator without changing its type.

Card line (one per integration)#

Style Shows Source
Text template {title} · {progress} any fields via {key}
Timer live countdown field time
Scale label and mini bar field progress or number with min/max
Avatars stack of circles with initials, "+N" above three field list

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 (—).

Card (expanded Island)#

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.

Controls and their actions#

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.

Priority#

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:

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.

Value TTL (time-to-live)#

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).

Binding to a process#

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.

Notify rate limit#

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.

Number history#

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.

Coercion#

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:

null (via field:=null) clears a field without removing it from the schema.

Type table#

Type Value in push Coercion from CLI string Note
text "prod" taken as-is truncated to length limit (default 120 chars), not rejected
number 72 Double(string) unit, min/max for scales and rings; accumulated for graph history
progress 0.7 number 0…1, or "70%" strictly 0…1 after coercion
bool true true/false/yes/no/on/off/1/0 (case-insensitive) for toggles and equalizer
state "failed" string, must be in the field's state list each state has icon, color, animation, flash on transition
time ISO 8601 or +5m +5m, -1h30m, ISO 8601 timers tick inside SparkU — no need to push every second
image path or base64 (≤ 256 KB) path /… or ~/…, or base64 (can have data:...,) PNG/JPEG only
url "https://…" string with scheme for button actions; javascript: and data: rejected
list [{"title": "…"}] only field:=json up to 50 rows

Push examples for each type#

sparku push deploy title="Production deploy"
sparku push server cpu=37.5
sparku push build progress=70%          # equivalent to progress=0.7
sparku push server ready=true
sparku push build stage=failed          # value must be declared in the field
sparku push timer ends=+5m              # 5 minutes from now
sparku push timer ends=2026-10-01T12:00:00Z   # ISO 8601
sparku push now cover=~/cover.png
sparku push deploy link=https://ci.example.com/builds/482
sparku push queue items:='[{"title": "Build"}, {"title": "Tests", "state": "running"}]'

Time format#

One of the following is accepted:

list — row objects#

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.

sparku push queue items:='[
  "Simple string",
  {"title": "Build", "subtitle": "main", "state": "running"},
  {"title": "Tests", "value": "42/50", "icon": "checkmark"}
]'

image#

Error texts for 422 (from code)#

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.

Installation#

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.

sparku version
sparku help

Token#

The token is searched in order until a non-empty value is found:

  1. --token <token>
  2. --token-file <path>
  3. SPARKU_TOKEN_<ADDRESS> (address uppercase, hyphens → _)
  4. SPARKU_TOKEN
  5. SPARKU_TOKEN_FILE (path to file with token)

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.

push#

Writes fields (upsert — partial update, old values of undeclared keys are left alone).

sparku push <address> field=value … [field:=json …] [--ttl 5m] [--bind]
sparku push deploy progress=0.4 stage=build
sparku push deploy progress=70% --ttl 10m
sparku push deploy stage=build --bind
sparku push queue items:='[{"title": "Build"}]'

end#

Clears all values for an integration (like TTL expiration, but immediate).

sparku end <address>
sparku end deploy

notify#

Flash indicator and line for a few seconds (at most once per 2 seconds per integration).

sparku notify <address> ["text"] [--state value] [--color red] [--duration 4]

At least text or --state is required.

sparku notify deploy "Build failed" --state failed
sparku notify server "CPU 95%" --color red --duration 6

glow#

Coordinate glow: spots of light at given places of the island outline. Each call replaces the whole frame.

sparku glow <address> position:color[:intensity[:width]] … [--intensity 1.2] [--ttl 5]
sparku glow <address> --off
sparku glow deploy 0.2:green 0.5:orange:0.6 0.8:#ff3b30
sparku glow build 0.5:blue:1:0.5 --ttl 1h      # a wide band for an hour
sparku glow deploy --off

For visualizations with dozens of frames per second, send POST …/glow directly over the socket — see HTTP API.

ask#

Ask a question on the Island. Waits for answer (connection stays open until answered or --timeout expires).

sparku ask <address> "question" [-o id:label …] [--multi] [--custom]
           [--accept Yes --decline No --comment] [--title …]
           [--timeout 10m] [--attach file]

Answer is printed to stdout: selected options — id one per line, custom answer — as text, decline reason (--comment) — as text too.

if sparku ask deploy "Deploy?"; then ./deploy.sh; fi
env=$(sparku ask deploy "Where?" -o prod:Prod -o stage:Staging)
sparku ask deploy "Delete branch?" --accept Delete --decline Keep --comment

An integration can have at most one unanswered question at a time — a second ask gets 409 dialog_pending.

listen#

Prints taps on card controls.

sparku listen <address> [--exec "command"] [--reconnect]
sparku listen deploy --exec './on-click.sh'   # $SPARKU_CONTROL, $SPARKU_VALUE
sparku listen deploy --reconnect | jq .

import#

Offers an integration from a .sparku file to you and waits for consent in SparkU Settings.

sparku import <file.sparku>

On consent, prints to stdout export SPARKU_TOKEN_<ADDRESS>=…, so an installer can do:

eval "$(sparku import deploy.sparku)"

User decline — exit code 77.

doctor#

Checks the environment step by step: socket, SparkU version, (if address is given) token, and integration existence.

sparku doctor [address]
sparku doctor
sparku doctor deploy

openapi#

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.

sparku openapi deploy > deploy.openapi.json

version / help#

sparku version
sparku help

Exit codes#

push, end, doctor, notify, listen, import#

Codes from <sysexits.h> to not overlap with ask codes:

Code Meaning When
0 success
64 invalid arguments (EX_USAGE) flag typo, missing required argument
65 values failed validation (EX_DATAERR) HTTP 400, 411, 413, 422
67 integration with this address not found (EX_NOUSER) HTTP 404
69 SparkU unavailable or API disabled (EX_UNAVAILABLE) no socket, HTTP 409, 503
70 SparkU internal error (EX_SOFTWARE) HTTP 5xx
75 too frequent, retry later (EX_TEMPFAIL) HTTP 429 (notify more than once per 2s)
76 unexpected response (EX_PROTOCOL) CLI and app versions don't match
77 no token or wrong token (EX_NOPERM) HTTP 401, 403; sparku import user decline

69 is checked first: a script can reliably distinguish "SparkU not running" from any other error, even if the token is also not configured.

ask#

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.

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}

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#

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.

Buttons and Events

Event stream /events, at-most-once delivery, sparku listen in NDJSON and with --exec, other button actions.

What is a listener#

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.

Stream GET …/events#

curl --unix-socket ~/.sparku/run/api.sock \
  -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
  http://sparku/v1/integrations/deploy/events

Response is text/event-stream; the connection stays open until one side closes it.

The first event is hello with a list of control elements in the card:

event: hello
data: {"integration":"deploy","controls":[{"id":"pause","kind":"toggle"},{"id":"restart","kind":"button"}]}

Then control on each tap:

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.

sparku listen#

sparku listen <slug> [--exec "command"] [--reconnect]

Without --exec, each event is printed to stdout as a JSON line (NDJSON) — handy for jq and while read:

sparku listen deploy | while read -r line; do
  control=$(echo "$line" | jq -r .control)
  value=$(echo "$line" | jq -r .value)
  echo "tapped $control = $value"
done

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
SPARKU_EVENT entire event as received over SSE (JSON string)
sparku listen deploy --exec './on-click.sh'
#!/bin/bash
# on-click.sh
case "$SPARKU_CONTROL" in
  pause)   echo "pause: $SPARKU_VALUE" ;;
  restart) ./restart-deploy.sh ;;
esac

--reconnect reconnects while SparkU is unreachable or API is disabled, instead of exiting. Without it, listen exits with code 69 when the connection breaks.

sparku listen deploy --reconnect --exec './on-click.sh' &

NOTE

sparku listen requires a token just like push: --token, --token-file or SPARKU_TOKEN_<ADDRESS> / SPARKU_TOKEN.

Example in Python (no dependencies)#

Reads SSE straight from the UNIX socket via http.client:

import http.client, json, os, socket

class SparkU(http.client.HTTPConnection):
    def __init__(self):
        super().__init__("sparku")
    def connect(self):
        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.sock.connect(os.path.expanduser("~/.sparku/run/api.sock"))

token = os.environ["SPARKU_TOKEN_DEPLOY"]
api = SparkU()
api.request("GET", "/v1/integrations/deploy/events", headers={
    "Authorization": f"Bearer {token}",
})
response = api.getresponse()

buffer = ""
for chunk in iter(lambda: response.read(1), b""):
    buffer += chunk.decode("utf-8")
    if buffer.endswith("\n\n"):
        for line in buffer.splitlines():
            if line.startswith("data: "):
                event = json.loads(line[len("data: "):])
                if "control" in event:
                    print("tapped", event["control"], event.get("value"))
        buffer = ""

Example in Node#

const http = require("node:http");
const os = require("node:os");

const request = http.request({
  socketPath: `${os.homedir()}/.sparku/run/api.sock`,
  path: "/v1/integrations/deploy/events",
  headers: { Authorization: `Bearer ${process.env.SPARKU_TOKEN_DEPLOY}` },
});

request.on("response", (response) => {
  let buffer = "";
  response.on("data", (chunk) => {
    buffer += chunk.toString("utf8");
    const frames = buffer.split("\n\n");
    buffer = frames.pop();
    for (const frame of frames) {
      const line = frame.split("\n").find((l) => l.startsWith("data: "));
      if (!line) continue;
      const event = JSON.parse(line.slice("data: ".length));
      if (event.control) console.log("tapped", event.control, event.value);
    }
  });
});
request.end();

Other button actions#

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):

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.

How it works#

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.

Request body#

A question with options — has options or custom:

{
  "message": "Deploy to prod?",
  "title": "Deploy",
  "options": ["yes", {"id": "no", "label": "Cancel", "detail": "do nothing"}],
  "multiple": false,
  "custom": false,
  "timeout": "10m"
}

Confirmation — no options or custom:

{"message": "Delete branch feature/x?", "accept": "Delete", "decline": "Keep", "comment": true}
Field Type Default Rule
message string — required, up to 600 characters
title string integration address up to 60 characters
options array of strings or {id, label, detail?} — up to 9 options; string is both id and label
multiple bool false select multiple — requires ≥ 2 options
custom bool false add free-text answer field
accept / decline string "Yes" / "No" confirmation button labels, up to 30 characters
comment bool false allow comment on decline
timeout number of seconds or "90s" / "10m" / "2h" 600 (10 min) 5 seconds to 1 day
attachment absolute path — 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.

Answer#

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.

Example: curl#

curl --unix-socket ~/.sparku/run/api.sock \
  -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
  -d '{"message": "Deploy to prod?", "accept": "Yes", "decline": "No"}' \
  http://sparku/v1/integrations/deploy/dialogs
{"answer": "accepted"}

sparku ask#

sparku ask <slug> "question" [-o id:label …] [--multi] [--custom]
           [--accept Yes --decline No --comment] [--title …] [--timeout 10m] [--attach file]
sparku ask deploy "Deploy to prod?" -o yes:Yes -o no:No
sparku ask deploy "Where to deploy?" -o prod:Prod -o staging:Staging --custom
sparku ask deploy "Delete branch?" --accept Delete --decline Keep --comment

Selected options are printed to stdout one per line (id, not label), custom answer as text, and the decline comment also to stdout.

Exit codes for ask#

Separate from <sysexits.h> used by push/end/doctor — so if sparku ask deploy "Deploy?"; then … works:

Code Meaning
0 answer received (choice or custom text — in stdout)
1 declined
2 timeout expired, question withdrawn, or "Hide", or SparkU closed

Transport errors (no socket, invalid token, 409 dialog_pending) return regular sparku codes — 69, 77, 69 respectively, see sparku CLI.

if sparku ask deploy "Deploy to prod?"; then
  ./deploy.sh
else
  echo "deploy cancelled"
fi

For AI agents#

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 42
else
  echo "cancelled by user" >&2
  exit 1
fi

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), ask
the user: sparku ask agent "<what and why>" --accept Yes --decline No --timeout 90s
Exit 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.

.sparku format#

An integration export is its schema without secrets: name, address, fields, island and card styling, control elements. The file is plain JSON:

{
  "format": "sparku.integration",
  "version": 1,
  "integration": {
    "slug": "deploy",
    "name": "Deploy",
    "icon": "shippingbox.fill",
    "accent": "blue",
    "fields": [
      {"key": "progress", "type": "progress", "label": "Progress"},
      {"key": "stage", "type": "state", "label": "Stage", "states": [
        {"id": "running", "label": "Running", "icon": "arrow.triangle.2.circlepath", "color": "blue"},
        {"id": "done", "label": "Done", "icon": "checkmark.circle.fill", "color": "success"},
        {"id": "failed", "label": "Failed", "icon": "xmark.octagon.fill", "color": "error"}
      ]},
      {"key": "title", "type": "text", "label": "Service", "maxLength": 60}
    ],
    "valueLifetime": null,
    "island": {"...": "indicator and text settings"},
    "card": {"...": "template, slots, control elements"}
  }
}

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.

Export#

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 in Settings#

"+" → "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.

POST /v1/imports#

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.

curl --unix-socket ~/.sparku/run/api.sock \
  --data-binary @deploy.sparku \
  http://sparku/v1/imports

The request blocks until the user opens Settings and answers the offer — SparkU opens that section itself. Consent:

{"slug": "deploy", "name": "Deploy", "token": "spk_9f2c...a31b"}

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.

sparku import#

sparku import deploy.sparku

Waits for consent just like a direct POST /v1/imports, and prints to stdout the token export line:

export SPARKU_TOKEN_DEPLOY=spk_9f2c...a31b

So an installer script does:

eval "$(sparku import deploy.sparku)"
sparku push deploy progress=0

User refusal — exit code 77 (permission denied, like invalid token); in all other cases codes are regular sparku, see sparku CLI.

Risks#

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.

Response codes#

Code HTTP When
— 201 user agreed, integration added
import_declined 403 user refused the offer
import_pending 409 SparkU is already waiting for an answer to another offer
invalid_package 422 file is not in .sparku format or fails schema validation
withdrawn 503 SparkU closed or API was disabled while waiting

Recipes

Ready-made examples — deploy progress, Xcode build, server monitoring, timer, task queue, agent confirmation.

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.

1. Deploy progress in CI with flash on error#

Integration: deploy, card template "Progress". Fields: progress (progress), stage (state: running/done/failed), title (text).

#!/bin/bash
# deploy.sh — run in CI
set -euo pipefail

sparku push deploy title="api · prod" stage=running progress=0

total_steps=5
for step in $(seq 1 $total_steps); do
  ./run-step.sh "$step" || {
    sparku push deploy stage=failed
    sparku notify deploy "Deploy failed at step $step" --state failed --color red
    exit 1
  }
  sparku push deploy progress="$(( step * 100 / total_steps ))%"
done

sparku push deploy stage=done progress=100%
sparku notify deploy "Deploy ready" --state done

2. Xcode build / swift build#

Integration: build, template "Status". Fields: phase (state: building/testing/passed/failed), title (text).

#!/bin/bash
# build.sh
sparku push build title="SparkU" phase=building

if ! xcodebuild -scheme SparkU -destination 'platform=macOS' build 2>build.log; then
  sparku push build phase=failed
  sparku notify build "Build failed" --state failed --color red
  exit 1
fi

sparku push build phase=testing
if ! xcodebuild -scheme SparkU -destination 'platform=macOS' test 2>test.log; then
  sparku push build phase=failed
  sparku notify build "Tests failed" --state failed --color red
  exit 1
fi

sparku push build phase=passed
sparku notify build "Build and tests green" --state passed

3. Server monitoring on Python every N seconds#

Integration: server, template "Meters". Fields: cpu (number, 0–100, %), memory (number, 0–100, %), latency (number, ms).

#!/usr/bin/env python3
import http.client, json, os, socket, time

TOKEN = os.environ["SPARKU_TOKEN_SERVER"]

class SparkU(http.client.HTTPConnection):
    def __init__(self):
        super().__init__("sparku")
    def connect(self):
        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.sock.connect(os.path.expanduser("~/.sparku/run/api.sock"))

def push(fields):
    api = SparkU()
    api.request("POST", "/v1/integrations/server/fields", json.dumps(fields), {
        "Authorization": f"Bearer {TOKEN}",
        "Content-Type": "application/json",
    })
    response = api.getresponse()
    response.read()

def read_metrics():
    # replace with real metric collection (psutil, /proc, ssh to prod, etc.)
    import random
    return {"cpu": random.uniform(10, 90), "memory": random.uniform(30, 80), "latency": random.uniform(40, 300)}

while True:
    push(read_metrics())
    time.sleep(5)

Run in background: nohup python3 monitor.py & or via launchd/systemd — the socket survives script restarts; the next push will just restore values.

4. Timer / countdown with one push#

Integration: timer, template "Timer". Field: until (time).

The timer ticks inside SparkU — no need to repeat push every second, just one call with the end time:

# 25-minute countdown (Pomodoro)
sparku push timer until=+25m

# specific time in ISO 8601
sparku push timer until=2026-09-28T15:00:00Z

Cancel the countdown — sparku end timer.

5. Task queue (list)#

Integration: queue, template "List". Field: tasks (list).

#!/bin/bash
# queue.sh — update build task list

tasks_json() {
  cat <<'JSON'
[
  {"title": "iOS build", "subtitle": "step 3 of 5", "value": "60%", "state": "running"},
  {"title": "API tests", "subtitle": "412 of 420", "value": "98%", "state": "done"},
  {"title": "DB migration", "subtitle": "staging", "state": "failed"},
  {"title": "Land deploy", "subtitle": "queued"}
]
JSON
}

sparku push queue "tasks:=$(tasks_json)"

tasks:=... — the colon before = in sparku push tells the CLI to parse the value as JSON, not a string (see sparku CLI).

6. Agent asks before a dangerous command#

Integration: agent, dialogs enabled in settings, card is optional.

#!/bin/bash
# cleanup-branch.sh — called by an AI agent (Claude Code, Codex, etc.) from shell
set -euo pipefail

if sparku ask agent "Delete branch feature/old and close PR #42?" \
     --accept Delete --decline "Don't touch" --timeout 5m; then
  git push origin --delete feature/old
  gh pr close 42
else
  echo "cancelled by user" >&2
  exit 1
fi

Details on ask exit codes and the "For AI agents" section — Questions to User.

7. Wrapper if sparku ask … for any command#

Integration: same agent as in recipe 6 — one integration for all confirmations in the terminal.

#!/bin/bash
# confirm.sh — wrapper: ask and run any command
# usage: ./confirm.sh "Delete node_modules?" rm -rf node_modules

question=$1; shift

if sparku ask agent "$question"; then
  "$@"
else
  echo "cancelled" >&2
  exit 1
fi
./confirm.sh "Restart prod server?" ssh prod 'sudo systemctl restart api'

If It Doesn't Work

sparku doctor, typical errors — from missing socket to 409, request log and "Test" console.

Start with sparku doctor#

sparku doctor
sparku 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)

Socket doesn't exist#

✗ socket doesn't exist: /Users/you/.sparku/run/api.sock

One of two things:

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.

401 — missing or wrong token#

{"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"}}

sparku exits with 77 in both cases.

404 — integration doesn't exist#

{"error": {"code": "unknown_integration", "message": "integration \"deploy\" doesn't exist; create it in SparkU Settings → Custom Integrations"}}

Typo in the address or the integration hasn't been created by the user yet — the API never creates integrations itself (Concepts). sparku — code 67.

422 — value failed validation#

{"error": {"code": "invalid_fields", "message": "progress: expected 0…1, got 70",
 "fields": [{"field": "progress", "message": "progress: expected 0…1, got 70"}]}}

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.

429 — notify too frequent#

{"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.

409 — state conflict#

Code Reason Fix
integration_disabled integration is disabled in Settings enable it in Custom Integrations
notify_disabled notify is disabled in this integration's settings enable in the "Island" section of the editor
dialog_pending integration already has an unanswered question wait for the answer or withdrawal
import_pending 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.

Integration request log#

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.

"Test" console in the editor#

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.

Other issues#