Documentation

Cookbook

Working recipes you can paste and run.

Each recipe below composes endpoints from the API reference into a sequence that does one thing. $FLOWFINDS_ORIGIN is the origin the engine is served from; get and post in the Python and JavaScript samples are the session-carrying helpers from the first recipe.

Hold a session across calls

Every session-scoped endpoint resolves through the ff_session cookie. A client that discards it is a new visitor on every request, with no hold and no claim.

#!/bin/sh
set -e
JAR=./flowfinds.jar
ff() {
  method=$1; path=$2; body=${3:-}
  if [ -n "$body" ]; then
    curl -s -c "$JAR" -b "$JAR" -X "$method" \
      -H 'Content-Type: application/json' -d "$body" "$FLOWFINDS_ORIGIN$path"
  else
    curl -s -c "$JAR" -b "$JAR" -X "$method" "$FLOWFINDS_ORIGIN$path"
  fi
}

ff GET /api/find
ff GET /api/offer
  • The first response sets the cookie; every later call must send it back.
  • Nothing else authenticates: there is no API key, bearer token or signature to add.
  • Signing out with POST /api/logout clears the association with the account, not the cookie itself.

Branch on status, not on the HTTP code

The engine treats a non-200 answer as 'no API here', so most refusals arrive as HTTP 200 with a named reason. A client that only checks the status line will read a refusal as a success.

import json, urllib.request, http.cookiejar

jar = http.cookiejar.CookieJar()
http_client = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))

def get(path):
    with http_client.open(ORIGIN + path) as r:
        return r.status, json.load(r)

status, body = get("/api/offer")

if isinstance(body, dict) and body.get("status") == "refused":
    # failed_leg says which leg failed: "research" or "pricing".
    print(body["failed_leg"], body["reason"], body["explanation"])
elif body.get("status") == "offer":
    print(body["offer"]["our_price_usd"], body["freshness"])
  • GET /api/reasons serves the whole closed set of reason codes with their sentences, so a client can enumerate rather than hardcode.
  • not_yet_researched is not the same as 'no viable offer'. Collapsing them would let an unrun script report itself as a market conclusion.
  • refresh_failed being true means the observed listings could not be re-read; the dated observation survives and says so.

Poll a store build to a terminal state

Store generation is asynchronous. The GET endpoint returns the running stage while a job is live, and one of four terminal states once it is not.

TERMINAL = {"ready", "failed", "not_started", "not_claimed"}

def wait_for_store(get, sleep, timeout_s=600):
    deadline = time.monotonic() + timeout_s
    while time.monotonic() < deadline:
        _, body = get("/api/store")
        if body.get("status") in TERMINAL:
            return body
        sleep(5)
    raise TimeoutError("store build did not reach a terminal state")

store = wait_for_store(get, time.sleep)
if store["status"] == "failed":
    # The generator's own words, not a generic apology.
    print(store["failed_stage"], store["reason"], store.get("diagnostic"))
  • A second POST /api/store while a build is running does not start a second build; the job lock is per product.
  • ready is only returned when the store exists on disk and its browsable path resolves. A manifest alone is the generator grading itself.
  • A finished job with no manifest is failed, never not_started — reporting it as not_started would lose the attempt.

Apply a guided edit without applying the wrong one

An apply that trusts its own request can commit a change the founder never saw. The engine requires the proposal to be echoed back, and refuses one that has gone stale.

proposal = post("/api/intent", {
    "surface": surface,
    "requested": "Lead with the delivery guarantee",
})

if proposal.get("status") == "refused":
    # no_row_justifies_this means the engine has no dated row to stand on.
    raise SystemExit(proposal["reason"])

applied = post("/api/intent/apply", {
    "surface": surface,
    "requested": "Lead with the delivery guarantee",
    "proposed": proposal,
    # Only needed when the engine says the change cannot be undone.
    # "confirm_irreversible": True,
})

if applied.get("reason") == "stale_proposal":
    # Re-propose and show the founder what changed before retrying.
    ...
elif applied["status"] == "applied":
    print(applied["because"], applied["source_row"], applied["reversible"])
  • stale_proposal and no_proposal_echoed are 409s. They are the point of the round trip, not incidental failures.
  • The applied response carries because and source_row read back from the record, so your interface can show the justification rather than restating the request.
  • POST /api/intent/revert undoes the last reversible intent and names what it reverted from.

Take an order and credit it only once the provider agrees

A browser that lands on a success URL is not evidence that a payment happened. Confirmation asks the provider.

started = post("/api/store/checkout", {"product_id": product_id})
# charged_yet is False. Opening a session is not a payment.
assert started["charged_yet"] is False
reference = started["reference"]
send_customer_to(started["pay_url"])

# Later — after the provider's redirect, or on a schedule.
_, confirmed = get(f"/api/store/confirm?ref={reference}")
  • credited_to is the owner recorded when the order opened, and that is the account the confirmation credits — not whoever owns the product later.
  • Repeated confirmation of the same reference is one ledger entry, not additional sales.
  • GET /api/adbalance/confirm answers 410 Gone: balance confirmation is no longer client-driven, for the same reason.
  • Where payments are not configured, checkout answers 503 with payment_unavailable rather than pretending a control works.

Read a store's funnel without over-reading it

Funnel counts describe what happened. They do not establish why, and inconsistent counts must not produce a plausible-looking rate.

metrics = get(f"/api/editor/metrics?product_id={pid}&days=14")[1]

sessions, cart = metrics["sessions"], metrics["cart"]
checkout, paid = metrics["checkout"], metrics["paid"]

# Compute a rate only when the stages are internally consistent.
consistent = sessions >= cart >= checkout >= paid
cart_rate = round(cart / sessions * 100, 2) if consistent and sessions else None

# Under a hundred sessions, treat it as a small sample and investigate
# before claiming a cause. This is the same threshold the commerce
# agent's store_funnel tool applies to its own output.
  • Paid browser events are unverified. Use GET /api/revenue for actual sales and revenue.
  • A store whose metrics cannot be fetched must not be treated as zero traffic.
  • The commerce agent's store_funnel tool applies exactly these rules and attaches them as stated limitations to every result.

Ask the commerce assistant and use only what it cited

The assistant's answer is only as good as the evidence behind it. The response tells you what it read, whether reading worked, and whether the answer came from the model at all.

const res = await fetch(`${ORIGIN}/api/intent`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ text: "What is my break-even CPA?", surface: "home" }),
  credentials: "include",
});

const body = await res.json();

if (res.status === 401) throw new Error("No commerce identity for this session.");

// degraded means the model path failed and the deterministic fallback answered.
// The answer is still honest; it is not reasoned, and it says so.
if (body.degraded) markAsFallback(body.message);

// evidence lists only the tool results the answer actually cited.
for (const item of body.evidence ?? []) {
  console.log(item.source, item.status, item.observed_at);
}

// uncertainties is what the agent could not establish. Show it.
for (const u of body.uncertainties ?? []) console.log("unresolved:", u);
  • surface must be home or supplier to reach the agent; omitted, the request goes to the navigation intent resolver instead.
  • A message over 3,000 characters is refused with a clarification rather than truncated.
  • kind: "route" only comes back when the founder explicitly asked to navigate. A question is answered in place.
  • run_id and engine identify the run and the version, so an answer can be traced to a recorded run.

Enumerate the surface instead of hardcoding it

Reason strings and control paths change. Both are served, so a client can read them rather than embed them.

reasons = get("/api/reasons")[1]      # {"reasons": {...}, "count": n}
controls = get("/api/controls")[1]    # {"controls": {"POST /api/select": ...}}

# Render a refusal using the engine's own sentence rather than your copy.
def explain(body):
    return body.get("explanation") or reasons["reasons"].get(body.get("reason"), "")

# Discover which endpoint sits behind a control, rather than inferring
# it from a button label.
method, path = "POST /api/intent/apply".split(" ", 1)
  • The reasons table holds roughly seventy codes and grows with the engine; count reports its size.
  • Control keys are literally METHOD /path strings, so they can be split and used directly.
  • Neither endpoint requires a session.

Next

For the longer sequences these recipes fit into, read the guides. For the contracts they rely on, read the API reference. For what the assistant can and cannot read, read agents and tools.

Next: Guides · API reference · SDKs and CLI