Skip to content

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:27777

Vault 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:

json
{
  "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.

rust
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:

json
{
  "app_name": "ChessChain",
  "client_id": "flowsta_app_abc123",
  "app_agent_pub_key": "uhCAk..."
}

Response (on approval):

json
{
  "success": true,
  "vault_agent_pub_key": "uhCAk...",
  "vault_signature": "base64-encoded-signature"
}

Response (on denial): HTTP 403 with the standard error envelope:

json
{
  "error": "user_denied",
  "description": "User rejected the identity link request."
}

POST /revoke-identity

Notify Vault that an identity link has been revoked.

Request:

json
{
  "app_name": "ChessChain",
  "app_agent_pub_key": "uhCAk..."
}

Response: { "success": true }

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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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"
}
FieldRequiredDescription
file_hashYesSHA-256 of the file as a 64-character hex string
labelNoHuman-readable name shown in the approval dialog
intentNoWhy the file is signed: authorship, approval, witness, receipt, agreement
ai_generationNoAI disclosure: none, assisted, generated
content_rightsNoContent rights manifest (license, AI training policy, contact preference)
app_nameNoShown in the approval dialog (falls back to your origin)
client_idNoYour developer client_id - enables usage tracking and sponsored signing
commentNoSigner note stored with a published signature (280 characters max)
perceptual_hashNoImage/audio/video fingerprint for fuzzy matching on the verify page
thumbnailNoSmall preview image (data:image/... URI, under 300 KB) committed with a published signature
supersedesNoHex action hash of an earlier signature this one amends
commitNoPublish the signature to the Sign It network from this device (Flowsta pages only - other origins get tier_forbidden)
jobNoRun as an async job: respond immediately with a job_id

Response:

json
{
  "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.

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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "success": true,
  "label": "backup-1774321234",
  "data_size": 1024,
  "created_at": 1774321234
}

GET /backup/list

List all backups stored in Vault.

Response:

json
{
  "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:

json
{
  "client_id": "flowsta_app_abc123",
  "label": "backup-1774321234"
}

POST /backup/delete

Delete a stored backup.

Request:

json
{
  "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.

json
{
  "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 what restoreFromVault hands to your restore_record dispatcher 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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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 CodeHTTP StatusDescription
vault_locked403Vault is locked - ask the user to unlock it
vault_never_unlocked403Vault hasn't been unlocked yet this session (backups need one unlock first)
user_denied403User rejected the approval dialog
not_linked403Your origin isn't a linked app - call /link-identity first
client_id_mismatch403The client_id in the request doesn't match the one linked to your origin
quota_exceeded403The period's signing quota is used up (sponsor pool and personal quota)
invalid_client_id400Client ID not registered
missing_client_id400No client_id provided
backup_too_large400Backup payload exceeds the Vault's 50 MB limit
app_not_found404App not registered at dev.flowsta.com
timeout408User didn't respond to the approval dialog within 60 seconds
api_unreachable502Cannot reach Flowsta API for app verification
conductor_not_ready503The Vault's conductor is still starting up - retry in a moment
signing_dna_not_installed503The Vault build predates document signing - ask the user to update

Next Steps

Documentation licensed under CC BY-SA 4.0.