Documentation

Errors

Every error code the API returns, and how to handle each one.

Errors in this API are named, enumerable and mostly not HTTP errors. Two properties matter more than any individual code, and getting them wrong is the usual cause of a brittle client.

Two rules before the table

1. Branch on reason, not only on status

A great many refusals are returned with HTTP 200. That is deliberate: the request was well-formed, it was understood, and the answer is a fact about your account or the catalogue rather than a protocol failure. “Every product is spoken for right now” is not a 4xx. The same reason code can also carry different HTTP statuses on different endpoints, so reason is the stable key.

{
  "status": "refused",
  "reason": "claimed_by_another_founder",
  "explanation": "Another founder claimed this one first. Your other candidates are still yours to take."
}

status is one of refused, error, abstained, or a domain state such as ok, proposed, not_claimed, quiet, attention. Some routes on the proof surface use the shape {"error":"not_found","reason":…} instead; handle both.

2. The vocabulary is served, so generate against it

GET /api/reasons returns the complete registry — every code the server can emit, with its sentence — and it is asserted in both directions: a code the server can emit but has not declared fails the build, and a code declared but unreachable fails it too. So a client can be generated from the live registry rather than from a list a human transcribed.

curl -s https://flowfinds.ai/api/reasons | python3 -c '
import json, sys
d = json.load(sys.stdin)
print(d["count"], "reason codes")
for code, sentence in sorted(d["reasons"].items()):
    print(f"{code:38} {sentence}")'

The table below is written from that registry, grouped by the surface that raises each code. Where a code is emitted by more than one endpoint with a different HTTP status, both statuses are given.

Every reason code

Account and sign-in

Raised by /api/account, /api/login/request, /login and every account-scoped surface.

ReasonStatusCauseWhat to do
account_required400 · 409This needs an account, which takes one email.400: the string cannot be an email address — fix the input. 409: the address already belongs to a verified account, so it is never bound from a form. A sign-in link has been sent; open it.
invalid_link303 to /#login?e=invalid_linkThat sign-in link has expired or was already used.Request a new one. Expired, spent and never-existed are deliberately one answer, so do not try to distinguish them.
no_account401A product edit must belong to a signed-in account.Bind an account with POST /api/account, or sign in.
no_account_yet200Claim a product or save your account before this exists.A state, not a failure. Render the empty surface honestly and offer the action that creates it.
invalid_email400The account email is missing or invalid.Send a syntactically valid address. Shape is checked; deliverability is not claimed.

Claiming and exclusivity

The exclusivity contract, speaking. Most of these are terminal for the id involved.

ReasonStatusCauseWhat to do
not_shown409You can only claim a candidate you were shown.Do not synthesise product ids. Claim only from what /api/find or /api/feed returned to this session.
claimed_by_another_founder409Another founder claimed it first.Terminal. Never retry this id — move to another candidate. Your other candidates are unaffected.
held_by_another_founder409Another founder is claiming it right now.A lost race at the moment of insert. Fetch a fresh candidate rather than retrying in a loop.
passed_by_you409You let this one go, and passing is final.Terminal by design. Passing is not reversible.
all_held200Every product is spoken for by other founders right now.Try later. Distinct from the three below — do not collapse them.
passed_out200You passed on everything we could show you.No exit exists for this session's catalogue. Say so plainly.
catalog_exhausted200There is nothing left in the catalogue.Nothing to retry.
claim_limit_reached200You are at your allowance; sharing unlocks one more.This one has an exit. Surface the share action rather than reporting emptiness.
not_servable200Your product is out of the served set right now.Read, do not write. The dashboard reports this rather than pretending the product is gone.
unknown_product400 · 200We do not have that product.The id is not in the catalogue at all.
no_product200 · 400 · 409This session has no product yet.Resolve a product first. Note the status varies by endpoint — branch on reason.
no_claim400An outcome is a fact about a product you own; a hold is not ownership.Claim before recording outcomes. A hold is explicitly not enough.
not_claimed200No product is resolved for this session at all.The first-run state of /api/dashboard. Not an error.

The store

Generation is asynchronous; publication is a verification, not a label.

ReasonStatusCauseWhat to do
not_yet_researched200The store has not been started.Start a build.
not_started409Your store has not been built yet, so there is nothing to edit.Build before editing or publishing.
build_in_progress200A build is already running for this product.Poll the job state instead of starting a second build.
generation_failed200The store could not be generated.Ours, not yours. The job reports failed; retry or contact support.
verify_failed500We could not re-check your store, so we are not calling it published.The pages may still be fine — publication merely could not be confirmed. Retry.
unknown_section200 · 400This store has no such section to lead with.Read the section list from the store before ordering it.
no_section400An ordering with no section named is not an ordering.Name the section.

Intents and guided editing

The evidence rule, enforced. See Concepts for the shape of a proposal.

ReasonStatusCauseWhat to do
not_an_intent200That is a pixel value, not an intent.Describe the effect, not the execution. A request carrying px/pt/em/rem/% or a hex colour is refused, because a dimension cannot be justified by an evidence row.
no_row_justifies_this200No dated evidence would justify that change.Not a bug. The change would be a guess at your buyer, so it is declined rather than made.
no_proposal_echoed409Send back the proposal you are accepting.Echo the intent_change you were served, unmodified.
stale_proposal409The evidence moved since that proposal was shown.Request a fresh proposal and show the new because and source_row before applying.
nothing_to_apply409That proposal changes no surface.Nothing to do.
nothing_to_revert409You have not applied a change to this product.Nothing to undo.
irreversible_without_confirmation409This cannot be undone without explicit confirmation.Confirm explicitly. Read the reversible flag on the proposal, and GET /api/controls for the endpoint's consequence.
intents_book_unreadable500The applied-intent record is unreadable.Ours. Report it.
apply_build_raised · revert_build_raised · order_build_raised500The regeneration after an apply, revert or reordering failed.Ours. The intent was understood; the rebuild was not completed.

The AI editors

POST /api/ai/edit and POST /api/growth/edit. These are the metered surfaces.

ReasonStatusCauseWhat to do
no_message400An edit with nothing asked is not an edit.Send a non-empty message.
message_too_long400That instruction is longer than the editor accepts.Shorten it, or split it into two edits.
cli_unavailable200 · 500The editor's AI is not available on this machine, so nothing was changed.Environmental, and deliberately distinct from a billing misconfiguration. Nothing was spent and nothing was altered.
cli_failed200 · 500The editor's AI could not be run, so nothing was changed.The run was attempted and did not complete. Retry once, then report.
editor_unavailable200The editing model refused, and said why.Show the accompanying explanation to the person; it is the model's own reason.
unbacked_claim200Your product record cannot back that wording.The same evidence rule as intents, applied to copy. Add the record or change the wording.
unknown_field · unknown_diff · unknown_version · no_diff · too_long200 · 400The requested edit does not name a field, shape, version or length this store has.Read the store's current shape before constructing an edit.
nothing_to_undo · nothing_to_redo · no_history · no_direction200Version navigation with nothing to navigate, or no direction given.Say undo or redo, and check history exists first.
invalid_platform400That is not a Facebook or TikTok ad.Only those two platforms are accepted here.
invalid_editor_response200The ad editor returned no safe editable fields.The model's output failed validation, so nothing was applied to the preview.

Usage and the meter

Declared in the meter itself. See Rate limits for windows, buckets and multipliers.

ReasonStatusCauseWhat to do
usage_limit_reached429You have used this feature's allowance for now.Back off until resets_at (epoch seconds, in the body — there is no Retry-After header). Other features are unaffected; the response names them.
weekly_limit_reached429You have used this feature's weekly allowance.Do not wait this one out in code. Surface it, with the referral and upgrade paths the body carries.
unknown_tier400We do not have that plan.Use a tier the meter recognises.
billing_unconfigured500Payments are not configured here, so no plan was changed.Deployment configuration. Deliberately distinct from cli_unavailable so an unconfigured billing key never reads as a broken AI editor.
unsigned_callback400A billing callback without a valid signature changes nothing.Sign the callback, or ignore it — an unsigned one is not a payment event.
payment_unavailable200SimplePay could not start, so nothing was charged.No money moved. Retry the checkout.
legacy_checkout_retired410That checkout link belongs to a store we no longer serve.Terminal. Start a new payment through the current flow; an old bookmarked callback must never credit a balance.

The support desk

GET and POST /api/helpdesk, and the per-store support settings.

ReasonStatusCauseWhat to do
ticket_not_found400No ticket on your desk carries that id.Read the desk first. Tickets are founder-scoped.
unknown_action400This desk takes set, reply, note or draft.Use one of the four.
bad_field400Only a ticket's status, priority or category can be set.Other fields are not settable through this endpoint.
resolution_required400No ticket closes without a documented resolution.Enforced here, not only in the screen. Supply the resolution text.
empty_reply · empty_note400There was nothing to send, or nothing to record.Send content.
unknown_op400This endpoint takes add or delete.Used by the keyword-rule surface.
no_question · empty_question · no_query_terms200A request with no question is not a question.Ask something. Returned as status refused or abstained depending on the surface.

Products, outcomes and analysis

The My Products editor, outcome labelling, and analyse-any-product.

ReasonStatusCauseWhat to do
invalid_name · invalid_description · invalid_price400The value is outside the accepted length, or not a valid positive amount.Validate before sending; the server validates again regardless.
invalid_images400The product images are invalid, unsafe or do not belong here.Images are checked for more than type. Re-encode from an original you control.
images_too_large413The product image upload is larger than the editor accepts.Compress or resize before upload.
nothing_to_record400An outcome with no fields records nothing — absence is not a value.Send at least one field. A bodyless outcome is refused rather than stored as zero.
invalid_outcome400Spend and revenue must be non-negative numbers.Send numbers.
no_claim_record400We have no claim record to attach that outcome to.The claim must exist before an outcome can hang off it.
nothing_to_analyze400Send a product name or description.The analyser needs a subject.
no_product_id400A choice with no product named is not a choice.Name the product.
not_presearch_qualified200The product no longer clears the Pre-Search evidence gate.Evidence aged out. Not recoverable by retrying.
no_worker200The 3D worker is not running, so this product stays a photograph.Environmental. Render the photograph; do not show a broken 3D control.
no_orders_yet · no_tickets_yet · nothing_to_watch200The surface is genuinely empty.States, not errors. An empty support desk means the agent handled everything, not that nothing happened.
order_not_found400No order of yours carries that id.Orders resolve through the same founder-scoped resolver as everything else.

Sourcing, domains and the proof surface

Supplier discovery, domain purchase, and the public free-fix pages.

ReasonStatusCauseWhat to do
not_available200The domain is unavailable or its price cannot be verified.One reason for both, because we will not sell a domain on an unverified price.
sample_unpriced409The product has no documented seller price for a sample.No invented figure will be substituted.
delivery_required409Sample checkout requires an email and complete delivery address.Complete the address.
unknown_report404The proof token does not name a retained report.Reports are retained for a period; an old token is not a weaker credential, it is not one.
unknown_profile404The token does not name a prospect proof page.Returned as {"error":"not_found","reason":"unknown_profile"}.
unknown_page404That page is not available.Also the shape returned when a page was removed by its owner.
unknown_event404That is not a declared first-party proof event.Only declared events are accepted.
detail_required400A correction must name what is wrong.Supply the detail.
schedule_not_found200There is no active conditional launch for this product.Nothing scheduled.

The curriculum layer

Abstentions, not failures. The layer declines rather than answers unsupported.

ReasonStatusCauseWhat to do
not_ingested200The curriculum index has not been built yet.Environmental.
no_passage_meets_threshold200No passage in the curriculum supports that.An abstention. Show it as one — the layer answered honestly rather than confabulating.

Ours

One code, one meaning: something on our side broke.

ReasonStatusCauseWhat to do
internal500Something broke on our side; nothing you did caused it.The dispatch guard's answer when a route raises. Retry once; if it persists, report it with the time and the path. It is the one reason no handler emits on purpose.
saved_not_published500Your work was saved, but publication did not complete.Do not re-enter the data. Retry the publish.

HTTP statuses, and what each one means here

StatusMeaning in this APIRetryable
200Understood. May still carry status: refused — read the body.Depends on the reason
302 / 303A human-facing redirect: referral entry, and sign-in link redemption. The cookie rides the redirect.n/a
400The request itself is malformed or incomplete.Not without changing it
401An account is required and this session has none.After signing in
404A named thing does not exist: an unknown proof token, a removed page, or a preview for a product with no live offer.No
409A conflict with state that already exists — a claim, a pass, a stale proposal. Usually terminal for that subject.Rarely; read the reason first
410A retired endpoint. Permanently gone, with the current path named.No
413An upload exceeds what the editor accepts.After shrinking it
429A usage bucket is exhausted. See Rate limits.After resets_at
500Ours. Always JSON, never an HTML error page — one dispatch guard wraps every route so a crash cannot answer in a format your parser will choke on.Once, then report

A handler that ages well

TERMINAL = {
    "claimed_by_another_founder", "passed_by_you", "not_shown",
    "catalog_exhausted", "passed_out", "legacy_checkout_retired",
}

def handle(response, reasons):
    body = response.json()
    reason = body.get("reason")
    if body.get("status") not in ("refused", "error") and "error" not in body:
        return body                                  # a real answer, or a domain state

    # The registry is the source of truth for the sentence. Never invent copy for a
    # code you do not recognise — print what the server says it means.
    sentence = reasons.get(reason, body.get("explanation", "refused"))

    if reason in TERMINAL:
        raise Terminal(reason, sentence)
    if response.status_code == 429:
        raise Throttled(reason, sentence, resets_at=body["resets_at"])
    if reason == "internal":
        raise ServerFault(sentence)
    raise Refused(reason, sentence)

reasons = requests.get(f"{BASE}/api/reasons").json()["reasons"]

The important line is the fallback. A code your build has never seen still has a correct sentence, because the sentence came from the same server that raised it.

Next: Troubleshooting · Rate limits and quotas · API reference