Skip to content

REST API ​

Base URL ​

http://localhost:8080/api

In production, replace it with your instance's domain.

Interactive documentation (Swagger) ​

The OpenAPI 3.1 documentation is generated automatically:

http://localhost:8080/swagger-ui.html

The document behind it is served at /v3/api-docs by the application itself — that is the API's source of truth, and it is what nginx exposes in production.

Static openapi.json file ​

If you need the document as a file (to import into Postman, to version a contract, to generate a client), ask for the openapi profile:

bash
cd backend
mvn verify -Popenapi

The result lands in backend/core/target/openapi.json.

Why a profile instead of part of mvn verify

The generator reads /v3/api-docs from a running application. The profile starts the application before and stops it after, so it needs a reachable database — the same variables as the local run instructions.

Outside the profile, mvn verify neither writes the file nor tries to start anything, so the build stays green on a machine that only wants to run the tests.

Authentication ​

JWT (user) ​

http
POST /auth/login
Content-Type: application/json

{
  "email": "user@company.com",
  "password": "yourPassword"
}

Response:

json
{
  "accessToken": "eyJ...",
  "refreshToken": "eyJ...",
  "user": { "id": "...", "email": "...", "firstName": "..." },
  "roles": ["USER"]
}

Send the accessToken on every request:

http
Authorization: Bearer eyJ...

Token refresh ​

When the accessToken expires (401), use the refreshToken to get a new pair:

http
POST /auth/refresh
Content-Type: application/json

{
  "refreshToken": "eyJ..."
}

API Key (external system) ​

http
X-API-Key: your-api-key

Every API key has a configurable per-minute request limit (default 100, 0 = unlimited), set when the key is created or later under Integrations & automation → API Keys. Going over the limit answers 429 Too Many Requests:

json
{ "error": "Too Many Requests", "message": "Rate limit exceeded for this API Key." }

Scopes ​

There are exactly two scopes, defined in ia.flow.model.ApiKeyScope:

ScopeAuthorityMethods it opens
readSCOPE_readGET, HEAD, OPTIONS
writeSCOPE_writePOST, PUT, PATCH, DELETE, and anything else

The rule is single and lives in one place: ApiKeyScope.requiredFor(httpMethod) decides the scope a request needs, and ApiKeyAuthFilter consults that decision before letting the chain continue. No route names a scope and no controller repeats the list — a new endpoint under /api/a/** is covered the moment it exists.

The two scopes do not imply one another: a write-only key is refused on a GET. A key that needs both sides carries both.

A request without the scope it needs is stopped in the filter — it never reaches the controller — and answers 403 Forbidden:

json
{
  "error": "Forbidden",
  "message": "Esta chave de API tem os escopos [read] e o método POST exige o escopo 'write'. Marque esse escopo na chave em Integrações e automação → API Keys, ou use uma chave que já o tenha."
}

Reading the scopes column fails closed at every ambiguity: an unknown scope is ignored and never promotes the key; an empty list, null or malformed JSON grant nothing, and the key is refused on every method. Before 2026-09-03 an empty list meant "no restriction" — today it means "no reach".

On creation (POST /api/a/api-keys) the scope list is required and must not be empty, and every entry must be a known scope: an invented value is refused with 400 and the message lists the ones that exist. PATCH takes scopes optionally — absent, the key keeps what it has; present, it replaces them.

Migration V0103__api_key_scopes.sql gave ["read","write"] to every key that already existed, including the empty ones and the ones with invented scopes, for the reason recorded in the migration itself (issue #57): what already runs in production keeps the reach it had, and what is created from here on is born minimal.

MCP server (/api/a/mcp) ​

Flow.IA exposes an MCP (Model Context Protocol) server so an external AI agent — Claude Code, a provisioning script, an IDE assistant — can configure the process in the tenant: the definition itself, forms, knowledge bases, CMS collections, AI agents and the process documentation.

Authentication is HTTP Basic, with a platform user:

json
{
  "mcpServers": {
    "flowi": {
      "url": "https://<host>/api/a/mcp",
      "headers": {
        "Authorization": "Basic <base64 of email:password>",
        "X-Tenant-ID": "<tenant slug>"
      }
    }
  }
}

This shape works in the clients that accept a fixed header: VS Code and Claude Code (CLI). Claude Desktop, claude.ai and ChatGPT add a remote server by URL and negotiate OAuth — they have no field for Basic and none for a header of your own, so they cannot talk to this endpoint directly. The endpoint's 401 answers WWW-Authenticate: Basic realm="flowi-mcp", which is how a client discovers the scheme; OAuth 2.1 is not implemented yet.

Basic rather than JWT because MCP client configuration is static and the token expires in one hour. Failed attempts count against the same limiter as the login endpoint.

X-Tenant-ID says which tenant the client wants; what decides whether it may is the membership check, run at authentication. A slug the user is not a member of does not authenticate — the header names the candidate, it never selects the tenant. With no header the client operates with no tenant, which is the case of a SUPER_ADMIN user.

Use a dedicated user

Create a user just for this (e.g. mcp-bot@...) and grant it ADMIN only on the tenants it should reach — the user's membership in the tenant is what grants access. Do not use a person's account: MCP clients store credentials in plain text, and a leaked personal password means UI login, password change and access to every tenant that person belongs to. A dedicated user can be deactivated without affecting anyone.

Available tools:

ToolWhat it does
flowi_form_listLists the forms of a process
flowi_form_getReturns a form with its full schema
flowi_form_field_typesLists the accepted field types and how to build the schema
flowi_form_createCreates a form (does not publish)
flowi_form_updateReplaces the schema, creating a new version (does not publish)
flowi_form_versionsVersion history with author — this is the undo path
flowi_form_publishPublishes a version, which is what makes it reach users
flowi_process_listLists the tenant's definitions, with the published version
flowi_process_versionsVersion history of one definition
flowi_process_get_xmlReturns the BPMN/CMMN XML of a version
flowi_process_validateChecks a BPMN without deploying: orphans, dead ends, a gateway with no default flow, a missing diagram
flowi_process_deployDeploys a new version of the definition (does not publish)
flowi_ai_model_listThe AI model catalogue, with the id flowi_agent_create requires and a relative cost tier (costTier: LOW, MEDIUM, HIGH), never the price
flowi_agent_list, flowi_agent_createThe process's AI agents
flowi_agent_attach_to_stepAttaches an agent to a step, with that step's tools
flowi_agent_suggest_knowledgePoints at the existing bases relevant to a step
flowi_agent_tool_catalogThe tools an agent step can declare, with what each does and what it needs
flowi_connector_listWhat a step can use besides an agent: decision tables, Java delegates and the e-mail inboxes that start a process. Robots are listed as scheduled, not as a step
flowi_kb_list, flowi_kb_create, flowi_kb_add_text, flowi_kb_link_processKnowledge bases (RAG)
flowi_cms_field_typesThe field types and classification levels a collection may carry
flowi_cms_collection_list, flowi_cms_collection_create, flowi_cms_records_load, flowi_cms_records_queryCMS collections
flowi_process_doc_getReads the process description
flowi_process_doc_proposeProposes a new description — returns the text, writes nothing
flowi_run_startable_listThe processes that can be started, each with its MCP policy
flowi_run_startStarts an instance (only if the policy allows it)
flowi_run_my_tasksThe caller's task inbox, as they see it on screen
flowi_run_task_getThe task and its form schema, in one answer
flowi_run_task_claimTakes a task offered to a role the caller holds
flowi_run_task_previewShows what completing would write and where the instance would go; writes nothing
flowi_run_task_completeCompletes one task, requiring the preview token
flowi_run_attach_textAttaches a text document to an instance
flowi_run_instance_getThe state of one instance
flowi_run_why_failedThe AI agent failures of an instance, by business key

Every tool requires a tenant administrator role (or SUPER_ADMIN). An ordinary user authenticates and reaches no tool at all — the check sits on the class of each tool group, not on a controller, because the MCP path goes through no controller at all.

Each tool declares itself to the client as a read or a write (readOnlyHint, destructiveHint), so a listing does not ask for the same approval a publish does. A write goes through the same bean validation as the equivalent REST route: a slug with a space is refused by the tool exactly as the screen would refuse it.

There are per-call limits: 5000 records on flowi_cms_records_load and 2 million characters on flowi_kb_add_text. Split anything larger.

There is no delete tool and no datasource tool. Deleting has no undo, and a datasource holds an outbound URL and credentials. Publishing is always an explicit call — update_form never publishes on its own, and tasks already open keep the version they locked at creation.

Deploying a version is not publishing it

flowi_process_deploy creates a new version and no tool publishes a definition. While a published version exists, the deployed one starts no instances — it sits there to be reviewed and published by a person. The exception is a definition that has never been published: instance start then falls back to the latest version, and the tool's answer says so.

Before deploying a BPMN a model wrote, call flowi_process_validate: deploying creates a version, and a version cannot be withdrawn. The check also warns when the XML arrived without its diagram section — the process runs, but opens blank in the modeler.

Running the process is closed by default

The flowi_run_* tools start instances and complete tasks. Completing advances the process and no tool undoes it, so each process decides for itself, in the AI assistants (MCP) field of its Configuration tab:

ValueStartComplete a task
Nothing (default)refusedrefused
Start and readallowedrefused
Start and completeallowedallowed, under the rules below

The default is closed, and no tool changes it — the person who owns the process does, on screen.

Completing requires three things at once: the process set to Start and complete, the task already claimed by the caller themselves, and a confirmationToken from flowi_run_task_preview with exactly those variables. The token lasts ten minutes and stops matching if any field changes after the preview — that is how the person confirms the final payload rather than an earlier draft of it. No tool claims a task on the way to completing it, and one task is completed per call. The completion records in the task history that it arrived over MCP, so a reading six months later distinguishes what the person typed from what an assistant submitted on their behalf.

The preview reads the diagram and says which activities the task can move to, with each path's condition where one exists. It does not evaluate the conditions — that would mean running the process — so a conditional path is reported as conditional rather than resolved.

To turn the server off, set MCP_SERVER_ENABLED=false. Behind a proxy the endpoint needs proxy_buffering off and a long read timeout: the listening channel is text/event-stream, and the 60s timeout the other API routes carry would sever the session every minute.

The tenant comes from the token, not from a header ​

The tenant a session operates in is a signed claim inside the access token. There is no tenant header to send: X-Tenant-ID never selects the tenant. Sending it with the same slug the token already carries is accepted and ignored; sending it with a different slug, or with no tenant in the token, answers 400 with a message pointing at /api/auth/select-tenant. The one credential that is not a token — HTTP Basic on the MCP endpoint — names its tenant in that header, and there the membership check runs at authentication: a slug the user does not belong to does not authenticate.

After logging in, choose the tenant:

http
POST /api/auth/select-tenant
Content-Type: application/json

{ "tenantSlug": "tenant-slug" }

The response has the same shape as login, with a new accessToken carrying the tenant and only the roles held in that tenant. Switching tenants means repeating this call and replacing the stored token — there is no in-place switch.

Selecting a tenant the user does not belong to answers 403.

Why the tenant lives in the token

A header is the client's choice, and checking it against the list of active tenants does not stop a user belonging to one tenant from asking for another's data by changing the value. With a signed claim the tenant stops being the client's guess: altering it invalidates the whole token.

Main endpoints ​

Endpoints follow the prefix convention below. Every path is relative to the /api base.

PrefixScopeAuth
/authAuthentication / sessionNo auth on /login, /refresh, /password/forgot, /password/reset and /sso/**; everything else needs a token
/admin/*Platform operationsJWT (SUPER_ADMIN only)
/a/*Tenant operationsJWT with a selected tenant

Authentication ​

MethodEndpointDescription
POST/auth/loginLog in with email/password
POST/auth/refreshRenew the token
POST/auth/select-tenantSwitch the session's tenant (returns a new token)
POST/auth/select-platformSuper Admin only: leave the chosen company and go back to a tenant-less token (returns a new token); any other role gets 403
GET/auth/meCurrent user
GET/auth/me/tenantsThe user's tenants
GET/auth/me/sessionThe current session
POST/auth/password/forgotAsk for a reset link — always answers 202, registered address or not, and is rate limited per IP
POST/auth/password/resetComplete the reset with the token from the e-mail
POST/auth/logoutInvalidate the session

Login response:

json
{
  "accessToken": "eyJ...",
  "refreshToken": "eyJ...",
  "user": { "id": "...", "email": "...", "firstName": "..." },
  "roles": ["USER"],
  "tenants": [
    { "id": "uuid", "slug": "company-abc", "name": "Company ABC" }
  ]
}

The tenants field lists the tenants the user belongs to. Use the slug in POST /api/auth/select-tenant to obtain that tenant's token.

What POST /auth/logout ends. The two tokens of a single login belong to the same session and carry the same identifier (sid). Logging out deletes the whole session — the access token and the refresh token — so the refresh token of that login stops renewing. Other sessions of the same person, on another device or another browser, stay active: closing one tab is not signing out everywhere.

The access token itself stays valid until it expires (one hour): it is not checked against the database on every request, and that is a cost decision recorded in specs/core/tenant-in-token.md. What logout closes is the window: without that session's refresh token, it is not reopened.

Processes and instances ​

MethodEndpointDescription
GET/a/definitionsList the tenant's definitions — the response is the whole list, not paginated
GET/a/definitions/{key}Detail + startFormKey — from the StartEvent in BPMN, from the casePlanModel's flowable:formKey in CMMN
GET/a/definitions/{key}/variablesThe process's configuration variables, not instance ones — requires a tenant Admin or Modeler. GET /a/definitions/{key}/variables/public returns the non-secret ones to any authenticated caller
GET/a/definitions/{key}/monitorWork parked per step, for one version of the process — ?version=
GET/a/definitions/{key}/monitor/{activityId}What is parked at that step — ?version=
GET/a/definitions/{key}/sla-metricsThe process's SLA metrics over the last days — ?days= (default 30)
PUT/a/definitions/{key}/suspendSuspend the definition — no new instance starts. BPMN only; a case answers 400, because the CMMN engine has no definition suspension
PUT/a/definitions/{key}/activateReactivate a suspended definition — BPMN only, for the same reason as suspend
POST/a/instancesStart an instance (BPMN or CMMN)
GET/a/instancesList active instances, paginated — ?page=&size=&type=
GET/a/instances/searchSearch instances by process variable, paginated — ?varName=&varValue=&page=&size=
GET/a/instances/{id}Detail + variables
DELETE/a/instances/{id}Cancel the instance
PATCH/a/instances/{id}/suspendSuspend the instance — BPMN only; a case answers 400
PATCH/a/instances/{id}/activateReactivate the instance — BPMN only; a case answers 400
GET/a/instances/{id}/diagramDiagram SVG/XML with active activities highlighted
GET/a/instances/failed-jobsThe tenant's failed jobs
POST/a/instances/failed-jobs/{jobId}/retryRetry a job
POST/a/instances/failed-jobs/{jobId}/deadletterMove the job to the dead-letter queue, stopping retries
GET/a/ai-task-failuresAI agent steps that failed, paginated (size 20 by default)
POST/a/ai-task-failures/{id}/retryRuns the step again (case) or reprocesses its dead-letter job (process)
POST/a/ai-task-failures/{id}/dismissDismisses the failure without running anything; idempotent

Roles for this table, since the columns do not carry them: starting, listing, reading and drawing an instance only require being authenticated. Cancelling, suspending and reactivating an instance, and the failed-jobs, monitor and sla-metrics routes, require a tenant Manager or Admin. Suspending and reactivating the definition requires a tenant Admin or Modeler. Below that the answer is 403.

When an AI agent fails during POST /a/instances, the instance never comes into existence: the process runs synchronously inside the request transaction, so the rollback takes the instance, its tasks and the attachment link with it. GET /a/ai-task-failures is where the attempt is recorded — process, business key, activity, failure type and reason, newest first. Accepts ?businessKey= to filter by document, ?processInstanceId= for one instance and ?open=true for open failures only. Managers and Admins see every failure in the tenant; everybody else only those of instances they can see. retry and dismiss require Manager or above; retry answers 409 when the failure is already resolved or the step cannot be redone. Each row carries open, resolution (RETRIED, DISMISSED, SUPERSEDED, INSTANCE_ENDED), resolvedBy and resolvedAt. The instance list carries aiFailureOpen on each row and the diagram carries aiFailures[] next to failedJobs.

For an AI step inside a case the reading changes: the case exists and stays open, only the step failed, and it can be run again. The engine field says which of the two this is — BPMN or CMMN — and on CMMN rows processKey carries the case key.

attemptCount counts the retries. There is one row per step, not one per attempt: when the engine repeats the same step, the existing row is updated and the counter goes up. Nine attempts of one step show as a single row with attemptCount: 9, which is information; nine identical rows would be noise.

Whoever submitted the document receives an AI_TASK_FAILED notification immediately, over the same channel as every other notification — once, on the first failure. Retries do not notify again: repeating the same warning nine times adds nothing and buries the first one. A start triggered by an API key has no user behind it, so the row is written and no notification is sent.

There is no retry and no dead-letter through the API: the record is a fact, not a work item. Reprocessing means submitting the document again.

Per-step monitoring ​

GET /a/definitions/{key}/monitor returns the diagram of one version and how much work is parked at each of its steps. It requires Manager or above in the tenant.

json
{
  "definitionKey": "fiscal-nfe",
  "definitionName": "Tax audit",
  "version": 3,
  "bpmnXml": "<definitions …>",
  "runningInstances": 12,
  "runningInstancesOtherVersions": 7,
  "versions": [ { "version": 3, "runningInstances": 12 }, { "version": 2, "runningInstances": 7 } ],
  "activities": [
    { "activityId": "review", "activityName": "Final review", "activityType": "UserTask",
      "count": 11, "byReason": { "USER_TASK": 11 } }
  ]
}

version is optional. Without it the response uses the version with the most running instances (ties go to the higher number) and, if no instance exists, the latest published one. A diagram belongs to a version and instances do not: counting instances from every version onto a single drawing produces a wrong number, so runningInstancesOtherVersions tells you how much was left out rather than hiding it.

A step with nothing parked does not appear in activities — the diagram already draws them all.

byReason classifies what is sitting there:

ReasonMeaning
USER_TASKtask waiting on a person
JOB_FAILEDretries exhausted, in the dead-letter queue
JOB_RETRYINGfailed and still has an attempt left
TIMERwaiting on a scheduled date
EXECUTINGexecution sitting at the step, with no job and no task

GET /a/definitions/{key}/monitor/{activityId} lists the items at that step — instanceId, businessKey, startTime, reason and, depending on the reason, taskId, assignee, dueDate or errorMessage. Pass the same version the summary returned, otherwise the detail may describe a different diagram from the one on screen.

Instance history ​

MethodEndpointDescription
GET/a/history/instancesCompleted instances, paginated — ?tenantId=&type=&page=&size=
GET/a/history/instances/{id}Completed instance detail
GET/a/history/instances/{id}/activitiesThe instance's activities

The listing and the variable search answer with the same page envelope as /a/instances (content, number, size, totalElements, totalPages). size defaults to 20 and is capped at 100 — asking for more returns 100, not an error. Two ever-growing tables sit behind them: ACT_HI_PROCINST and ACT_CMMN_HI_CASE_INST. With no page, a tenant two years in received tens of thousands of rows in a single response.

Tasks ​

MethodEndpointDescription
GET/a/tasksList the user's tasks (?page=&size=&mine=)
GET/a/tasks/completedList already-completed tasks (history) (?page=&size=)
GET/a/tasks/{id}Detail + form
POST/a/tasks/{id}/claimClaim the task
POST/a/tasks/{id}/completeComplete the task
POST/a/tasks/{id}/unclaimReturn the task to the group
POST/a/tasks/{id}/delegateDelegate
GET/a/tasks/{id}/variablesRead the task's visible variables
POST/a/tasks/{id}/variablesWrite variables without completing the task
POST/a/tasks/{id}/ai-suggestFill suggestions for the fields still empty. The body {"draft": {"field": "value"}} carries the form as it stands on screen — without it the server only sees the variables already saved. An absent body or an empty draft is valid. Nothing is written: suggestions go back to the form, and each one carries conflictsWithCurrent (true when the field already holds a different value, and then the client never applies it on its own)
GET/a/tasks/{id}/commentsList comments
POST/a/tasks/{id}/commentsAdd a comment

Who may act on and who may read a task

Claiming, completing, saving a draft, returning, delegating and commenting require being the task's assignee, owner (delegate) or a candidate; Manager, Admin and Super Admin act on any task. Reading (GET of the task, its variables, comments, provenance, and ai-suggest) needs the same relation or having started the instance. With no relation at all the answer is 404; with read access but no right to act, 403. An API key gets 403 with an explicit message on all these routes: they are acts of a person, so use a user token. Reserved variables (aiDecision, escalateToHuman, ai_*, _*, tenantId...) sent in any body answer 400.

Task list and detail responses include formVersionId, so you can fetch the exact form version through GET /a/definitions/{processKey}/forms/{key}/versions/by-id/{versionId}.

POST /a/tasks/{id}/complete validates the form on the server

When the task has a formKey and the platform holds that form, its required fields are checked before the task is completed. If one is missing the answer is 400 with Campos obrigatórios não preenchidos: <fields>, and nothing is written — neither the variables nor the process moving on. The check uses the same form version locked when the task was created (the one in formVersionId) and honours conditional rules: a field hidden by a rule is not demanded.

An API client that already sent what the screen sends is unaffected. One that sent less now gets a 400 — fetch the schema by formVersionId before building the body.

Not validated: POST /a/instances (start-form variables), and each field's format, length and range validations, which still run in the browser only.

GET /a/tasks and GET /a/tasks/completed return a page window, not the whole queue. On both, page is zero-based and size defaults to 100, capped at 200 — asking for more returns 200. The response is still a plain array with the same fields; only the number of rows per call is bounded. To go past the first page, ask for ?page=1, and so on.

Both lists also follow the same visibility rule: Manager, Admin and Super Admin see the tasks of the whole tenant, open and completed alike; everyone else sees their own, the ones they are a candidate for, and the ones offered to a group of their process roles.

GET /a/tasks?mine=true narrows the list to the tasks assigned to the caller, ignoring group and process role — including for someone who sees the whole tenant. The default is mine=false, which keeps the full list the caller's visibility allows. There is no mine on /a/tasks/completed.

Forms ​

A form always belongs to a process, and the path reflects that: the process key comes before the form key. There is no /a/forms/{key} route — calling it returns 404.

MethodEndpointDescription
GET/a/definitions/{processKey}/formsList the process's forms
POST/a/definitions/{processKey}/formsCreate a form
GET/a/definitions/{processKey}/forms/{key}Form detail
PUT/a/definitions/{processKey}/forms/{key}Save the form (creates a new version)
DELETE/a/definitions/{processKey}/forms/{key}Delete the form
GET/a/definitions/{processKey}/forms/{key}/versionsList versions (includes createdBy, the UUID of whoever saved it)
GET/a/definitions/{processKey}/forms/{key}/versions/{num}Version by number
GET/a/definitions/{processKey}/forms/{key}/versions/by-id/{versionId}Version by UUID
POST/a/definitions/{processKey}/forms/{key}/publishPublish a version

Data sources are the exception: they do not belong to a process and sit directly under /a/forms.

MethodEndpointDescription
GET/a/forms/datasourcesList data sources
POST/a/forms/datasourcesCreate a data source
GET/a/forms/datasources/{key}Data source detail
PUT/a/forms/datasources/{key}Update a data source
DELETE/a/forms/datasources/{key}Delete a data source
POST/a/forms/datasources/{key}/executeExecute a data source

Schema validation on POST and PUT

The schema you send has to be valid JSON and, when it carries a fields array, every field must declare a recognised type. An unknown type is rejected with 400, and the message names the culprit and lists the accepted types:

json
{ "message": "Unknown field type: 'markdown'. Accepted types: text, textarea, ..." }

Without this check an invented type would be stored and only break at render time. Anyone building forms through the UI never hits it, because the palette only offers real types; anyone writing the JSON straight against the API does.

Form.io-shaped schemas (with components instead of fields) are accepted without this validation.

Decision Tables ​

MethodEndpointDescription
GET/a/decisionsList the tenant's decisions
POST/a/decisionsDeploy a .dmn file
POST/a/decisions/from-template/{key}Clone from a global template
GET/a/decisions/{key}Details
PUT/a/decisions/{key}Update the rules (new version)
POST/a/decisions/{key}/evaluateTest with inputs
DELETE/a/decisions/{key}Delete

Every route above requires a tenant Admin or Modeler, or Super Admin, with one exception: POST /a/decisions/{key}/evaluate also admits the tenant's Manager and User — that is, Admin, Modeler, Manager, User or Super Admin —, because that is what a form calls to test the rule.

CMS ​

Structured content collections and their records. Reading only requires being authenticated in the tenant, and the answer already comes trimmed by the collection's confidentiality level: anything above your clearance does not appear. Writing requires tenant Manager or above; an API key reaches writing with the write scope.

MethodEndpointPermissionDescription
GET/a/cms/collectionsAuthenticatedList the collections visible to the caller
POST/a/cms/collectionsManagerCreate a collection
GET/a/cms/collections/{id}AuthenticatedCollection details
PUT/a/cms/collections/{id}ManagerUpdate a collection
DELETE/a/cms/collections/{id}ManagerDelete a collection
GET/a/cms/collections/{id}/recordsAuthenticatedList records, paginated (size 20 by default)
POST/a/cms/collections/{id}/recordsManagerCreate one record (201)
POST/a/cms/collections/{id}/records/bulkManagerBulk load
PUT/a/cms/collections/{id}/records/{recordId}ManagerUpdate a record
DELETE/a/cms/collections/{id}/records/{recordId}ManagerDelete a record
DELETE/a/cms/collections/{id}/recordsManagerEmpty the collection

The bulk load takes { "records": [...], "keyField": "code" } and answers { "inserted": n, "updated": n, "total": n }. At most 20,000 records per request — a longer list is refused with 400 — and keyField is capped at 100 characters. With keyField the load is idempotent: a record whose value for that field already exists is updated instead of duplicated. Without keyField everything is inserted, so a clean reload goes through DELETE /a/cms/collections/{id}/records first.

Deleting a record or a collection is permanent: there is no bin and no undo.

Knowledge bases (RAG) ​

The documents that feed the agent's answer. Every route below requires a tenant Admin, Modeler or Manager (or Super Admin) — reading included, because the listing reveals what the AI consults. A caller without that role gets a 403. Creating or changing a base flagged as global stays exclusive to the Super Admin, and that refusal comes from the service itself.

MethodEndpointPermissionDescription
GET/a/knowledge-basesAdmin, Modeler or ManagerList the tenant's bases and the global ones
POST/a/knowledge-basesAdmin, Modeler or ManagerCreate a base (201); isGlobal: true requires Super Admin
GET/a/knowledge-bases/{id}Admin, Modeler or ManagerBase details, with the document count, ftsLanguage, retrievableChunks, keywordIndexedChunks and keywordIndexStatus (READY/REBUILDING)
PATCH/a/knowledge-bases/{id}/search-settingsAdmin, Modeler or Manager{ftsLanguage: SIMPLE|PORTUGUESE|ENGLISH|SPANISH} — keyword search language; 202 and the index is recomputed in the background, with no embedding. A global base only by the Super Admin
DELETE/a/knowledge-bases/{id}Admin, Modeler or ManagerDelete a base — refused while a process is still linked
GET/a/knowledge-bases/{id}/documentsAdmin, Modeler or ManagerList documents, paginated (size 20 by default)
POST/a/knowledge-bases/{id}/documentsAdmin, Modeler or ManagerUpload PDF, TXT or MD (202 — indexing is asynchronous)
DELETE/a/knowledge-bases/{id}/documents/{docId}Admin, Modeler or ManagerDelete a document and its vectors
GET/a/knowledge-bases/process/{definitionKey}Admin, Modeler or ManagerBases linked to a process
POST/a/knowledge-bases/process/{definitionKey}/link/{id}Admin, Modeler or ManagerLink a base to the process (201)
DELETE/a/knowledge-bases/process/{definitionKey}/unlink/{id}Admin, Modeler or ManagerUnlink
GET/a/knowledge-bases/{id}/reference-questionsAdmin, Modeler or ManagerThe base's reference questions; the name of a document above your level comes hidden
POST/a/knowledge-bases/{id}/reference-questionsAdmin, Modeler or ManagerCreate a question (201): {question, targets: [{documentId, page?, snippet?}]}, 1 to 5 targets, up to 100 questions per base; a document outside the base 400, above your level 403
PUT, DELETE/a/knowledge-bases/{id}/reference-questions/{questionId}Admin, Modeler or ManagerEdit / delete (204)
POST/a/knowledge-bases/{id}/retrieval-evaluations/estimateAdmin, Modeler or Manager{configurations: [{mode, topK, minSimilarity?}], questionIds?} — 1 to 6 distinct configurations; questions, approximate tokens and quota
POST/a/knowledge-bases/{id}/retrieval-evaluationsAdmin, Modeler or ManagerStart the retrieval evaluation (202), at the requester's access level; 409 while another run of the base is queued or running
GET/a/knowledge-bases/{id}/retrieval-evaluationsAdmin, Modeler or ManagerThe base's runs, newest first, with stale
GET/a/knowledge-bases/{id}/retrieval-evaluations/{runId}Admin, Modeler or ManagerResult: summary per configuration with the difference from the previous run, result per question and tokensUsed
GET/a/knowledge-bases/{id}/chunks/{chunkId}/previewAdmin, Modeler or ManagerText of one chunk of this base, up to the viewer's level; 404 outside it

Another tenant's base answers 404, and a write to a global base by a tenant user answers 403, on every route above.

Agent CRUD (tenant and global) accepts ragSearchMode (VECTOR/HYBRID), ragTopK (1 to 20) and ragMinSimilarity (0 to 1), with 400 out of range. On a PUT, omitting ragSearchMode or ragTopK keeps the agent's value, but omitting ragMinSimilarity clears the floor — empty means "no floor", not "leave it".

Administration ​

The whole of /admin/* is SUPER_ADMIN

The rule lives in the security filter, not in each controller: every path under /api/admin/** requires the SUPER_ADMIN platform role. A tenant Admin gets 403 on any row of the table below, the member rows included — there is no block of /admin/* they reach. What a tenant Admin administers lives under /a/*.

MethodEndpointDescription
GET/admin/usersList users, paginated (?search=&page=&size=&sort=)
POST/admin/usersCreate a user
PUT/admin/users/{id}Update a user
DELETE/admin/users/{id}Deactivate a user
PUT/admin/users/{id}/passwordChange a user's password
GET/admin/tenantsList tenants, paginated
POST/admin/tenantsCreate a tenant
PUT/admin/tenants/{id}Update a tenant
PATCH/admin/tenants/{id}/suspendSuspend a tenant
PATCH/admin/tenants/{id}/reactivateReactivate a tenant
GET/admin/tenants/{id}/membersList the tenant's members
POST/admin/tenants/{id}/membersAdd a member
DELETE/admin/tenants/{id}/members/{membershipId}Remove a member
PATCH/admin/tenants/{id}/members/{membershipId}/roleChange a member's role
GET/admin/dmn-templatesList DMN templates
POST/admin/dmn-templatesCreate a DMN template
PUT/admin/dmn-templates/{key}Update a DMN template
DELETE/admin/dmn-templates/{key}Delete a DMN template

To read the members of the tenant the session is in without being Super Admin, there is GET /a/members, which requires a tenant Admin or Manager.

Process definitions (BPMN/CMMN) ​

There is no /admin/definitions. Deploying, versioning and publishing a definition all live under /a/definitions, tenant-scoped through the token. The routes in the table require a tenant Admin or Modeler, or Super Admin; outside it, listing (GET /a/definitions) and reading (GET /a/definitions/{key}) only require being authenticated.

MethodEndpointDescription
POST/a/definitions/deployDeploy a BPMN/CMMN file (multipart)
GET/a/definitions/{key}/versionsVersion history
POST/a/definitions/{key}/versions/{definitionId}/publishPublish a version
GET/a/definitions/{key}/versions/{definitionId}/xmlExport the XML
PATCH/a/definitions/{key}/toggleEnable/disable the process in the tenant's catalog
PUT/a/definitions/{key}/configBusiness Key template and attachment policy

Deploying is not publishing

POST /a/definitions/deploy creates a new version but does not change which version runs. If the tenant has already published something, instances keep starting that one — the deploy answers 200, the version number goes up, and nothing you just uploaded reaches execution.

To activate it: GET /a/definitions/{key}/versions shows which row has published: true, and POST /a/definitions/{key}/versions/{definitionId}/publish moves the publication. The deploy also logs a WARN when the deployed version is not the published one.

Webhooks ​

Every route requires a tenant Admin (or Super Admin).

MethodEndpointDescription
GET/a/webhooksList the tenant's webhooks
POST/a/webhooksCreate a webhook (201)
PUT/a/webhooks/{id}Update a webhook
DELETE/a/webhooks/{id}Delete a webhook (204)
GET/a/webhooks/{id}/deliveriesThe webhook's delivery history
POST/a/webhooks/{id}/testFire a test delivery; optional body {"event": "..."}
POST/a/webhooks/{id}/secretGenerate a new signing secret
GET/a/webhooks/egressHosts the tenant may send webhooks to
PUT/a/webhooks/egressUpdate the allowed host list

Company variables and secrets ​

Variables for the whole tenant, outside any process — the Administration → Variables and secrets screen. Every route requires a tenant Admin (or Super Admin), except where the description says otherwise.

MethodEndpointDescription
GET/a/tenant-variablesList the variables; secret values come back masked
POST/a/tenant-variables/{key}Create a variable (201) — create only: a key that already exists answers 409
PUT/a/tenant-variables/{key}Create or update; a blank value keeps the stored secret
DELETE/a/tenant-variables/{key}Delete a variable
GET/a/secrets/backendsVault schemes a value may reference (e.g. vault) — never an address or a credential. Also accepts the Modeler

Data retention ​

The Administration → Data retention screen. Every route requires a tenant Admin (or Super Admin).

MethodEndpointDescription
GET/a/retentionThe retention window of each table, with the installation bounds and a preview
GET/a/retention/{table}/previewHow many rows a candidate window would remove — ?days=
PUT/a/retention/{table}Choose the installation default, off, or a number of days for one table
GET/a/retention/changesThe latest changes to the tenant's retention windows

AI models and model classes ​

MethodEndpointPermissionDescription
GET/a/ai-models/activeAdmin, Modeler or ManagerActive models, for the agent editor's picker
GET/a/ai-models/copilot-choicesAuthenticatedModels a person may pick in either copilot — name only, no price or provider
GET/a/ai-model-classesAdmin, Modeler or ManagerModel classes an agent can use
GET/admin/ai-model-classesSuper AdminEvery class, with its ordered members, the agents bound to it and its warnings
POST/admin/ai-model-classesSuper AdminCreate a class (201); the code is permanent
PUT/admin/ai-model-classes/{code}Super AdminRename a class or change its description
DELETE/admin/ai-model-classes/{code}Super AdminDelete a class — refused while agents bind it
PUT/admin/ai-model-classes/{code}/membersSuper AdminReplace the ordered members; refuses retired models
GET/admin/ai-model-rolesSuper AdminThe copilot model and its fallback, when set
PUT/admin/ai-model-roles/{role}Super AdminAssign a model to COPILOT or COPILOT_FALLBACK; refuses a retired or inactive model, and a fallback equal to the copilot model
DELETE/admin/ai-model-roles/{role}Super AdminClear a role

Reference cases and model evaluation ​

The /a/... routes require a tenant Admin or Modeler (or Super Admin); their /admin/global-agents/... twins, for the agents of a global template, require Super Admin.

MethodEndpointDescription
GET/a/agents/{agentId}/reference-casesList the agent's reference cases
GET/a/agents/{agentId}/reference-cases/{caseId}Read one case
POST/a/agents/{agentId}/reference-casesCreate a case (201)
PUT/a/agents/{agentId}/reference-cases/{caseId}Update a case
DELETE/a/agents/{agentId}/reference-cases/{caseId}Delete a case (204)
POST/a/instances/{instanceId}/activities/{activityId}/reference-caseSave a completed AI step as a reference case of its agent (201)
POST/a/agents/{agentId}/evaluations/estimateEstimate the calls, tokens and quota of an evaluation
POST/a/agents/{agentId}/evaluationsStart a model evaluation (202)
GET/a/agents/{agentId}/evaluationsList the agent's evaluations
GET/a/agents/{agentId}/evaluations/{runId}Result, side by side per model
GET, POST, PUT, DELETE/admin/global-agents/{agentId}/reference-cases/**The same cases, for a global agent
POST, GET/admin/global-agents/{agentId}/evaluations/**The same evaluation (/estimate, start, list, result), for a global agent
GET/admin/model-evaluations/{runId}/costsCost per model of any evaluation
GET/admin/retrieval-evaluations/{runId}/costsTokens and cost per currency of any retrieval evaluation

Robots and runners ​

Every route requires a tenant Admin (or Super Admin).

MethodEndpointDescription
GET/a/robotsList the tenant's robots
POST/a/robotsCreate a robot (201). runtime is JAVA (default) or PYTHON; on a Python robot, mainClass is the module:function entry point
GET/a/robots/runtimesWhat the installation runs: pythonEnabled, pythonDisabledProperty (the property that turns Python on, when off), minimumPython and maxArtifactSizeMb. Because it takes that name, the robot keys runtimes and sdk are refused; a robot keyed runtimes created before this release loses its detail GET /a/robots/runtimes
GET/a/robots/sdk/pythonDownload RobotRunnerPython.zip (the flowia_robot SDK and FakeContext; the flowia-robot.json example is in Robots), with X-Content-SHA256. Read from app.robots.python-runner-path
GET/a/robots/{key}Read a robot
PUT/a/robots/{key}Update a robot. An omitted field keeps its stored value; a different runtime on a robot that already has an artifact is 400; in parameters, the extra keys sent replace the stored ones, and a key the JAR declares that the body does not send stays as it was
DELETE/a/robots/{key}Delete a robot
POST/a/robots/{key}/artifactUpload the robot's artifact (multipart, file part): a JAR or, with ROBOTS_PYTHON_ENABLED, a Python package. Ceiling ROBOTS_MAX_ARTIFACT_SIZE_MB (200 MB), read as a stream; above it, 413
POST/a/robots/{key}/jarAlias of /artifact
POST/a/robots/{key}/executeRun the robot now
GET, PUT/a/robots/{key}/configRead and write the parameter configuration (where a secret reference goes). The PUT replaces the keys the JAR declares — a declared one left out is removed — and keeps the extras it does not send
GET, PUT, DELETE/a/robots/{key}/stateRead, write and clear the robot's persisted state
GET/a/robots/{key}/executionsThe robot's executions. Each carries triggerType (MANUAL, SCHEDULED or TEST) and artifactVersion, the artifact version pinned at enqueue
GET/a/robots/{key}/executions/{executionId}Read one execution
GET/a/robots/{key}/executions/{executionId}/logDownload the execution log
POST/a/robots/{key}/executions/{executionId}/cancelCancel an execution
POST/a/robot-generationsGenerate a robot from a description: {key, name, description, requestId}. A description carrying a secret is refused (400) before the model is called, and a taken key is 409; without quota, 402; an unusable model answer, a provider failure or no answer within app.robots.generation.model-timeout-seconds (300 s), 502 (the tokens were already counted and nothing is written). The same requestId returns the revision already written (201) without calling the model again; while that request is still running, 409. Needs ROBOTS_PYTHON_ENABLED, and a tenant with robotGeneration.enabled = false gets 400. Secret detection (on the name, the description, the instruction and the code) is best-effort: it catches the common shapes, and the person who reads the code before approving it is the final barrier
GET/a/robot-generationsThe tenant's generations that were not discarded (without their revisions)
GET/a/robot-generations/{id}One generation with its revision list, without the code
GET/a/robot-generations/{id}/revisions/{n}One revision: code, sourceSha256, declared parameters, capabilities, findings, hosts, approval and its test executions
POST/a/robot-generations/{id}/revisionsNew revision: {instruction, requestId} refines with the model; {source, declaredParameters, capabilities, requestId} stores a hand edit, with no model call (code carrying a written secret is 400 and is not stored). 201
POST/a/robot-generations/{id}/revisions/{n}/approve{sourceSha256}: approves exactly the code that was read. A hash that differs from the stored one is 409; a blocking finding is 400 naming each one. The first approval creates the robot disabled and with no active version, and builds the revision's package
POST/a/robot-generations/{id}/revisions/{n}/test-runEnqueues a TEST execution pinned to the approved revision's package (202, RobotExecutionResponse). An unapproved revision or an empty required parameter is 400; another execution in flight is 400 with the reason. Log, cancel and parameters use the /a/robots/{key} routes
POST/a/robot-generations/{id}/revisions/{n}/activate{clearTestState, enable, acceptFailedTest}: makes the revision the robot's active version. It needs a finished test of that revision (SUCCESS, or FAILED with acceptFailedTest: true); clearTestState (default true) clears the state the tests wrote, and enable turns the robot on with the suggested schedule
POST/a/robot-generations/{id}/discardDiscards the draft (204) and deletes the robot created at approval if it was never activated. An activated generation is 409
GET/a/robot-runnersList the runners
POST/a/robot-runnersRegister a runner (201)
PUT/a/robot-runners/{id}Update a runner
DELETE/a/robot-runners/{id}Delete a runner
GET/a/robot-runners/{id}/logsThe runner's logs
GET/a/robot-runners/agent-jarDownload the runner agent JAR, with its SHA-256 in a response header

The routes under /api/internal/runners/** and /api/internal/robot-rpc/** are not for integration: they are the channel of the runner agent and of a running robot, authenticated by their own tokens and not by a user JWT.

Pagination ​

List endpoints accept pagination parameters:

GET /admin/users?page=0&size=20&sort=createdAt,desc

Paginated response:

json
{
  "content": [...],
  "totalElements": 45,
  "totalPages": 3,
  "number": 0,
  "size": 20
}

number is the index of the page returned, counted from zero — the same value you sent in page. The name comes from Spring Data's page format, and every paginated route in this API uses that same envelope, /a/instances included.

size is capped at 100 on /a/instances; asking for more returns 100 without an error instead of scanning the whole tenant.

Errors ​

Every error handled by the application follows this shape:

json
{
  "status": 400,
  "error": "Bad Request",
  "message": "Description of the problem",
  "path": "/api/a/instances",
  "timestamp": "2026-04-15T10:30:00"
}

When the error is a field validation, the same body carries fieldErrors, an array of { "field": "slug", "message": "Slug is required" }.

Two families of response do not go through it, because they are written before the controller:

  • the API key filter returns only error and message on scope 403s and rate-limit 429s, with no status, path or timestamp;
  • an X-Tenant-ID refusal comes out through the application server's own error handler, in its default format.

Do not treat status or path as guaranteed when reading an error; always read the HTTP status code of the response.

StatusMeaning
400Invalid request data
401Not authenticated (token missing or expired)
403Not permitted
404Resource not found
409Conflict (e.g. email already registered; or an AI feature called on an installation with no active model — the message says where to register one)
500Internal server error

Flowi Agentic — Plataforma de Gestão de Processos com IA