Skip to content

Integration Guide

API Overview

All API endpoints are available at /api/. Authentication endpoints are at /api/auth.

Authentication is via IDP-issued access tokens. The Floh web UI uses httpOnly session cookies (BFF pattern). External API clients use Authorization: Bearer <token> with a valid IDP access token. See External API Access for details.

Interactive API Documentation

The server provides auto-generated OpenAPI 3.0 documentation via Swagger UI:

Resource URL
Swagger UI (interactive explorer) https://<host>/api/docs
OpenAPI JSON spec (machine-readable) https://<host>/api/docs/json

For local development this is https://localhost:7070/api/docs. The Swagger UI lets you browse all endpoints grouped by tag, inspect request/response schemas, and try out API calls directly. The JSON spec can be imported into tools like Postman, Insomnia, or used with OpenAPI code generators.

Identity provider setup

Provider Document
Okta (OIDC login + inbound SCIM into Floh) Okta setup for Floh
Microsoft Entra ID (OIDC + inbound SCIM) Entra setup for Floh
Okta (create / update / deactivate in Okta) Okta outbound user lifecycle
Inbound SCIM (any IdP) Inbound SCIM
Outbound SCIM (Floh → any IdP) SCIM outbound

Endpoints

Auth (at /api/auth, not versioned)

  • GET /api/auth/config — check if OIDC is enabled
  • GET /api/auth/me — current user info

Browser login lives on the console or portal BFF (/bff/login, /bff/callback). The API does not register /api/auth/login or /api/auth/callback.

Workflows

  • GET /api/workflows — list definitions (paginated)
  • POST /api/workflows — create definition
  • GET /api/workflows/:id — get definition
  • PUT /api/workflows/:id — update definition (draft only)
  • POST /api/workflows/:id/publish — publish (draft → active)
  • POST /api/workflows/:id/start — start run
  • DELETE /api/workflows/:id — delete definition

See also:

  • AI Assistant Integration — scoped API tokens, workflow validation, run diagnosis, documentation generation, and MCP tool access.
  • AI Agents in Floh Workflows — product strategy, safe use cases, guardrails, MVP patterns, and implementation path for using LLMs inside workflow design and execution.

Runs

  • GET /api/runs — list runs (paginated, filterable by status, definitionId)
  • GET /api/runs/:id — get run
  • PUT /api/runs/:id — edit run variables (pending/running only)
  • POST /api/runs/:id/cancel — cancel run

Tasks & Approvals

  • GET /api/tasks — list assigned tasks
  • POST /api/tasks/:id/complete — complete task
  • GET /api/approvals — list pending approvals
  • POST /api/approvals/:id/decide — approve/reject

Other

  • GET /api/users
  • GET /api/connectors, POST /api/connectors, POST /api/connectors/:id/test
  • GET /api/schedules, POST /api/schedules, PATCH /api/schedules/:id
  • GET /api/audit-logs — with filters
  • GET /api/reports/* — workflow-stats, sla-compliance, approver-performance
  • GET /api/health — health check

Authorization

Floh does not store roles or permissions. Every authenticated request carries an OIDC access token, and the token's scope claim is the caller's permission set. When scope is absent (Entra v2 and Okta access tokens often emit scp instead — a string or string[]), Floh reads scp. Grant or revoke access by changing the scopes Authifi (or the IdP) issues for a user, not by editing anything in Floh.

Two things narrow the effective permission set on top of scope:

  • Channel membership. Each permission is reachable from a fixed subset of the console / portal / MCP clients. A portal token carrying a console-only scope such as system:administer does not get that permission. The mapping lives in packages/shared/src/azp-allow-set.ts.
  • system:administer is console-only. It replaces every place the product previously asked "is this caller an admin?".

The full permission catalog, with per-channel availability, lives in Roles & Entitlements.

Note: role_definition / role_assignment / role_entitlement and the /api/roles UI are a provisioning feature — they describe roles Floh grants in downstream systems (Google Workspace, LDAP, ...). They have never governed access to Floh itself and are unaffected by the above.

External API Access

A client of your own is not currently accepted

Beyond verifying aud, authenticate resolves the token's authorized party (azp / client_id) through resolveKnownOidcClient, which matches only the configured console client (OIDC_CLIENT_ID), the configured portal client (PORTAL_OIDC_CLIENT_ID), and the reserved portal and MCP client identifiers. Any other client is rejected with 403 CHANNEL_DENIED, however correctly its token is signed, scoped, and audienced.

In practice API access today is channel-scoped: you call through the console or portal client rather than one you register yourself. (MCP is a registered resource server but not a registered client, so it is not a third option here.) Adding a registration path for external clients is tracked in #1190; the steps below describe the token requirements and remain accurate for the existing channel clients.

External applications call Floh APIs with an access token from the IDP (Authifi or any compliant OIDC/OAuth 2.0 provider), issued to one of the channel clients above.

Prerequisites

  1. Register the Floh resource in your IDP. The resource identifier URI becomes the aud claim in issued access tokens. Floh verifies that claim against its channel-audience allow-list — FLOH_CONSOLE_AUDIENCE, FLOH_PORTAL_AUDIENCE, and FLOH_MCP_AUDIENCE — so request a token for whichever channel your integration uses. (FLOH_RESOURCE_ID, alias OIDC_AUDIENCE, names the API resource for operator tooling but is not accepted as aud.)
  2. Use one of the registered channel clients — console or portal — with the authorization code grant. A newly registered client of your own will fail the authorized-party check described above. MCP is not a third option: setup-authifi-oidc-clients creates only the console and portal clients, and floh-mcp-client is reserved in the allow-set but not registered, so selecting it fails at the identity provider before a token is issued. MCP integrations use a console-channel token today (see MCP setup).
  3. Request a token for the channel audience your integration targets.

No service-to-service path into the Floh API today

This limit is about inbound calls to Floh's API. It does not describe Floh's own outbound integrations, which do use client credentials — see the note below.

A caller cannot reach the Floh API with client_credentials. The channel clients are registered with grantTypes: ["authorization_code", "refresh_token"] (scripts/setup-authifi-oidc-clients-lib.mjs:207), and no dedicated MCP client is registered at all, so the request is refused by the identity provider before it ever reaches Floh. Every supported inbound integration is user-delegated: obtain the token through the authorization code flow.

Provisioning a machine client whose identifier the API accepts is part of #1190.

Floh's own outbound client-credentials flows

Floh acts as a client-credentials client in two places, both optional and neither required to run the platform:

Flow Reached when Code
Authifi vault (secrets manager) SECRETS_BACKEND=authifi. The default is env, and AuthifiSecretProvider is dynamically imported only on that branch config/authifi-secret-provider.ts:153
Authifi connector A workflow runs an Authifi connector command; the client is built per command, not at startup modules/connectors/authifi-client.ts:102

The dedicated floh-vault-client is registered only when you pass --vault to setup-authifi-oidc-clients, and it is deliberately restricted to grantTypes: ["client_credentials"] — the script rejects any other grant for that client. A baseline deployment (SECRETS_BACKEND=env, no Authifi connector configured) performs no client-credentials exchange at all.

Connectors for non-Authifi systems have their own independent client-credentials support — outbound SCIM (authType=oauth2_client_credentials) and the Entra Graph client — and are likewise per-connector, not global.

Calling the API

Include the IDP-issued access token in the Authorization header:

curl -H "Authorization: Bearer <access_token>" \
  https://floh.example.com/api/workflows

Floh validates the token against the IDP's JWKS endpoint, checking:

  • Signature — via the IDP's published JSON Web Key Set
  • Issuer (iss) — must match OIDC_ISSUER
  • Audience (aud) — must match one of the configured channel audiences (FLOH_CONSOLE_AUDIENCE, FLOH_PORTAL_AUDIENCE, FLOH_MCP_AUDIENCE). FLOH_RESOURCE_ID / OIDC_AUDIENCE is not accepted as aud
  • Expiry (exp) — token must not be expired

Auto-provisioned Users

When an API request arrives with a valid IDP token for a sub (subject) that has no account in Floh, a user record is created automatically so audit and ownership columns have a row to point at. That row has no Floh permissions of its own — the next request still authorizes only from the token's scope / scp. Grant access by issuing those scopes at the IdP, not by editing the Floh user record.

Configuration

Variable Description Example
OIDC_ISSUER IDP issuer URL https://a-ci.ncats.io/_api/auth/ls
FLOH_CONSOLE_AUDIENCE Console channel resource identifier — an accepted access-token aud http://console.floh.api
FLOH_PORTAL_AUDIENCE Portal channel resource identifier — an accepted access-token aud http://portal.floh.api
FLOH_MCP_AUDIENCE MCP channel resource identifier — an accepted access-token aud http://mcp.floh.api
FLOH_RESOURCE_ID API resource id used by operator tooling. Not accepted as aud, and not required at startup http://floh.api
OIDC_AUDIENCE Deprecated alias of FLOH_RESOURCE_ID (omit; use FLOH_RESOURCE_ID)
OIDC_CLIENT_ID Client ID for the Floh web app's own OIDC flow floh-client

Startup requires OIDC_ISSUER, OIDC_CLIENT_ID, and at least one of the three channel audiences. It does not require FLOH_RESOURCE_ID.

Custom Connectors

Connector Interface

interface ConnectorHandler {
  name: string;
  version: string;
  execute(context: ConnectorContext): Promise<ConnectorResult>;
}

interface ConnectorContext {
  config: Record<string, unknown>;
  variables: Record<string, unknown>;
  stepId: string;
  instanceId: string;
}

interface ConnectorResult {
  success: boolean;
  /** Step's domain payload — namespaced under the step's `outputKey` in the run record. */
  payload?: Record<string, unknown>;
  /** Top-level keys merged into the run variable bag on success. */
  variables?: Record<string, unknown>;
  /** Failure-only structured context (HTTP status, response body, validation details). */
  diagnostics?: Record<string, unknown>;
  error?: string;
}

Registering a Connector

import { registerConnector } from "./modules/connectors/registry.js";

registerConnector({
  name: "slack",
  version: "1.0.0",
  async execute(ctx) {
    const { webhookUrl, message } = ctx.config as any;
    const resp = await fetch(webhookUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text: message }),
    });
    return { success: resp.ok };
  },
});

Built-in Connectors

Name Description
http Makes HTTP requests to external APIs
delay Pauses execution for a specified duration
authifi Manages Authifi group membership via the Admin API

To create a new instance of a built-in connector type (e.g. connecting to a second Authifi tenant), use the Connectors → New Connector → Built-in wizard in the UI, or send a POST /api/connectors request with the type set to the registered handler name and executionModel set to built_in. See the Connector Creation Guide for details.

Test Connectors

Three simulated connectors are registered automatically and provide realistic seed data for development and testing without external dependencies.

Name Description Default Command
test-ldap Simulated LDAP directory with users and groups search
test-db Simulated relational database with tables query
test-activedirectory Simulated Active Directory with users, groups, and account management findUser

Commands

  • test-ldaptest, search, bind, add, modify, delete
  • test-dbtest, query, insert, update, delete, list-tables
  • test-activedirectorytest, findUser, findGroup, authenticate, getGroupMembers, enableAccount, disableAccount, addToGroup, removeFromGroup, setPassword, resetPassword (deprecated alias for setPassword, LSA-8655), listUsers, listGroups, checkGroupMembership, checkAccountExists, createAccount (now accepts an optional password for create-and-set in one round-trip)

Simulating Failures

All test connectors accept a simulateFailure config option. When set, the connector returns { success: false } immediately without executing the command. This is useful for testing error-handling paths in workflows.

// Boolean — uses a default error message
{ "connectorName": "test-ldap", "command": "search", "simulateFailure": true }
// → { success: false, error: "Simulated failure (test-ldap connector)" }

// String — uses the value as a custom error message
{ "connectorName": "test-db", "command": "query", "simulateFailure": "Connection refused" }
// → { success: false, error: "Connection refused" }

When simulateFailure is absent, false, or an empty string, the connector executes normally.

Pagination

All list endpoints support pagination:

  • page — page number (default: 1)
  • pageSize — items per page (default: 20, max: 100)
  • sortBy — field to sort by
  • sortOrderasc or desc

Response format:

{
  "data": [...],
  "total": 100,
  "page": 1,
  "pageSize": 20,
  "totalPages": 5
}