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 Integration → 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çã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
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_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)
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 tenant Admin. 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=
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)

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 and monitor routes, require tenant Manager or above. Suspending and reactivating the definition requires tenant Admin. 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.

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

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 tenant Admin or Super Admin, with one exception: POST /a/decisions/{key}/evaluate is open to any authenticated role, 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 tenant Manager or above — 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-basesManagerList the tenant's bases and the global ones
POST/a/knowledge-basesManagerCreate a base (201); isGlobal: true requires Super Admin
GET/a/knowledge-bases/{id}ManagerBase details, with the document count
DELETE/a/knowledge-bases/{id}ManagerDelete a base — refused while a process is still linked
GET/a/knowledge-bases/{id}/documentsManagerList documents, paginated (size 20 by default)
POST/a/knowledge-bases/{id}/documentsManagerUpload PDF, TXT or MD (202 — indexing is asynchronous)
DELETE/a/knowledge-bases/{id}/documents/{docId}ManagerDelete a document and its vectors
GET/a/knowledge-bases/process/{definitionKey}ManagerBases linked to a process
POST/a/knowledge-bases/process/{definitionKey}/link/{id}ManagerLink a base to the process (201)
DELETE/a/knowledge-bases/process/{definitionKey}/unlink/{id}ManagerUnlink

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 tenant Admin.

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. Every route in this table requires tenant Admin or Super Admin — 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.

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