Integrate Eniac. It turns one natural-language query into a table of companies and people. Each row includes source URLs and a written rationale for every filter it passed. Do not invent rows. Only display rows returned by the API.
Base URL: https://eniac.floworks.ai
CREDENTIAL
Every request needs an Authorization: Bearer <credential> header. A session token and an API key both work: an API key starts lf_live_ and a session token starts eyJ, so you can tell at a glance which one you were handed. Take the credential from configuration or an environment variable. Never hardcode it and never invent one. If the user has not given you a credential, stop and tell them to sign in with Google at https://eniac.floworks.ai/?panel=keys, create an API key, and copy it once because it is shown only that one time. Do not try to obtain, guess or create a credential yourself, and do not start the search without one: the server refuses it and no run starts.
A search is a server-side job, so there are two calls at minimum: start it, then follow it. There is no blocking single-call endpoint. Do not use a WebSocket; there is none.
1. START THE SEARCH
POST /v1/searches
Content-Type: application/json
{"q":"<the user query, 3 to 2000 characters>","target":50,"speed":"fast","relax":true,"sites":""}
- q is required.
- target is an integer from 1 to 50, and 50 is both the default and the maximum. target drives cost: the server keeps searching until it reaches that many rows, so a higher target means more web searches and a larger bill. Send a lower number when the user asks for a quick look.
- speed is "fast" or "advanced". Send "fast" unless the user asks for advanced. If speed is omitted, the server uses "advanced". Fast reads fewer pages.
- relax is a boolean and defaults to true. true loosens a filter slightly when almost nobody passes it. false keeps the original filters for the whole run.
- sites is optional. Domains such as "linkedin.com, x.com" keep every search on those sites, up to 20. A phrase such as "job portals, case studies" prefers those page types. Leave it out, or send "", to search the web. The names linkedin, x, and twitter are accepted.
- Do not put the credential in the body. It goes in the header.
The response is 202 with exactly two fields:
{"id":"<32 hex characters>","status":"running"}
Keep the id. It is the only handle on the run.
402 Payment Required means the user's balance cannot cover the $5.00 held before a search starts. The body is {"detail":{"error":"insufficient_balance","message":"...","availableMicrousd":N,"availableUsd":"...","heldMicrousd":N,"neededMicrousd":N,"neededUsd":"..."}}. Show detail.message and stop. Do NOT retry: nothing about the request was wrong and a retry fails identically until the user tops up or a run in flight releases its hold.
422 means a field failed validation (q outside 3 to 2000 characters, target outside 1 to 50). 400 means q was only whitespace. 401 means the credential is missing or does not resolve. 403 means the account is closed. Report these and stop; none of them is worth retrying.
There is no idempotency key. If the POST times out on your side, call GET /v1/searches and look for the run before sending it again, or the user pays for two searches.
2a. FOLLOW IT BY STREAMING (do this when the user wants live progress)
GET /v1/searches/{id}/events with the same Authorization header. Server-sent events.
- Each frame is an "id: <seq>" line, then a "data: {json}" line, then a blank line. The event kind is the "type" field inside the JSON, not an SSE event name.
- A line starting with ":" is a keepalive. Ignore it entirely.
- Remember the last seq you saw. If the connection drops, reconnect with the header Last-Event-ID: <that seq> (or the query parameter ?since=<that seq>) and the server sends only what you missed. Do NOT reconnect without a cursor: replaying from zero duplicates every row you already handled, and resuming from "now" loses what happened while you were away, silently.
- Advance your stored seq only after you have handled the event, so a crash replays one event rather than skipping it.
- Do not use EventSource. It cannot send an Authorization header. Use fetch with a stream reader in a browser, or a streaming HTTP client elsewhere.
- The server closes the stream once the run is terminal and you have caught up. The stream ending is not proof the search succeeded. Confirm with GET /v1/searches/{id}. A run can take many minutes, so set a long read timeout on this request and expect quiet stretches punctuated by keepalives.
- Events expire 24 hours after they are written. Collect the result inside a day.
2b. OR FOLLOW IT BY POLLING (do this for a script that only wants the table)
GET /v1/searches/{id} every few seconds, not every few hundred milliseconds. It returns the run, including "status", "result", "where" and "usdDisplay".
"result" is the final event once the run is terminal, and null before then. "usdDisplay" is the charged figure to show when you are polling and never saw a cost event.
3. TERMINAL STATES. There are exactly five, and status is "running" until one of them:
- done: the run finished. result.met says whether it reached target.
- stopped: it was asked to stop and unwound early.
- budget_exhausted: it hit a spend ceiling, or the user's balance ran out mid-run. Charged for what it spent.
- interrupted: the service ended it. The process went away, or the run passed its deadline, published as deadlineAt and an hour out on the hosted service (then stopReason is "deadline"). The run field "where" says how far it got.
- error: it failed. The reason is an error event in the stream, not a field on the run.
Stop polling on any of those five. Do not treat anything other than "done" as an empty failure: stopped and budget_exhausted carry a partial table of rows that passed the full funnel, usually with result.met false, and those rows are already paid for. Equally, do not assume a terminal run has a result: result can be null, so check the field and not the status.
4. TO CANCEL
POST /v1/searches/{id}/stop -> 202 {"id":"...","status":"running","stopRequested":true}
status is the run's status at that moment, so on a live run it still reads "running": the stop is a request. Keep reading or polling. The run lands as "stopped" within seconds with whatever had passed the funnel. Closing the stream does NOT cancel anything; the run keeps going and keeps costing money.
EVENTS TO HANDLE
- final: the result. table is the rows. target is the requested count. met is true when enough rows were found. filters is [{index, title, criterion}] in funnel order. stopped is true, with stopped_at naming the stage, whenever the run ended early for ANY reason, so do not read it as "the user cancelled" on its own; the run's status says which reason it was.
- cost: {stage, usd}. usd is the running dollar total to show the user. Replace the previous number. It is not a delta. The last cost event is the final cost. Do not show any other cost or usage breakdown.
- error: {message}. Show it and stop.
- progress: {have, target, round, leads}. Optional live count. leads uses the same row shape as final.table.
- done: the last event of a run that got to write one. An interrupted run can end without it, so never make done the only thing that ends your loop.
Ignore every other type, including usage, plan, status, funnel_open, funnel_filter_start, funnel_filter_done, and filter_relaxed, unless you are building a live progress grid.
EACH ROW IN final.table (and in result.table)
- name (string)
- entity_type: company, professional, researcher, product, clinician, lawyer, book, paper, music_video, exhibition, conference, freelancer, or news
- details (string)
- intent_signal (string)
- discovery_rationale (string)
- rationale (string, latest filter)
- source_urls (array of URL strings)
- supporting_passages: [{text, url, score, published}]
- by_filter: [{index, title, rationale, rationale_mode, verification, relaxed, passages:[{text, url}]}]
index matches final.filters[].index. Show by_filter rationales in full, one column per filter.
Collect page links from passages[].url. Use source_urls only when a rationale has no passage URL.
- relaxed: string[], present when a loosened filter was required
- status (string)
- round (integer, 1-based)
If met is false, tell the user how many rows came back versus target. Do not pad the table.
Date ranges in the query are applied by the server. Do not add date fields to the request.
Do not call /api/runs, /api/runs/ws or /api/search. Those endpoints have been deleted and do not answer.
Integrate
Agent prompt
Paste this into a coding agent to integrate Eniac.
Paste the block below into Cursor, Claude, or another coding agent. It is enough to integrate the API without the rest of these docs open.

