API Reference
Floyi’s External API lets you access your data programmatically through a versioned REST API. Use it to connect AI agents, automate workflows with Zapier or Make, or build custom dashboards on top of your Floyi workspace.
What You’ll Learn
Section titled “What You’ll Learn”- How to create and manage API keys
- How to authenticate API requests
- All available V1 endpoints with examples
- Permission scopes, rate limits, and error handling
Terminology & Hierarchy Levels
Section titled “Terminology & Hierarchy Levels”Floyi uses a 4-level topical hierarchy. The API field names (used in JSON responses) differ from the display labels shown in the Floyi dashboard. Every API response includes a _meta object with the mapping, but here is the complete reference:
| Level | API Field Name | Display Label | Description |
|---|---|---|---|
| 0 | main_topic | Pillar | Top-level topic category (e.g., “Email Marketing”) |
| 1 | subtopic_2 | Hub | Major subtopic grouping (e.g., “Email Automation”) |
| 2 | subtopic_3 | Branch | Specific topic area (e.g., “Drip Campaigns”) |
| 3 | subtopic_4 | Resource | Individual content asset — can be an article, video, tool, calculator, or any content format (e.g., “Welcome Sequence Templates”) |
Node types in the authority hierarchy also use internal labels that map to the same display names:
| Internal Type | Display Label |
|---|---|
PILLAR | Pillar |
CLUSTER | Hub |
TOPIC | Branch |
PAGE | Resource |
[!IMPORTANT] Always use the display labels (Pillar, Hub, Branch, Resource) when presenting hierarchy data to users. The API field names (
main_topic,subtopic_2, etc.) are internal identifiers that should not be shown in user-facing interfaces.
Understanding _meta in Responses
Section titled “Understanding _meta in Responses”Every V1 API response includes a _meta object with:
description— What this endpoint returns and how to interpret the datalevel_labels— Mapping from API field names to display labels (included on hierarchy endpoints)node_type_labels— Mapping from internal node types to display labels (included on authority endpoints)
Example:
{ "_meta": { "description": "Returns the full 4-level topical hierarchy...", "level_labels": { "main_topic": "Pillar", "subtopic_2": "Hub", "subtopic_3": "Branch", "subtopic_4": "Resource" } }, "brand_id": "...", "research_data": [...]}Topical Research vs. Topical Maps vs. Authority
Section titled “Topical Research vs. Topical Maps vs. Authority”| Module | API Endpoint | Contains | Search Metrics? |
|---|---|---|---|
| Topical Research | /api/v1/research/ | Topic hierarchy (taxonomy) with keyword annotations | No — purely structural |
| Topical Maps | /api/v1/maps/ | Raw keyword clusters with search volume, CPC, competition, SERP data | Yes |
| Topical Authority | /api/v1/authority/ | Enriched hierarchy with coverage, SERP rankings, AI search presence | Yes |
Standalone vs. Authority Content Workflows
Section titled “Standalone vs. Authority Content Workflows”Floyi’s API supports two content generation workflows:
| Workflow | Brief Endpoint | Article Endpoint | Use Case |
|---|---|---|---|
| Standalone | /api/v1/briefs/ | /api/v1/content/articles/ | One-off briefs for any topic (query_text). Not linked to the authority hierarchy. |
| Authority | /api/v1/authority/{brand_id}/briefs/ | /api/v1/authority/{brand_id}/articles/ | Hierarchy-linked briefs by node_id. Tied to your topical map for the Planner tab. |
[!TIP] Which should I use? If you have a topical map and want briefs/articles tracked in the Planner tab, use the Authority endpoints. If you want to generate a quick brief for any topic without hierarchy linkage, use the Standalone endpoints.
Machine-Readable Documentation
Section titled “Machine-Readable Documentation”Floyi provides machine-readable API documentation for developer tools, AI coding assistants, and automation platforms.
| Resource | URL | Description |
|---|---|---|
| Interactive Docs (Swagger UI) | api.floyi.com/api/v1/docs/ | Browse and test endpoints in your browser |
| Alternative Viewer (ReDoc) | api.floyi.com/api/v1/redoc/ | Clean, readable API reference |
| OpenAPI 3.0 Schema (YAML) | api.floyi.com/api/v1/schema/ | Import into Postman, Insomnia, or any API client |
| LLM Reference | api.floyi.com/llms-full.txt | Full API reference optimized for AI coding assistants |
| LLM Summary | api.floyi.com/llms.txt | Compact endpoint listing for AI agents |
The OpenAPI schema can be imported directly into tools like Postman or Insomnia to auto-generate request templates. AI coding assistants (Claude, ChatGPT, Cursor, etc.) can read the llms-full.txt file to understand the full API without needing authentication.
Part 1: Getting Started
Section titled “Part 1: Getting Started”Requirements
Section titled “Requirements”| Requirement | Details |
|---|---|
| Floyi Plan | Agency |
| API Key | Created in Settings > API Keys |
| Authentication | X-API-Key header on every request |
| Workspace | X-Team-ID header (optional — omit for personal workspace) |
| Base URL | https://api.floyi.com/api/v1/ |
[!NOTE] The External API is currently in beta. Access is limited to approved users. Contact support to request access.
API key creation requires an Agency plan. If you are on the Free, Creator, or Pro plan, the API Keys tab in Settings will prompt you to upgrade.
Creating Your First API Key
Section titled “Creating Your First API Key”- Go to Settings > API Keys.
- Click Create API Key.
- Enter a name for the key (e.g., “My AI Agent” or “Zapier Integration”).
- Choose a key type (see Key Types below).
- Review and customize permissions if needed.
- Click Create.
- Copy the API key immediately. It is shown only once and cannot be retrieved later.
[!IMPORTANT] Store your API key securely. Treat it like a password. Never commit it to version control, share it in chat, or expose it in client-side code.
Key Types
Section titled “Key Types”| Type | Description | Rate Limit | Default Permissions |
|---|---|---|---|
| Integration | For Zapier, Make, n8n, and workflow automation. Read and write access to core features. | 120 requests/min | Brands, briefs, content, maps (read + write) |
| Developer | For custom apps, dashboards, and external tools. Read-only access to your own data. | 60 requests/min | Brands, briefs, content, maps (read only) |
Key Format
Section titled “Key Format”All Floyi API keys follow the format:
fyi_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxThe fyi_live_ prefix identifies Floyi keys in logs and secret scanners. The full key is 44 characters.
Part 2: Authentication
Section titled “Part 2: Authentication”Every API request must include your key in the X-API-Key header.
For POST, PUT, and PATCH requests, you must also set the Content-Type: application/json header and send a JSON request body.
Example Request
Section titled “Example Request”# GET request (read)curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/brands/
# POST request (write)curl -X POST \ -H "X-API-Key: fyi_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"brand_name": "My Brand"}' \ https://api.floyi.com/api/v1/brands/import requests
API_KEY = "fyi_live_your_key_here"BASE_URL = "https://api.floyi.com/api/v1"headers = {"X-API-Key": API_KEY}
# GET request (read)response = requests.get(f"{BASE_URL}/brands/", headers=headers)brands = response.json()
# POST request (write)response = requests.post( f"{BASE_URL}/brands/", headers=headers, json={"brand_name": "My Brand"},)new_brand = response.json()const API_KEY = "fyi_live_your_key_here";const BASE_URL = "https://api.floyi.com/api/v1";const headers = { "X-API-Key": API_KEY };
// GET request (read)const brandsRes = await fetch(`${BASE_URL}/brands/`, { headers });const brands = await brandsRes.json();
// POST request (write)const createRes = await fetch(`${BASE_URL}/brands/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ brand_name: "My Brand" }),});const newBrand = await createRes.json();Authentication Errors
Section titled “Authentication Errors”| Status | Meaning |
|---|---|
401 Unauthorized | Missing, invalid, expired, or revoked API key |
403 Forbidden | Valid key but missing the required permission scope |
429 Too Many Requests | Rate limit exceeded. Wait and retry. |
A 401 response includes a description in the response body:
{ "detail": "Invalid API key."}Possible messages: "Invalid API key.", "API key has expired.", "API key has been revoked.", "User account is no longer active.", "Request from unauthorized IP address.", "The External API is currently in beta. Contact support to request access."
A 403 response means the key is valid but lacks the required scope:
{ "detail": "You do not have permission to perform this action."}Check your key’s assigned scopes in Settings > API Keys and ensure they include the scope listed for the endpoint you are calling (e.g., briefs:write for brief generation).
Workspace Selection (X-Team-ID)
Section titled “Workspace Selection (X-Team-ID)”API keys are account-level — they are not tied to a specific workspace. To control which workspace your request operates on, use the optional X-Team-ID header.
| X-Team-ID Header | Result |
|---|---|
| Omitted | Personal workspace (your own brands, briefs, etc.) |
| Valid team UUID | Team workspace (brands, briefs, etc. belonging to that team) |
| Invalid UUID | 403 Forbidden |
| UUID of a team you don’t belong to | 403 Forbidden |
Discovering your team UUIDs:
Call the /api/v1/me/teams/ endpoint to list all teams you are an active member of. Each team object includes the id (UUID) you need for the X-Team-ID header.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/me/teams/{ "_meta": { "description": "Lists all teams the authenticated user is an active member of..." }, "results": [ { "team": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Acme Marketing", "slug": "acme-marketing", "plan_name": "Agency Plan", "created_at": "2025-09-15T10:00:00Z" }, "role": "owner", "status": "active" } ]}Using the team UUID in requests:
# Personal workspace (no header)curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/brands/
# Team workspacecurl -H "X-API-Key: fyi_live_your_key_here" \ -H "X-Team-ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ https://api.floyi.com/api/v1/brands/# Personal workspace (no header)response = requests.get(f"{BASE_URL}/brands/", headers=headers)
# Team workspaceteam_headers = {**headers, "X-Team-ID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}response = requests.get(f"{BASE_URL}/brands/", headers=team_headers)// Personal workspace (no header)const res = await fetch(`${BASE_URL}/brands/`, { headers });
// Team workspaceconst teamHeaders = { ...headers, "X-Team-ID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" };const teamRes = await fetch(`${BASE_URL}/brands/`, { headers: teamHeaders });[!TIP] You can use the same API key for both personal and team workspaces. The
X-Team-IDheader is the only thing that determines which workspace a request operates on. You can access any team where you are an active member.
Part 3: Permission Scopes
Section titled “Part 3: Permission Scopes”Each API key has a set of permission scopes that control which endpoints it can access. You can customize scopes when creating a key.
| Scope | Description |
|---|---|
authority:read | Read topical authority hierarchy, SERP data, and AI search presence |
authority:write | Toggle published status on authority map nodes |
brands:read | List and read brand details |
brands:write | Create and update brands |
briefs:read | List and read content briefs |
briefs:write | Create and trigger brief generation |
clustering:read | List and read clustering reports and SERP data |
clustering:write | Start clustering, recluster, and delete reports |
content:read | List and read articles and content |
content:write | Create articles, trigger draft generation |
maps:read | List and read raw topical map data |
research:read | Read topical research data, stats, task status, diff comparison |
research:write | Add, rename, delete, move, merge nodes, update keywords |
user:read | Read your own profile, credit balance, teams, and audit logs |
Part 4: Rate Limits
Section titled “Part 4: Rate Limits”Rate limits are applied per key based on the key type.
| Key Type | Limit |
|---|---|
| Developer | 60 requests per minute |
| Integration | 120 requests per minute |
When you exceed the rate limit, the API returns a 429 Too Many Requests response with a Retry-After header indicating how many seconds to wait.
{ "detail": "Request was throttled. Expected available in 12 seconds."}Part 5: API Endpoints
Section titled “Part 5: API Endpoints”All endpoints are under /api/v1/. Responses are JSON. List endpoints return arrays of objects. Detail endpoints return a single object.
Brands
Section titled “Brands”Scope required: brands:read (GET), brands:write (POST)
List Brands
Section titled “List Brands”GET /api/v1/brands/Returns all brands in your workspace, ordered by most recently created.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
search | string | Search brands by name (case-insensitive) |
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/brands/
# Search by namecurl -H "X-API-Key: fyi_live_your_key_here" \ "https://api.floyi.com/api/v1/brands/?search=floyi"response = requests.get(f"{BASE_URL}/brands/", headers=headers)brands = response.json()
# Search by nameresponse = requests.get( f"{BASE_URL}/brands/", headers=headers, params={"search": "floyi"},)const res = await fetch(`${BASE_URL}/brands/`, { headers });const brands = await res.json();
// Search by nameconst searchRes = await fetch(`${BASE_URL}/brands/?search=floyi`, { headers });Response:
[ { "id": "a1b2c3d4-...", "brand_name": "Floyi", "website_url": "https://floyi.com", "mission": "...", "vision": "...", "tagline": "...", "target_audience": "...", "brand_voice": "...", "values": "...", "marketplace": "...", "market_position": "...", "key_competitors": "...", "unique_selling_proposition": "...", "brand_personality": "...", "brand_story": "...", "language": "en", "country_code": "US", "country_name": "United States", "stage": "completed", "content_scope": "...", "created_at": "2026-02-01T10:00:00Z", "updated_at": "2026-02-15T14:30:00Z" }]Get Brand Details
Section titled “Get Brand Details”GET /api/v1/brands/{id}/Returns full details for a single brand. The response shape is the same as each object in the list endpoint. Returns 404 if the brand is not found or you do not have access.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/brands/a1b2c3d4-.../brand_id = "a1b2c3d4-..."response = requests.get(f"{BASE_URL}/brands/{brand_id}/", headers=headers)brand = response.json()const brandId = "a1b2c3d4-...";const res = await fetch(`${BASE_URL}/brands/${brandId}/`, { headers });const brand = await res.json();Response:
{ "id": "a1b2c3d4-...", "brand_name": "Floyi", "website_url": "https://floyi.com", "mission": "...", "vision": "...", "tagline": "...", "target_audience": "...", "brand_voice": "...", "values": "...", "marketplace": "...", "market_position": "...", "key_competitors": "...", "unique_selling_proposition": "...", "brand_personality": "...", "brand_story": "...", "language": "en", "country_code": "US", "country_name": "United States", "stage": "completed", "content_scope": "...", "created_at": "2026-02-01T10:00:00Z", "updated_at": "2026-02-15T14:30:00Z"}Create a Brand
Section titled “Create a Brand”POST /api/v1/brands/Request body:
{ "brand_name": "My New Brand", "website_url": "https://example.com", "mission": "Help teams create better content", "target_audience": "Content marketers and SEO professionals", "language": "en"}Required fields: brand_name
Optional fields: website_url, mission, vision, tagline, target_audience, brand_voice, values, marketplace, key_competitors, unique_selling_proposition, description, language
curl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{ "brand_name": "My New Brand", "website_url": "https://example.com", "mission": "Help teams create better content", "target_audience": "Content marketers and SEO professionals", "language": "en" }' \ https://api.floyi.com/api/v1/brands/response = requests.post( f"{BASE_URL}/brands/", headers=headers, json={ "brand_name": "My New Brand", "website_url": "https://example.com", "mission": "Help teams create better content", "target_audience": "Content marketers and SEO professionals", "language": "en", },)brand = response.json()const res = await fetch(`${BASE_URL}/brands/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ brand_name: "My New Brand", website_url: "https://example.com", mission: "Help teams create better content", target_audience: "Content marketers and SEO professionals", language: "en", }),});const brand = await res.json();Response (201 Created):
{ "id": "a1b2c3d4-...", "brand_name": "My New Brand", "website_url": "https://example.com", "mission": "Help teams create better content", "vision": null, "tagline": null, "target_audience": "Content marketers and SEO professionals", "brand_voice": null, "values": null, "marketplace": null, "market_position": null, "key_competitors": null, "unique_selling_proposition": null, "brand_personality": null, "brand_story": null, "language": "en", "country_code": null, "country_name": null, "stage": "new", "content_scope": null, "created_at": "2026-02-22T10:00:00Z", "updated_at": "2026-02-22T10:00:00Z"}The response returns the full brand object, including the id you need for subsequent API calls (briefs, maps, articles).
Error responses:
| Status | Cause |
|---|---|
400 Bad Request | Missing brand_name or validation error. Response body contains field-level errors. |
{ "brand_name": ["This field is required."]}Content Briefs
Section titled “Content Briefs”Scope required: briefs:read (GET), briefs:write (POST)
List Briefs
Section titled “List Briefs”GET /api/v1/briefs/Returns all content briefs in your workspace.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
brand_id | UUID | Filter briefs by brand |
status | string | Filter by brief status (e.g., COMPLETE, PENDING, PROCESSING) |
search | string | Search by topic/query text or generated title (case-insensitive) |
curl -H "X-API-Key: fyi_live_your_key_here" \ "https://api.floyi.com/api/v1/briefs/?brand_id=a1b2c3d4-..."
# Search by topic namecurl -H "X-API-Key: fyi_live_your_key_here" \ "https://api.floyi.com/api/v1/briefs/?search=what+is+topical+authority"response = requests.get( f"{BASE_URL}/briefs/", headers=headers, params={"brand_id": "a1b2c3d4-..."},)briefs = response.json()
# Search by topic nameresponse = requests.get( f"{BASE_URL}/briefs/", headers=headers, params={"search": "what is topical authority"},)const res = await fetch( `${BASE_URL}/briefs/?brand_id=a1b2c3d4-...`, { headers },);const briefs = await res.json();
// Search by topic nameconst searchRes = await fetch( `${BASE_URL}/briefs/?search=what+is+topical+authority`, { headers },);Response:
[ { "id": "b5c6d7e8-...", "query_text": "best seo tools for agencies", "brand_name": "Floyi", "status": "COMPLETE", "generated_title": "Best SEO Tools for Agencies in 2026", "generated_meta_description": "Discover the top SEO tools...", "created_at": "2026-02-10T08:00:00Z", "updated_at": "2026-02-10T08:15:00Z" }]Get Brief Details
Section titled “Get Brief Details”GET /api/v1/briefs/{id}/Returns the full brief including the complete result_json and curated_brief_json with all SERP analysis, competitor data, headings, and recommendations. Returns 404 if the brief is not found or you do not have access.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/briefs/b5c6d7e8-.../brief_id = "b5c6d7e8-..."response = requests.get(f"{BASE_URL}/briefs/{brief_id}/", headers=headers)brief = response.json()const briefId = "b5c6d7e8-...";const res = await fetch(`${BASE_URL}/briefs/${briefId}/`, { headers });const brief = await res.json();Response:
{ "id": "b5c6d7e8-...", "query_text": "best seo tools for agencies", "brand_name": "Floyi", "status": "COMPLETE", "generated_title": "Best SEO Tools for Agencies in 2026", "generated_meta_description": "Discover the top SEO tools...", "result_json": { "header_sections": [ ... ], "serp_analysis": { ... }, "competitor_data": [ ... ], "keyword_recommendations": [ ... ] }, "curated_brief_json": { "header_sections": [ ... ], "meta_title": "...", "meta_description": "..." }, "input_params": { "query_text": "best seo tools for agencies", "brand_id": "a1b2c3d4-...", "selected_serp_data": [ ... ] }, "created_at": "2026-02-10T08:00:00Z", "updated_at": "2026-02-10T08:15:00Z"}Detail-only fields (not included in the list endpoint):
| Field | Type | Description |
|---|---|---|
result_json | object | The raw brief output from the AI agent. Contains header_sections (outline with headings, subheadings, and talking points), serp_analysis, competitor_data, and keyword_recommendations. Structure varies based on the brief agent version. |
curated_brief_json | object | The user-edited version of the brief (if the user customized it in the Floyi editor). Same structure as result_json. null if no edits have been made. |
input_params | object | The original request parameters used to generate this brief, including query_text, brand_id, selected_serp_data, and any optional fields. |
[!NOTE] The
result_jsonandcurated_brief_jsonfields contain complex nested structures produced by the brief generation agent. The exact shape may vary, but both always include aheader_sectionsarray representing the content outline.
Check Brief Status
Section titled “Check Brief Status”GET /api/v1/briefs/{id}/status/Returns just the status of a brief. Useful for polling during brief generation.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/briefs/b5c6d7e8-.../status/brief_id = "b5c6d7e8-..."response = requests.get(f"{BASE_URL}/briefs/{brief_id}/status/", headers=headers)status = response.json()const briefId = "b5c6d7e8-...";const res = await fetch(`${BASE_URL}/briefs/${briefId}/status/`, { headers });const status = await res.json();Response:
{ "id": "b5c6d7e8-...", "status": "COMPLETE", "query_text": "best seo tools for agencies", "generated_title": "Best SEO Tools for Agencies in 2026"}Status values:
| Status | Description |
|---|---|
DRAFT | Brief created but generation has not started |
PENDING | Brief is queued for generation |
PROCESSING | Brief generation is actively running |
AWAITING_OUTLINE_REVIEW | Generation paused for user outline review (interactive mode) |
RESUMING_PROCESSING | Generation resuming after outline review |
RETRY_PAUSED | Generation paused due to a transient error; will retry automatically |
COMPLETE | Brief generation finished successfully |
PARTIAL_COMPLETE | Brief generated but some sections may be incomplete |
FAILED | Brief generation failed |
[!TIP] When polling, treat
COMPLETEandPARTIAL_COMPLETEas terminal success states, andFAILEDas a terminal error state. All other statuses indicate the brief is still in progress.
Generate a Brief
Section titled “Generate a Brief”POST /api/v1/briefs/generate/Triggers standalone content brief generation for any topic. This is a freeform endpoint — you provide the topic text directly. For hierarchy-linked briefs (tied to your topical authority map), use the Authority Briefs endpoints instead.
Returns immediately with a 202 Accepted response. Poll the status endpoint for progress.
Request body:
{ "query_text": "best seo tools for agencies", "brand_id": "a1b2c3d4-...", "ai_model_id": "gpt-5-mini"}Required fields: query_text, brand_id
Optional fields:
| Field | Type | Description |
|---|---|---|
selected_serp_data | object[] | SERP competitor pages to analyze (max 15, each must include url). If omitted, auto-fetched from cached SERP data for this keyword in the topical map. Returns 400 if no cached data exists. |
user_provided_keywords | string[] | Additional keywords to include in the brief |
internal_link_suggestions | object[] | Internal links to suggest (each: url, anchor_text) |
content_info_context | object | Additional context (SERP features, AI overview data, etc.) |
ai_model_id | string | AI model to use (e.g. "gpt-5-mini"). Uses default if not specified. |
# Minimalcurl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{ "query_text": "best seo tools for agencies", "brand_id": "a1b2c3d4-..." }' \ https://api.floyi.com/api/v1/briefs/generate/
# With SERP data and keywordscurl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{ "query_text": "best seo tools for agencies", "brand_id": "a1b2c3d4-...", "selected_serp_data": [ {"url": "https://example.com", "title": "Example", "snippet": "..."} ], "user_provided_keywords": ["seo software", "agency tools"] }' \ https://api.floyi.com/api/v1/briefs/generate/# Minimalresponse = requests.post( f"{BASE_URL}/briefs/generate/", headers=headers, json={ "query_text": "best seo tools for agencies", "brand_id": "a1b2c3d4-...", },)brief = response.json()
# With SERP data and keywordsresponse = requests.post( f"{BASE_URL}/briefs/generate/", headers=headers, json={ "query_text": "best seo tools for agencies", "brand_id": "a1b2c3d4-...", "selected_serp_data": [ {"url": "https://example.com", "title": "Example", "snippet": "..."} ], "user_provided_keywords": ["seo software", "agency tools"], },)// Minimalconst res = await fetch(`${BASE_URL}/briefs/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ query_text: "best seo tools for agencies", brand_id: "a1b2c3d4-...", }),});const brief = await res.json();
// With SERP data and keywordsconst res2 = await fetch(`${BASE_URL}/briefs/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ query_text: "best seo tools for agencies", brand_id: "a1b2c3d4-...", selected_serp_data: [ { url: "https://example.com", title: "Example", snippet: "..." }, ], user_provided_keywords: ["seo software", "agency tools"], }),});Response (202 Accepted):
{ "id": "b5c6d7e8-...", "status": "PENDING", "query_text": "best seo tools for agencies", "brand_name": "Floyi", "message": "Brief generation started. Poll /api/v1/briefs/{id}/status/ for progress."}[!TIP] After generating a brief, poll
GET /api/v1/briefs/{id}/status/every 10-15 seconds until the status changes toCOMPLETE. Brief generation typically takes 1-3 minutes depending on the number of SERP pages to analyze.
Error responses:
| Status | Cause |
|---|---|
400 Bad Request | Validation error (missing required fields, SERP entry missing url, or no SERP data available) |
402 Payment Required | Insufficient credits to generate a brief |
404 Not Found | Brand not found or you do not have access |
400 — No SERP data available (auto-fetch):
When selected_serp_data is omitted, the API attempts to auto-fetch cached SERP data for the topic from the brand’s topical map. If no cached SERP data exists for that keyword, the request fails with a 400:
{ "detail": "No SERP data available for this topic. Please provide selected_serp_data or ensure SERP data has been collected for this topic in the topical map."}To resolve this, either:
- Provide
selected_serp_datamanually with competitor pages you want analyzed, or - Ensure SERP data has been collected for this topic in the topical map first (via the Floyi dashboard)
400 — SERP entry missing url:
{ "detail": "selected_serp_data[0] must include a 'url' field."}402 — Insufficient credits:
{ "detail": "Insufficient credits."}404 — Brand not found:
{ "detail": "Brand not found or you do not have access."}[!NOTE] Standalone briefs are not linked to the authority hierarchy. If you need briefs tied to specific topics in your topical map (for the Planner tab), use
POST /api/v1/authority/{brand_id}/briefs/generate/instead.
Content Articles
Section titled “Content Articles”Scope required: content:read (GET), content:write (POST)
List Articles
Section titled “List Articles”GET /api/v1/content/articles/Returns all content articles in your workspace.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
brand_id | UUID | Filter articles by brand |
search | string | Search by article title (case-insensitive) |
curl -H "X-API-Key: fyi_live_your_key_here" \ "https://api.floyi.com/api/v1/content/articles/?brand_id=a1b2c3d4-..."
# Search by titlecurl -H "X-API-Key: fyi_live_your_key_here" \ "https://api.floyi.com/api/v1/content/articles/?search=seo+tools"response = requests.get( f"{BASE_URL}/content/articles/", headers=headers, params={"brand_id": "a1b2c3d4-..."},)articles = response.json()
# Search by titleresponse = requests.get( f"{BASE_URL}/content/articles/", headers=headers, params={"search": "seo tools"},)const res = await fetch( `${BASE_URL}/content/articles/?brand_id=a1b2c3d4-...`, { headers },);const articles = await res.json();
// Search by titleconst searchRes = await fetch( `${BASE_URL}/content/articles/?search=seo+tools`, { headers },);Response:
[ { "id": "c7d8e9f0-...", "title": "Best SEO Tools for Agencies in 2026", "brand_id": "a1b2c3d4-...", "brand_name": "Floyi", "machine_state": "completed", "editorial_state": "draft", "created_at": "2026-02-12T09:00:00Z", "updated_at": "2026-02-14T11:00:00Z" }]Get Article Details
Section titled “Get Article Details”GET /api/v1/content/articles/{id}/Returns the full article including brief_id, additional_directions, and all metadata. Returns 404 if the article is not found or you do not have access.
Response:
{ "id": "c7d8e9f0-...", "title": "Best SEO Tools for Agencies in 2026", "brand_id": "a1b2c3d4-...", "brand_name": "Floyi", "brief_id": "b5c6d7e8-...", "machine_state": "completed", "editorial_state": "draft", "additional_directions": "Focus on enterprise pricing tiers and team collaboration features.", "created_at": "2026-02-12T09:00:00Z", "updated_at": "2026-02-14T11:00:00Z"}Detail-only fields (not included in the list endpoint):
| Field | Type | Description |
|---|---|---|
brief_id | UUID or null | The content brief this article was created from. null if the article was created without a brief. |
additional_directions | string or null | Custom instructions provided for draft generation. null if none were set. |
Generate Draft from Brief
Section titled “Generate Draft from Brief”POST /api/v1/content/articles/generate/Creates a new article from a completed content brief and immediately starts AI draft generation — one step instead of two. The brief must be in COMPLETE or PARTIAL_COMPLETE status and have generated sections.
Request body:
{ "brief_id": "b5c6d7e8-...", "ai_model_id": "claude-sonnet-4", "intent": "human", "specialists": { "web_research": true, "fact_check": true, "intro_key_takeaways": true, "web_research_mode": "basic" }}Required fields: brief_id, ai_model_id
Optional fields:
| Field | Type | Default | Description |
|---|---|---|---|
intent | string | "human" | Writing style — "human" for natural tone |
specialists | object | all false | Specialist agents to enable during generation (see below) |
Specialist agents:
| Specialist | Description | Availability |
|---|---|---|
web_research | Researches the web for up-to-date information | Resource pages only |
fact_check | Verifies claims and data accuracy | Resource pages only |
intro_key_takeaways | Generates a polished intro and key takeaways section | Resource pages only |
conversion_coach | Adds conversion-optimized CTAs and messaging | Local/landing pages only |
web_research_mode | "basic" or "advanced" depth for web research | When web_research is true |
[!NOTE] Specialist availability depends on the article type. Resource pages (blog posts, guides) can use
web_research,fact_check, andintro_key_takeaways. Local/landing pages automatically getconversion_coachenabled. The API filters specialists to match the article type.
curl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{"brief_id": "b5c6d7e8-...", "ai_model_id": "claude-sonnet-4"}' \ https://api.floyi.com/api/v1/content/articles/generate/response = requests.post( f"{BASE_URL}/content/articles/generate/", headers=headers, json={ "brief_id": "b5c6d7e8-...", "ai_model_id": "claude-sonnet-4", },)article = response.json()# article["id"] -> use for status pollingconst res = await fetch(`${BASE_URL}/content/articles/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ brief_id: "b5c6d7e8-...", ai_model_id: "claude-sonnet-4", }),});const article = await res.json();// article.id -> use for status pollingResponse (202 Accepted):
{ "id": "c7d8e9f0-...", "brief_id": "b5c6d7e8-...", "title": "Best SEO Tools for Agencies in 2026", "task_id": "e1f2a3b4-...", "status": "generating", "message": "Article created and draft generation started. Poll /api/v1/content/articles/{id}/status/ for progress."}[!TIP] After triggering generation, poll
GET /api/v1/content/articles/{id}/status/every 15-30 seconds. Draft generation typically takes 3-10 minutes depending on the number of sections and specialists enabled.
Error responses:
| Status | Cause |
|---|---|
400 Bad Request | brief_id or ai_model_id missing, brief not in COMPLETE/PARTIAL_COMPLETE status, brief has no associated brand, or brief has no content sections |
402 Payment Required | Insufficient credits for draft or specialist agents |
404 Not Found | Brief or brand not found, or you do not have access |
Regenerate Draft
Section titled “Regenerate Draft”POST /api/v1/content/articles/{id}/generate/Regenerates draft for an existing article. Creates a new version, re-syncs sections from the brief, resets all sections, and generates fresh content. Optionally accepts a different brief_id to switch briefs.
Request body:
{ "ai_model_id": "claude-sonnet-4", "brief_id": "b5c6d7e8-...", "intent": "human", "specialists": { "web_research": true, "fact_check": true, "intro_key_takeaways": true, "web_research_mode": "basic" }}Required fields: ai_model_id
Optional fields: brief_id (switch to a different brief), intent, specialists
curl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{"ai_model_id": "claude-sonnet-4"}' \ https://api.floyi.com/api/v1/content/articles/c7d8e9f0-.../generate/article_id = "c7d8e9f0-..."response = requests.post( f"{BASE_URL}/content/articles/{article_id}/generate/", headers=headers, json={"ai_model_id": "claude-sonnet-4"},)result = response.json()const articleId = "c7d8e9f0-...";const res = await fetch( `${BASE_URL}/content/articles/${articleId}/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ ai_model_id: "claude-sonnet-4" }), },);const result = await res.json();Response (202 Accepted):
{ "id": "c7d8e9f0-...", "brief_id": "b5c6d7e8-...", "title": "Best SEO Tools for Agencies in 2026", "task_id": "e1f2a3b4-...", "status": "regenerating", "message": "Draft regeneration started. Poll /api/v1/content/articles/{id}/status/ for progress."}Error responses:
| Status | Cause |
|---|---|
400 Bad Request | Missing ai_model_id |
402 Payment Required | Insufficient credits for draft or specialist agents |
409 Conflict | Article generation already in progress |
409 — Already in progress:
{ "detail": "Article generation already in progress."}402 — Insufficient credits:
{ "detail": "Insufficient credits."}Check Draft Status
Section titled “Check Draft Status”GET /api/v1/content/articles/{id}/status/Returns the current generation progress of an article. Useful for polling during draft generation.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/content/articles/c7d8e9f0-.../status/article_id = "c7d8e9f0-..."response = requests.get( f"{BASE_URL}/content/articles/{article_id}/status/", headers=headers,)status = response.json()const articleId = "c7d8e9f0-...";const res = await fetch( `${BASE_URL}/content/articles/${articleId}/status/`, { headers },);const status = await res.json();Response:
{ "id": "c7d8e9f0-...", "machine_state": "in_progress", "sections_total": 8, "sections_approved": 3, "sections_drafting": 2, "sections_not_started": 3, "sections_needs_attention": 0, "completion_percentage": 37.5, "specialists_running": true, "specialists_complete": false}Machine state values:
| State | Description |
|---|---|
ready | Article created, waiting for draft generation |
in_progress | Draft generation is actively running |
draft_done | All sections drafted successfully |
incomplete | Generation finished but some sections may need attention |
[!TIP] When polling, treat
draft_doneas the terminal success state. Once complete, fetch the full article withGET /api/v1/content/articles/{id}/to see the final content.
Topical Maps
Section titled “Topical Maps”Scope required: maps:read
Returns the raw topical map data as created during keyword research and clustering, before any Topical Authority overrides are applied. For the enriched authority view (with organizer edits, SERP rankings, importance scores, and AI search presence), use the Topical Authority endpoints instead.
List Topical Maps
Section titled “List Topical Maps”GET /api/v1/maps/Returns all topical maps in your workspace.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
brand_id | UUID | Filter maps by brand |
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/maps/
# Filter by brandcurl -H "X-API-Key: fyi_live_your_key_here" \ "https://api.floyi.com/api/v1/maps/?brand_id=a1b2c3d4-..."response = requests.get(f"{BASE_URL}/maps/", headers=headers)maps = response.json()
# Filter by brandresponse = requests.get( f"{BASE_URL}/maps/", headers=headers, params={"brand_id": "a1b2c3d4-..."},)const res = await fetch(`${BASE_URL}/maps/`, { headers });const maps = await res.json();
// Filter by brandconst filteredRes = await fetch( `${BASE_URL}/maps/?brand_id=a1b2c3d4-...`, { headers },);Response:
[ { "brand_id": "a1b2c3d4-...", "brand_name": "Floyi", "is_uploaded": false, "created_at": "2026-01-20T12:00:00Z", "updated_at": "2026-02-18T16:00:00Z" }]Get Raw Map Clusters
Section titled “Get Raw Map Clusters”GET /api/v1/maps/{brand_id}/Returns the full raw keyword clusters from the topical map, including all search metrics, SERP analysis, and content metadata. Each cluster contains its centroid keyword and all associated keywords with their complete data. Returns 404 if no map exists for this brand or you do not have access.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/maps/a1b2c3d4-.../brand_id = "a1b2c3d4-..."response = requests.get( f"{BASE_URL}/maps/{brand_id}/", headers=headers)map_data = response.json()const brandId = "a1b2c3d4-...";const res = await fetch(`${BASE_URL}/maps/${brandId}/`, { headers });const mapData = await res.json();Response:
{ "brand_id": "a1b2c3d4-...", "brand_name": "Floyi", "is_uploaded": false, "cluster_count": 85, "clusters": [ { "id": "c1d2e3f4-...", "centroid": "best keyword research tools", "keyword_count": 3, "keywords": [ { "keyword": "best keyword research tools", "main_topic": "SEO Strategy", "subtopic_2": "Keyword Research", "subtopic_3": "Keyword Tools", "subtopic_4": "best keyword research tools", "sort_order": 0, "search_volume": 2400, "cpc": 3.50, "competition": 0.72, "url_slug": "best-keyword-research-tools", "has_serp_data": true, "serp_analysis": "Informational listicle format dominates...", "serp_competitors": "ahrefs.com, semrush.com, moz.com", "serp_freshness": "2025-12", "content_title": "Best Keyword Research Tools", "search_intent": "informational", "buyers_journey": "consideration", "content_type": "listicle", "snippet": "Discover the top keyword research tools...", "similarity": 0.95 } ] } ], "created_at": "2026-01-20T12:00:00Z", "updated_at": "2026-02-18T16:00:00Z"}Cluster fields:
| Field | Type | Description |
|---|---|---|
id | string | Cluster identifier |
centroid | string | The primary keyword representing this cluster |
keyword_count | integer | Number of keywords in this cluster |
keywords | object[] | All keywords in this cluster |
Keyword fields — Hierarchy:
| Field | Type | Description |
|---|---|---|
keyword | string | The keyword text |
main_topic | string | Main Topic (level 1 of the hierarchy) |
subtopic_2 | string | Subtopic 2 (level 2) |
subtopic_3 | string | Subtopic 3 (level 3) |
subtopic_4 | string | Subtopic 4 (level 4) |
sort_order | integer | Sort position within the cluster |
Keyword fields — Search Metrics:
| Field | Type | Description |
|---|---|---|
search_volume | number | Monthly search volume |
cpc | number | Cost per click (USD) |
competition | number | Competition score (0–1) |
Keyword fields — SERP Data:
| Field | Type | Description |
|---|---|---|
has_serp_data | boolean | Whether SERP analysis data is available |
serp_analysis | string | SERP analysis summary |
serp_competitors | string | Top competing domains in SERP |
serp_freshness | string | SERP freshness / recency indicator |
Keyword fields — Content & URL:
| Field | Type | Description |
|---|---|---|
url_slug | string | Suggested URL slug |
content_title | string | Suggested content title |
search_intent | string | Search intent classification |
buyers_journey | string | Buyer’s journey stage |
content_type | string | Recommended content format |
snippet | string | Content snippet / description |
similarity | number/string | Similarity score to cluster centroid |
[!TIP] This returns the original keyword research data. If the user has made edits in Floyi’s Organizer (renames, moves, combines), those changes are not reflected here. Use the Topical Authority endpoints to see the current effective hierarchy with all edits applied.
Topical Research
Section titled “Topical Research”Scope required: research:read (GET), research:write (PATCH/POST)
Access the pre-clustering topical research tree — the 4-level topic hierarchy (Main Topics > Subtopic 2 > Subtopic 3 > Subtopic 4 > Keywords) that users build during the research phase before clustering into a topical map. These endpoints support both reading the tree and making atomic modifications (rename, add, delete, move, merge, keyword updates).
List Research Records
Section titled “List Research Records”GET /api/v1/research/Returns all topical research records in your workspace.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/research/response = requests.get(f"{BASE_URL}/research/", headers=headers)records = response.json()const res = await fetch(`${BASE_URL}/research/`, { headers });const records = await res.json();Response:
[ { "brand_id": "a1b2c3d4-...", "brand_name": "Floyi", "core_topic": "content strategy", "created_at": "2026-02-01T10:00:00Z", "updated_at": "2026-02-20T14:30:00Z" }]Get Research Tree
Section titled “Get Research Tree”GET /api/v1/research/{brand_id}/Returns the full 4-level topic hierarchy with computed stats and generation status. Returns 404 if no research data exists for this brand.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/research/{BRAND_ID}/brand_id = "a1b2c3d4-..."response = requests.get( f"{BASE_URL}/research/{brand_id}/", headers=headers)tree = response.json()const brandId = "a1b2c3d4-...";const res = await fetch(`${BASE_URL}/research/${brandId}/`, { headers });const tree = await res.json();Response:
{ "brand_id": "a1b2c3d4-...", "brand_name": "Floyi", "core_topic": "content strategy", "research_data": [ { "main_topic": "email marketing", "subtopic_2": [ { "name": "email automation", "subtopic_3": [...], "subtopic_4": [], "keywords": [] } ], "subtopic_3": [], "subtopic_4": [], "keywords": [] } ], "stats": { "main_topic_count": 5, "st2_count": 23, "st3_count": 67, "st4_count": 142, "keyword_count": 890, "leaf_nodes_without_keywords": 12, "total_nodes": 237 }, "generation_status": { "has_running_task": false }, "created_at": "2026-02-01T10:00:00Z", "updated_at": "2026-02-20T14:30:00Z"}Get Tree Stats
Section titled “Get Tree Stats”GET /api/v1/research/{brand_id}/stats/Returns only the stats without the full tree. Includes per-main-topic breakdown.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/research/{BRAND_ID}/stats/brand_id = "a1b2c3d4-..."response = requests.get( f"{BASE_URL}/research/{brand_id}/stats/", headers=headers)stats = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/research/${brandId}/stats/`, { headers });const stats = await res.json();Response:
{ "brand_id": "a1b2c3d4-...", "main_topic_count": 5, "st2_count": 23, "st3_count": 67, "st4_count": 142, "keyword_count": 890, "leaf_nodes_without_keywords": 12, "total_nodes": 237, "main_topics": [ {"name": "email marketing", "st2_count": 6, "st3_count": 18, "st4_count": 42, "keyword_count": 234} ], "generation_status": {"has_running_task": false}}Get Task Status
Section titled “Get Task Status”GET /api/v1/research/{brand_id}/task-status/Poll the status of any running AI generation task. Performs Celery reconciliation — if the model says a task is running but Celery reports it as finished, the status is synced.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/research/{BRAND_ID}/task-status/brand_id = "a1b2c3d4-..."response = requests.get( f"{BASE_URL}/research/{brand_id}/task-status/", headers=headers)task = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/research/${brandId}/task-status/`, { headers });const task = await res.json();Response (task running):
{ "has_running_task": true, "task_id": "3a8c1d2e-...", "task_type": "generate_st2_st3_v2", "task_status": "running", "progress": {"current": 3, "total": 5, "message": "Generating ST2+ST3 for main topic 3 of 5..."}, "started_at": "2026-02-23T14:10:00+00:00", "error": null}Response (no task):
{ "has_running_task": false}[!IMPORTANT] All write operations below reject with
409 Conflictif a generation task is currently running. Wait for the task to complete or cancel it from the Floyi dashboard before making changes.
Rename Node
Section titled “Rename Node”PATCH /api/v1/research/{brand_id}/nodes/rename/Scope required: research:write
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
path | string[] | Yes | Path to the node (e.g., ["email marketing", "email automation"]) |
new_name | string | Yes | New name for the node (lowercased automatically) |
curl -X PATCH \ -H "X-API-Key: fyi_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"path": ["email marketing", "email automation"], "new_name": "marketing automation"}' \ https://api.floyi.com/api/v1/research/{BRAND_ID}/nodes/rename/brand_id = "a1b2c3d4-..."response = requests.patch( f"{BASE_URL}/research/{brand_id}/nodes/rename/", headers=headers, json={ "path": ["email marketing", "email automation"], "new_name": "marketing automation", },)result = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/research/${brandId}/nodes/rename/`, { method: "PATCH", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ path: ["email marketing", "email automation"], new_name: "marketing automation", }), },);const result = await res.json();Response:
{ "success": true, "operation": "rename", "path": ["email marketing", "email automation"], "old_name": "email automation", "new_name": "marketing automation", "updated_path": ["email marketing", "marketing automation"]}Add Node (Single)
Section titled “Add Node (Single)”POST /api/v1/research/{brand_id}/nodes/add/Scope required: research:write
Adds one node per request. To add multiple nodes at once, use Bulk Operations below instead.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
parent_path | string[] | Yes | Path to parent node. The depth determines the level of the new node (see examples below). |
name | string | Yes | Name of the new topic (lowercased automatically) |
position | integer or null | No | Insertion index. null appends to end. |
parent_path determines the node level:
parent_path | New node level |
|---|---|
[] (empty array) | Pillar (root level) |
["email marketing"] | Hub under that Pillar |
["email marketing", "automation"] | Branch |
["email marketing", "automation", "drip campaigns"] | Resource (leaf level) |
curl -X POST \ -H "X-API-Key: fyi_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"parent_path": ["email marketing"], "name": "email deliverability"}' \ https://api.floyi.com/api/v1/research/{BRAND_ID}/nodes/add/brand_id = "a1b2c3d4-..."response = requests.post( f"{BASE_URL}/research/{brand_id}/nodes/add/", headers=headers, json={ "parent_path": ["email marketing"], "name": "email deliverability", },)result = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/research/${brandId}/nodes/add/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ parent_path: ["email marketing"], name: "email deliverability", }), },);const result = await res.json();Response:
{ "success": true, "operation": "add", "parent_path": ["email marketing"], "name": "email deliverability", "level": "ST2", "path": ["email marketing", "email deliverability"]}Delete Node
Section titled “Delete Node”POST /api/v1/research/{brand_id}/nodes/delete/Scope required: research:write
Deletes a node and all its descendants (subtopics and keywords).
curl -X POST \ -H "X-API-Key: fyi_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"path": ["email marketing", "email automation", "trigger-based emails"]}' \ https://api.floyi.com/api/v1/research/{BRAND_ID}/nodes/delete/brand_id = "a1b2c3d4-..."response = requests.post( f"{BASE_URL}/research/{brand_id}/nodes/delete/", headers=headers, json={ "path": ["email marketing", "email automation", "trigger-based emails"], },)result = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/research/${brandId}/nodes/delete/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ path: ["email marketing", "email automation", "trigger-based emails"], }), },);const result = await res.json();Response:
{ "success": true, "operation": "delete", "path": ["email marketing", "email automation", "trigger-based emails"], "deleted_node": "trigger-based emails", "deleted_descendants": 4, "deleted_keywords": 12}Update Keywords (Single Node)
Section titled “Update Keywords (Single Node)”PATCH /api/v1/research/{brand_id}/nodes/keywords/Scope required: research:write
Updates keywords on one node per request (but you can add/remove multiple keywords in that call). To update keywords on many nodes at once, use Bulk Operations below with the update_keywords action.
Keywords can be added to any level of the tree — Pillars, Hubs, Branches, and Resources all hold keywords.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
path | string[] | Yes | Path to the target node. Works at any level (Pillar, Hub, Branch, or Resource). |
add | string[] | No | Keywords to add (lowercased, duplicates skipped). Max 200 per request. |
remove | string[] | No | Keywords to remove (missing ones skipped). Max 200 per request. |
At least one of add or remove must be a non-empty array. Each array accepts up to 200 keywords per request — if you need more, split across multiple calls.
Which nodes can have keywords?
All levels. The path depth determines which node receives the keywords:
path | Target node |
|---|---|
["email marketing"] | Pillar node |
["email marketing", "email automation"] | Hub node |
["email marketing", "email automation", "drip campaigns"] | Branch node |
["email marketing", "email automation", "drip campaigns", "welcome sequences"] | Resource node |
curl -X PATCH \ -H "X-API-Key: fyi_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"path": ["email marketing", "email automation"], "add": ["drip campaign"], "remove": ["old keyword"]}' \ https://api.floyi.com/api/v1/research/{BRAND_ID}/nodes/keywords/brand_id = "a1b2c3d4-..."response = requests.patch( f"{BASE_URL}/research/{brand_id}/nodes/keywords/", headers=headers, json={ "path": ["email marketing", "email automation"], "add": ["drip campaign"], "remove": ["old keyword"], },)result = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/research/${brandId}/nodes/keywords/`, { method: "PATCH", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ path: ["email marketing", "email automation"], add: ["drip campaign"], remove: ["old keyword"], }), },);const result = await res.json();Response:
{ "success": true, "operation": "update_keywords", "path": ["email marketing", "email automation"], "added": ["drip campaign"], "removed": ["old keyword"], "current_keywords": ["email tools", "drip campaign"]}Move Node
Section titled “Move Node”POST /api/v1/research/{brand_id}/nodes/move/Scope required: research:write
Move a node (and its entire subtree) to a different parent. Use an empty target_parent_path ([]) to promote a node to a Main Topic.
curl -X POST \ -H "X-API-Key: fyi_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"source_path": ["content marketing", "outreach"], "target_parent_path": ["seo", "link building"]}' \ https://api.floyi.com/api/v1/research/{BRAND_ID}/nodes/move/brand_id = "a1b2c3d4-..."response = requests.post( f"{BASE_URL}/research/{brand_id}/nodes/move/", headers=headers, json={ "source_path": ["content marketing", "outreach"], "target_parent_path": ["seo", "link building"], },)result = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/research/${brandId}/nodes/move/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ source_path: ["content marketing", "outreach"], target_parent_path: ["seo", "link building"], }), },);const result = await res.json();Response:
{ "success": true, "operation": "move", "source_path": ["content marketing", "outreach"], "target_parent_path": ["seo", "link building"], "new_path": ["seo", "link building", "outreach"]}Merge Nodes
Section titled “Merge Nodes”POST /api/v1/research/{brand_id}/nodes/merge/Scope required: research:write
Merge two same-level nodes under the same parent. Children and keywords from merge_path are moved to keep_path, then merge_path is deleted.
curl -X POST \ -H "X-API-Key: fyi_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"keep_path": ["email marketing", "email automation"], "merge_path": ["email marketing", "marketing automation"]}' \ https://api.floyi.com/api/v1/research/{BRAND_ID}/nodes/merge/brand_id = "a1b2c3d4-..."response = requests.post( f"{BASE_URL}/research/{brand_id}/nodes/merge/", headers=headers, json={ "keep_path": ["email marketing", "email automation"], "merge_path": ["email marketing", "marketing automation"], },)result = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/research/${brandId}/nodes/merge/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ keep_path: ["email marketing", "email automation"], merge_path: ["email marketing", "marketing automation"], }), },);const result = await res.json();Response:
{ "success": true, "operation": "merge", "keep_path": ["email marketing", "email automation"], "merged_from": "marketing automation", "children_moved": 3, "keywords_merged": 8}Bulk Operations (Recommended for Multiple Changes)
Section titled “Bulk Operations (Recommended for Multiple Changes)”POST /api/v1/research/{brand_id}/nodes/bulk/Scope required: research:write
This is the recommended endpoint for building or modifying hierarchies. Execute up to 200 operations in a single atomic request (all-or-nothing). Operations are applied sequentially — later operations see changes from earlier ones, so you can create a parent node and immediately add children to it in the same request.
Supported actions: add, rename, delete, move, merge, update_keywords
Each action uses the same parameters as its individual endpoint (see above).
Individual vs Bulk — when to use which:
| Endpoint | Use case |
|---|---|
nodes/add | Add a single node |
nodes/keywords | Update keywords on a single node |
nodes/bulk | Build hierarchies, batch edits, or any multi-step change (up to 200 ops, atomic) |
Example: Build a full Pillar → Hub → Branch → Resource hierarchy with keywords in one request:
curl -X POST \ -H "X-API-Key: fyi_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "operations": [ {"action": "add", "parent_path": [], "name": "email marketing"}, {"action": "add", "parent_path": ["email marketing"], "name": "email automation"}, {"action": "add", "parent_path": ["email marketing"], "name": "email deliverability"}, {"action": "add", "parent_path": ["email marketing", "email automation"], "name": "drip campaigns"}, {"action": "add", "parent_path": ["email marketing", "email automation"], "name": "triggered emails"}, {"action": "add", "parent_path": ["email marketing", "email automation", "drip campaigns"], "name": "welcome sequences"}, {"action": "add", "parent_path": ["email marketing", "email automation", "drip campaigns"], "name": "onboarding flows"}, {"action": "update_keywords", "path": ["email marketing"], "add": ["email marketing strategy", "email campaigns"]}, {"action": "update_keywords", "path": ["email marketing", "email automation"], "add": ["marketing automation tools", "email workflow"]}, {"action": "update_keywords", "path": ["email marketing", "email automation", "drip campaigns", "welcome sequences"], "add": ["welcome email series", "new subscriber emails", "welcome email template"]} ] }' \ https://api.floyi.com/api/v1/research/{BRAND_ID}/nodes/bulk/brand_id = "a1b2c3d4-..."response = requests.post( f"{BASE_URL}/research/{brand_id}/nodes/bulk/", headers=headers, json={ "operations": [ {"action": "add", "parent_path": [], "name": "email marketing"}, {"action": "add", "parent_path": ["email marketing"], "name": "email automation"}, {"action": "add", "parent_path": ["email marketing"], "name": "email deliverability"}, {"action": "add", "parent_path": ["email marketing", "email automation"], "name": "drip campaigns"}, {"action": "add", "parent_path": ["email marketing", "email automation"], "name": "triggered emails"}, {"action": "add", "parent_path": ["email marketing", "email automation", "drip campaigns"], "name": "welcome sequences"}, {"action": "add", "parent_path": ["email marketing", "email automation", "drip campaigns"], "name": "onboarding flows"}, {"action": "update_keywords", "path": ["email marketing"], "add": ["email marketing strategy", "email campaigns"]}, {"action": "update_keywords", "path": ["email marketing", "email automation"], "add": ["marketing automation tools", "email workflow"]}, {"action": "update_keywords", "path": ["email marketing", "email automation", "drip campaigns", "welcome sequences"], "add": ["welcome email series", "new subscriber emails", "welcome email template"]}, ], },)result = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/research/${brandId}/nodes/bulk/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ operations: [ { action: "add", parent_path: [], name: "email marketing" }, { action: "add", parent_path: ["email marketing"], name: "email automation" }, { action: "add", parent_path: ["email marketing"], name: "email deliverability" }, { action: "add", parent_path: ["email marketing", "email automation"], name: "drip campaigns" }, { action: "add", parent_path: ["email marketing", "email automation"], name: "triggered emails" }, { action: "add", parent_path: ["email marketing", "email automation", "drip campaigns"], name: "welcome sequences" }, { action: "add", parent_path: ["email marketing", "email automation", "drip campaigns"], name: "onboarding flows" }, { action: "update_keywords", path: ["email marketing"], add: ["email marketing strategy", "email campaigns"] }, { action: "update_keywords", path: ["email marketing", "email automation"], add: ["marketing automation tools", "email workflow"] }, { action: "update_keywords", path: ["email marketing", "email automation", "drip campaigns", "welcome sequences"], add: ["welcome email series", "new subscriber emails", "welcome email template"] }, ], }), },);const result = await res.json();This creates the following structure in a single atomic request:
email marketing (Pillar) ← keywords: email marketing strategy, email campaigns├── email automation (Hub) ← keywords: marketing automation tools, email workflow│ ├── drip campaigns (Branch)│ │ ├── welcome sequences (Resource) ← keywords: welcome email series, new subscriber emails, welcome email template│ │ └── onboarding flows (Resource)│ └── triggered emails (Branch)└── email deliverability (Hub)Simple batch example:
curl -X POST \ -H "X-API-Key: fyi_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "operations": [ {"action": "rename", "path": ["seo tools", "keyword research"], "new_name": "keyword research tools"}, {"action": "add", "parent_path": ["seo tools"], "name": "link building tools"}, {"action": "delete", "path": ["seo tools", "deprecated category"]} ] }' \ https://api.floyi.com/api/v1/research/{BRAND_ID}/nodes/bulk/brand_id = "a1b2c3d4-..."response = requests.post( f"{BASE_URL}/research/{brand_id}/nodes/bulk/", headers=headers, json={ "operations": [ {"action": "rename", "path": ["seo tools", "keyword research"], "new_name": "keyword research tools"}, {"action": "add", "parent_path": ["seo tools"], "name": "link building tools"}, {"action": "delete", "path": ["seo tools", "deprecated category"]}, ], },)result = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/research/${brandId}/nodes/bulk/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ operations: [ { action: "rename", path: ["seo tools", "keyword research"], new_name: "keyword research tools" }, { action: "add", parent_path: ["seo tools"], name: "link building tools" }, { action: "delete", path: ["seo tools", "deprecated category"] }, ], }), },);const result = await res.json();Atomicity: If any operation fails, all previous operations in the batch are rolled back. No partial changes are applied.
Response (success):
{ "success": true, "operations_applied": 3, "results": [ {"operation": "rename", "success": true, "old_name": "keyword research", "new_name": "keyword research tools"}, {"operation": "add", "success": true, "name": "link building tools", "level": "ST2"}, {"operation": "delete", "success": true, "deleted_node": "deprecated category", "deleted_descendants": 2, "deleted_keywords": 5} ]}Response (failure — all rolled back):
{ "success": false, "error": "Operation 2 (add) failed: Name already exists at this level.", "failed_at_index": 1, "operations_rolled_back": 1, "message": "All operations rolled back. No changes were applied."}Compute Diff
Section titled “Compute Diff”POST /api/v1/research/{brand_id}/diff/Scope required: research:read
Compare two tree states and return a structured diff. Useful for agents to summarize changes before confirming with the user.
curl -X POST \ -H "X-API-Key: fyi_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"before": [...], "after": [...]}' \ https://api.floyi.com/api/v1/research/{BRAND_ID}/diff/brand_id = "a1b2c3d4-..."response = requests.post( f"{BASE_URL}/research/{brand_id}/diff/", headers=headers, json={"before": [...], "after": [...]},)diff = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/research/${brandId}/diff/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ before: [...], after: [...] }), },);const diff = await res.json();Response:
{ "changes": [ {"type": "added", "level": "ST2", "path": ["seo", "link building tools"], "name": "link building tools"}, {"type": "deleted", "level": "ST3", "path": ["seo", "old category"], "name": "old category", "descendants_deleted": 3} ], "summary": { "nodes_added": 1, "nodes_deleted": 1, "nodes_renamed": 0, "nodes_moved": 0, "keywords_added": 0, "keywords_removed": 0 }}Limits: Combined before + after payload must not exceed 2MB. Combined node count must not exceed 5,000.
Error responses for all write endpoints:
| Status | When |
|---|---|
400 Bad Request | Invalid request body, missing required fields, validation failure |
404 Not Found | Brand or research record not found |
409 Conflict | Name already exists, or generation task is running |
413 Payload Too Large | Diff payload exceeds 2MB |
Topical Authority
Section titled “Topical Authority”Scope required: authority:read (GET), authority:write (PATCH)
Returns the authority-enriched hierarchy with all organizer overrides applied, plus coverage data (importance scores, published status), SERP rankings, and AI search presence. This is the same view shown in the Floyi dashboard’s Topical Authority section.
List Authority Maps
Section titled “List Authority Maps”GET /api/v1/authority/Returns all authority maps in your workspace.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
brand_id | UUID | Filter by brand |
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/authority/
# Filter by brandcurl -H "X-API-Key: fyi_live_your_key_here" \ "https://api.floyi.com/api/v1/authority/?brand_id=a1b2c3d4-..."response = requests.get(f"{BASE_URL}/authority/", headers=headers)maps = response.json()
# Filter by brandresponse = requests.get( f"{BASE_URL}/authority/", headers=headers, params={"brand_id": "a1b2c3d4-..."},)const res = await fetch(`${BASE_URL}/authority/`, { headers });const maps = await res.json();
// Filter by brandconst filteredRes = await fetch( `${BASE_URL}/authority/?brand_id=a1b2c3d4-...`, { headers },);Response:
[ { "brand_id": "a1b2c3d4-...", "brand_name": "Floyi", "is_uploaded": false, "created_at": "2026-01-20T12:00:00Z", "updated_at": "2026-02-18T16:00:00Z" }]Get Current Hierarchy
Section titled “Get Current Hierarchy”GET /api/v1/authority/{brand_id}/Returns the current effective hierarchy for an authority map. This includes all user edits from the Organizer (renames, moves, combines, new topics) applied on top of the base map data. It returns the same view of the map that the Floyi dashboard shows. Returns 404 if no map exists for this brand or you do not have access.
Each node in the hierarchy array represents a topic cluster with its position in the 4-level hierarchy (Main Topic > Subtopic 2 > Subtopic 3 > Subtopic 4), keywords, published status, importance score, SERP ranking, and AI search presence.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../brand_id = "a1b2c3d4-..."response = requests.get( f"{BASE_URL}/authority/{brand_id}/", headers=headers)hierarchy = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/`, { headers });const hierarchy = await res.json();Response:
{ "brand_id": "a1b2c3d4-...", "brand_name": "Floyi", "is_uploaded": false, "node_count": 142, "hierarchy": [ { "node_id": "f1a2b3c4-...", "name": "best keyword research tools", "type": "PAGE", "mt": "SEO Strategy", "st2": "Keyword Research", "st3": "Keyword Tools", "st4": "best keyword research tools", "keywords": ["best keyword research tools", "keyword research software", "top keyword tools"], "keyword_count": 3, "url_slug": "best-keyword-research-tools", "published": true, "importance": 82.3, "priority_category": "Medium", "serp_position": 4, "serp_all_positions": [4, 12], "aio_status": "cited", "aimode_status": "mentioned", "chatgpt_status": "N/A" }, { "node_id": "a5b6c7d8-...", "name": "SEO Strategy", "type": "PILLAR", "mt": "SEO Strategy", "st2": "SEO Strategy", "st3": "SEO Strategy", "st4": "SEO Strategy", "keywords": ["seo strategy", "seo planning", "search engine optimization strategy"], "keyword_count": 3, "url_slug": "seo-strategy", "published": false, "importance": 95.1, "priority_category": "High", "serp_position": null, "serp_all_positions": [], "aio_status": "not_present", "aimode_status": "N/A", "chatgpt_status": "N/A" } ], "created_at": "2026-01-20T12:00:00Z", "updated_at": "2026-02-18T16:00:00Z"}Hierarchy node fields:
| Field | Type | Description |
|---|---|---|
node_id | UUID | Unique identifier for this topic node |
name | string | Topic name (reflects any renames from the Organizer) |
type | string | Hierarchy level: PILLAR, CLUSTER, TOPIC, or PAGE |
mt | string | Main Topic (level 1) |
st2 | string | Subtopic 2 (level 2) |
st3 | string | Subtopic 3 (level 3) |
st4 | string | Subtopic 4 (level 4) |
keywords | string[] | All keywords in this topic cluster |
keyword_count | integer | Number of keywords in this cluster |
url_slug | string | Suggested URL slug for the centroid keyword |
published | boolean | Whether content has been published for this topic |
importance | float | Importance score (0 to 100) based on topical authority analysis. Matches the score shown in the Floyi dashboard. |
priority_category | string | Priority bucket: High, Medium, or Low |
serp_position | integer or null | Your brand’s best SERP ranking position (1-based) for this topic. null if not ranking. |
serp_all_positions | integer[] | All SERP positions where your brand ranks for this topic |
aio_status | string | Your brand’s presence in Google AI Overviews: cited, mentioned, not_present, or N/A (not tracked) |
aimode_status | string | Your brand’s presence in Google AI Mode: mentioned_cited, cited, mentioned, not_present, or N/A |
chatgpt_status | string | Your brand’s presence in ChatGPT Search: mentioned_cited, cited, mentioned, not_present, or N/A |
[!TIP] The hierarchy reflects all edits made in Floyi’s Organizer. If you need to build a tree structure, group nodes by
mt(pillars), thenst2(clusters), thenst3(topics), thenst4(pages). Usetypeto identify the level of each node.AI presence statuses:
citedmeans your URL appears in source links,mentionedmeans your brand name appears in the AI-generated text,mentioned_citedmeans both,not_presentmeans the AI response exists but your brand is absent, andN/Ameans tracking has not been enabled or no AI response has been captured for this topic.
Get SERP Data for a Node
Section titled “Get SERP Data for a Node”GET /api/v1/authority/{brand_id}/serp-data/{node_id}/Returns all stored SERP (Search Engine Results Page) results for a specific topic node. Use this to see which competitor pages rank for a topic before generating a brief. The node_id comes from the hierarchy response above.
[!NOTE] This endpoint returns all stored SERP data without filtering. Your AI agent or workflow should select which competitors are most relevant for the brief.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../serp-data/f1a2b3c4-.../brand_id = "a1b2c3d4-..."node_id = "f1a2b3c4-..."response = requests.get( f"{BASE_URL}/authority/{brand_id}/serp-data/{node_id}/", headers=headers,)serp_data = response.json()const brandId = "a1b2c3d4-...";const nodeId = "f1a2b3c4-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/serp-data/${nodeId}/`, { headers },);const serpData = await res.json();Response:
{ "node_id": "f1a2b3c4-...", "keyword": "best keyword research tools", "serp_results": [ { "position": 1, "url": "https://ahrefs.com/blog/keyword-research-tools/", "title": "12 Best Keyword Research Tools (Free & Paid)", "snippet": "We tested and reviewed the best keyword research tools..." }, { "position": 2, "url": "https://backlinko.com/best-keyword-research-tools", "title": "Best Keyword Research Tools in 2026", "snippet": "A hands-on comparison of the top keyword tools..." } ], "total_results": 10, "serp_updated_at": "2026-02-15T12:00:00Z", "snapshot_date": "2026-02-15"}SERP result fields:
| Field | Type | Description |
|---|---|---|
position | integer | Rank position in Google search results |
url | string | URL of the ranking page |
title | string | Page title from the SERP |
snippet | string | Description snippet from the SERP |
Error responses:
| Status | Cause |
|---|---|
404 Not Found | Node not found in the hierarchy, or no SERP data available for this node |
Toggle Published Status
Section titled “Toggle Published Status”PATCH /api/v1/authority/{brand_id}/nodes/{node_id}/published/Scope required: authority:write
Marks a topic node as published or unpublished. This updates the topical authority coverage tracking. Use this after you publish content for a topic to keep your authority metrics accurate.
Request body:
{ "published": true}Required fields: published (boolean)
curl -X PATCH \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{"published": true}' \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../nodes/f1a2b3c4-.../published/brand_id = "a1b2c3d4-..."node_id = "f1a2b3c4-..."response = requests.patch( f"{BASE_URL}/authority/{brand_id}/nodes/{node_id}/published/", headers=headers, json={"published": True},)result = response.json()const brandId = "a1b2c3d4-...";const nodeId = "f1a2b3c4-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/nodes/${nodeId}/published/`, { method: "PATCH", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ published: true }), },);const result = await res.json();Response:
{ "node_id": "f1a2b3c4-...", "published": true, "message": "Published status updated."}After updating, Floyi automatically recalculates coverage rollups for the topical authority view.
Error responses:
| Status | Cause |
|---|---|
400 Bad Request | Invalid node_id format (must be a valid UUID) or missing published field |
404 Not Found | Map not found or you do not have access |
Authority Briefs
Section titled “Authority Briefs”Scope required: authority:read (GET), authority:write (POST)
Authority briefs are linked to topics in your authority hierarchy. Unlike standalone briefs (which accept freeform query_text), authority briefs require a node_id from the hierarchy. The topic name, search intent, and content context are derived from the node automatically.
List Authority Briefs
Section titled “List Authority Briefs”GET /api/v1/authority/{brand_id}/briefs/Returns all authority-linked content briefs for a brand.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by brief status (e.g., COMPLETE, PENDING) |
search | string | Search by topic/query text or generated title (case-insensitive) |
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../briefs/
# Filter by statuscurl -H "X-API-Key: fyi_live_your_key_here" \ "https://api.floyi.com/api/v1/authority/a1b2c3d4-.../briefs/?status=COMPLETE"brand_id = "a1b2c3d4-..."response = requests.get( f"{BASE_URL}/authority/{brand_id}/briefs/", headers=headers)briefs = response.json()
# Filter by statusresponse = requests.get( f"{BASE_URL}/authority/{brand_id}/briefs/", headers=headers, params={"status": "COMPLETE"},)const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/briefs/`, { headers });const briefs = await res.json();
// Filter by statusconst filteredRes = await fetch( `${BASE_URL}/authority/${brandId}/briefs/?status=COMPLETE`, { headers },);Response:
{ "brand_id": "a1b2c3d4-...", "brand_name": "Floyi", "results": [ { "id": "b5c6d7e8-...", "query_text": "best keyword research tools", "brand_name": "Floyi", "status": "COMPLETE", "generated_title": "Best Keyword Research Tools in 2026", "generated_meta_description": "Discover the top keyword research tools...", "created_at": "2026-02-10T08:00:00Z", "updated_at": "2026-02-10T08:15:00Z" } ]}Get Authority Brief Details
Section titled “Get Authority Brief Details”GET /api/v1/authority/{brand_id}/briefs/{id}/Returns the full brief including result_json, curated_brief_json, input_params, and topic_node linkage to the authority hierarchy.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../briefs/b5c6d7e8-.../brand_id = "a1b2c3d4-..."brief_id = "b5c6d7e8-..."response = requests.get( f"{BASE_URL}/authority/{brand_id}/briefs/{brief_id}/", headers=headers,)brief = response.json()const brandId = "a1b2c3d4-...";const briefId = "b5c6d7e8-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/briefs/${briefId}/`, { headers },);const brief = await res.json();Response:
{ "id": "b5c6d7e8-...", "query_text": "best keyword research tools", "brand_name": "Floyi", "status": "COMPLETE", "generated_title": "Best Keyword Research Tools in 2026", "generated_meta_description": "Discover the top keyword research tools...", "result_json": { ... }, "curated_brief_json": { ... }, "input_params": { ... }, "topic_node": "f1a2b3c4-...", "created_at": "2026-02-10T08:00:00Z", "updated_at": "2026-02-10T08:15:00Z"}Detail-only fields:
| Field | Type | Description |
|---|---|---|
result_json | object | The raw brief output from the AI agent |
curated_brief_json | object | The user-edited version (if customized). null if no edits. |
input_params | object | Original request parameters including node_id, brand_id, selected_serp_data |
topic_node | UUID | The authority hierarchy node this brief is linked to |
Check Authority Brief Status
Section titled “Check Authority Brief Status”GET /api/v1/authority/{brand_id}/briefs/{id}/status/Returns just the status of an authority brief. Useful for polling during generation.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../briefs/b5c6d7e8-.../status/brand_id = "a1b2c3d4-..."brief_id = "b5c6d7e8-..."response = requests.get( f"{BASE_URL}/authority/{brand_id}/briefs/{brief_id}/status/", headers=headers,)status = response.json()const brandId = "a1b2c3d4-...";const briefId = "b5c6d7e8-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/briefs/${briefId}/status/`, { headers },);const status = await res.json();Response:
{ "id": "b5c6d7e8-...", "status": "COMPLETE", "query_text": "best keyword research tools", "generated_title": "Best Keyword Research Tools in 2026", "topic_node": "f1a2b3c4-..."}Generate Authority Brief
Section titled “Generate Authority Brief”POST /api/v1/authority/{brand_id}/briefs/generate/Triggers content brief generation for a topic from the authority hierarchy. The topic must exist in the brand’s hierarchy — use GET /api/v1/authority/{brand_id}/ to browse available topics and get node_id values.
Returns immediately with a 202 Accepted response. Poll the status endpoint for progress.
Request body:
{ "node_id": "f1a2b3c4-...", "ai_model_id": "gpt-5-mini"}Required fields: node_id
Optional fields:
| Field | Type | Description |
|---|---|---|
selected_serp_data | object[] | SERP competitor pages to analyze (max 15, each must include url). Auto-fetched from node’s stored SERP data if omitted. |
user_provided_keywords | string[] | Additional keywords to include in the brief |
internal_link_suggestions | object[] | Internal links to suggest (each: url, anchor_text) |
ai_model_id | string | AI model to use (e.g. "gpt-5-mini"). Uses default if not specified. |
[!TIP] How it works: First call
GET /api/v1/authority/{brand_id}/to get the topic hierarchy. Pick a topic and use itsnode_idin the generate request. The API automatically derives the topic name, search intent, and content context from the hierarchy node. Ifselected_serp_datais omitted, SERP data is auto-fetched from the node’s stored data.
[!TIP] Use
GET /api/v1/authority/{brand_id}/serp-data/{node_id}/to preview available SERP data before generating a brief, so you or your AI agent can choose which competitors to include.
# Minimal (topic name, SERP data, and context all derived from the node)curl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{"node_id": "f1a2b3c4-..."}' \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../briefs/generate/
# With custom SERP data (you choose which competitors to analyze)curl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{ "node_id": "f1a2b3c4-...", "selected_serp_data": [ {"url": "https://example.com", "title": "Example", "snippet": "..."} ] }' \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../briefs/generate/brand_id = "a1b2c3d4-..."
# Minimalresponse = requests.post( f"{BASE_URL}/authority/{brand_id}/briefs/generate/", headers=headers, json={"node_id": "f1a2b3c4-..."},)brief = response.json()
# With custom SERP dataresponse = requests.post( f"{BASE_URL}/authority/{brand_id}/briefs/generate/", headers=headers, json={ "node_id": "f1a2b3c4-...", "selected_serp_data": [ {"url": "https://example.com", "title": "Example", "snippet": "..."} ], },)const brandId = "a1b2c3d4-...";
// Minimalconst res = await fetch( `${BASE_URL}/authority/${brandId}/briefs/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ node_id: "f1a2b3c4-..." }), },);const brief = await res.json();
// With custom SERP dataconst res2 = await fetch( `${BASE_URL}/authority/${brandId}/briefs/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ node_id: "f1a2b3c4-...", selected_serp_data: [ { url: "https://example.com", title: "Example", snippet: "..." }, ], }), },);Response (202 Accepted):
{ "id": "b5c6d7e8-...", "status": "PENDING", "query_text": "best keyword research tools", "brand_name": "Floyi", "node_id": "f1a2b3c4-...", "message": "Brief generation started. Poll /api/v1/authority/{brand_id}/briefs/{id}/status/ for progress."}[!TIP] After generating, poll
GET /api/v1/authority/{brand_id}/briefs/{id}/status/every 10-15 seconds until the status changes toCOMPLETE. Brief generation typically takes 1-3 minutes.
Error responses:
| Status | Cause |
|---|---|
400 Bad Request | Validation error (SERP entry missing url, or no SERP data available) |
402 Payment Required | Insufficient credits |
404 Not Found | Brand not found, node not found in hierarchy, or no hierarchy exists |
404 — Node not in hierarchy:
{ "detail": "Topic node not found in the current authority hierarchy. Use GET /api/v1/authority/{brand_id}/ to browse valid topics."}400 — No SERP data available:
{ "detail": "No SERP data available for this topic. Please provide selected_serp_data or ensure SERP data has been collected for this topic in the topical map."}Authority Articles
Section titled “Authority Articles”Scope required: authority:read (GET), authority:write (POST)
Authority articles are linked to topics in your authority hierarchy via topic_node. Unlike standalone articles, authority articles are created from authority briefs and maintain the hierarchy linkage throughout the workflow.
List Authority Articles
Section titled “List Authority Articles”GET /api/v1/authority/{brand_id}/articles/Returns all authority-linked articles for a brand.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
search | string | Search by article title (case-insensitive) |
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../articles/brand_id = "a1b2c3d4-..."response = requests.get( f"{BASE_URL}/authority/{brand_id}/articles/", headers=headers,)articles = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/articles/`, { headers },);const articles = await res.json();Response:
{ "brand_id": "a1b2c3d4-...", "brand_name": "Floyi", "results": [ { "id": "c7d8e9f0-...", "title": "Best Keyword Research Tools in 2026", "brand_id": "a1b2c3d4-...", "brand_name": "Floyi", "topic_node": "f1a2b3c4-...", "machine_state": "draft_done", "editorial_state": "draft", "created_at": "2026-02-12T09:00:00Z", "updated_at": "2026-02-14T11:00:00Z" } ]}Get Authority Article Details
Section titled “Get Authority Article Details”GET /api/v1/authority/{brand_id}/articles/{id}/Returns the full article including brief reference, topic_node linkage, and all draft sections with their content.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../articles/c7d8e9f0-.../brand_id = "a1b2c3d4-..."article_id = "c7d8e9f0-..."response = requests.get( f"{BASE_URL}/authority/{brand_id}/articles/{article_id}/", headers=headers,)article = response.json()const brandId = "a1b2c3d4-...";const articleId = "c7d8e9f0-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/articles/${articleId}/`, { headers },);const article = await res.json();Response:
{ "id": "c7d8e9f0-...", "title": "Best Keyword Research Tools in 2026", "brand_id": "a1b2c3d4-...", "brand_name": "Floyi", "brief_id": "b5c6d7e8-...", "topic_node": "f1a2b3c4-...", "machine_state": "draft_done", "editorial_state": "draft", "additional_directions": null, "sections": [ { "id": "d1e2f3a4-...", "position": 0, "heading": "Introduction", "status": "approved", "ai_draft": "Keyword research is the foundation of any successful SEO strategy...", "word_count_target": 200 }, { "id": "e2f3a4b5-...", "position": 1, "heading": "Top Keyword Research Tools Compared", "status": "approved", "ai_draft": "Here are the best keyword research tools for 2026...", "word_count_target": 800 } ], "created_at": "2026-02-12T09:00:00Z", "updated_at": "2026-02-14T11:00:00Z"}Section fields:
| Field | Type | Description |
|---|---|---|
id | UUID | Section identifier |
position | integer | Order position (0-based) |
heading | string | Section heading |
status | string | Draft status: not_started, drafting, approved, needs_attention |
ai_draft | string or null | The AI-generated draft content for this section |
word_count_target | integer or null | Target word count for this section |
Check Authority Article Status
Section titled “Check Authority Article Status”GET /api/v1/authority/{brand_id}/articles/{id}/status/Returns generation progress including section-level status.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../articles/c7d8e9f0-.../status/brand_id = "a1b2c3d4-..."article_id = "c7d8e9f0-..."response = requests.get( f"{BASE_URL}/authority/{brand_id}/articles/{article_id}/status/", headers=headers,)status = response.json()const brandId = "a1b2c3d4-...";const articleId = "c7d8e9f0-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/articles/${articleId}/status/`, { headers },);const status = await res.json();Response:
{ "id": "c7d8e9f0-...", "machine_state": "in_progress", "topic_node": "f1a2b3c4-...", "sections_total": 8, "sections_approved": 3, "sections_drafting": 2, "sections_not_started": 3, "sections_needs_attention": 0, "completion_percentage": 37.5, "specialists_running": true, "specialists_complete": false}Generate Authority Article Draft from Brief
Section titled “Generate Authority Article Draft from Brief”POST /api/v1/authority/{brand_id}/articles/generate/Creates a new article from a completed authority brief and immediately starts AI draft generation — one step instead of two. The brief must be in COMPLETE or PARTIAL_COMPLETE status, must be an authority brief (not standalone), and must have generated sections. The article inherits the topic_node linkage from the brief.
Request body:
{ "brief_id": "b5c6d7e8-...", "ai_model_id": "claude-sonnet-4", "intent": "human", "specialists": { "web_research": true, "fact_check": true, "intro_key_takeaways": true, "web_research_mode": "basic" }}Required fields: brief_id, ai_model_id
Optional fields: intent, specialists (same options as standalone article generation)
curl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{"brief_id": "b5c6d7e8-...", "ai_model_id": "claude-sonnet-4"}' \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../articles/generate/brand_id = "a1b2c3d4-..."response = requests.post( f"{BASE_URL}/authority/{brand_id}/articles/generate/", headers=headers, json={ "brief_id": "b5c6d7e8-...", "ai_model_id": "claude-sonnet-4", },)article = response.json()# article["id"] -> use for status pollingconst brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/articles/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ brief_id: "b5c6d7e8-...", ai_model_id: "claude-sonnet-4", }), },);const article = await res.json();// article.id -> use for status pollingResponse (202 Accepted):
{ "id": "c7d8e9f0-...", "brief_id": "b5c6d7e8-...", "title": "Best Keyword Research Tools in 2026", "topic_node": "f1a2b3c4-...", "task_id": "e1f2a3b4-...", "status": "generating", "message": "Article created and draft generation started. Poll /api/v1/authority/{brand_id}/articles/{id}/status/ for progress."}[!TIP] After triggering generation, poll
GET /api/v1/authority/{brand_id}/articles/{id}/status/every 15-30 seconds. Draft generation typically takes 3-10 minutes depending on sections and specialists enabled.
Error responses:
| Status | Cause |
|---|---|
400 Bad Request | brief_id or ai_model_id missing, brief not complete, brief is not an authority brief, or brief has no sections |
402 Payment Required | Insufficient credits for draft or specialist agents |
404 Not Found | Brief or brand not found, or you do not have access |
Regenerate Authority Article Draft
Section titled “Regenerate Authority Article Draft”POST /api/v1/authority/{brand_id}/articles/{id}/generate/Regenerates draft for an existing authority article. Creates a new version, re-syncs sections from the brief, resets all sections, and generates fresh content. Optionally accepts a different brief_id to switch briefs (must also be an authority brief). The topic_node linkage is updated if the new brief has a different node.
Request body:
{ "ai_model_id": "claude-sonnet-4", "brief_id": "b5c6d7e8-...", "intent": "human", "specialists": { "web_research": true, "fact_check": true, "intro_key_takeaways": true, "web_research_mode": "basic" }}Required fields: ai_model_id
Optional fields: brief_id (switch to a different authority brief), intent, specialists
curl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{"ai_model_id": "claude-sonnet-4"}' \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../articles/c7d8e9f0-.../generate/brand_id = "a1b2c3d4-..."article_id = "c7d8e9f0-..."response = requests.post( f"{BASE_URL}/authority/{brand_id}/articles/{article_id}/generate/", headers=headers, json={"ai_model_id": "claude-sonnet-4"},)result = response.json()const brandId = "a1b2c3d4-...";const articleId = "c7d8e9f0-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/articles/${articleId}/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ ai_model_id: "claude-sonnet-4" }), },);const result = await res.json();Response (202 Accepted):
{ "id": "c7d8e9f0-...", "brief_id": "b5c6d7e8-...", "title": "Best Keyword Research Tools in 2026", "topic_node": "f1a2b3c4-...", "task_id": "e1f2a3b4-...", "status": "regenerating", "message": "Draft regeneration started. Poll /api/v1/authority/{brand_id}/articles/{id}/status/ for progress."}Error responses:
| Status | Cause |
|---|---|
400 Bad Request | Missing ai_model_id |
402 Payment Required | Insufficient credits for draft or specialist agents |
409 Conflict | Article generation already in progress |
User Profile, Credits, Teams & Audit Logs
Section titled “User Profile, Credits, Teams & Audit Logs”Scope required: user:read
Get Your Profile
Section titled “Get Your Profile”GET /api/v1/me/profile/Returns the authenticated user’s profile.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/me/profile/response = requests.get(f"{BASE_URL}/me/profile/", headers=headers)profile = response.json()const res = await fetch(`${BASE_URL}/me/profile/`, { headers });const profile = await res.json();Response:
{ "uuid": "e1f2a3b4-...", "email": "user@example.com", "first_name": "Jane", "last_name": "Doe", "subscription_plan": "Agency Plan", "subscription_interval": "monthly", "account_status": "active", "date_joined": "2025-06-01T00:00:00Z", "last_active": "2026-02-21T09:30:00Z"}Get Your Credit Balance
Section titled “Get Your Credit Balance”GET /api/v1/me/credits/Returns your current credit balance across all credit types.
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/me/credits/response = requests.get(f"{BASE_URL}/me/credits/", headers=headers)credits = response.json()const res = await fetch(`${BASE_URL}/me/credits/`, { headers });const credits = await res.json();Response:
{ "free_credits": 0, "monthly_credits": 450, "payg_credits": 100, "total_credits": 550, "monthly_credits_last_reset": "2026-02-01T00:00:00Z", "subscription_renewal_date": "2026-03-01T00:00:00Z"}List Your Teams
Section titled “List Your Teams”GET /api/v1/me/teams/Returns all teams you are an active member of. Use the team id as the X-Team-ID header value to access that team’s workspace data (see Workspace Selection).
curl -H "X-API-Key: fyi_live_your_key_here" \ https://api.floyi.com/api/v1/me/teams/response = requests.get(f"{BASE_URL}/me/teams/", headers=headers)teams = response.json()const res = await fetch(`${BASE_URL}/me/teams/`, { headers });const teams = await res.json();Response:
{ "_meta": { "description": "Lists all teams the authenticated user is an active member of..." }, "results": [ { "team": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Acme Marketing", "slug": "acme-marketing", "plan_name": "Agency Plan", "created_at": "2025-09-15T10:00:00Z" }, "role": "owner", "status": "active" } ]}Response fields:
| Field | Description |
|---|---|
team.id | The team UUID — use this as the X-Team-ID header value |
team.name | Display name of the team |
team.slug | URL-friendly team identifier |
role | Your role in this team: owner, admin, or member |
status | Membership status (always active in this response) |
Standalone Clustering
Section titled “Standalone Clustering”The Clustering Tool lets you cluster any set of keywords by SERP URL overlap — no brand required. It fetches SERP data for each keyword, then groups keywords that share similar search results.
Scope: clustering:read (read), clustering:write (write)
List Clustering Reports
Section titled “List Clustering Reports”GET /api/v1/clustering/Returns all standalone clustering reports.
curl -H "X-API-Key: fyi_live_your_key" \ https://api.floyi.com/api/v1/clustering/response = requests.get(f"{BASE_URL}/clustering/", headers=headers)reports = response.json()const res = await fetch(`${BASE_URL}/clustering/`, { headers });const reports = await res.json();Get Clustering Report Details
Section titled “Get Clustering Report Details”GET /api/v1/clustering/{id}/Returns full report with cluster results, keyword metrics, and SERP data timestamps.
curl -H "X-API-Key: fyi_live_your_key" \ https://api.floyi.com/api/v1/clustering/{id}/report_id = "abc-123"response = requests.get(f"{BASE_URL}/clustering/{report_id}/", headers=headers)report = response.json()const reportId = "abc-123";const res = await fetch(`${BASE_URL}/clustering/${reportId}/`, { headers });const report = await res.json();Check Clustering Task Progress
Section titled “Check Clustering Task Progress”GET /api/v1/clustering/{id}/status/Polls the progress of a clustering task. Returns task state, progress percentage, and message.
curl -H "X-API-Key: fyi_live_your_key" \ https://api.floyi.com/api/v1/clustering/{id}/status/report_id = "abc-123"response = requests.get(f"{BASE_URL}/clustering/{report_id}/status/", headers=headers)status = response.json()const reportId = "abc-123";const res = await fetch(`${BASE_URL}/clustering/${reportId}/status/`, { headers });const status = await res.json();Response:
{ "_meta": { "description": "..." }, "id": "abc-123", "status": "in_progress", "state": "PENDING", "progress": 45, "message": "Fetching SERPs and clustering in progress..."}Get SERP Data for a Keyword
Section titled “Get SERP Data for a Keyword”GET /api/v1/clustering/{id}/serp-data/?keyword=seo+toolsReturns stored SERP results for a specific keyword within a clustering report. Does not trigger live scraping.
curl -H "X-API-Key: fyi_live_your_key" \ "https://api.floyi.com/api/v1/clustering/{id}/serp-data/?keyword=seo+tools"report_id = "abc-123"response = requests.get( f"{BASE_URL}/clustering/{report_id}/serp-data/", headers=headers, params={"keyword": "seo tools"},)serp = response.json()const reportId = "abc-123";const res = await fetch( `${BASE_URL}/clustering/${reportId}/serp-data/?keyword=seo+tools`, { headers },);const serp = await res.json();Create Report + Start Clustering
Section titled “Create Report + Start Clustering”POST /api/v1/clustering/Creates a new clustering report and starts SERP-based keyword clustering. Costs credits based on keyword count. Poll GET /clustering/{id}/status/ for progress.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
report_name | string | Yes | Name for this clustering report |
keywords | string[] | Yes | List of keywords to cluster (max 10,000) |
country | string | No | Country code for SERP data (default: "us") |
location | string | No | Location for SERP data (e.g. "New York, NY") |
language | string | No | Language code (default: "en") |
overlap_percentage | float | No | URL overlap threshold 0.2-0.9 (default: 0.4) |
ai_model_id | string | No | AI model to use (uses default if omitted) |
curl -X POST \ -H "X-API-Key: fyi_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "report_name": "SEO Tools Cluster", "keywords": ["seo tools", "keyword research", "backlink checker", "rank tracker"], "country": "us", "overlap_percentage": 0.4 }' \ https://api.floyi.com/api/v1/clustering/response = requests.post( f"{BASE_URL}/clustering/", headers=headers, json={ "report_name": "SEO Tools Cluster", "keywords": ["seo tools", "keyword research", "backlink checker", "rank tracker"], "country": "us", "overlap_percentage": 0.4, },)report = response.json()# Poll report["id"] via /clustering/{id}/status/ for progressconst res = await fetch(`${BASE_URL}/clustering/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ report_name: "SEO Tools Cluster", keywords: ["seo tools", "keyword research", "backlink checker", "rank tracker"], country: "us", overlap_percentage: 0.4, }),});const report = await res.json();// Poll report.id via /clustering/{id}/status/ for progressResponse (202):
{ "_meta": { "description": "..." }, "id": "abc-123", "report_name": "SEO Tools Cluster", "status": "in_progress", "task_id": "celery-task-id", "group_id": "group-uuid", "total_batches": 1, "total_keywords": 4, "message": "Clustering started. Poll /api/v1/clustering/{id}/status/ for progress."}Errors:
| Status | Meaning |
|---|---|
| 400 | No valid keywords provided |
| 402 | Insufficient credits |
Re-cluster with New Overlap
Section titled “Re-cluster with New Overlap”POST /api/v1/clustering/{id}/recluster/Re-clusters keywords using existing SERP data with a new overlap percentage. No new SERP fetching, so no additional credit cost for SERPs.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
overlap_percentage | float | Yes | New overlap threshold 0.2-0.9 |
curl -X POST \ -H "X-API-Key: fyi_live_your_key" \ -H "Content-Type: application/json" \ -d '{"overlap_percentage": 0.6}' \ https://api.floyi.com/api/v1/clustering/{id}/recluster/report_id = "abc-123"response = requests.post( f"{BASE_URL}/clustering/{report_id}/recluster/", headers=headers, json={"overlap_percentage": 0.6},)result = response.json()const reportId = "abc-123";const res = await fetch( `${BASE_URL}/clustering/${reportId}/recluster/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ overlap_percentage: 0.6 }), },);const result = await res.json();Errors:
| Status | Meaning |
|---|---|
| 404 | Report not found |
| 409 | Clustering already in progress |
Delete a Clustering Report
Section titled “Delete a Clustering Report”DELETE /api/v1/clustering/{id}/Permanently deletes a clustering report and all associated SERP data.
curl -X DELETE \ -H "X-API-Key: fyi_live_your_key" \ https://api.floyi.com/api/v1/clustering/{id}/report_id = "abc-123"response = requests.delete(f"{BASE_URL}/clustering/{report_id}/", headers=headers)# Returns 204 No Content on successconst reportId = "abc-123";const res = await fetch( `${BASE_URL}/clustering/${reportId}/`, { method: "DELETE", headers },);// Returns 204 No Content on successReturns 204 No Content on success.
Authority Clustering
Section titled “Authority Clustering”Authority Clustering is the brand-tied version. Keywords are automatically extracted from the brand’s topical research hierarchy — you don’t provide them manually. Results feed into the topical map generation pipeline.
Scope: clustering:read (read), clustering:write (write)
Get Brand Clustering Results
Section titled “Get Brand Clustering Results”GET /api/v1/authority/{brand_id}/clustering/Returns the most recent clustering results for a brand, including cluster centroids, keyword associations, and metrics.
curl -H "X-API-Key: fyi_live_your_key" \ https://api.floyi.com/api/v1/authority/{brand_id}/clustering/brand_id = "a1b2c3d4-..."response = requests.get( f"{BASE_URL}/authority/{brand_id}/clustering/", headers=headers,)results = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/clustering/`, { headers },);const results = await res.json();Check Brand Clustering Progress
Section titled “Check Brand Clustering Progress”GET /api/v1/authority/{brand_id}/clustering/status/Polls the progress of a brand clustering task.
curl -H "X-API-Key: fyi_live_your_key" \ https://api.floyi.com/api/v1/authority/{brand_id}/clustering/status/brand_id = "a1b2c3d4-..."response = requests.get( f"{BASE_URL}/authority/{brand_id}/clustering/status/", headers=headers,)status = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/clustering/status/`, { headers },);const status = await res.json();Get Brand SERP Data for a Keyword
Section titled “Get Brand SERP Data for a Keyword”GET /api/v1/authority/{brand_id}/clustering/serp-data/?keyword=seo+toolsReturns stored SERP results for a keyword from the brand’s clustering data.
curl -H "X-API-Key: fyi_live_your_key" \ "https://api.floyi.com/api/v1/authority/{brand_id}/clustering/serp-data/?keyword=seo+tools"brand_id = "a1b2c3d4-..."response = requests.get( f"{BASE_URL}/authority/{brand_id}/clustering/serp-data/", headers=headers, params={"keyword": "seo tools"},)serp = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/clustering/serp-data/?keyword=seo+tools`, { headers },);const serp = await res.json();Start Brand Clustering
Section titled “Start Brand Clustering”POST /api/v1/authority/{brand_id}/clustering/start/Starts SERP-based keyword clustering for a brand. Keywords are automatically extracted from the brand’s topical research. Costs credits based on keyword count. Poll GET /authority/{brand_id}/clustering/status/ for progress.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
country | string | No | Country code for SERP data (default: "us") |
location | string | No | Location for SERP data |
language | string | No | Language code (default: "en") |
overlap_percentage | float | No | URL overlap threshold 0.2-0.9 (default: 0.4) |
ai_model_id | string | No | AI model to use |
curl -X POST \ -H "X-API-Key: fyi_live_your_key" \ -H "Content-Type: application/json" \ -d '{"country": "us", "overlap_percentage": 0.4}' \ https://api.floyi.com/api/v1/authority/{brand_id}/clustering/start/brand_id = "a1b2c3d4-..."response = requests.post( f"{BASE_URL}/authority/{brand_id}/clustering/start/", headers=headers, json={"country": "us", "overlap_percentage": 0.4},)result = response.json()# Poll via /authority/{brand_id}/clustering/status/ for progressconst brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/clustering/start/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ country: "us", overlap_percentage: 0.4 }), });const result = await res.json();// Poll via /authority/{brandId}/clustering/status/ for progressResponse (202):
{ "_meta": { "description": "..." }, "brand_id": "abc-123", "status": "in_progress", "task_id": "celery-task-id", "group_id": "group-uuid", "total_batches": 3, "total_keywords": 250, "message": "Clustering started. Poll /api/v1/authority/{brand_id}/clustering/status/ for progress."}Errors:
| Status | Meaning |
|---|---|
| 400 | No keywords found in topical research |
| 402 | Insufficient credits |
| 404 | Brand not found |
| 409 | Clustering already in progress |
Re-cluster Brand with New Overlap
Section titled “Re-cluster Brand with New Overlap”POST /api/v1/authority/{brand_id}/clustering/recluster/Re-clusters a brand’s keywords using existing SERP data with a new overlap percentage.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
overlap_percentage | float | Yes | New overlap threshold 0.2-0.9 |
curl -X POST \ -H "X-API-Key: fyi_live_your_key" \ -H "Content-Type: application/json" \ -d '{"overlap_percentage": 0.6}' \ https://api.floyi.com/api/v1/authority/{brand_id}/clustering/recluster/brand_id = "a1b2c3d4-..."response = requests.post( f"{BASE_URL}/authority/{brand_id}/clustering/recluster/", headers=headers, json={"overlap_percentage": 0.6},)result = response.json()const brandId = "a1b2c3d4-...";const res = await fetch( `${BASE_URL}/authority/${brandId}/clustering/recluster/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ overlap_percentage: 0.6 }), },);const result = await res.json();Errors:
| Status | Meaning |
|---|---|
| 404 | Brand or clustering results not found |
| 409 | Clustering already in progress |
Part 6: Managing API Keys
Section titled “Part 6: Managing API Keys”You can manage your API keys from the Settings > API Keys tab in Floyi or programmatically through the key management endpoints.
Viewing Your Keys
Section titled “Viewing Your Keys”The API Keys tab shows all your keys with:
- Name — The label you gave the key
- Prefix — The first 12 characters (e.g.,
fyi_live_a3b...) for identification - Type — Integration or Developer
- Status — Active or Revoked
- Last Used — When the key was last used to make a request
- Created — When the key was created
Revoking a Key
Section titled “Revoking a Key”Revoking permanently deactivates a key. Any request using a revoked key will receive a 401 error.
- Find the key in the API Keys tab.
- Click the Revoke button (trash icon).
- Confirm the revocation.
Revocation is immediate and irreversible.
Rotating a Key
Section titled “Rotating a Key”Rotating creates a new key with the same name, type, scopes, and settings as the old key, then revokes the old key. Use this to periodically refresh your keys without changing your configuration.
- Find the key in the API Keys tab.
- Click the Rotate button.
- Copy the new key immediately.
The old key stops working as soon as the new key is created.
Part 7: Audit Logging
Section titled “Part 7: Audit Logging”Every request made to the /api/v1/ endpoints using an API key is logged automatically. Audit logs capture:
- Endpoint path and HTTP method
- Response status code
- Client IP address
- Response time in milliseconds
- Timestamp
Use the audit log endpoint to view your request history, debug issues, and monitor for unauthorized access.
View Audit Logs
Section titled “View Audit Logs”GET /api/v1/me/audit-logs/Scope required: user:read
Returns a paginated list of API request logs for all of your API keys. Results are ordered by most recent first. Audit logs are retained for 90 days.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
api_key_id | UUID | Filter by a specific API key |
method | string | Filter by HTTP method (GET, POST, PUT, DELETE) |
status_code | integer | Filter by response status code (e.g., 200, 400, 429) |
endpoint | string | Filter by endpoint path (partial match) |
page | integer | Page number (default: 1) |
page_size | integer | Results per page (default: 50, max: 200) |
# View recent audit logscurl -H "X-API-Key: fyi_live_your_key" \ https://api.floyi.com/api/v1/me/audit-logs/
# Filter by specific key and methodcurl -H "X-API-Key: fyi_live_your_key" \ "https://api.floyi.com/api/v1/me/audit-logs/?api_key_id=a1b2c3d4-...&method=POST"
# Filter by status code (e.g., find failed requests)curl -H "X-API-Key: fyi_live_your_key" \ "https://api.floyi.com/api/v1/me/audit-logs/?status_code=429&page_size=20"# View recent audit logsresponse = requests.get(f"{BASE_URL}/me/audit-logs/", headers=headers)logs = response.json()
# Filter by specific key and methodresponse = requests.get( f"{BASE_URL}/me/audit-logs/", headers=headers, params={"api_key_id": "a1b2c3d4-...", "method": "POST"},)
# Filter by status coderesponse = requests.get( f"{BASE_URL}/me/audit-logs/", headers=headers, params={"status_code": 429, "page_size": 20},)// View recent audit logsconst res = await fetch(`${BASE_URL}/me/audit-logs/`, { headers });const logs = await res.json();
// Filter by specific key and methodconst params = new URLSearchParams({ api_key_id: "a1b2c3d4-...", method: "POST",});const res2 = await fetch( `${BASE_URL}/me/audit-logs/?${params}`, { headers },);
// Filter by status codeconst res3 = await fetch( `${BASE_URL}/me/audit-logs/?status_code=429&page_size=20`, { headers },);Response:
{ "_meta": { "description": "Paginated list of API request audit logs..." }, "count": 342, "next": "https://api.floyi.com/api/v1/me/audit-logs/?page=2", "previous": null, "results": [ { "id": "e1f2a3b4-...", "api_key_name": "My Integration Key", "api_key_prefix": "fyi_live_a3b...", "endpoint": "/api/v1/brands/", "method": "GET", "status_code": 200, "ip_address": "203.0.113.42", "response_time_ms": 45, "timestamp": "2026-02-27T14:30:00Z" }, { "id": "d5c6b7a8-...", "api_key_name": "My Integration Key", "api_key_prefix": "fyi_live_a3b...", "endpoint": "/api/v1/research/tr_abc123/tree/", "method": "GET", "status_code": 200, "ip_address": "203.0.113.42", "response_time_ms": 120, "timestamp": "2026-02-27T14:29:55Z" } ]}Response fields:
| Field | Description |
|---|---|
count | Total number of matching audit log entries |
next | URL for the next page (null if on the last page) |
previous | URL for the previous page (null if on the first page) |
id | Unique audit log entry ID |
api_key_name | Name of the API key that made the request |
api_key_prefix | Prefix of the API key (e.g., fyi_live_a3b...) |
endpoint | The API endpoint that was called |
method | HTTP method (GET, POST, PUT, DELETE) |
status_code | HTTP response status code |
ip_address | Client IP address |
response_time_ms | Server response time in milliseconds |
timestamp | When the request was made (ISO 8601) |
Part 8: Security Best Practices
Section titled “Part 8: Security Best Practices”Key Storage
Section titled “Key Storage”- Store API keys in environment variables or a secrets manager. Never hardcode them.
- Floyi stores only the SHA-256 hash of your key. The plaintext is shown once at creation and cannot be recovered.
IP Restrictions
Section titled “IP Restrictions”IP allowlists can be configured for API keys. When set, requests from IPs not on the list receive a 401 error.
Key Expiration
Section titled “Key Expiration”You can set an optional expiration date when creating a key. Expired keys automatically stop working. Use this for temporary access (e.g., contractor integrations or time-limited automations).
Rotation Schedule
Section titled “Rotation Schedule”Rotate your API keys periodically, especially if they are used in shared environments. Floyi’s rotate feature makes this seamless — same settings, new key, old key revoked instantly.
Part 9: Common Use Cases
Section titled “Part 9: Common Use Cases”AI Agent: Full Content Pipeline (Authority Flow)
Section titled “AI Agent: Full Content Pipeline (Authority Flow)”An AI agent can automate your entire content workflow — from analyzing topic gaps to generating published drafts. This uses the authority endpoints so all briefs and articles are linked to your topical hierarchy.
# 1. Fetch the authority hierarchy to analyze coveragecurl -H "X-API-Key: fyi_live_your_key" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../
# 2. Get SERP data for the chosen topiccurl -H "X-API-Key: fyi_live_your_key" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../serp-data/f1a2b3c4-.../
# 3. Generate an authority brief (node_id from step 1)curl -X POST \ -H "X-API-Key: fyi_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "node_id": "f1a2b3c4-...", "selected_serp_data": [ {"url": "https://competitor1.com/...", "title": "...", "snippet": "..."}, {"url": "https://competitor2.com/...", "title": "...", "snippet": "..."} ] }' \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../briefs/generate/
# 4. Poll until brief is complete (every 10-15 seconds)curl -H "X-API-Key: fyi_live_your_key" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../briefs/b5c6d7e8-.../status/
# 5. Generate an authority article draft from the completed briefcurl -X POST \ -H "X-API-Key: fyi_live_your_key" \ -H "Content-Type: application/json" \ -d '{"brief_id": "b5c6d7e8-...", "ai_model_id": "claude-sonnet-4", "specialists": {"web_research": true, "fact_check": true}}' \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../articles/generate/
# 6. Poll until draft is complete (every 15-30 seconds)curl -H "X-API-Key: fyi_live_your_key" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../articles/c7d8e9f0-.../status/
# 7. Mark the topic as publishedcurl -X PATCH \ -H "X-API-Key: fyi_live_your_key" \ -H "Content-Type: application/json" \ -d '{"published": true}' \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../nodes/f1a2b3c4-.../published/import timeimport requests
API_KEY = "fyi_live_your_key"BASE_URL = "https://api.floyi.com/api/v1"headers = {"X-API-Key": API_KEY}brand_id = "a1b2c3d4-..."
# 1. Fetch the authority hierarchyhierarchy = requests.get( f"{BASE_URL}/authority/{brand_id}/", headers=headers).json()
# 2. Pick a topic and get its SERP datanode_id = "f1a2b3c4-..."serp = requests.get( f"{BASE_URL}/authority/{brand_id}/serp-data/{node_id}/", headers=headers,).json()
# 3. Generate an authority briefbrief = requests.post( f"{BASE_URL}/authority/{brand_id}/briefs/generate/", headers=headers, json={ "node_id": node_id, "selected_serp_data": serp["serp_results"][:5], },).json()
# 4. Poll until brief is completewhile True: status = requests.get( f"{BASE_URL}/authority/{brand_id}/briefs/{brief['id']}/status/", headers=headers, ).json() if status["status"] in ("COMPLETE", "PARTIAL_COMPLETE", "FAILED"): break time.sleep(12)
# 5. Generate an authority article draftarticle = requests.post( f"{BASE_URL}/authority/{brand_id}/articles/generate/", headers=headers, json={ "brief_id": brief["id"], "ai_model_id": "claude-sonnet-4", "specialists": {"web_research": True, "fact_check": True}, },).json()
# 6. Poll until draft is completewhile True: progress = requests.get( f"{BASE_URL}/authority/{brand_id}/articles/{article['id']}/status/", headers=headers, ).json() if progress["completion_percentage"] == 100.0: break time.sleep(20)
# 7. Mark the topic as publishedrequests.patch( f"{BASE_URL}/authority/{brand_id}/nodes/{node_id}/published/", headers=headers, json={"published": True},)const API_KEY = "fyi_live_your_key";const BASE_URL = "https://api.floyi.com/api/v1";const headers = { "X-API-Key": API_KEY };const brandId = "a1b2c3d4-...";
// 1. Fetch the authority hierarchyconst hierarchy = await fetch( `${BASE_URL}/authority/${brandId}/`, { headers }).then(r => r.json());
// 2. Pick a topic and get its SERP dataconst nodeId = "f1a2b3c4-...";const serp = await fetch( `${BASE_URL}/authority/${brandId}/serp-data/${nodeId}/`, { headers }).then(r => r.json());
// 3. Generate an authority briefconst brief = await fetch( `${BASE_URL}/authority/${brandId}/briefs/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ node_id: nodeId, selected_serp_data: serp.serp_results.slice(0, 5), }), }).then(r => r.json());
// 4. Poll until brief is completeconst poll = async (url, check, interval) => { while (true) { const data = await fetch(url, { headers }).then(r => r.json()); if (check(data)) return data; await new Promise(r => setTimeout(r, interval)); }};await poll( `${BASE_URL}/authority/${brandId}/briefs/${brief.id}/status/`, d => ["COMPLETE", "PARTIAL_COMPLETE", "FAILED"].includes(d.status), 12000,);
// 5. Generate an authority article draftconst article = await fetch( `${BASE_URL}/authority/${brandId}/articles/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ brief_id: brief.id, ai_model_id: "claude-sonnet-4", specialists: { web_research: true, fact_check: true }, }), }).then(r => r.json());
// 6. Poll until draft is completeawait poll( `${BASE_URL}/authority/${brandId}/articles/${article.id}/status/`, d => d.completion_percentage === 100.0, 20000,);
// 7. Mark the topic as publishedawait fetch( `${BASE_URL}/authority/${brandId}/nodes/${nodeId}/published/`, { method: "PATCH", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ published: true }), });The AI agent can loop through this flow for each topic in the map, prioritizing by importance score and unpublished status.
Zapier: Create a Brand When a Client is Added
Section titled “Zapier: Create a Brand When a Client is Added”Use a Zapier Webhook step to call the brands endpoint when a new client appears in your CRM:
curl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{"brand_name": "New Client Inc", "website_url": "https://newclient.com"}' \ https://api.floyi.com/api/v1/brands/response = requests.post( f"{BASE_URL}/brands/", headers=headers, json={"brand_name": "New Client Inc", "website_url": "https://newclient.com"},)await fetch(`${BASE_URL}/brands/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ brand_name: "New Client Inc", website_url: "https://newclient.com", }),});Quick Content Pipeline (Authority Flow)
Section titled “Quick Content Pipeline (Authority Flow)”Generate an authority brief, create an article, and trigger a draft — the minimum steps to go from a hierarchy topic to content:
# 1. Generate an authority brief (node_id from GET /api/v1/authority/{brand_id}/)curl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{"node_id": "f1a2b3c4-..."}' \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../briefs/generate/# Returns: {"id": "b5c6d7e8-...", "status": "PENDING"}
# 2. Poll until brief is complete (every 10-15 seconds)curl -H "X-API-Key: fyi_live_your_integration_key" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../briefs/b5c6d7e8-.../status/
# 3. Generate an authority article draftcurl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{"brief_id": "b5c6d7e8-...", "ai_model_id": "claude-sonnet-4"}' \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../articles/generate/
# 4. Poll until draft is complete (every 15-30 seconds)curl -H "X-API-Key: fyi_live_your_integration_key" \ https://api.floyi.com/api/v1/authority/a1b2c3d4-.../articles/c7d8e9f0-.../status/brand_id = "a1b2c3d4-..."
# 1. Generate an authority briefbrief = requests.post( f"{BASE_URL}/authority/{brand_id}/briefs/generate/", headers=headers, json={"node_id": "f1a2b3c4-..."},).json()
# 2. Poll until brief is completewhile True: s = requests.get( f"{BASE_URL}/authority/{brand_id}/briefs/{brief['id']}/status/", headers=headers, ).json() if s["status"] in ("COMPLETE", "PARTIAL_COMPLETE", "FAILED"): break time.sleep(12)
# 3. Generate an authority article draftarticle = requests.post( f"{BASE_URL}/authority/{brand_id}/articles/generate/", headers=headers, json={"brief_id": brief["id"], "ai_model_id": "claude-sonnet-4"},).json()
# 4. Poll until draft is completewhile True: p = requests.get( f"{BASE_URL}/authority/{brand_id}/articles/{article['id']}/status/", headers=headers, ).json() if p["completion_percentage"] == 100.0: break time.sleep(20)const brandId = "a1b2c3d4-...";
// 1. Generate an authority briefconst brief = await fetch( `${BASE_URL}/authority/${brandId}/briefs/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ node_id: "f1a2b3c4-..." }), }).then(r => r.json());
// 2. Poll until brief is completelet status;do { await new Promise(r => setTimeout(r, 12000)); status = await fetch( `${BASE_URL}/authority/${brandId}/briefs/${brief.id}/status/`, { headers }, ).then(r => r.json());} while (!["COMPLETE", "PARTIAL_COMPLETE", "FAILED"].includes(status.status));
// 3. Generate an authority article draftconst article = await fetch( `${BASE_URL}/authority/${brandId}/articles/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ brief_id: brief.id, ai_model_id: "claude-sonnet-4" }), }).then(r => r.json());
// 4. Poll until draft is completelet progress;do { await new Promise(r => setTimeout(r, 20000)); progress = await fetch( `${BASE_URL}/authority/${brandId}/articles/${article.id}/status/`, { headers }, ).then(r => r.json());} while (progress.completion_percentage < 100.0);Quick Content Pipeline (Standalone)
Section titled “Quick Content Pipeline (Standalone)”For one-off briefs not tied to the authority hierarchy:
# 1. Generate a standalone brief with any topiccurl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{"query_text": "best seo tools for agencies", "brand_id": "a1b2c3d4-..."}' \ https://api.floyi.com/api/v1/briefs/generate/
# 2. Poll until brief is completecurl -H "X-API-Key: fyi_live_your_integration_key" \ https://api.floyi.com/api/v1/briefs/b5c6d7e8-.../status/
# 3. Generate an article draft from the briefcurl -X POST \ -H "X-API-Key: fyi_live_your_integration_key" \ -H "Content-Type: application/json" \ -d '{"brief_id": "b5c6d7e8-...", "ai_model_id": "claude-sonnet-4"}' \ https://api.floyi.com/api/v1/content/articles/generate/
# 4. Poll until completecurl -H "X-API-Key: fyi_live_your_integration_key" \ https://api.floyi.com/api/v1/content/articles/c7d8e9f0-.../status/# 1. Generate a standalone briefbrief = requests.post( f"{BASE_URL}/briefs/generate/", headers=headers, json={ "query_text": "best seo tools for agencies", "brand_id": "a1b2c3d4-...", },).json()
# 2. Poll until brief is completewhile True: s = requests.get( f"{BASE_URL}/briefs/{brief['id']}/status/", headers=headers ).json() if s["status"] in ("COMPLETE", "PARTIAL_COMPLETE", "FAILED"): break time.sleep(12)
# 3. Generate an article draftarticle = requests.post( f"{BASE_URL}/content/articles/generate/", headers=headers, json={"brief_id": brief["id"], "ai_model_id": "claude-sonnet-4"},).json()
# 4. Poll until completewhile True: p = requests.get( f"{BASE_URL}/content/articles/{article['id']}/status/", headers=headers, ).json() if p["completion_percentage"] == 100.0: break time.sleep(20)// 1. Generate a standalone briefconst brief = await fetch(`${BASE_URL}/briefs/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ query_text: "best seo tools for agencies", brand_id: "a1b2c3d4-...", }),}).then(r => r.json());
// 2. Poll until brief is completelet status;do { await new Promise(r => setTimeout(r, 12000)); status = await fetch( `${BASE_URL}/briefs/${brief.id}/status/`, { headers } ).then(r => r.json());} while (!["COMPLETE", "PARTIAL_COMPLETE", "FAILED"].includes(status.status));
// 3. Generate an article draftconst article = await fetch(`${BASE_URL}/content/articles/generate/`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ brief_id: brief.id, ai_model_id: "claude-sonnet-4", }),}).then(r => r.json());
// 4. Poll until completelet progress;do { await new Promise(r => setTimeout(r, 20000)); progress = await fetch( `${BASE_URL}/content/articles/${article.id}/status/`, { headers } ).then(r => r.json());} while (progress.completion_percentage < 100.0);Endpoint Reference
Section titled “Endpoint Reference”| Endpoint | Method | Scope | Description |
|---|---|---|---|
| Brands | |||
/api/v1/brands/ | GET | brands:read | List all brands (?search=) |
/api/v1/brands/{id}/ | GET | brands:read | Get brand details |
/api/v1/brands/ | POST | brands:write | Create a brand |
| Content Briefs (Standalone) | |||
/api/v1/briefs/ | GET | briefs:read | List standalone briefs (?brand_id= ?status= ?search=) |
/api/v1/briefs/{id}/ | GET | briefs:read | Get brief with full data |
/api/v1/briefs/{id}/status/ | GET | briefs:read | Check brief generation status |
/api/v1/briefs/generate/ | POST | briefs:write | Generate brief (freeform query_text) |
| Content Articles (Standalone) | |||
/api/v1/content/articles/ | GET | content:read | List standalone articles (?brand_id= ?search=) |
/api/v1/content/articles/{id}/ | GET | content:read | Get article details with sections |
/api/v1/content/articles/{id}/status/ | GET | content:read | Check draft generation progress |
/api/v1/content/articles/generate/ | POST | content:write | Generate article draft from a brief |
/api/v1/content/articles/{id}/generate/ | POST | content:write | Regenerate draft for existing article |
| Topical Maps | |||
/api/v1/maps/ | GET | maps:read | List topical maps (?brand_id=) |
/api/v1/maps/{brand_id}/ | GET | maps:read | Get raw map clusters |
| Topical Research | |||
/api/v1/research/ | GET | research:read | List research records |
/api/v1/research/{brand_id}/ | GET | research:read | Get full research tree with stats |
/api/v1/research/{brand_id}/stats/ | GET | research:read | Get tree stats only |
/api/v1/research/{brand_id}/task-status/ | GET | research:read | Poll AI generation task status |
/api/v1/research/{brand_id}/nodes/rename/ | PATCH | research:write | Rename a node |
/api/v1/research/{brand_id}/nodes/add/ | POST | research:write | Add a new node |
/api/v1/research/{brand_id}/nodes/delete/ | POST | research:write | Delete a node and descendants |
/api/v1/research/{brand_id}/nodes/keywords/ | PATCH | research:write | Add/remove keywords on a node |
/api/v1/research/{brand_id}/nodes/move/ | POST | research:write | Move node to new parent |
/api/v1/research/{brand_id}/nodes/merge/ | POST | research:write | Merge two same-level nodes |
/api/v1/research/{brand_id}/nodes/bulk/ | POST | research:write | Execute multiple operations atomically |
/api/v1/research/{brand_id}/diff/ | POST | research:read | Compare two tree states |
| Topical Authority | |||
/api/v1/authority/ | GET | authority:read | List authority maps (?brand_id=) |
/api/v1/authority/{brand_id}/ | GET | authority:read | Get enriched hierarchy with overrides |
/api/v1/authority/{brand_id}/serp-data/{node_id}/ | GET | authority:read | Get stored SERP results for a node |
/api/v1/authority/{brand_id}/nodes/{node_id}/published/ | PATCH | authority:write | Toggle published status |
| Authority Briefs | |||
/api/v1/authority/{brand_id}/briefs/ | GET | authority:read | List authority briefs (?status= ?search=) |
/api/v1/authority/{brand_id}/briefs/{id}/ | GET | authority:read | Get authority brief with full data |
/api/v1/authority/{brand_id}/briefs/{id}/status/ | GET | authority:read | Check authority brief status |
/api/v1/authority/{brand_id}/briefs/generate/ | POST | authority:write | Generate brief for a hierarchy node |
| Authority Articles | |||
/api/v1/authority/{brand_id}/articles/ | GET | authority:read | List authority articles (?search=) |
/api/v1/authority/{brand_id}/articles/{id}/ | GET | authority:read | Get authority article with sections |
/api/v1/authority/{brand_id}/articles/{id}/status/ | GET | authority:read | Check draft generation progress |
/api/v1/authority/{brand_id}/articles/generate/ | POST | authority:write | Generate article draft from authority brief |
/api/v1/authority/{brand_id}/articles/{id}/generate/ | POST | authority:write | Regenerate draft for existing authority article |
| Standalone Clustering | |||
/api/v1/clustering/ | GET | clustering:read | List clustering reports |
/api/v1/clustering/{id}/ | GET | clustering:read | Get report with full cluster results |
/api/v1/clustering/{id}/status/ | GET | clustering:read | Check clustering task progress |
/api/v1/clustering/{id}/serp-data/?keyword= | GET | clustering:read | Get SERP data for a keyword |
/api/v1/clustering/ | POST | clustering:write | Create report + start clustering |
/api/v1/clustering/{id}/recluster/ | POST | clustering:write | Re-cluster with new overlap |
/api/v1/clustering/{id}/ | DELETE | clustering:write | Delete clustering report |
| Authority Clustering | |||
/api/v1/authority/{brand_id}/clustering/ | GET | clustering:read | Get brand clustering results |
/api/v1/authority/{brand_id}/clustering/status/ | GET | clustering:read | Check brand clustering progress |
/api/v1/authority/{brand_id}/clustering/serp-data/?keyword= | GET | clustering:read | Get brand SERP data for keyword |
/api/v1/authority/{brand_id}/clustering/start/ | POST | clustering:write | Start brand clustering |
/api/v1/authority/{brand_id}/clustering/recluster/ | POST | clustering:write | Re-cluster brand with new overlap |
| User | |||
/api/v1/me/profile/ | GET | user:read | Get your profile |
/api/v1/me/credits/ | GET | user:read | Get your credit balance |
/api/v1/me/teams/ | GET | user:read | List your teams (for X-Team-ID header) |
/api/v1/me/audit-logs/ | GET | user:read | Paginated API request logs (?api_key_id= ?method= ?status_code= ?endpoint=) |
Frequently Asked Questions
Section titled “Frequently Asked Questions”Who can create API keys?
Section titled “Who can create API keys?”Only users on the Agency Plan can create API keys.
Can I recover a lost API key?
Section titled “Can I recover a lost API key?”No. Floyi stores only the SHA-256 hash of the key, not the plaintext. If you lose a key, revoke it and create a new one.
How do I know which scopes my key has?
Section titled “How do I know which scopes my key has?”The scopes are set when you create the key based on the key type (Integration or Developer) and any customizations you made. You can also call GET /api/v1/me/ scopes endpoint to see all available scopes and the defaults for each key type.
What happens if I downgrade my plan?
Section titled “What happens if I downgrade my plan?”Existing API keys will stop working immediately. All API requests will return a 401 Unauthorized error until you upgrade back to the Agency plan. Your keys are not deleted — they will resume working once you re-subscribe. You also cannot create new keys while on a non-Agency plan.
Can I use the API from a browser?
Section titled “Can I use the API from a browser?”The API is designed for server-to-server communication. While technically possible, we recommend against calling the API from client-side JavaScript since it would expose your API key. Use a backend proxy instead.
Is there a sandbox or test environment?
Section titled “Is there a sandbox or test environment?”Not currently. All API requests operate on your live Floyi data. Use a dedicated test brand or workspace to experiment without affecting production content.
How do I find a resource without knowing its ID?
Section titled “How do I find a resource without knowing its ID?”Use the ?search= query parameter on list endpoints. Search is case-insensitive and supports partial matches:
- Brands:
?search=floyisearches by brand name - Briefs:
?search=topical+authoritysearches by topic/query text and generated title - Articles:
?search=seo+toolssearches by article title
You can combine search with other filters: ?search=topical&brand_id=...&status=COMPLETE
What format are IDs in?
Section titled “What format are IDs in?”All resource IDs are UUIDs (e.g., a1b2c3d4-e5f6-7890-abcd-ef1234567890).
Are responses paginated?
Section titled “Are responses paginated?”Most list endpoints return all matching results without pagination. Use query filters (e.g., brand_id, status, search) to narrow results. The audit logs endpoint (/api/v1/me/audit-logs/) is paginated with ?page= and ?page_size= parameters.
Can I use the API with team workspaces?
Section titled “Can I use the API with team workspaces?”Yes. API keys are account-level and work with any workspace you have access to. Include the X-Team-ID header with a team UUID to access that team’s data, or omit the header to access your personal workspace. Use GET /api/v1/me/teams/ to discover your team UUIDs. You can only access teams where you are an active member (owner, admin, or member role).
Can team members share an API key?
Section titled “Can team members share an API key?”No. Each API key is tied to the user who created it. Only users on the Agency Plan can create keys. If a team member needs API access, they must be on an Agency plan themselves and create their own key. The key inherits the creator’s team memberships — it can only access teams that the key owner belongs to.