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

  • curl with 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/reasons

The 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=31536000

The 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:

StatusBodyWhat 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/home

A 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