Verification API
All verification endpoints are public - no authentication required, free on every plan, rate-limited per IP address (limits are listed per endpoint). Base URL: https://auth-api.flowsta.com
Signatures are created in the user's Flowsta Vault and reach the network by gossip, typically within minutes of signing - a just-signed file may briefly return no results. The POST /sign and POST /sign-batch endpoints below are deprecated; see the callouts on each.
GET /api/v1/sign-it/verify
Look up signatures for a file by its SHA-256 hash.
curl "https://auth-api.flowsta.com/api/v1/sign-it/verify?hash=a7f3b9c1e2d4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0"Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
hash | query | string | Yes | SHA-256 hex string (exactly 64 characters). Missing: 400 missing_hash; malformed: 400 invalid_hash |
Response
{
"signatures": [
{
"file_hash": "a7f3b9c1...",
"action_hash": "842924c550353c6a...",
"signer": "uhCAkynvT4p77x...",
"signer_did": "did:flowsta:z6Mk...",
"signer_display_name": "Alice",
"signer_username": "alice",
"signer_profile_picture": "https://...",
"signed_at": 1680000000000,
"intent": "Authorship",
"ai_generation": "None",
"content_rights": {
"license": "CCBY",
"commercial_licensing": "OpenToLicensing",
"ai_training": "NotAllowed",
"contact_preference": "AllowContactRequests"
},
"integrity_report": {
"checks_performed": ["PostEofData", "LsbAnalysis", "MetadataInspection", "EntropyAnalysis", "AppendedFileDetection"],
"issues_found": [],
"checked_at": 1680000000000
},
"perceptual_hash": {
"hash_type": "ImagePHash",
"hash_value": [/* 8 bytes for images; longer for audio and video */],
"bands": [/* 8 bands: 1 byte each for images, 4 bytes each for audio and video */]
},
"comment": "Original master file for the 2026 print run.",
"supersedes": null,
"superseded_by": null,
"expires_at": null,
"tags": null,
"revoked": false,
"revoked_at": null,
"revocation_reason": null,
"contactable": true
}
],
"count": 1
}Field notes:
signer_display_name,signer_username,signer_profile_picture- public profile fields;nullfor signers who haven't granted those scopes yet. Handle nulls gracefully.signed_at- Unix milliseconds.content_rights.license- a string enum (CCBY,AllRightsReserved, ...) or, for a custom license,{ "Custom": "..." }.integrity_report,perceptual_hash- present on signatures made in the Vault's own Sign It page;nullon signatures made through the SDKs.checks_performedlists only the checks that ran for that file type (LsbAnalysisfor PNG/BMP/TIFF,UnicodeStegfor text-like files).comment- the signer's Public Note (up to 280 characters), ornull.supersedes/superseded_by- hex action hashes linking amended signature chains:supersedespoints to the signature this one replaces;superseded_byis set when a newer signature in the same result set replaces this one. Use them to show only the latest version and walk the edit history.expires_at,tags- optional signature metadata;nullon signatures that don't set them.
Rate Limit
30 requests/minute per IP (429 when exceeded).
POST /api/v1/sign-it/verify-file
Upload a file for server-side perceptual fingerprinting and fuzzy matching. The file is fingerprinted and immediately discarded - not stored. This is what the verify page calls when a file has no exact match.
Supports images (PNG, JPEG, BMP, TIFF, GIF), audio (MP3, WAV, FLAC, OGG), and video (MP4, MKV, AVI, WebM).
curl -X POST "https://auth-api.flowsta.com/api/v1/sign-it/verify-file" \
--data-binary @photo.jpgParameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| body | body | binary | Yes | Raw file bytes, any content type. Max 50 MB. Empty body: 400 no_file |
Response
A reduced signature object - not the full GET /verify shape. No profile fields, no action_hash, no revocation status: look the file_hash up with GET /verify for those.
{
"signatures": [
{
"file_hash": "a7f3b9c1...",
"signer": "uhCAkynvT4p77x...",
"signed_at": 1680000000000,
"intent": "Authorship",
"ai_generation": "None",
"content_rights": { "license": "CCBY", "ai_training": "NotAllowed" },
"similarity": 94,
"match_type": "perceptual"
}
],
"count": 1
}similarity- 0-100, Hamming similarity between the uploaded file's fingerprint and the signature's. Only matches at 60% or above are returned.nullwhen the stored signature has no comparable fingerprint (such candidates are returned).- Unsupported file types, and files the fingerprinter cannot read, return
{ "signatures": [], "count": 0, "match_type": "none" }.
Rate Limit
30 requests/minute per IP.
POST /api/v1/sign-it/verify-fuzzy
Fuzzy lookup using pre-computed perceptual hash bands. For when you've already generated the perceptual hash client-side.
curl -X POST "https://auth-api.flowsta.com/api/v1/sign-it/verify-fuzzy" \
-H "Content-Type: application/json" \
-d '{"bands": [[212],[19],[140],[7],[255],[64],[33],[180]], "hash_value": [212,19,140,7,255,64,33,180]}'Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
bands | body | number[][] | Yes, unless pixels is given | LSH band byte arrays. Vault-made image signatures store 8 bands of 1 byte (the 8 bytes of the 64-bit gradient hash); audio and video store 8 bands of 4 bytes. Send the same layout or nothing will match. Missing: 400 missing_bands |
hash_value | body | number[] | No | Full perceptual hash bytes, for the Hamming similarity. Without it every candidate comes back with similarity: null |
pixels | body | number[] | No | 72 grayscale pixels (9x8, row-major) - the server computes the gradient hash and its 8 bands itself and ignores bands / hash_value |
Response
Same reduced shape as POST /verify-file (signatures[] with file_hash, signer, signed_at, intent, ai_generation, content_rights, similarity, match_type: "perceptual", plus count). Only matches with similarity at 70% or above are returned; candidates with similarity: null are returned as well.
Rate Limit
30 requests/minute per IP.
GET /api/v1/sign-it/quota
Return the authenticated user's (or, with an OAuth token, your org's) current signing quota state. Use it to render a quota meter without attempting a sign. See Signing quotas & sponsored signing.
curl "https://auth-api.flowsta.com/api/v1/sign-it/quota" \
-H "Authorization: Bearer <jwt_or_oauth_token>"Authentication
JWT session token or OAuth access token with verify scope.
Response
{
"allowed": true,
"scope": "community",
"tier": "premium",
"used": 12,
"limit": 100,
"resets_at": "2026-08-01T00:00:00.000Z"
}| Field | Type | Description |
|---|---|---|
allowed | boolean | Whether another sign would be accepted |
scope | string | community (personal quota) or org (developer pool, when authenticated via OAuth) |
tier | string | Plan name: free, premium, premium_plus for personal quotas; free, starter (Spark), pro, enterprise for org pools; unknown for an OAuth app with no organization |
used | number | Signs used this period |
limit | number | null | Included signs per period (null when unlimited) |
resets_at | string | null | When the period rolls over (null for an OAuth app with no organization) |
upgrade_tier | string | Present when blocked - the suggested next tier |
overage_signs, overage_cents_per_100 | number | Org scope, paid tiers only - metered overage state and price |
Responses are Cache-Control: no-store.
GET /api/v1/sign-it/quota/by-agent
Public quota lookup by agent key (agent keys are public information - they're on the network). Used by Flowsta Vault to fetch quota state without minting a token; useful for any client that holds the user's agent key.
curl "https://auth-api.flowsta.com/api/v1/sign-it/quota/by-agent?agent_pub_key=uhCAk...&client_id=your_client_id"Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
agent_pub_key | query | string | Yes | The signer's agent key (uhCAk... or plain base64). Missing: 400 invalid_agent_pub_key |
client_id | query | string | No | Your app's client ID - resolves the sponsored signing pool |
Response
Without client_id: the same shape as GET /quota (the user's personal quota).
With client_id (and an active app with an organization): 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 }
}If the org pool is exhausted, the response is the user's personal quota with sponsor: { "app_name": "...", "exhausted": true } - this is how the Vault approval dialog stays honest about whose quota a published signature draws from.
Returns 404 agent_not_found if no user matches the agent key.
Rate Limit
60 requests/minute per IP. Responses are Cache-Control: no-store.
GET /api/v1/sign-it/creator/:id
Public creator profile: the signatures a signer has published, with stats. :id is the signer's agent key (uhCAk...) or DID (did:flowsta:...). Anything else returns 400 invalid_id.
curl "https://auth-api.flowsta.com/api/v1/sign-it/creator/uhCAkynvT4p77x..."Response
{
"creator": {
"agent_key": "uhCAkynvT4p77x...",
"did": "did:flowsta:z6Mk...",
"member_since": 1680000000000,
"contactable": true
},
"stats": {
"total": 12,
"active": 11,
"revoked": 1,
"by_intent": { "Authorship": 12 },
"by_license": { "CCBY": 9, "AllRightsReserved": 3 }
},
"signatures": [ /* signature objects with revocation state, as in GET /verify */ ]
}member_since-signed_at(Unix milliseconds) of the signer's oldest signature, ornullwhen they have none.signaturesandstatscount each amend chain once, by its latest signature.contactableistruewhen any active signature allows contact requests.
Responses are Cache-Control: no-store.
Rate Limit
30 requests/minute per IP.
POST /api/v1/sign-it/sign
Deprecated - sign through the Vault
Signing happens in the user's Flowsta Vault, where they approve each signature and the keys never leave their device. Use signDocument() from a linked desktop app to publish, or signFile() from a web app for an unpublished signature. This server-side endpoint signs inside a server-hosted cell, which device-hosted identities (the norm) do not have - for them it fails with 500 signing_failed. It is documented only for completeness.
Sign a file hash. Requires authentication. Quota-gated (not rate-limited): over-quota requests return 402 with error quota_exceeded - see quotas. A successful call fires the app's sign.created webhook; this is the only signing path that does.
curl -X POST "https://auth-api.flowsta.com/api/v1/sign-it/sign" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <jwt_or_oauth_token>" \
-d '{
"file_hash": "a7f3b9c1e2d4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0",
"intent": "Authorship",
"comment": "Original master file for the 2026 print run.",
"content_rights": {
"license": "cc-by",
"ai_training": "not_allowed"
}
}'Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
file_hash | body | string | Yes | SHA-256 hex (64 characters). Missing: 400 missing_fields; malformed: 400 invalid_hash |
intent | body | string | No | Authorship, Approval, Witness, Receipt, Agreement. Default: Authorship. Not validated here - anything else fails at commit |
ai_generation | body | string | No | none, assisted, generated |
content_rights | body | object | No | snake_case keys: license, commercial_licensing, ai_training, contact_preference - values as on the Content Rights page |
comment | body | string | No | Public Note shown to verifiers. Max 280 characters (400 invalid_comment) |
supersedes | body | string | No | Hex action hash of a previous signature this one amends (400 invalid_supersedes) |
perceptual_hash | body | object | No | Pre-computed perceptual hash ({ hash_type, hash_value, bands }, from POST /fingerprint). Not validated here |
Authentication
- JWT session token (from flowsta.com login)
- OAuth access token with
signscope (from third-party apps)
Response
{
"success": true,
"file_hash": "a7f3b9c1...",
"agent_pub_key": "uhCAkynvT4p77x...",
"signed_at": 1680000000000,
"action_hash": "842924c550353c6a..."
}Over quota:
{
"error": "quota_exceeded",
"message": "You have used all 2 of your monthly signs on the free tier.",
"scope": "community",
"tier": "free",
"used": 2,
"limit": 2,
"resets_at": "2026-08-01T00:00:00.000Z",
"upgrade_tier": "premium"
}POST /api/v1/sign-it/sign-batch
Deprecated - no batch path
Same as POST /sign, including the server-hosted-cell limitation. There is no batch path through the Vault - it approves one document at a time by design. Users batch-sign in the Vault's own Sign It page; apps call signFile() per file.
Sign multiple file hashes in one request. Maximum 100 files per batch. Quota-gated: the batch must fit within the remaining quota or the whole request returns 402 quota_exceeded (the body carries the /sign fields plus remaining and requested). Each successful signature fires the app's sign.created webhook.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
files | body | array | Yes | Array of { file_hash, intent?, ai_generation?, content_rights?, comment?, integrity_report?, perceptual_hash? } |
shared_metadata | body | object | No | Applied to all files unless overridden per-file. May include a shared comment (max 280 characters) |
Authentication
Same as /sign.
Response
{
"results": [
{ "file_hash": "a7f3...", "action_hash": "8429...", "success": true },
{ "file_hash": "b8c4...", "action_hash": null, "success": false, "error": "..." }
],
"signed": 1,
"failed": 1
}GET /api/v1/sign-it/my-signatures
Get all signatures created by the authenticated user and their linked agents.
Authentication
- JWT session token, or
- OAuth access token with
verifyscope
Response
Each item is the raw signature record - the same record fields a GET /verify signature carries (file_hash, action_hash, signer, signed_at, intent, ai_generation, content_rights, integrity_report, perceptual_hash, comment, supersedes, superseded_by, expires_at, tags) plus the base64 signature itself - with the revocation state and the signer's thumbnail added. There are no profile fields - they are yours.
{
"signatures": [
{
"file_hash": "a7f3...",
"action_hash": "8429...",
"signer": "uhCAkynvT4p77x...",
"signed_at": 1680000000000,
"intent": "Authorship",
"ai_generation": "None",
"content_rights": { "license": "CCBY" },
"signature": "base64...",
"comment": null,
"supersedes": null,
"superseded_by": null,
"expires_at": null,
"tags": null,
"revoked": false,
"revoked_at": null,
"revoked_reason": null,
"thumbnail": "data:image/jpeg;base64,..."
}
]
}Responses are Cache-Control: no-store.
POST /api/v1/sign-it/revoke
Revoke a signature. Only the original signer can revoke. Like POST /sign, this acts on a server-hosted cell; device-hosted identities revoke in their Vault's Sign It page instead. A successful call fires the app's sign.revoked webhook.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
action_hash | body | string | Yes | Hex-encoded action hash of the signature. Missing: 400 missing_fields |
reason | body | string | No | Revocation reason (max 280 characters, enforced by the network's rules) |
Authentication
JWT session token or OAuth access token with sign scope.
Response
{
"success": true
}A revocation the cell refuses (not the signer, unknown hash, reason too long) returns 500 revocation_failed with the cell's message.
POST /api/v1/sign-it/fingerprint
Upload a file so the server computes its perceptual hash with the same algorithm Vault uses - guaranteed matching hashes. Useful when your app wants to pre-compute a perceptual hash before calling /sign with a perceptual_hash field. The file is immediately discarded after fingerprinting.
Supports images (PNG, JPEG, BMP, TIFF, GIF), audio (MP3, WAV, FLAC, OGG), and video (MP4, MKV, AVI, WebM). Unsupported file types return { "fingerprint": null }.
curl -X POST "https://auth-api.flowsta.com/api/v1/sign-it/fingerprint" \
-H "Authorization: Bearer <jwt_or_oauth_token>" \
--data-binary @photo.jpgParameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| body | body | binary | Yes | Raw file bytes. Max 50 MB |
Authentication
JWT session token or OAuth access token with sign scope.
Response
{
"fingerprint": {
"hash_type": "ImagePHash",
"hash_value": [212, 19, 140, 7, 255, 64, 33, 180],
"bands": [[212], [19], [140], [7], [255], [64], [33], [180]]
}
}fingerprint is null if the file type is unsupported. hash_type is ImagePHash, AudioChromaprint or VideoPHash. The fingerprinter has a 60-second timeout.
POST /api/v1/sign-it/set-thumbnail
Attach a thumbnail to a previously signed entry on the network. Only the original signer can set the thumbnail. Like POST /sign, this acts on a server-hosted cell; the Vault attaches thumbnails itself for signatures made there.
curl -X POST "https://auth-api.flowsta.com/api/v1/sign-it/set-thumbnail" \
-H "Authorization: Bearer <jwt_or_oauth_token>" \
-H "Content-Type: application/json" \
-d '{
"action_hash": "842924c550353c6a...",
"thumbnail": "data:image/webp;base64,..."
}'Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
action_hash | body | string | Yes | Hex-encoded action hash of the signature |
thumbnail | body | string | Yes | data:image/... URI of at most 15,000 characters (about 15 KB) |
Authentication
JWT session token or OAuth access token with sign scope. Must be the original signer.
Response
{
"success": true
}POST /api/v1/sign-it/contact
Send a message to a signer via blind email relay. The signer's email is never exposed. The relay only delivers when the signer opted in: per signature through the Allow contact requests content right, or, for a profile contact without file_hash, through "Let people contact me through Flowsta" in their Flowsta settings.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
signer_agent_key | body | string | Yes | Agent key of the signer |
file_hash | body | string | For a signature contact | The signed file the message is about. Omit it for a profile contact |
sender_name | body | string | Yes | Your name (max 100 characters) |
sender_email | body | string | Yes | Your email (max 254 characters) |
message | body | string | Yes | Your message (max 500 characters) |
purpose | body | string | No | licensing, ai_training, collaboration, other (anything else is recorded as other) |
Rate Limit
3 requests/hour per IP.
Response
Always returns success to prevent user enumeration - including for invalid input, an unknown signer, a signer who did not opt in, and requests over the rate limit:
{
"success": true,
"message": "Your message has been sent to the signer."
}