REST — apps

JSON endpoints for native and web apps, protected by the same OAuth 2.1 tokens and scopes as MCP. Auth endpoints: /register (dynamic client registration), /authorize, /token — standard PKCE flow; public clients use token_endpoint_auth_method: "none".

GET /api/context

Scope: any of identity, work, projects, preferences, relationships, communication-style. Returns only the categories your grant covers.

GET https://vault.helix.ai/api/context?category=preferences&label=helix
Authorization: Bearer <token>

200 → {
  "context": "# preferences\n\n- Vegetarian [#0k2f9x1]\n...",
  "granted_categories": ["preferences"],
  "labels_in_use": ["helix"],
  "pending_reviews": 3,
  "vault_kind": "personal"
}

Both query parameters are optional and narrow only. Asking for a category you were not granted is a 403, never a silent empty result, so you can tell "no access" from "nothing there". Entry ids in context are what replacesaccepts below. Every read is written to the user's audit log with your client name against it.

POST /api/propose

Scope: propose, plus read access to the category you are proposing into. This is not a write. It puts one item in the user's review queue and returns; nothing reaches the vault unless they approve it themselves. There is no endpoint that writes directly, by design.

POST https://vault.helix.ai/api/propose
Authorization: Bearer <token>
Content-Type: application/json

{ "category": "preferences",
  "fact": "Prefers morning workouts, four days a week",
  "replaces": "0k2f9x1",     // optional, from GET /api/context
  "labels": ["fitness"],     // optional
  "expires": "2026-12-31",   // optional, YYYY-MM-DD
  "scope": "me" }            // optional; "me" | "household"

201 → { "id": "d333affe", "status": "pending_review" }

Shared vaults

A grant names a vault, not a person, so an owner can hand your app their household rather than themselves. vault_kind on the context response tells you which you are talking to. That is the kind only; membership is never disclosed.

scope is how you say who a proposal is about, and it takes two values rather than a name. "me" (the default) means the person who connected you; "household" means the household as a whole. You cannot name another member, and you are never told who they are. See Households & delegation.

Proposal etiquette

The queue is a person's attention, not a write buffer. Apps that treat it as a log get rejected item by item, and users disconnect them.

  • Propose what is durable. A changed preference, a new project, a constraint they are working under. Not events, not activity, not anything you could recompute from your own database.
  • Read before you propose. Call /api/context first, and pass replaces when a fact updates one already there. Approving an unlinked correction leaves the user holding both versions.
  • Let the user trigger it. The best proposals come from something they just did or said, not from a nightly job.
  • Expect rejection. It is not an error condition. It is the mechanism working.

Limits: 20 proposals per hour per app per user (429), 500 characters per fact, and a 100-item ceiling on the queue itself. If you are hitting the hourly limit, you are almost certainly proposing things that belong in your own storage.

GET /api/subjects

Scope: likeness. The user's cast — refs only.

GET https://vault.helix.ai/api/subjects
Authorization: Bearer <token>

200 → { "subjects": [
  { "id": "a1b2c3d4", "name": "Fergus", "species": "dog",
    "photo_count": 5, "thumb": "data:image/jpeg;base64,..." }
]}

Refs and thumbnails are yours to cache for UI (a cast picker, a roster). Full photos are not exposed by any endpoint. Reads are audited to the user.

POST /api/subjects

Scope: likeness:write. Device-direct subject creation — call this from client code so photos travel user-device → vault and your servers never receive them. Downscale on device (~1024px JPEG works well).

POST https://vault.helix.ai/api/subjects
Authorization: Bearer <token>
Content-Type: application/json

{ "name": "Fergus", "species": "dog",
  "thumb":  "data:image/jpeg;base64,...",     // ~96px
  "photos": ["data:image/jpeg;base64,...", ...] }  // 1–8

201 → { "subject": { "id", "name", "species", "photo_count", "thumb" } }

User-initiated writes save directly (no review queue) but are audited: "added subject 'Fergus' — user-initiated, device-direct." Limits: 8 photos per request, 20 subjects per vault, 413 on oversized payloads.

POST /api/generate

Scope: likeness. Server-side generation from vault photos.

POST https://vault.helix.ai/api/generate
Authorization: Bearer <token>
Content-Type: application/json

{ "subject_ids": ["a1b2c3d4", "e5f6a7b8"],   // max 4
  "prompt": "A vintage four-panel photobooth strip...",
  "refs_per_subject": 2,                      // optional, 1–3
  "quality": "low",                           // optional: low | medium | high
  "size": "1536x1536" }                       // optional (default)

200 → { "image_b64": "...", "mime": "image/png",
        "model": "gpt-5.6-sol", "subjects": ["James","Fergus"],
        "image_id": "..." }

Reference photos go from the vault to the image provider only; your app receives finished pixels. Expect 30–60s — design your UI for it. Fewer, better references beat more: 2 per subject is the sweet spot.

Pass a previous image_id back as refine_image_id to edit that image instead of creating a new one — the iterate loop, without re-sending references.

See it wired up: A real integration is a shipping iOS app calling these three endpoints, with the Swift and server code and the audit entries each call produced.

Errors

StatusMeaningDo
401Token expired or revokedClear token, re-run OAuth. Never retry blindly.
403Scope not grantedRe-authorize requesting the scope; explain why in your UI.
404Unknown subject idRefresh the cast — the user may have removed it.
409vault_awaiting_subjectNot your fault and not retryable. The vault holds facts about someone who has not accepted that yet. Tell the user; do not fall back on your own stored guesses about them.
409Subject limit reachedSurface to the user.
502Image provider errorShow the message; safe to retry once.