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:
| Path | For | SDK | How It Signs |
|---|---|---|---|
| Web Apps (OAuth) | Websites using "Sign in with Flowsta" | @flowsta/auth | Detects 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/holochain | Direct IPC call to Vault - user approves each signature |
Prerequisites
- Register your app at dev.flowsta.com to get a
clientId - Choose your SDK:
- Web apps:
npm install @flowsta/auth - Desktop Holochain apps:
npm install @flowsta/holochain
- Web apps:
Web Apps (OAuth)
Setup
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
// 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 keysignFile() 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
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.
// 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
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
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
| Scope | Purpose | Required For |
|---|---|---|
sign | Request signatures on behalf of the user | signFile(), revocation |
verify | Authenticated verification and quota lookups | GET /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
npm install @flowsta/holochainCheck if Vault is Available
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
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 hashThe 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
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 happens | The user's device - keys never leave it |
| User approval | Explicit dialog per signature |
| Whose quota | Your org's sponsored pool when your app initiates - the user's personal quota is untouched |
| File integrity checks | Vault runs 6 checks automatically (Vault UI signing) |
| Perceptual hashing | Vault generates automatically (Vault UI signing) |
| Network visibility | Minutes (gossip from local to network) |
| Offline support | Yes (signature stored locally, gossips when online) |
Signing Quotas & Sponsored Signing
Sponsored signing: your org's pool pays
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 tier | Included signs / month | Overage |
|---|---|---|
| Free | 250 | None - hard cap |
| Spark | 2,500 | $1.00 per additional 100 signs |
| Pro | 25,000 | $0.50 per additional 100 signs |
| Enterprise | Custom | Custom |
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 plan | Signs / month |
|---|---|
| Free | 2 |
| Premium | 100 |
| Premium Plus | 1,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):
# Authenticated (JWT session or OAuth token with `verify` scope)
curl "https://auth-api.flowsta.com/api/v1/sign-it/quota" \
-H "Authorization: Bearer <token>"{
"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.
# 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:
{
"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
| Endpoint | Limit |
|---|---|
POST /sign, POST /sign-batch (deprecated) | Quota-gated, not rate-limited - over-quota returns 402 quota_exceeded |
GET /verify | 30 requests/minute per IP |
POST /verify-file | 30 requests/minute per IP |
POST /verify-fuzzy | 30 requests/minute per IP |
GET /badge | 60 requests/minute per IP |
GET /content-rights | 60 requests/minute per IP |
POST /contact | 3 requests/hour per IP |
Next Steps
- Verification API - Raw API endpoints for custom integrations
- SDK Reference - Complete method reference
- Content Rights - All content rights fields and values