Documentation
Authentication
API keys, scopes and how requests are authorised.
FlowFinds has no API keys. There is no key to provision, no Authorization header to send, no scopes to select, and no secret to rotate. If you have arrived here expecting a bearer token, the rest of this page is the mechanism that exists in its place, and it is different enough to change how you build.
Authentication is a session cookie plus an email sign-in link. That is the whole of it, in the product and in the API alike — the console you can open in a browser calls exactly these routes.
The session cookie
Identity is ff_session, a cookie the server issues to any request that arrives without one. It is minted as a UUID, stored and handed back in the same request — an id given out but not recorded would mean holds never match.
Set-Cookie: ff_session=<32 hex characters>; Path=/; Max-Age=31536000One year, path-wide, one format. Send it back on every subsequent request. A client that discards cookies gets a brand-new anonymous session on every call and will never see its own data — the single most common integration mistake against this API.
A session on its own is useful, not merely a placeholder. An anonymous session can be shown candidates, take a hold, claim a product and have a store built for it. What it cannot do is be recovered: clear the cookie with no account bound and that journey is unreachable.
Binding an account
An account is an email address. POST /api/account binds the current session to one, and everything the session has already done becomes the account’s from that moment.
curl -b ff.jar -c ff.jar -X POST https://flowfinds.ai/api/account \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]"}'The address is checked for shape only. We cannot observe that an address receives mail, so we do not assert it; a string that cannot be an address at all is refused with 400 and account_required.
The trust boundary
A typed address is not believed outright. If the address already belongs to a verified account, binding is refused with 409, a sign-in link is sent to that address, and the response says so:
{
"status": "refused",
"reason": "account_required",
"explanation": "That address already has an account. We sent it a sign-in link — open it to continue here.",
"verified": true,
"email_sent": true
}A brand-new address still binds immediately, because the journey this session already walked is real and must not be discarded while an inbox is checked. Either way a link goes out, and the account stays unverified until that link comes back. Verification is recorded once, on first redemption, and is never re-derived.
Signing in: the link
There is no password, so there is no password route. To attach a second device — or to verify a newly bound address — request a link.
curl -X POST https://flowfinds.ai/api/login/request \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]"}'{
"status": "link_requested",
"email_sent": true,
"explanation": "If that address has an account, a sign-in link is on its way. It works once and expires in 15 minutes."
}The answer is the same for every address. Whether an account exists is not disclosed here: a form that says “no such account” is a free membership oracle for anyone holding a list of email addresses. A link is only actually minted and sent when there is something to sign in to; a request for an unknown address returns the identical body.
email_sent reports only what was observed on the server — whether the mailer accepted the send. A false value means mail is not working on that deployment, so a client can avoid promising an inbox. It says nothing about the address.
Token properties
| Property | Value | Why |
|---|---|---|
| Lifetime | 15 minutes | Long enough to walk to your inbox, short enough to expire. |
| Uses | Exactly one | Spending is conditional on still being unused, so two clicks in the same second cannot both bind. |
| Derivation | 32 bytes of OS randomness, hashed | A token derived from the address would be the same token every time — a permanent password sitting in an inbox. |
| Records | The requesting session | So a link redeemed in the browser that asked for it can adopt that browser’s anonymous journey. |
| Account creation | None | An unredeemed link leaves no trace of an account, or typing a stranger’s address would conjure one for them. |
Redeeming: GET /login
The link points at GET /login?token=…. It is a GET because it is opened from an inbox, and it is the one place a session becomes an authenticated account. It answers with a redirect rather than JSON, because a human clicked it.
HTTP/1.1 303 See Other
Cache-Control: private, no-store, must-revalidate
Set-Cookie: ff_session=<session>; Path=/; Max-Age=31536000
Location: /?view=home#/app/homeOn failure the status is still 303 and the cookie is still set — only the destination differs: /#login?e=invalid_link. Dropping the cookie on a failed redemption would silently discard an anonymous journey because a link had expired.
Every failure is one answer. An expired token, an already-spent token and a token that never existed all return invalid_link. Telling them apart would tell an attacker which addresses have accounts.
Redemption is also when the account’s durable referral code is settled and its account_saved milestone recorded.
Signing out
curl -b ff.jar -X POST https://flowfinds.ai/api/logout
{"status":"signed_out"}This unbinds this browser. The account, its claims and its revenue are untouched — signing out is a statement about a cookie, never about an account. The cookie itself remains, now anonymous again.
Knowing whether you are signed in
GET /api/account answers with {"status":"ok","account":"[email protected]"} when a session is bound, and account: null when it is not. GET /api/usage carries the same signal as account: true|false; a false value means there is no subject to meter, and the feature list is empty rather than fabricated.
Authorisation
There are no scopes, because there is nothing to scope. A session either resolves to an account or does not, and every founder-scoped endpoint resolves what you may see through one resolver — a product another session claimed can never surface on yours. That is the exclusivity contract, not a permission list; it is described in Concepts.
Endpoints that require an account, rather than merely a session, refuse with:
| Status | Reason | Meaning |
|---|---|---|
| 401 | no_account | This action must belong to a signed-in account. |
| 401 | (explanation only) | Research routes answer Sign in to research new products. |
| 200 | no_account_yet | A surface that has nothing to show until you claim a product or save an account. A state, not a failure. |
Transport and handling
- Use HTTPS. The cookie is the entire credential; on plain HTTP it is readable in transit.
- Store the cookie the way you would store a long-lived token, and treat a sign-in link in an inbox with the same care — for its 15 minutes it is the account.
- Everything under
/api/and/loginis servedprivate, no-store, must-revalidate. Do not add a caching layer of your own in front of them. - One session, one client. Sharing a cookie jar between concurrent unrelated users will hand them each other’s operation.
If you need machine credentials
A cookie minted for a browser is a poor fit for a server-to-server integration, and we are not going to pretend otherwise. Today the honest options are to drive the same session-and-link flow from your own client and persist the cookie, or to talk to us before you build. Write to [email protected]; when a machine-credential mechanism ships it will be announced on the changelog under the notice policy in Versioning.
Next: Rate limits and quotas · Errors · Troubleshooting