# 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.json) (OpenAPI 3.1).
> [`sparku openapi <address>`](/docs/api/cli.md#openapi) 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](/docs/api/troubleshooting.md)).

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.

```bash
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](/docs/api/packages.md)). 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](/docs/api/dialogs.md) |
| `GET` | `/v1/integrations/{slug}/events` | stream of taps (SSE) — [Buttons and Events](/docs/api/events.md) |
| `POST` | `/v1/imports` | import `.sparku` — [.sparku Files and Import](/docs/api/packages.md) |

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

```bash
curl --unix-socket ~/.sparku/run/api.sock http://sparku/v1/health
```

```json
{"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).

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

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

```bash
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
```

```json
{"ok": true, "updated": ["progress", "stage", "title"], "cleared": []}
```

Value types in the body — see [Field types](/docs/api/fields.md). 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`:

```json
{"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](/docs/api/concepts.md#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:

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

```bash
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.

```bash
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 |

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

```bash
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 |

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

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](/docs/api/dialogs.md).

### `GET /v1/integrations/{slug}/events`

Stream of taps in the card (SSE). Details — [Buttons and Events](/docs/api/events.md).

### `POST /v1/imports`

Import `.sparku`. Details — [.sparku Files and Import](/docs/api/packages.md).

## Unified error format

Any error is JSON of the form:

```json
{"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

- **`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.

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