IPC Endpoints
Reference for Flowsta Vault's local IPC API.
Flowsta Vault runs an HTTP server on 127.0.0.1:27777 that apps use for identity linking, authentication, backups, and document signing. The @flowsta/holochain SDK wraps these endpoints, but you can call them directly if needed. Every signature happens on the user's device with keys that never leave it - no one in between.
Base URL
http://127.0.0.1:27777Vault tries ports 27777, 27778, 27779 in sequence if the previous port is in use.
Status
GET /status
Check if Vault is running and unlocked.
Response:
{
"unlocked": true,
"agent_pub_key": "uhCAk7JpEWfkiV...",
"did": "did:flowsta:uhCAk...",
"version": "0.1.0",
"display_name": "John Doe",
"web_username": "johndoe",
"profile_picture": "data:image/svg+xml;base64,..."
}TIP
If the request fails to connect, Vault is not running. The running field is not part of the response - a successful response means Vault is running.
Scope enforcement
Profile fields (display_name, web_username, profile_picture) are only returned if your app was granted those scopes by the user at link time - the username scope gates the web_username field. Fields for ungranted scopes are returned as null. Configure which scopes your app requests in your app settings at dev.flowsta.com.
The agent_pub_key and did fields are always present when the Vault is unlocked - they are not scope-gated.
:::caution Desktop apps: send an Origin header Vault resolves scope grants and session approvals by the Origin request header. Browsers send it automatically, but native HTTP clients (Rust reqwest, Go, Python, …) do not - and without it, scope-gated /status fields stay null and approvals won't stick.
Pick one stable, app-specific origin (for example yourapp://app) and send it on every Vault request - /link-identity, /authenticate, and /status must all use the same value.
client.post(format!("http://127.0.0.1:{port}/authenticate"))
.header("Origin", "yourapp://app")
// ...:::
Agent Linking
POST /link-identity
Request an identity link. Shows an approval dialog in Vault.
Request:
{
"app_name": "ChessChain",
"client_id": "flowsta_app_abc123",
"app_agent_pub_key": "uhCAk..."
}Response (on approval):
{
"success": true,
"vault_agent_pub_key": "uhCAk...",
"vault_signature": "base64-encoded-signature"
}Response (on denial): HTTP 403 with the standard error envelope:
{
"error": "user_denied",
"description": "User rejected the identity link request."
}POST /revoke-identity
Notify Vault that an identity link has been revoked.
Request:
{
"app_name": "ChessChain",
"app_agent_pub_key": "uhCAk..."
}Response: { "success": true }
GET /link-status
Check if a specific agent is still linked.
Query parameters: client_id, app_agent_pub_key
GET /link-status?client_id=flowsta_app_abc123&app_agent_pub_key=uhCAk...Response:
{
"linked": true,
"app_name": "ChessChain"
}Authentication (Desktop Apps)
POST /authenticate
Authenticate a desktop app. Shows an approval dialog in Vault with a 60-second timeout.
Approval model: if the user approved a /link-identity request from the same origin during the current unlock session, /authenticate is approved automatically - so the standard first-run sequence (link, then authenticate) shows one dialog, not two. The approval is session-scoped: after Vault relocks, /authenticate prompts again (the dialog's "remember" option re-establishes it). Requires a current Vault version.
Request:
{
"app_name": "Your Desktop App",
"client_id": "flowsta_app_abc123",
"challenge": "base64-challenge-to-sign",
"reason": "Login to Your Desktop App"
}The client_id and challenge fields are optional. The reason field is shown in the approval dialog.
Response:
{
"success": true,
"did": "did:flowsta:uhCAk...",
"agent_pub_key": "uhCAk...",
"signature": "base64-encoded-signature",
"signed_at": "2026-07-07T18:30:00Z"
}signed_at is an ISO-8601 string and is always present. Only signature is conditional - it is null unless a challenge was provided.
Document Signing (Sign It)
POST /sign-document
Sign a file hash with the user's identity key - the Sign It endpoint. Shows an approval dialog in Vault with the app name, file label, and hash. The SDK wraps this as signDocument().
Request:
{
"file_hash": "a7f3b9c1e2d4...",
"label": "Illustration.png",
"intent": "authorship",
"ai_generation": "none",
"content_rights": { "license": "cc-by", "ai_training": "not_allowed" },
"app_name": "ArtStudio",
"client_id": "flowsta_app_abc123"
}| Field | Required | Description |
|---|---|---|
file_hash | Yes | SHA-256 of the file as a 64-character hex string |
label | No | Human-readable name shown in the approval dialog |
intent | No | Why the file is signed: authorship, approval, witness, receipt, agreement |
ai_generation | No | AI disclosure: none, assisted, generated |
content_rights | No | Content rights manifest (license, AI training policy, contact preference) |
app_name | No | Shown in the approval dialog (falls back to your origin) |
client_id | No | Your developer client_id - enables usage tracking and sponsored signing |
comment | No | Signer note stored with a published signature (280 characters max) |
perceptual_hash | No | Image/audio/video fingerprint for fuzzy matching on the verify page |
thumbnail | No | Small preview image (data:image/... URI, under 300 KB) committed with a published signature |
supersedes | No | Hex action hash of an earlier signature this one amends |
commit | No | Publish the signature to the Sign It network from this device (Flowsta pages only - other origins get tier_forbidden) |
job | No | Run as an async job: respond immediately with a job_id |
Response:
{
"success": true,
"file_hash": "a7f3b9c1e2d4...",
"signature": "base64-encoded-ed25519-signature",
"agent_pub_key": "uhCAk...",
"signed_at": "2026-07-07T18:30:00Z",
"action_hash": "84202484..."
}action_hash is set when the signature was published to the Sign It network (commit: true) - it's the hex action hash of the committed record. Otherwise it is null and your app receives the raw signature to use as it sees fit.
Async job mode: with job: true the endpoint validates and immediately returns { "job_id": "sign-..." }. Poll GET /op-status/:job_id for progress; the request rides through unlock, conductor startup, and the approval dialog without holding a connection open. Identical double-submits join the in-flight job instead of signing twice.
Sponsored signing
If the request carries a client_id whose organization sponsors signing, a published signature draws from the organization's signing pool instead of the user's personal quota - and the approval dialog tells the user exactly that. If the sponsor pool for the period is used up, the signature falls back to the user's personal quota, and the dialog labels that honestly too. When both are dry, the request fails with quota_exceeded and a description explaining the state.
Signature & Profile Management
These endpoints let the flowsta.com dashboard operate on the user's own device-held records. They are restricted to Flowsta origins (tier_forbidden otherwise) and each mutation passes a per-action approval dialog in Vault. All of them accept job: true for async job mode.
POST /profile-update
Update the display name and/or profile picture stored in the user's own cell.
Request: { "display_name": "New Name", "profile_picture": "data:image/...", "job": false } - at least one of the two fields. Response: { "success": true }.
POST /revoke-signature
Revoke a published signature.
Request: { "action_hash": "<hex 39-byte action hash>", "reason": "optional, 280 chars max" }. Response: { "success": true, "revocation_hash": "..." }.
POST /set-thumbnail
Attach a preview image to a published signature.
Request: { "action_hash": "<hex 39-byte action hash>", "thumbnail": "data:image/... (under 300 KB)" }. Response: { "success": true, "thumbnail_hash": "..." }.
GET /op-status/:job_id
Poll an async job started with job: true.
Response:
{
"job_id": "sign-1774321234-a1b2c3d4",
"stage": "awaiting_approval"
}stage is one of waiting_unlock, preparing, awaiting_approval, publishing, done, failed. When done, the response includes result (the operation's normal response body); when failed, it includes error and description. Jobs expire after 10 minutes (unknown_job, 404).
GET /signatures
Read the user's signatures from the Vault - the source of truth for their own records. Flowsta origins only; read-only, so no approval dialog.
Response: { "source": "vault", "signatures": [...] } - the user's own signatures plus linked-agent history.
GET /connections
Read the registry of identity-linked apps and trusted origins. Flowsta origins only; read-only.
Response:
{
"apps": [
{
"app_name": "ChessChain",
"app_agent_pub_key": "uhCAk...",
"linked_at": 1774321234,
"client_id": "flowsta_app_abc123",
"scopes": ["openid", "display_name"]
}
],
"trusted_origins": [{ "origin": "yourapp://app", "first_seen": 1774321234 }]
}Backups
POST /backup
Store app data in the Vault's encrypted local storage. Works while the Vault is locked (after first unlock in session).
Each call without a label creates a new timestamped snapshot (up to 10 per app, oldest auto-rotated). Pass an explicit label to overwrite a named backup.
Request:
{
"client_id": "flowsta_app_abc123",
"app_name": "ChessChain",
"content_type": "application/json",
"data": {
"polls_created": { "count": 2, "polls": [...] },
"votes_cast": { "count": 5, "votes": [...] },
"private_data": {
"_readme": "Decrypted from encrypted DHT entries.",
"vote_rationales": { "count": 1, "rationales": [...] },
"drafts": { "count": 0, "drafts": [] }
}
}
}Human-readable backups
Include _readme fields, use human-readable names (poll_title not just hashes), and decrypt any encrypted entries before including them. The Vault encrypts the backup at rest - no need to double-encrypt.
Use the canonical payload shape
When data follows the canonical v1 backup shape (top-level version: 1 + cells[].records[] with human_readable + raw_record per record), the Vault lights up extra UI: per-entry-type counts on the Your Data page ("12 polls, 38 votes"), and the user's CAL §4.2.1 data export inlines the readable view of each record instead of raw bytes. Any other JSON shape still works, but you don't get those features.
Response:
{
"success": true,
"label": "backup-1774321234",
"data_size": 1024,
"created_at": 1774321234
}GET /backup/list
List all backups stored in Vault.
Response:
{
"app_count": 1,
"total_backups": 3,
"total_size": 4096,
"apps": [
{
"client_id": "flowsta_app_abc123",
"app_name": "ChessChain",
"backup_count": 3,
"total_size": 4096,
"last_backup_at": 1709251200,
"latest_summary": {
"counts_by_entry_type": { "Game": 12, "Move": 84 },
"total_records": 96
}
}
]
}The latest_summary field is populated when the most recent backup follows the canonical v1 shape. It's null for older / non-canonical backups.
POST /backup/retrieve
Retrieve a stored backup. Omit label to get the most recent snapshot.
Request:
{
"client_id": "flowsta_app_abc123",
"label": "backup-1774321234"
}POST /backup/delete
Delete a stored backup.
Request:
{
"client_id": "flowsta_app_abc123",
"label": "latest"
}Canonical backup payload (v1)
When you post a backup whose data follows this shape, the Vault recognizes it and unlocks per-entry-type summaries on the Your Data page plus human-readable inlining in the user's CAL §4.2.1 data export.
{
"version": 1,
"_readme": "Your YourApp data, backed up automatically by Flowsta Vault…",
"license": "Cryptographic Autonomy License v1.0 (CAL-1.0)",
"app": { "name": "YourApp", "client_id": "flowsta_app_…" },
"agent_pub_key": "uhCAk…",
"exported_at_iso": "2026-05-29T03:42:11Z",
"_summary": {
"countsByEntryType": { "Game": 12, "Move": 84 },
"totalRecords": 96
},
"cells": [
{
"role_name": "games",
"_readme": "Each record below is one thing you did…",
"records": [
{
"entryType": "Game",
"actionHash": "uhCkk…",
"createdAtMs": 1716969780000,
"human_readable": {
"name": "Friday Night Chess",
"opponent": "uhCAk…",
"started_at": 1716969780000
},
"raw_record": {
"entry_b64": "<base64-encoded MessagePack bytes>",
"action_address": "uhCkk…",
"action_type": "Create"
}
}
]
}
]
}Two views per record, both required for canonical-shape recognition:
human_readable- the decoded entry as plain JSON. This is what the user sees in their downloadable CAL data export.raw_record- at minimum,entry_b64(the entry's MessagePack bytes, base64-encoded). This is whatrestoreFromVaulthands to yourrestore_recorddispatcher when the user reinstalls.
Optional top-level fields are preserved verbatim through the export pipeline. Apps that generate an independent Holochain agent key not derived from the Flowsta seed can add an app_keys block at the top level so their data export remains self-sufficient:
{
"version": 1,
…
"app_keys": {
"_readme": "The cryptographic seed YourApp generates locally for its own DNA.",
"device_seed_hex": "abc123…"
}
}The SDK's dumpCellStateForBackup() builds this shape from an AdminWebsocket instance. For apps whose frontend only has an AppWebsocket, generate it from a Tauri command using zome queries - see ProofPoll's build_canonical_backup for a worked example.
Raw Signing
POST /sign
Sign raw data with the Vault's agent key. Requires user approval. For document/file signing, use /sign-document instead.
Request:
{
"type": "bytes",
"bytes": "base64-encoded-data",
"reason": "Sign this document"
}The type field is required and must be "bytes" or "action". The reason field is shown in the approval dialog.
Response:
{
"success": true,
"signature": "base64-encoded-ed25519-signature",
"agent_pub_key": "uhCAk..."
}Error Responses
All endpoints return errors as a non-2xx HTTP status with this envelope - there is no success or message field:
{
"error": "error_code",
"description": "Human-readable description"
}description may be null for some errors. The SDK maps these codes to typed error classes (VaultLockedError, UserDeniedError, …).
| Error Code | HTTP Status | Description |
|---|---|---|
vault_locked | 403 | Vault is locked - ask the user to unlock it |
vault_never_unlocked | 403 | Vault hasn't been unlocked yet this session (backups need one unlock first) |
user_denied | 403 | User rejected the approval dialog |
not_linked | 403 | Your origin isn't a linked app - call /link-identity first |
client_id_mismatch | 403 | The client_id in the request doesn't match the one linked to your origin |
quota_exceeded | 403 | The period's signing quota is used up (sponsor pool and personal quota) |
invalid_client_id | 400 | Client ID not registered |
missing_client_id | 400 | No client_id provided |
backup_too_large | 400 | Backup payload exceeds the Vault's 50 MB limit |
app_not_found | 404 | App not registered at dev.flowsta.com |
timeout | 408 | User didn't respond to the approval dialog within 60 seconds |
api_unreachable | 502 | Cannot reach Flowsta API for app verification |
conductor_not_ready | 503 | The Vault's conductor is still starting up - retry in a moment |
signing_dna_not_installed | 503 | The Vault build predates document signing - ask the user to update |
Next Steps
- @flowsta/holochain SDK - SDK that wraps these endpoints
- Agent Linking - Identity attestation guide
- Building Holochain Apps - Integration guide
- Sign It Developer Guide - Document signing integration