Skip to content

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.

bash
curl "https://auth-api.flowsta.com/api/v1/sign-it/verify?hash=a7f3b9c1e2d4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0"

Parameters ​

NameInTypeRequiredDescription
hashquerystringYesSHA-256 hex string (exactly 64 characters). Missing: 400 missing_hash; malformed: 400 invalid_hash

Response ​

json
{
  "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; null for 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; null on signatures made through the SDKs. checks_performed lists only the checks that ran for that file type (LsbAnalysis for PNG/BMP/TIFF, UnicodeSteg for text-like files).
  • comment - the signer's Public Note (up to 280 characters), or null.
  • supersedes / superseded_by - hex action hashes linking amended signature chains: supersedes points to the signature this one replaces; superseded_by is 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; null on 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).

bash
curl -X POST "https://auth-api.flowsta.com/api/v1/sign-it/verify-file" \
  --data-binary @photo.jpg

Parameters ​

NameInTypeRequiredDescription
bodybodybinaryYesRaw 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.

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

bash
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 ​

NameInTypeRequiredDescription
bandsbodynumber[][]Yes, unless pixels is givenLSH 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_valuebodynumber[]NoFull perceptual hash bytes, for the Hamming similarity. Without it every candidate comes back with similarity: null
pixelsbodynumber[]No72 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.

bash
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 ​

json
{
  "allowed": true,
  "scope": "community",
  "tier": "premium",
  "used": 12,
  "limit": 100,
  "resets_at": "2026-08-01T00:00:00.000Z"
}
FieldTypeDescription
allowedbooleanWhether another sign would be accepted
scopestringcommunity (personal quota) or org (developer pool, when authenticated via OAuth)
tierstringPlan name: free, premium, premium_plus for personal quotas; free, starter (Spark), pro, enterprise for org pools; unknown for an OAuth app with no organization
usednumberSigns used this period
limitnumber | nullIncluded signs per period (null when unlimited)
resets_atstring | nullWhen the period rolls over (null for an OAuth app with no organization)
upgrade_tierstringPresent when blocked - the suggested next tier
overage_signs, overage_cents_per_100numberOrg 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.

bash
curl "https://auth-api.flowsta.com/api/v1/sign-it/quota/by-agent?agent_pub_key=uhCAk...&client_id=your_client_id"

Parameters ​

NameInTypeRequiredDescription
agent_pub_keyquerystringYesThe signer's agent key (uhCAk... or plain base64). Missing: 400 invalid_agent_pub_key
client_idquerystringNoYour 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 -

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 }
}

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.

bash
curl "https://auth-api.flowsta.com/api/v1/sign-it/creator/uhCAkynvT4p77x..."

Response ​

json
{
  "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, or null when they have none.
  • signatures and stats count each amend chain once, by its latest signature. contactable is true when 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.

bash
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 ​

NameInTypeRequiredDescription
file_hashbodystringYesSHA-256 hex (64 characters). Missing: 400 missing_fields; malformed: 400 invalid_hash
intentbodystringNoAuthorship, Approval, Witness, Receipt, Agreement. Default: Authorship. Not validated here - anything else fails at commit
ai_generationbodystringNonone, assisted, generated
content_rightsbodyobjectNosnake_case keys: license, commercial_licensing, ai_training, contact_preference - values as on the Content Rights page
commentbodystringNoPublic Note shown to verifiers. Max 280 characters (400 invalid_comment)
supersedesbodystringNoHex action hash of a previous signature this one amends (400 invalid_supersedes)
perceptual_hashbodyobjectNoPre-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 sign scope (from third-party apps)

Response ​

json
{
  "success": true,
  "file_hash": "a7f3b9c1...",
  "agent_pub_key": "uhCAkynvT4p77x...",
  "signed_at": 1680000000000,
  "action_hash": "842924c550353c6a..."
}

Over quota:

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

NameInTypeRequiredDescription
filesbodyarrayYesArray of { file_hash, intent?, ai_generation?, content_rights?, comment?, integrity_report?, perceptual_hash? }
shared_metadatabodyobjectNoApplied to all files unless overridden per-file. May include a shared comment (max 280 characters)

Authentication ​

Same as /sign.

Response ​

json
{
  "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 verify scope

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.

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

NameInTypeRequiredDescription
action_hashbodystringYesHex-encoded action hash of the signature. Missing: 400 missing_fields
reasonbodystringNoRevocation reason (max 280 characters, enforced by the network's rules)

Authentication ​

JWT session token or OAuth access token with sign scope.

Response ​

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

bash
curl -X POST "https://auth-api.flowsta.com/api/v1/sign-it/fingerprint" \
  -H "Authorization: Bearer <jwt_or_oauth_token>" \
  --data-binary @photo.jpg

Parameters ​

NameInTypeRequiredDescription
bodybodybinaryYesRaw file bytes. Max 50 MB

Authentication ​

JWT session token or OAuth access token with sign scope.

Response ​

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

bash
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 ​

NameInTypeRequiredDescription
action_hashbodystringYesHex-encoded action hash of the signature
thumbnailbodystringYesdata: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 ​

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

NameInTypeRequiredDescription
signer_agent_keybodystringYesAgent key of the signer
file_hashbodystringFor a signature contactThe signed file the message is about. Omit it for a profile contact
sender_namebodystringYesYour name (max 100 characters)
sender_emailbodystringYesYour email (max 254 characters)
messagebodystringYesYour message (max 500 characters)
purposebodystringNolicensing, 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:

json
{
  "success": true,
  "message": "Your message has been sent to the signer."
}

Documentation licensed under CC BY-SA 4.0.