Skip to content

Developer Guide

Add document signing to your app. All signing goes through the user's Flowsta Vault: the keys never leave their device, and the user approves every signature in a Vault dialog - no one in between. Your app's job is to hash the file, request the signature, and handle the user's answer.

Two SDKs, depending on your platform:

PathForSDKHow It Signs
Web Apps (OAuth)Websites using "Sign in with Flowsta"@flowsta/authDetects the user's local Vault and signs through it - user approves each signature
Desktop Apps (Vault IPC)Holochain or Tauri apps running alongside Vault@flowsta/holochainDirect IPC call to Vault - user approves each signature

Prerequisites

  1. Register your app at dev.flowsta.com to get a clientId
  2. Choose your SDK:
    • Web apps: npm install @flowsta/auth
    • Desktop Holochain apps: npm install @flowsta/holochain

Web Apps (OAuth)

Setup

typescript
import { FlowstaAuth, hashFile } from '@flowsta/auth';

const flowsta = new FlowstaAuth({
  clientId: 'your_client_id',
  redirectUri: 'https://your-app.com/callback',
  scopes: ['profile', 'sign'],  // Request signing permission
});

The sign scope means the user will see "This app wants to sign files on your behalf" in the consent screen. Even with the scope granted, every individual signature still requires the user's explicit approval in their Vault.

Sign a File

typescript
// 1. Hash the file client-side (never uploaded)
const hash = await hashFile(file);

// 2. Request the signature - the user approves in their Vault
const result = await flowsta.signFile({
  fileHash: hash,
  intent: 'Authorship',
  contentRights: {
    license: 'cc-by',
    aiTraining: 'not_allowed',
    contactPreference: 'allow_contact_requests',
  },
});

// 3. Use the result
console.log(result.action_hash);   // DHT record reference
console.log(result.agent_pub_key); // Signer's public key

signFile() probes for a running Vault on the user's machine. When Vault is running, it signs there: the user sees an approval dialog with your app's name and the metadata, and the key never leaves their device. The signature is committed to their local conductor and publishes to Flowsta's tamper-proof network, built on Holochain, over the next few minutes via gossip. Anyone can then verify it at flowsta.com/sign-it by dropping the same file.

Handle the User's Answer

typescript
import { VaultRequiredError, UserDeniedError } from '@flowsta/auth';

try {
  const result = await flowsta.signFile({ fileHash: hash, intent: 'Authorship' });
} catch (e) {
  if (e instanceof UserDeniedError) {
    // The user declined in the Vault dialog - respect that, don't retry automatically
  } else if (e instanceof VaultRequiredError) {
    // No Vault can sign for this account - prompt the user to open
    // (or install) Flowsta Vault: https://flowsta.com/vault
  }
}

Signing needs the Vault

When no Vault is reachable, signFile() throws VaultRequiredError - signing keys live only in the user's Vault, so there's nothing else that can sign. Prompt the user to open (or install) Flowsta Vault and try again.

Batch Signing

One approval per signature

The Vault approves one document at a time - that per-signature approval is the point. signBatch() therefore throws FlowstaAuthError with code batch_requires_individual_approval; call signFile() per file instead, and each shows its own Vault prompt. Users can batch-sign in the Vault's own Sign It page.

typescript
// Instead of signBatch - one approval per file:
for (const file of files) {
  const hash = await hashFile(file);
  await flowsta.signFile({ fileHash: hash, intent: 'Authorship' });
}

Users who want true batch signing can drop the whole folder into the Vault's own Sign It page.

Verify a File

typescript
const result = await flowsta.verifyFile(hash);

if (result.count > 0) {
  result.signatures.forEach(sig => {
    console.log(`Signed by: ${sig.signer_did || sig.signer}`);
    console.log(`License: ${sig.content_rights?.license}`);
    console.log(`AI Training: ${sig.content_rights?.ai_training}`);
    console.log(`Revoked: ${sig.revoked}`);
  });
}

Verification is a public endpoint - no authentication required, and it's free and unlimited on every plan.

Note: signatures made in a Vault reach the server nodes by gossip, typically within minutes of signing. A just-signed file may briefly show no results.

Check Content Rights

typescript
const rights = await flowsta.getContentRights(hash);

if (rights.signed) {
  console.log(`${rights.signerCount} signer(s)`);
  rights.rights.forEach(r => {
    if (r.aiTraining === 'NotAllowed') {
      console.log('Do not use for AI training');
    }
  });
}

OAuth Scopes

ScopePurposeRequired For
signRequest signatures on behalf of the usersignFile(), revocation
verifyAuthenticated verification and quota lookupsGET /quota (verification endpoints are public and work without auth)

Desktop Apps (Vault IPC)

For Holochain apps or Tauri desktop apps running alongside Flowsta Vault. Signing happens locally via IPC - the user approves in Vault.

Setup

bash
npm install @flowsta/holochain

Check if Vault is Available

typescript
import { getSigningStatus } from '@flowsta/holochain';

const status = await getSigningStatus();
if (status.available) {
  // Vault is running, unlocked, and ready to sign
  showSignButton();
} else if (status.vaultRunning && !status.vaultUnlocked) {
  showMessage('Please unlock Flowsta Vault to sign files');
} else {
  showMessage('Install Flowsta Vault to sign files');
}

Sign a File

typescript
import { signDocument } from '@flowsta/holochain';

const result = await signDocument({
  clientId: 'your_client_id',       // From dev.flowsta.com
  appName: 'ArtStudio',             // Shown in Vault approval dialog
  fileHash: 'a7f3b9c1e2d4...',      // SHA-256 hex (64 chars)
  label: 'Illustration.png',        // Optional: shown in approval dialog
  intent: 'authorship',
  contentRights: {
    license: 'cc-by',
    aiTraining: 'not_allowed',
    contactPreference: 'allow_contact_requests',
  },
});

console.log(result.signature);    // Base64 Ed25519 signature
console.log(result.agentPubKey);  // uhCAk... format
console.log(result.actionHash);   // DHT action hash

The user sees an approval dialog in Vault showing your app name, the file label, the metadata, and whose signing quota the signature draws from. If they approve, Vault signs the hash with their Ed25519 key and commits the signature to the local Holochain conductor. The signature gossips to Flowsta's network automatically - typically verifiable everywhere within minutes.

Handle Errors

typescript
import {
  signDocument,
  VaultNotFoundError,
  VaultLockedError,
  UserDeniedError,
  SigningDnaNotInstalledError,
} from '@flowsta/holochain';

try {
  const result = await signDocument({ ... });
} catch (e) {
  if (e instanceof VaultNotFoundError) {
    // Vault is not running - prompt user to open it
  } else if (e instanceof VaultLockedError) {
    // Vault is locked - prompt user to unlock
  } else if (e instanceof UserDeniedError) {
    // User rejected the signing request
  } else if (e instanceof SigningDnaNotInstalledError) {
    // Vault is too old - prompt user to update
  }
}

What Vault Signing Gives You

Vault signing
Where signing happensThe user's device - keys never leave it
User approvalExplicit dialog per signature
Whose quotaYour org's sponsored pool when your app initiates - the user's personal quota is untouched
File integrity checksVault runs 6 checks automatically (Vault UI signing)
Perceptual hashingVault generates automatically (Vault UI signing)
Network visibilityMinutes (gossip from local to network)
Offline supportYes (signature stored locally, gossips when online)

Signing Quotas & Sponsored Signing

When your app initiates a Vault signature (your client_id is on the request), the signature draws from your organization's monthly signing pool - the user's personal quota is untouched. The Vault approval dialog tells the user whose quota pays, so there's never a hidden cost.

Developer tierIncluded signs / monthOverage
Free250None - hard cap
Spark2,500$1.00 per additional 100 signs
Pro25,000$0.50 per additional 100 signs
EnterpriseCustomCustom

If your org's pool is exhausted (Free tier at its cap), the signature falls back to the user's personal quota - and the Vault dialog says so, honestly labeled. Paid tiers never block: overages are metered and billed automatically.

Personal quotas (user plans)

Signatures a user initiates themselves (Vault UI, no sponsoring app) count against their personal plan:

User planSigns / month
Free2
Premium100
Premium Plus1,000

Personal quotas are a hard block: an over-quota sign fails with HTTP 402 and error code quota_exceeded (the response includes tier, used, limit, resets_at, and upgrade_tier).

Verification is free and unlimited on every plan - quotas apply to signing only.

Quota endpoints

Check quota state before attempting a sign (e.g. to render a quota meter):

bash
# Authenticated (JWT session or OAuth token with `verify` scope)
curl "https://auth-api.flowsta.com/api/v1/sign-it/quota" \
  -H "Authorization: Bearer <token>"
json
{
  "allowed": true,
  "scope": "community",
  "tier": "premium",
  "used": 12,
  "limit": 100,
  "resets_at": "2026-08-01T00:00:00.000Z"
}

scope is "community" (personal quota) or "org" (your app's pool, when authenticated with an OAuth token). Org responses on paid tiers also include overage_signs and overage_cents_per_100; a blocked response includes upgrade_tier.

bash
# Public lookup by agent key (used by Vault). Add client_id to see the
# sponsor pool your app would draw from:
curl "https://auth-api.flowsta.com/api/v1/sign-it/quota/by-agent?agent_pub_key=uhCAk...&client_id=your_client_id"

With client_id, the response is the sponsoring org's pool while it has room, plus a sponsor object:

json
{
  "allowed": true,
  "scope": "org",
  "tier": "starter",
  "used": 480,
  "limit": 2500,
  "resets_at": "2026-08-01T00:00:00.000Z",
  "overage_signs": 0,
  "overage_cents_per_100": 100,
  "sponsor": { "app_name": "ArtStudio", "exhausted": false }
}

When the org pool is dry, the same call returns the user's personal quota with sponsor.exhausted: true.


Shared Features

Public Note

Signatures can carry a signer comment - a Public Note of up to 280 characters shown to verifiers alongside the signature. Users add it in the Vault signing flow; it also exists as a comment field on the legacy /sign API.

Deep Linking to Verification

Link directly to the verification page for a specific file:

https://flowsta.com/sign-it/?hash=a7f3b9c1e2d4...

The page auto-verifies and shows results.

Rate Limits

EndpointLimit
POST /sign, POST /sign-batch (deprecated)Quota-gated, not rate-limited - over-quota returns 402 quota_exceeded
GET /verify30 requests/minute per IP
POST /verify-file30 requests/minute per IP
POST /verify-fuzzy30 requests/minute per IP
GET /badge60 requests/minute per IP
GET /content-rights60 requests/minute per IP
POST /contact3 requests/hour per IP

Next Steps

Documentation licensed under CC BY-SA 4.0.