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/contextfirst, and passreplaceswhen 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
| Status | Meaning | Do |
|---|---|---|
| 401 | Token expired or revoked | Clear token, re-run OAuth. Never retry blindly. |
| 403 | Scope not granted | Re-authorize requesting the scope; explain why in your UI. |
| 404 | Unknown subject id | Refresh the cast — the user may have removed it. |
| 409 | vault_awaiting_subject | Not 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. |
| 409 | Subject limit reached | Surface to the user. |
| 502 | Image provider error | Show the message; safe to retry once. |