Developer documentation
Spark API — Ingestion Spec
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:
- Dumb clients. A client should be able to send a spark with one request and no knowledge of where it goes.
- 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.
- Async. Ingest returns immediately; delivery and enrichment happen server-side.
- Versioned & boring. REST + JSON, standard status codes, no surprises.
2Auth
Authorization: Bearer <token>
- The token is issued by JazzIsNow and pasted into the client (no accounts, no login flow). One token = one sender identity.
- The token is bound, server-side, to a set of destinations (see Routing). The client never has to know the destinations.
- Tokens are revocable. A revoked/invalid token →
401.
3Endpoints
POST /v1/sparks — ingest (or update) a spark
multipart/form-data with two parts:
| Part | Type | Notes |
|---|---|---|
spark | application/json | The metadata document (§4). Required. |
audio | audio/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):
- first time → create,
201 Created - same
idagain → update (re-send after an edit),200 OK
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
- Versioning: path-based (
/v1/). Breaking changes →/v2/. - Errors: standard HTTP + a JSON body
{ "error": { "code": "…", "message": "…" } }.401auth ·413payload too large ·422badsparkdoc ·429rate limit ·5xxserver. - Limits: audio ≤ 25 MB; JSON ≤ 64 KB; rate limit per token.
- Time: ISO-8601 UTC everywhere.
- Transport: HTTPS only.
7Decisions (locked)
- Routing → token-scoped destinations. The client stays dumb; per-spark
routesreserved for later. - Idempotency → upsert on the spark
id(201create /200update). Safe retries; hinge for a future sync. - Enrichment → the backend may enrich, but a
voicespark is fundamentally the recording (audio required).textsparks are a first-class kind and carry no audio. - Scope → pure send for v1. No
PATCH/DELETE/list. Upsert keeps the door open to sync later. - Delivery receipts → deferred.
GET /sparks/{id}is out of v1. - 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.