Spark ← spark.jazzisnow.com

Developer documentation

Spark API — Ingestion Spec

v0.2 Decisions locked REST · JSON

A small, client-agnostic HTTP API that ingests a spark (audio + metadata) from any client and fans it out to that sender's configured destinations — email, Weave, storage, webhooks. One authenticated POST; the backend does the routing. The Spark iOS app is just the first client that implements this contract.

Founding principle — a spark always has content, captured by voice or text. Voice sparks are audio-backed (you might whistle a melody — audio required, transcript optional). Text sparks are typed (no audio). Every spark carries a kind (voice | text).

1Purpose & goals

Design goals, in priority order:

  1. Dumb clients. A client should be able to send a spark with one request and no knowledge of where it goes.
  2. Safe to retry / re-send. Networks drop; users edit. The same spark sent twice must not duplicate — which also makes a future sync ("mirror my sparks") possible.
  3. Async. Ingest returns immediately; delivery and enrichment happen server-side.
  4. Versioned & boring. REST + JSON, standard status codes, no surprises.

2Auth

Authorization: Bearer <token>

3Endpoints

POST /v1/sparks — ingest (or update) a spark

multipart/form-data with two parts:

PartTypeNotes
sparkapplication/jsonThe metadata document (§4). Required.
audioaudio/mp4 (.m4a)The recording. Required for voice sparks (missing → 422); omitted for text sparks.

Idempotency / upsert. The spark carries a stable client-generated id (UUID). The server keys on (token, id):

So retries are safe, and "I edited the transcript, send again" just overwrites. Returns:

{ "id": "…", "status": "accepted", "receivedAt": "2026-07-06T09:12:00Z" }

Processing (routing, delivery, any server-side enrichment) is async — a 202 Accepted is also valid to signal delivery hasn't happened yet.

GET /v1/sparks/{id} — status (deferred)

Returns delivery status per destination. Nice for a future "delivered ✓" UI; not part of v1.

GET /v1/health — liveness

200  { "status": "ok", "version": "…" }

4The spark JSON document

Mirrors the .yml sidecar the app writes, so the two stay in lockstep.

{
  "id": "1F2E…-UUID",
  "kind": "voice",                  // voice | text  (text sparks omit the audio part)
  "client": { "name": "spark-ios", "version": "1.0.0" },
  "capturedAt": "2026-07-06T09:11:44Z",
  "durationSeconds": 37.4,
  "language": "de-DE",
  "title": "Idea about the onboarding flow",
  "transcript": {
    "status": "complete",           // complete | partial | unavailable | failed
    "text": "…"
  },
  "classification": "todo",         // idea|observation|todo|calendar|question|note|melody|null
  "rating": 4,                      // 1–5 or null
  "importance": 2,                  // 0=none,1=low,2=med,3=high
  "urgency": 1,                     // 0=none,1=low,2=med,3=high
  "colors": ["red", "blue"],        // opaque user colour tags (macOS Finder colours), any subset
  "routes": null                    // reserved (§5); always null in v1
}

Unknown fields are ignored (forward-compatible). Audio filename/format is conveyed by the multipart part's headers, not duplicated here. Audio-only sparks (a hummed melody, an ambient note) carry transcript.status: "unavailable" — the audio is still there, which is the point.

5Routing

Token-scoped destinations. The token maps (on the backend) to a configured destination set — e.g. "email michael@…", "Weave workspace X". The client sends a spark; the backend delivers to all of them. The client stays dumb — it never knows or chooses destinations.

This is what makes email "just a route": the email relay is one destination type behind the same POST — no separate email API.

The routes field stays in the schema as null for now — reserved for a future per-spark override. Not part of v1; clients always send null.

6Conventions

7Decisions (locked)

  1. Routing → token-scoped destinations. The client stays dumb; per-spark routes reserved for later.
  2. Idempotency → upsert on the spark id (201 create / 200 update). Safe retries; hinge for a future sync.
  3. Enrichment → the backend may enrich, but a voice spark is fundamentally the recording (audio required). text sparks are a first-class kind and carry no audio.
  4. Scope → pure send for v1. No PATCH/DELETE/list. Upsert keeps the door open to sync later.
  5. Delivery receipts → deferred. GET /sparks/{id} is out of v1.
  6. Identity → one token = one person. No multi-sender workspaces in v1.

Implementing a client? The contract above is stable for v1. Questions or a token: spark@jazzisnow.com.