Documentation
Quickstart
From an API key to a first successful call.
This page takes you from nothing to four successful calls against https://flowfinds.ai: an anonymous session, an account bound to it, a signed-in read of your dashboard, and a read of your own usage meter. Every request and response below matches what the server actually does.
There is no API key to obtain. The credential is a session cookie, and you get one by making a request. If you were looking for the key-provisioning step, read Authentication first — the model is different enough that assuming a bearer token will waste your afternoon.
Before you start
curlwith a cookie jar, or any HTTP client that persists cookies between requests.- An email address you can read. Signing in is a link sent to an inbox; there is no password.
Step 1 — Get a session
Any request without an ff_session cookie is issued one. The server mints a UUID, hands it back in a Set-Cookie header, and uses the same id for the request it was minted on. Start with a route that reads nothing about you:
curl -i -c ff.jar https://flowfinds.ai/api/reasonsThe interesting part of the response is its headers:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, no-store, must-revalidate
Set-Cookie: ff_session=3f9c1a…; Path=/; Max-Age=31536000The body is the complete refusal registry — every reason code the server can return, with the sentence it means. It is served rather than documented-only precisely so a client can enumerate it:
{
"reasons": {
"not_claimed": "no product is resolved for this session at all",
"claimed_by_another_founder": "another founder claimed it first",
"usage_limit_reached": "you have used this feature's allowance for now",
"…": "…"
},
"count": 90
}count is the size of the registry on the server you just called; do not hardcode it. Read Errors for how to consume this.
Step 2 — Make your first authenticated-shaped call
GET /api/dashboard is the surface that restates every other one. Called from a session with nothing in it, it does not fail — it answers with the honest state:
curl -b ff.jar -c ff.jar https://flowfinds.ai/api/dashboard{
"status": "not_claimed",
"explanation": "no product is resolved for this session at all"
}That is a successful call. HTTP 200, a named state, and an explanation drawn from the same registry step 1 returned. This is the pattern to build against: the absence of data is served as a state, never as an error and never as an empty object you have to guess about.
Step 3 — Bind an account to the session
One email turns the anonymous session into an account. Everything the session already did belongs to that account from this moment.
curl -b ff.jar -c ff.jar -X POST https://flowfinds.ai/api/account \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]"}'Three outcomes, and you must handle all three:
| Status | Body | What happened |
|---|---|---|
| 200 | {"status":"ok","account":"[email protected]", …} | The address was new to us. This session is now bound to it. A sign-in link is also sent, and the account stays unverified until that link is opened. |
| 400 | {"status":"refused","reason":"account_required","verified":false} | The string cannot be an email address. This is a shape check, not a deliverability claim. |
| 409 | {"status":"refused","reason":"account_required","verified":true} | That address already belongs to a verified account, so it is never bound from a typed form. A sign-in link has been sent to it. Go to step 4. |
Step 4 — Sign in from anywhere else
To attach a new browser or client to an existing account, ask for a link. The response is deliberately identical whether or not the address has an account — a form that says “no such account” is a free membership oracle.
curl -b ff.jar -c ff.jar -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."
}email_sent is the one thing that is observed: it reports whether the mailer accepted the send on this machine, so a client can avoid promising an inbox when mail is not configured. It does not tell you whether the address has an account.
The link in the inbox is GET /login?token=…. Opening it redeems the token, binds this browser to the account, and answers 303 with the session cookie on the redirect. Tokens are single-use and expire after 15 minutes; expired, spent and never-existed are all answered identically.
HTTP/1.1 303 See Other
Set-Cookie: ff_session=3f9c1a…; Path=/; Max-Age=31536000
Location: /?view=home#/app/homeA failed redemption is also a 303, to /#login?e=invalid_link, and it still sets the cookie — losing a link must not throw away the anonymous journey the browser already walked.
Step 5 — Read your meter
Every AI-backed feature draws credits from its own bucket. Reading the meter costs nothing; the read is deliberately separated from the spend.
curl -b ff.jar https://flowfinds.ai/api/usage{
"account": true,
"tier": "free",
"tier_label": "Free",
"features": [
{
"bucket": "website_edit",
"label": "AI website editor",
"cost": 50,
"tier": "free",
"multiplier": 0.1,
"referral_multiplier": 1.0,
"referrals": 0,
"used_5h": 0,
"limit_5h": 100,
"used_week": 0,
"limit_week": 1000,
"resets_at_5h": 1756900800,
"resets_at_week": 1757116800,
"allowed": true,
"exhausted_window": null
}
]
}account: false means no email is bound to this session yet, and features is empty — an unbound session has no subject to meter. The numbers above are Free tier; the whole ladder is on Rate limits and quotas.
The same four steps in Python
Nothing here needs a library beyond a session-aware HTTP client. The only rule is that the cookie jar must persist across calls.
import requests
BASE = "https://flowfinds.ai"
s = requests.Session() # the cookie jar IS the credential
# 1. a session is minted on the first request
s.get(f"{BASE}/api/reasons").raise_for_status()
# 2. state, not an error, when there is nothing yet
print(s.get(f"{BASE}/api/dashboard").json()["status"]) # -> "not_claimed"
# 3. bind an account
r = s.post(f"{BASE}/api/account", json={"email": "[email protected]"})
if r.status_code == 409:
print("that address is verified elsewhere; open the link we just emailed")
elif r.status_code == 400:
print("not an email address")
else:
print("bound:", r.json()["account"])
# 4. read the meter (reading never spends)
for f in s.get(f"{BASE}/api/usage").json()["features"]:
print(f["label"], f["used_5h"], "/", f["limit_5h"])What to build next
You now hold a session that the rest of the API will recognise. The endpoints that matter next depend on what you are doing: reading the operation (/api/dashboard, /api/store, /api/supplier-connection), proposing changes (/api/intent), or working the support desk (/api/helpdesk). Before you call any of them, read Concepts — the exclusivity rule in particular changes what a 409 means to your retry logic.
Next: Concepts · Authentication · API reference