Floqer Public API — Complete Reference for AI Agents API endpoint base URL: https://api.floqer.com/api/v1 (for /api/v1/... endpoint paths in this file; all other URLs are absolute) Authentication: Bearer token. All requests require Authorization: Bearer floq_YOUR_API_KEY Rate limits: 200 requests/minute, 10,000 requests/day per API key. ================================================================================ FILE MAP — READ IN THIS ORDER ================================================================================ New here and about to BUILD or DEBUG a workflow? Read https://floqer.com/docs/docs.txt first — the short front door with the task reading order and the core build rules. This file is the deep endpoint reference you jump into for exact request/response shapes. https://floqer.com/docs/llms-full.txt — You are here. The master reference. https://floqer.com/docs/concepts.txt — How Floqer works. Mental model, vocabulary, data types. https://floqer.com/docs/action-catalog.txt — Find the right action for your data. https://floqer.com/docs/action-detail/{id}.txt — Full spec for a specific action. https://floqer.com/docs/source-catalog.txt — Find the right data source to import records into Floqer. https://floqer.com/docs/source-detail/{id}.txt — Full spec for a data source (preview/create body, filter fields, lifecycle). https://floqer.com/docs/use-case-catalog.txt — End-to-end workflow recipes indexed by GTM problem. https://floqer.com/docs/use-case-detail/{slug}.txt — Full recipe for one use case (chain of actions + narrative). https://floqer.com/docs/openapi.json — Machine-readable OpenAPI 3.0 spec. Use when you have OpenAPI tooling; otherwise stay in this file. https://floqer.com/docs/docs.txt — START HERE. Front-door orientation: the build-task reading order (know-the-customer → concepts → use-case-catalog → action/source details → llms-full) plus the core build rules. https://floqer.com/docs/reference — HTML page (not plain text). Human-readable single-page API reference with every endpoint, parameters, schemas, and curl examples. Open in a browser. ================================================================================ REQUIRED READING — https://floqer.com/docs/concepts.txt ================================================================================ Before using the endpoints below, fetch https://floqer.com/docs/concepts.txt. It is the single source of truth for Floqer's mental model and vocabulary: 1. Workflows and Sheets — container model, main sheet, sheet-scoped paths 2. Inputs — columns, types, reference strings 3. Actions — action_id vs action_instance_id, reference outputs 4. Data Types — scalars (string, url, email, number, boolean) for inputs and outputs; nested types (raw_array, json, structured_array) for action outputs only 5. Variable References — {{input.field}} and {{action_id.field}} syntax; structured_array column refs ({{action_id.list.column}}) 6. Action Chain — execution order, filter, push_data_to_sheet 7. Running a Workflow — add rows, run, check results, cache settings 8. Get Action Field Options — resolve dynamic dropdowns before configuring an action 9. Conditional Execution (run_if) — per-action row gating 10. Data Sources — list/preview/create/get data/sync/pause sources (CRM, list builders, monitors, pixels), selection + pull_mode lifecycle 11. Get Source Field Options — resolve dynamic dropdowns for source payloads 12. Caller Identity — GET /api/v1/user bootstrap (who the API key belongs to, org membership); org knowledge file (GET/PUT /api/v1/user/knowledge) The endpoint reference below assumes this mental model. Do not skip it. ================================================================================ QUICK START — BUILD AND RUN A WORKFLOW ================================================================================ 0. Confirm caller identity (recommended first call) GET /api/v1/user → returns {email, first_name, last_name, role, org_id, org_members} No scope required. See concepts.txt §12. 1. Create a workflow POST /api/v1/workflows {"name": "Lead Enrichment Pipeline"} → returns workflow_id (this is also the main sheet's sheet_id) 2. Add inputs to the main sheet POST /api/v1/workflows/{workflow_id}/sheets/{workflow_id}/inputs [{"name": "linkedin_url", "type": "url"}, {"name": "company_name", "type": "string"}] → returns inputs with reference strings: {{input.linkedin_url}} 3. Find the right action Read https://floqer.com/docs/action-catalog.txt — match your input types to action "Needs" 4. Add an action to the main sheet POST /api/v1/workflows/{workflow_id}/sheets/{workflow_id}/actions/add {"action_id": "enrich_company_linkedin_profile"} → returns action_instance_id, inputs to configure, outputs with references 5. Configure the action PATCH /api/v1/workflows/{workflow_id}/sheets/{workflow_id}/actions/{action_instance_id} {"inputs": {"linkedin_url": "{{input.linkedin_url}}"}} 6. Add data rows (start with 10–50 during build; use run_after_add="first_10" to verify safely) POST /api/v1/workflows/{workflow_id}/sheets/{workflow_id}/rows {"rows": [{"linkedin_url": "https://linkedin.com/company/floqer", "company_name": "Floqer"}], "run_after_add": "first_10"} → returns {row_ids: [...], rows_queued_for_run: 1} 7. Run POST /api/v1/workflows/{workflow_id}/sheets/{workflow_id}/run {"row_ids": ["returned-row-id"]} 8. Check results POST /api/v1/workflows/{workflow_id}/sheets/{workflow_id}/rows/list {"row_ids": ["returned-row-id"]} // or empty body to browse all rows paginated ================================================================================ ENDPOINTS — BUILD A WORKFLOW ================================================================================ Inputs (sheet-scoped) --------------------- GET /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/inputs — List Inputs Returns current input fields with reference strings. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/inputs — Add Inputs Body: JSON array of {name, type, description?} Response: [{name, type, description, reference}] Declared `type` is metadata only — NOT enforced at row-ingest time. A field declared `"type": "number"` will accept the string `"Us"` (or any other value) without coercion or rejection on Add Rows / webhook ingest (see Add Rows note below) — the value is even stored back as a string (`5` round-trips as `"5"`). Type mismatches surface at downstream action runtime, not at ingest. Defensive pattern: coerce in JS formatters (`Number(String("{{...}}") || "0") || 0`). Verified 2026-06-08. DELETE /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/inputs/{field_name} — Delete Input Removes an input. Breaks action references to {{input.field_name}}. Actions (sheet-scoped) ---------------------- POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/add — Add Action to Workflow Adds an action to the chain. Returns action_instance_id, inputs to configure, and outputs with reference strings. Body: {action_id, after?, name?} `after` (optional) is the immediate predecessor at insertion time — the new action goes right after the named anchor and everything previously downstream of that anchor shifts down by one. To chain N actions sequentially in a single batch, set `after` of each call to the `action_instance_id` returned by the previous call. Repeated calls with the same `after` value (e.g. `after: "input"` N times) produce a chain in REVERSE of call order, because each new action is inserted immediately after the anchor and pushes the previously-inserted one further down. `name` (optional) overrides the action template's default display name. Whitespace-only values are ignored. The actual stored value is returned in `display_name` on the response. RESPONSE IS NOT THE CANONICAL INPUT LIST. The add response may OMIT some of the action's inputs — pre-filled or hidden ones the UI sets automatically. E.g. `hubspot_update_object` omits `hubspot_object_id` from the add response, but a subsequent GET Action returns it. Don't treat the add response's `inputs[]` as the full set; GET the action (or read its action-detail/{action_id}.txt) to see every input. Verified 2026-06-08. PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id} — Configure Action Wires input variables to the action's fields. Body: {inputs?, run_if?, continue_workflow_if_action_fails?, note?} `run_if` (optional) gates whether this action runs for a row. It is an array of AND/OR condition GROUPS — the same shape the `filter` action uses for `path_conditions`: run_if: { conditions: [ { conditions: [ { variable: "{{input.country}}", operator: "is", values: ["US"] }, { variable: "{{input.tier}}", operator: "is", values: ["A"], combinator: "AND" } ] }, { conditions: [ { variable: "{{input.name}}", operator: "starts with", values: ["a"] } ], combinator: "OR" } ], continue_workflow_if_run_condition_not_met: false } • Outer array = groups, joined by each group's `combinator` (AND/OR). Each group's `conditions` = leaf conditions, joined by each leaf's `combinator`. The first group's / first leaf's `combinator` is ignored; omitted defaults to AND. • A single condition is just one group with one leaf. • `operator` is the comparison (`is`, `greater than`, ...); `combinator` is the AND/OR join between conditions. Operator support depends on the variable's stored type — see concepts.txt §9 for the full list. • `continue_workflow_if_run_condition_not_met` lives INSIDE `run_if` (default false), NOT at the body's top level — don't confuse it with the top-level `continue_workflow_if_action_fails`. Verified 2026-06-08. • Pass `run_if: null` to clear an existing condition. `run_if: {}` is ACCEPTED as a no-op (leaves any existing condition untouched) — it is NOT rejected; use `null`, not `{}`, when you actually want to clear. Verified 2026-06-08. • Legacy: the old flat single-condition form `{variable, operator, values}` is still ACCEPTED on write for backward compatibility (NOT rejected) — it is auto-normalized to the nested form — but is no longer documented; use the nested `conditions` form. Writing it returns a non-fatal `warnings[]` entry (`code: "deprecated_format"`) asking you to migrate. Get Action and Get Action Graph ALWAYS return the condition in the nested form, whichever way it was written. Verified 2026-06-08. `continue_workflow_if_action_fails` (optional, top-level; defaults true) controls what happens to the REST of the chain when THIS action fails on a row. It has a clear, observable effect — don't treat it as a no-op: • `true` (default) — downstream actions still run even though this action's cell is `failed`/`error`. Use when the failing step is optional enrichment and later steps don't strictly need its output. • `false` — a failure here HALTS the chain for that row; downstream actions never run and their cells never appear in `cells{}`. Either way the row's `row_status` becomes the sticky `has_failures` (the failed cell is what trips it) — the flag changes downstream EXECUTION, not the row-level failure flag. It is a top-level body field; do NOT confuse it with `continue_workflow_if_run_condition_not_met`, which lives inside `run_if` and governs the run_if gate, not failures. Verified 2026-06-08. `note` (optional) saves a free-text note on the action — same upsert as Save Action Note. Whitespace-only values are ignored. PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/name — Rename Action Sets an action instance's display name. Single-field PATCH — the only body field is `name` (required, trimmed, non-empty). Body: {name: string} Renaming does NOT invalidate the row-level cache for this action — display name doesn't change what the action does. Only the workflow's `updated_at` timestamp is bumped. Reserved `action_instance_id` values (`"input"`, `"graph"`) return 404. Response: {status, data: {action_instance_id, display_name}}. PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/move — Move Action Re-orders an action within the sheet's chain. Updates the chain pointers (`nextAction` / `prevAction`) on the moved action, on its old neighbours (relinked so the chain skips it at the source position), and on its new neighbours (relinked so the chain points through it at the destination position). The action's configured inputs / outputs are not touched. Body: {after: string} -- required • "input" → move to the start (right after the synthetic input node) • → move immediately after that instance `after` cannot equal the action being moved. Cannot move `id1` (the input node — chain anchor); reserved values `"input"` / `"graph"` return 404. Reference safety: Move does NOT rewrite configured inputs. If the new position pushes the action ahead of one of its upstream references — or pushes a downstream consumer ahead of this action — those refs will fail at run time. Fix afterwards via Configure Action. Response: {status, data: {moved: true, action_instance_id, after}}. `after` in the response is the canonical public form — `"input"` when the action is now at the start of the chain. PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/waterfall/{field_name} — Reorder Waterfall Providers Re-orders the provider list on a waterfall (`stepDownSearch`) input field. Pass the snake-cased `field_name` from Add Action's `inputs[]` (e.g. `work_email`) and a `providers` array listing every currently configured provider `apiId` exactly once, in the desired execution order. Body: {providers: ["hunter", "prospeo", "findymail"]} Reorder only — does not add or remove providers. To change which providers are enabled, use Configure Action. Same cache invalidation as Configure Action (this action and every downstream action on the sheet). Response: {status, data: {reordered: true, action_instance_id, field_name, providers}}. DELETE /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id} — Delete Action Removes an action from the sheet's chain by relinking its previous and next neighbours so the chain skips it. The deleted action's configured inputs / outputs are gone. Cannot delete the input node — pass any other `action_instance_id`. Reserved values (`"input"`, `"graph"`) return 404. No request body. Response: {status, data: {deleted: true, action_instance_id, dependent_actions}}. `dependent_actions[]` lists every action whose configuration referenced one of the deleted action's outputs. Each entry carries `action_instance_id` plus an `inputs` object containing ONLY the affected field keys (snake-cased); values are the stored references translated to public reference form so the broken refs are obvious. Unrelated fields on the same dependent action are not included. Empty array when nothing depended on the deleted action. Reference safety: any field surfaced in `dependent_actions[]` now contains an `unresolved_reference` to the deleted action. Fix each via Configure Action before the next row run, otherwise those rows will fail. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/notes — Save Action Note Saves a free-text note on an action instance. Upsert against the composite primary key `(action_instance_id, sheet_id)` on `public_api_notes` — calling again for the same action overwrites the existing note. Notes are surfaced on Get Action and on each node of Get Action Graph as `note` (absent when no note has been saved). Body: {note: string} -- required, trimmed, non-empty Response: {status, data: {action_instance_id, sheet_id, note}}. GET /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/graph — Get Action Graph Returns every node in the sheet (input node + actions) in topological execution order, each with its {{reference}} strings and next-edge topology. Each node also carries `note` when one has been saved on the action via Save Action Note or Configure Action. GET /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id} — Get Action Returns a single action's node shape. Same schema as one node of Get Action Graph (so any saved note is included as `note`). Round-trips with Configure Action on the same URL. Includes validation warnings for broken references. Readback caveat for structured-output actions (`llm_models`, `llm_web_agents`): the `output_format` field in this response is a type DISCRIMINATOR string — `"fields"` for the structured-fields form, `"json"` for raw JSON — NOT the schema object you sent in Configure Action. The persisted schema is not echoed back through Get Action. To verify what fields the action will emit, look at `outputs[]` (or call Get Action Outputs on the same instance). POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/options/{field_name} — Get Action Field Options Resolves a dynamic dropdown for the named field. Required before configuring any action that has a dynamicDropdown / spreadsheetsDropdown / hubspot_properties_dropdown / externalIdDropdown / inSheetDropdown / etc. Body: {"context"?: {"": ""}} Some fields depend on other selections — e.g. hubspot_object_properties needs {"context": {"hubspot_object": "contacts"}}. Pass {} when no context is required. Response: {"options": [{value, label}, ...]}. Pass the chosen `value` directly into the matching field in Configure Action. See per-action HOW TO CONFIGURE in https://floqer.com/docs/action-detail/{action_id}.txt for the exact pre-call(s) each action needs. GET /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/outputs — Get Action Outputs Returns the resolved output schema for an action — every field it produces, each with a ready-to-paste `reference` token for downstream wiring. No request body. Static actions (Salesforce / HubSpot / etc.) carry their schema from the moment they're added — calling this on a static action is fine but not necessary; the same `outputs[]` is on Get Action / Get Action Graph. Dynamic-output actions (`http_api_call`, `raw_to_structured_array`) only know their output shape after a row runs. The discovery sequence is: 1. Configure the action with realistic inputs (Configure Action). 2. Add ONE example row whose data exercises the action, then Run Rows on just that row. The action's worker stores the upstream response under `action.ping`. 3. Call Get Action Outputs. The handler reads the captured `ping`, derives the field/column schema, mints stable `responseId`s, persists the new schema onto the action, and returns it. 4. ⚠ The example row from step 2 ran BEFORE the schema existed — its cell in List Rows will not surface the discovered outputs (the runtime had no response to associate values with). To get a row whose cell carries the typed outputs, either re-run the example row (Run Rows again) or delete it (Delete Rows) and run fresh rows. From this point on every new run sees the persisted schema. See https://floqer.com/docs/action-detail/http_api_call.txt and https://floqer.com/docs/action-detail/raw_to_structured_array.txt for the per-action walkthrough. Sheets (workflow-scoped) ------------------------ POST /api/v1/workflows/{workflow_id}/sheets — Create Sheet Body: {name, auto_run?, cache_enabled?, cache_since?} Returns: full sheet object {sheet_id, workflow_id, name, auto_run, cache_enabled, cache_since} GET /api/v1/workflows/{workflow_id} — Get Workflow Overview Bootstrap call after List Workflows. Returns workflow metadata (name, timestamps, last_run_at), sharing info (is_owner, shared_users), and a per-sheet roster with settings (auto_run, cache_*) and webhook URLs for pushing rows. data.sheets[0] is always the main sheet (sheet_id === workflow_id, is_main_sheet: true). Child sheets follow sheets_config.sheetsOrder when set, otherwise creation order. shared_users is populated only when is_owner is true; other callers get []. DELETE /api/v1/workflows/{workflow_id}/sheets/{sheet_id} — Delete Sheet Scope: workflows:write. Permanently deletes a child sheet and every row on it. Archives the sheet and its action chain; row data and run history on the sheet are no longer reachable. Cannot be undone. No request body. Pass the child sheet's UUID as sheet_id — not the parent workflow ID. Response: {status: 200, data: {workflow_id, sheet_id, deleted: true}}. Only the sheet owner can delete a child sheet. Shared users and org members with workflow access → 403 "Only the sheet owner can delete this sheet." Main sheet (sheet_id === workflow_id) cannot be deleted → 400 "Cannot delete the main sheet. Delete the parent workflow instead, or pass a child sheet_id." To remove the main sheet, delete the parent workflow via DELETE /api/v1/workflows/{workflow_id}. Unknown / already-deleted sheet_id → 404 "Sheet not found". Status codes: 200 / 400 (main sheet) / 401 / 403 (not sheet owner) / 404 (sheet not found) / 429. ================================================================================ ENDPOINTS — RUN A WORKFLOW ================================================================================ POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows — Add Rows Body: {rows: [{: , ...}], run_after_add?: "none" | "first_10" | "all"} Response: {row_count, row_ids: [...], rejected: [{row: {...original payload...}, errors: [...]}], rows_queued_for_run} No type validation — values written as-is; type issues surface at action runtime. Missing fields → null. Unknown fields → row echoed in rejected with error details. Partial success, never atomic. row_ids pipes directly into Run Rows's row_ids. run_after_add is scoped to THIS call's rows only — pre-existing rows on the sheet are not touched. Max 1,000 rows per call. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/run — Run Rows Body: {row_ids: ["id1", "id2"]} -- specific rows OR {first_10: true} -- shortcut: first 10 rows on the sheet (by created_at asc) Exactly one of row_ids or first_10 required. Both or neither → 400. Execution is async — returns immediately. Response: {status, message, data: {rows_queued}}. Rows run CONCURRENTLY, not one-after-another. Queuing N rows runs them in parallel — wall-clock time does NOT scale linearly with row count. Anchor any "how long will this take" estimate on the length of a SINGLE row's action chain, not on row_count. Verified 2026-06-08. STALE-CHAIN GOTCHA — re-running an already-run row after adding a NEW downstream action can report `row_status: "complete"` PREMATURELY without ever running the new action. An action added since the row last ran has no cell on that row; on re-run it may queue separately and NOT be counted toward `row_status` — the row flips to `complete` with the new action's cell absent from `cells{}` entirely and never executed. This is intermittent (sometimes the re-run does reach the appended cell), so do not rely on Run Rows to pick up freshly-added downstream actions. Reliable fix: call Run Action with `run_next_action: true` on the FIRST newly-added action — that mints the missing cell, runs it plus everything downstream, and settles `row_status` correctly. Verified 2026-06-08. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/run-all — Run All Rows No body required. Queues every row on the sheet. ⚠️ Full credit cost = row_count × actions. Never call without first verifying on a sample. Response: {status, message, data: {rows_queued}}. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/run — Run Action Runs a specific action against a set of rows. By default ONLY the targeted action runs — downstream actions are not touched. Set `run_next_action: true` to continue down the chain after this action finishes, re-running every action wired off its outputs. Body: {row_ids: [...], run_next_action?: boolean} `row_ids` is required. Pass `[]` to queue every row on the sheet (analogous to Run All Rows but scoped to one action). Pass explicit IDs (from Add Rows / List Rows) to target a subset. `run_next_action` `false` (default) → just this action; `true` → this action plus everything downstream. Useful after Configure Action when you want to refresh a single action and everything wired off its outputs in one call. Reserved `action_instance_id` values (`"input"`, `"graph"`) return 404. Async — returns immediately. Response: {status, message, data: {rows_queued}}. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows/list — List Rows POST (not GET) — request body carries filter + pagination so large row_id arrays don't overflow the URL. Body (all optional): { row_ids?: [...] (max 200), page_no?: 1, page_size?: 20 (max 200), filters?: [...], // input-column value filters status_filters?: [...], // per-action cell-status filters created_at?: { operator, values } // row creation time filter } Omit row_ids to browse all rows on the sheet (paginated). Include row_ids to fetch just those. row_ids cannot combine with filters / status_filters / created_at — picking explicit row IDs and filtering at the same time is rejected with 400. Browse-mode filtering: `filters[]` — value filters on **sheet input columns** only (action-output filtering not exposed yet). Each entry: { "variable": "{{input.}}", "operator": "", "values": ["v1", ...] } Operators (matches the frontend's filter-bar dialect): is equal to (1 value) not equal to (1 value) is empty (0 values) not empty (0 values) contains (1 value, substring) does not contain (1 value, substring) contains any of (1+ values, exact match each) not contains any of (1+ values, exact match each) greater than (1 value, numeric) less than (1 value, numeric) `status_filters[]` — match rows by per-action cell status. Each entry: { "action_instance_id", "is" | "is_not": [...] }. Exactly one of `is` / `is_not`. Status values are the public vocabulary (queued | running | complete | failed | error | condition_not_met). `queued` matches any cell waiting to run. `created_at` — single time filter on row creation. Shape: { "operator": "", "values": [""] } Operators (separate dialect from filters[]): equals to (1 ISO timestamp) greater than (1 ISO timestamp) greater than or equals to (1 ISO timestamp) less than (1 ISO timestamp) less than or equals to (1 ISO timestamp) is between (2 ISO timestamps: [start, end]) Pattern — filter rows by action OUTPUT content (the filter-as-tag idiom) `filters[]` is sheet-inputs-only. To filter rows by an action output value, put a `filter` action in the chain that gates on the upstream output, then query that filter action's status via `status_filters[]`: Workflow setup: ... -> upstream action producing field X -> filter (`{{.X}}` is/is_not ) -> ... downstream Query body: {"status_filters": [ {"action_instance_id": "", "is": ["complete"]} ]} `is: ["complete"]` returns rows where the gate passed. `is: ["condition_not_met"]` returns the inverse — rows the gate rejected. The filter action's cell-status mirrors the condition outcome (see action-detail/filter.txt §4 KEY NOTES). Multi-condition / non-trivial-predicate variant — JS sentinel + filter When the gate is a predicate that doesn't fit `filter`'s operators (nested OR/AND over several action outputs, arithmetic combined with substring tests, type-coerced comparisons), wrap the predicate in a `format_data_using_js_expression` returning "YES" / "NO" and put a filter on `is "YES"` after it: ... -> formatter (returns "YES" or "NO") -> filter (`{{.formatted_data}}` is "YES") -> ... downstream Query the filter's status the same way. This is the canonical way to make any computable predicate over action outputs queryable later via rows/list. Pagination — pages past `total_count` return `rows: []` with the actual sheet size in `total_count`. Detect end-of-pagination by `rows.length === 0`, not by checking `total_count` against your page math (which would be ambiguous when filters shrink the result set). Response envelope (full shape): { "status": 200, "data": { "rows": [...], // RowView[], one per row on this page "total_count": , // total rows being paginated — sheet-wide // count in browse mode, or the size of the // `row_ids` filter you passed "page_no": , // echoes back the requested page (default 1) "page_size": // echoes back the requested page size (default 20) } } Each row (RowView): {row_id, row_status, created_at (ISO 8601 UTC), inputs, cells}. row_status: "pending" (never run) | "running" (any cell queued/running) | "complete" (all cells complete) | "has_failures" (terminal, at least one cell failed or errored). `has_failures` is STICKY — once any cell fails on a row, the row-level flag stays even if every other cell completes cleanly afterwards (and even if the failing cell later passes on a re-run). Use per-cell `cells[].status` to verify whether downstream data is actually present; don't rely on `row_status` alone for "is this row usable." Verified 2026-06-08. cells: object keyed by `action_instance_id` (the public form, exactly the same ID Add Action / Get Action returned). Empty {} for rows that have never been run. Each cell: {status: "queued"|"running"|"complete"|"failed"|"error"|"condition_not_met", outputs?: {...}, outputs_ref?: {url, size_bytes}, error?: string}. `failed` and `error` are both terminal failure states — `failed` is a known failure path (integration 404, validation rejected); `error` is an unexpected runtime error. Both carry an `error` message; treat them the same when polling for completion. `outputs` keys are the action's snake-cased public output names — same keys you'd see in the Add Action `outputs[].name` and the `{{ref}}` token after the dot. `outputs` (inline) and `outputs_ref` (large-payload URL — fetch with API key) are mutually exclusive. `structured_array` outputs (e.g. `list_of_employees`) come back as a flat array of objects keyed by the column's snake_case name — `[{first_name: "Satya", last_name: "Nadella", ...}, ...]`. Empty / missing values for an output may surface as `""` rather than the typed empty (e.g. `[]` for arrays, `null` for booleans). Treat all outputs as nullable on the consumer side. Poll this after runRows; branch on row_status for the quick check, cells[action_id].status for per-cell detail. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows/delete — Delete Rows Body: {row_ids: ["uuid1", "uuid2", ...]} (max 200). Response: {deleted_count, deleted_row_ids: [...], rejected: [{row_id, error}]}. Partial success — UUIDs not found on this sheet come back in rejected. Cannot be undone. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows/delete-all — Delete All Rows Body: send an empty JSON object `{}`. No fields in the body are used. The empty-body rejection is CONDITIONAL on the Content-Type header: when you send `Content-Type: application/json` with an empty body the framework returns 400 `FST_ERR_CTP_EMPTY_JSON_BODY` ("Body cannot be empty when content-type is set to 'application/json'"). (Omitting the Content-Type header entirely returns 200 with no body — but most HTTP clients set it automatically when you send JSON, so always pass `{}` to satisfy the validator.) Verified 2026-06-08. Deletes every row on the sheet. Cannot be undone. Response: {deleted_count}. ================================================================================ ENDPOINTS — MANAGE WORKFLOWS ================================================================================ POST /api/v1/workflows — Create Workflow Body: {name} Returns: {workflow_id, name, created_at} — workflow_id is also the main sheet's sheet_id. GET /api/v1/workflows — List Workflows Scope: workflows:read. Returns all workflows the caller can access as summaries: [{workflow_id, name, created_at, updated_at, last_run_at}]. last_run_at is null for workflows that have never been run. Status codes: 200 / 401 / 403 / 429. GET /api/v1/workflows/{workflow_id} — Get Workflow Overview Scope: workflows:read. Bootstrap call after List Workflows — returns workflow metadata, sharing info, and a per-sheet roster with settings and webhook URLs. Response: {status: 200, data: { workflow_id, name, created_at, updated_at, last_run_at, is_owner, -- true when caller owns the workflow shared_users: [...], -- populated only when is_owner; else [] sheets: [{ sheet_id, name, is_main_sheet, -- sheets[0] is always the main sheet auto_run, cache_enabled, cache_since, webhook_url -- POST URL for webhook row ingest; null if unset }, ...] }}. Child sheets follow sheets_config.sheetsOrder when set, otherwise creation order. Use data.sheets[].sheet_id when configuring send_to_sheet, lookup_another_floqer_workflow_row, or any sheet-scoped path. There is no GET .../sheets list endpoint — sheet IDs live here. Status codes: 200 / 401 / 403 / 404 / 429. DELETE /api/v1/workflows/{workflow_id} — Delete Workflow Deletes the workflow and all its sheets, inputs, actions, rows, and run history. Cannot be undone. Returns: {workflow_id, deleted: true}. ================================================================================ ENDPOINTS — USER ================================================================================ GET /api/v1/user — Get User Info Returns the API-key owner's profile plus a summary of their organization and its members. No request body, no query params. Response: {status: 200, data: { email, first_name, last_name, -- API-key owner role, -- e.g. "admin" org_id, -- organization UUID org_members: { count, -- total member count emails: [...] -- raw invite list; may contain -- duplicates if the same address was -- invited more than once (no dedup -- at invite time). `count` equals -- `emails.length`, not the distinct -- count. } }}. GET /api/v1/user/knowledge — Get Org Knowledge Reads the organization's knowledge file: a single free-form text/markdown blob describing the org (who they are, what workflows they run, their goals). One file per org. Read this for context before acting. Response: {status: 200, data: { content, -- the file text, or null if never written exists, -- boolean; false when no file has been written yet updated_at -- ISO 8601 timestamp of last write, or null }}. Note: when unset this returns exists:false / content:null with 200 — it is NOT a 404. No request body, no query params. PUT /api/v1/user/knowledge — Set Org Knowledge Creates or replaces the organization's knowledge file. Idempotent full replacement — `content` becomes the entire file. Send an empty string to clear it. Body: { content: string } -- max 256 KB (UTF-8) Response: {status: 200, data: { content, exists: true, updated_at }}. ================================================================================ ENDPOINTS — DATA SOURCES ================================================================================ Sources pull external data (e.g. from HubSpot) into Floqer, where it becomes available to your workflows for enrichment, scoring, outreach, and the like. See https://floqer.com/docs/concepts.txt §10 for the mental model and https://floqer.com/docs/source-detail/{source_id}.txt for per-source body shapes, filter operator catalogues, and configuration walkthroughs. GET /api/v1/sources — List Sources Scope: sources:read. Lists the sources you've created, newest first. Only source types this API supports are listed. Response: {status: 200, data: [{source_instance_id, name, source_id, status, lead_count, created_at, expiration_date}, ...]}. `source_instance_id` is the source INSTANCE UUID — pass it to `POST /api/v1/sources/{source_instance_id}/sync` or `GET /api/v1/sources/{source_instance_id}/data`. `source_id` is the source-type slug (e.g. `import_from_hubspot`) used on the preview / create / options endpoints. `status` is one of: `active` (live, re-importing), `completed` (one-time import, won't refresh), `paused`, `paused_out_of_credits`, `expired`, `deleted`. `lead_count` and `expiration_date` may be null. Status codes: 200 / 401 / 403 / 429. Three parametric operations serve every source TYPE. `{source_id}` is the source type slug (e.g. `import_from_hubspot`). Three instance-scoped operations apply to a created source — Sync, Get Source Data, and Pause / Resume — keyed by `{source_instance_id}`, the UUID returned by Create. Preview / Create bodies are per-source — the route schema only enforces that the body is a JSON object; per-source validation runs server-side and returns 400 with a specific message. Read source-detail/{source_id}.txt for the exact body. POST /api/v1/sources/{source_id}/preview — Preview Source Scope: sources:read. Returns up to 100 records that WOULD be imported for the given body, without creating anything. Use this to validate filters / selections before committing to Create. Response: {status: 200, data: {data: [{...}], metadata: {total_results}}}. `data[]` is capped at 100 records. `metadata.total_results` is the total match count (best-effort; may lag writes by a few seconds). Status codes: 200 / 400 (per-source validation) / 404 (unknown source_id) / 424 (integration not connected) / 401 / 403 / 429. POST /api/v1/sources/{source_id} — Create Source Scope: sources:write. Creates the source, imports matching records immediately (when the source's body opts into it, e.g. `pull_existing`), and — for an ongoing source — keeps re-importing on a schedule. Response: {status: 201, data: {source_instance_id: "", name, created_at}}. `source_instance_id` is the new source's UUID — distinct from the `{source_id}` type slug in the URL (which names the source TYPE). Status codes: 201 / 400 (per-source validation) / 402 (credit consumption failed — the source is created but its import can't start; top up and retry) / 404 (unknown source_id) / 424 (integration not connected) / 401 / 403 / 429. Async behavior: the call returns as soon as the source is created and the import is queued. Records arrive over seconds/minutes depending on import size — not by the time the call returns. POST /api/v1/sources/{source_instance_id}/sync — Sync Source to Workflow Scope: sources:write. Connects a CREATED source to a workflow so its records flow into that workflow, and (by default) backfills the workflow with the source's existing rows. Here `{source_instance_id}` is the source INSTANCE UUID returned by Create. Body: { workflow_id: "" -- required field_mapping: { "input.": "", ... } -- required, non-empty push_existing: boolean -- optional, defaults true run: "none" | "first_10" | "all" -- optional, defaults "all" } `field_mapping` keys are public workflow input references (`input.`, same form as `{{input.}}` without braces); each is resolved to that workflow input. Each value is the source field to pull — the row's top-level key exactly as it appears in Preview (e.g. `email`, `Name`, `website`), never a nested path like `Name.value`. Cells that render as `{label, value}` are unwrapped automatically. Keys that don't match a workflow input → 400. `run` (only relevant when push_existing): "all" runs the workflow on every backfilled row, "first_10" runs the first 10 and just loads the rest, "none" loads rows without running. Building field_mapping (two lookups): 1. GET /api/v1/workflows (List Workflows) → pick the destination workflow_id. 2. GET /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/inputs (List Inputs) → each input has a `reference` like `{{input.email}}`. 3. Per input you want to fill: key = that reference WITHOUT braces (`input.email`), value = the source field (from Preview rows, or the hubspot_properties options call). e.g. {"input.email": "email", "input.first_name": "firstname"} Keys are CASE-SENSITIVE — copy the reference verbatim (lowercase snake_case); `Input.Email` / `input.firstName` return 400. Response: {status: 201, data: {source_instance_id, workflow_id, push_existing, run, fields_mapped}}. Status codes: 201 / 400 (bad body or unknown field_mapping input) / 403 (no access to the workflow) / 404 (source not found / not owned) / 409 (source already connected to this workflow — update the existing connection instead of re-syncing) / 401 / 429. GET /api/v1/sources/{source_instance_id}/data — Get Source Data Scope: sources:read. Returns paginated rows imported into a created source. `{source_instance_id}` is the source INSTANCE UUID returned by Create. Query params: `page_no` (1-indexed, default 1), `page_size` (default 20, max 200). Response: {status: 200, data: {rows: [{...}], total_count, page_no, page_size}}. Row field names match Preview for that source type. Use after Create to poll while import runs, or to page the full dataset before Sync. When `page_no * page_size > total_count`, `rows` is []. Status codes: 200 / 400 (invalid pagination) / 404 (source not found / not owned) / 401 / 403 / 429. PATCH /api/v1/sources/{source_instance_id}/status — Pause or Resume Source Scope: sources:write. Pauses or resumes a CREATED source. `{source_instance_id}` is the source INSTANCE UUID returned by Create. Body: {status: "active" | "paused"} `paused` stops recurring runs (schedule, webhooks where applicable); `active` resumes them. Static / one-time sources have nothing recurring to pause, but the status still updates. Response: {status: 200, data: {source_instance_id, status}}. Status codes: 200 / 400 / 404 / 401 / 403 / 429. POST /api/v1/sources/{source_id}/options/{field_name} — Get Source Field Options Scope: sources:read. Resolves dynamic option values for a source-payload field (behaves like the workflow Get Action Field Options endpoint). Body: {"context"?: {"": ""}}. Response: {status: 200, data: {options: [{value, label, extras?}, ...]}}. Pipe the chosen `value` straight into the matching field in the source's preview / create body. Status codes: 400 — that field has no options for this source, OR a required context key is missing. 404 — unknown source_id. 424 — integration not connected. 502 — integration call failed. ──────────────────────────────────────────────────────────────────────── DISCOVERING SOURCES + PER-SOURCE BODIES ──────────────────────────────────────────────────────────────────────── The operations above are identical for every source. What differs is the per-source preview/create BODY (the filters / fields you send). 1. Browse https://floqer.com/docs/source-catalog.txt for the full list of source types grouped by category — each entry shows the source_id, its category, whether it needs a connection, and its key body fields. 2. Read https://floqer.com/docs/source-detail/{source_id}.txt for the exact body shape, the full filter-field catalogue with allowed values, the dynamic-options fields, and an end-to-end walkthrough. ALWAYS read this before constructing a create body — filter operators and enums are source-specific. Bodies are snake_case and reject unknown top-level keys. Sources that take a `filters` object reject unknown filter keys too — resolve allowed values via the source's options endpoint (Get Source Field Options above) or the source-detail filter catalogue; never guess. UI LINKS — PRESENTING WORKFLOWS TO THE USER ================================================================================ When directing a user to view a workflow in the UI, construct a full link with view + sheet — the bare `/workflow/{id}` redirects to the build view of the main sheet, which is usually not specific enough. URL template: https://app.floqer.com/workflow/{workflow_id}?action={view}&v2=true&sheetId={sheet_id} Path and query parameters: workflow_id UUID. Required. action=build Node-graph view. UI label "Floq". Surface after chain edits or when the user is auditing pipeline shape. action=table Spreadsheet view. UI label "Data". Surface after row runs or when the user is reviewing per-row outputs. v2=true Required UI version flag. sheetId UUID. Required. Equal to workflow_id for the main sheet; a different UUID for sub-sheets created via POST /workflows/{id}/sheets. Always include — the bare link redirects to the main sheet. UI label ≠ URL param. The UI labels these views "Floq" (build) and "Data" (table). When writing to the user, refer to them by their UI labels — "here's the Floq view of the chain", "results are in the Data view" — not by the URL params, which the user isn't seeing. Defaults: - Reporting chain edits → Floq link - Reporting row results → Data link - Touching a sub-sheet → link directly to that sub-sheet's Data view (sheetId = sub-sheet UUID), not the parent main sheet Examples: Floq (chain): https://app.floqer.com/workflow/?action=build&v2=true&sheetId= Data (rows): https://app.floqer.com/workflow/?action=table&v2=true&sheetId= Sub-sheet rows: https://app.floqer.com/workflow/?action=table&v2=true&sheetId= When linking both views in one response, label clearly: - Floq (build view): - Data (table view): ================================================================================ ERRORS ================================================================================ Success: {"status": 200, "data": {...}} Single errors (401, 403, 404, 429, 500): {"status": 401, "error": "Unauthorized", "message": "API key is required"} Validation errors (400) — all field errors at once: {"status": 400, "error": "Validation Error", "message": "2 validation errors", "errors": [{"field": "name", "message": "required"}]} 429 includes retryAfter: {"status": 429, "error": "Too Many Requests", "message": "...", "retryAfter": 42} ================================================================================ This file is maintained manually. Last updated: 2026-06-02. Full interactive reference: https://floqer.com/docs/reference